diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,8 @@
 # Version history of koji-tool
 
+# 0.8.2 (2022-03-28)
+- use the formatting library for rendering aligned output
+
 # 0.8.1 (2022-03-21)
 - query results ordered by taskid or buildid for speed
 - 'builds', 'tasks': argument is now a package otherwise use --pattern (replaces --package)
diff --git a/TODO b/TODO
--- a/TODO
+++ b/TODO
@@ -1,6 +1,3 @@
-# general ideas
-- use formatting or fmt (instead of format-numbers)
-
 # install
 - autodetect nvr
 - --nodeps
diff --git a/koji-tool.cabal b/koji-tool.cabal
--- a/koji-tool.cabal
+++ b/koji-tool.cabal
@@ -1,5 +1,5 @@
 name:                koji-tool
-version:             0.8.1
+version:             0.8.2
 synopsis:            Koji CLI tool for querying tasks and installing builds
 description:
         koji-tool is a CLI interface to Koji with commands to query
@@ -45,7 +45,7 @@
                        directory,
                        extra,
                        filepath,
-                       format-numbers,
+                       formatting >= 7,
                        Glob,
                        http-conduit,
                        http-directory >= 0.1.9,
diff --git a/src/BuildlogSizes.hs b/src/BuildlogSizes.hs
--- a/src/BuildlogSizes.hs
+++ b/src/BuildlogSizes.hs
@@ -24,12 +24,13 @@
 import Distribution.Koji (fedoraKojiHub)
 import Distribution.Koji.API --(listBuilds)
 
+import Formatting
+
 #if !MIN_VERSION_http_directory(0,1,5)
 import Network.HTTP.Client (Manager)
 #endif
 import Network.HTTP.Directory
 
-import Data.Text.Format.Numbers
 import SimpleCmdArgs
 
 import Common (commonBuildQueryOptions)
@@ -93,13 +94,8 @@
 doGetBuildlogSize buildlog arch = do
   exists <- httpExists' buildlog
   msize <- if exists then httpFileSize' buildlog else return Nothing
-  whenJust msize $ \ size -> do
-    let kb = kiloBytes size
-    putStrLn $ arch ++ replicate (16 - length arch - length kb) ' ' ++ kb
-
-kiloBytes :: Integer -> String
-kiloBytes size =
-  T.unpack $ prettyI (Just ',') (fromInteger size `div` 1000) <> "kB"
+  whenJust msize $ \ size ->
+    fprintLn (right 8 ' ' % lpadded 7 ' ' commas % "kB") arch (size `div` 1000)
 
 #if !MIN_VERSION_http_directory(0,1,9)
 infixr 5 +/+
diff --git a/src/Builds.hs b/src/Builds.hs
--- a/src/Builds.hs
+++ b/src/Builds.hs
@@ -204,9 +204,6 @@
     _ -> error' $! "unknown task state: " ++ s
 #endif
 
-data LastLog = WholeBuild | BuildTail | RootLog
-  deriving Eq
-
 getBuildState :: Struct -> Maybe BuildState
 getBuildState st = readBuildState <$> lookup "state" st
 
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -88,7 +88,8 @@
     , Subcommand "progress"
       "Track running Koji tasks by buildlog size" $
       progressCmd
-      <$> optionalWith auto 'i' "interval" "MINUTES" "Polling interval between updates (default 2 min)" 2
+      <$> switchWith 'D' "debug" "Pretty-print raw XML result"
+      <*> optionalWith auto 'i' "interval" "MINUTES" "Polling interval between updates (default 2 min)" 2
       <*> switchWith 'm' "modules" "Track module builds"
       <*> many (TaskId <$> argumentWith auto "TASKID")
 
diff --git a/src/Progress.hs b/src/Progress.hs
--- a/src/Progress.hs
+++ b/src/Progress.hs
@@ -13,6 +13,8 @@
 #endif
 import Control.Monad
 
+import Formatting
+
 #if !MIN_VERSION_http_directory(0,1,5)
 import Network.HTTP.Client (Manager)
 #endif
@@ -21,6 +23,7 @@
 import Control.Concurrent (threadDelay)
 
 import Data.Fixed
+import Data.Int (Int64)
 import Data.List.Extra
 import Data.Maybe
 #if !MIN_VERSION_base(4,11,0)
@@ -28,8 +31,6 @@
 #endif
 import Data.Text (Text)
 import qualified Data.Text as T
-import qualified Data.Text.IO as T
-import Data.Text.Format.Numbers
 
 import Distribution.Koji
 
@@ -37,8 +38,8 @@
 
 import System.FilePath (takeBaseName, (</>))
 
-progressCmd :: Int -> Bool -> [TaskID] -> IO ()
-progressCmd waitdelay modules tids = do
+progressCmd :: Bool -> Int -> Bool -> [TaskID] -> IO ()
+progressCmd debug waitdelay modules tids = do
   when (waitdelay < 1) $ error' "minimum interval is 1 min"
   when (modules && not (null tids)) $ error' "cannot combine --modules with tasks"
   tasks <-
@@ -47,7 +48,7 @@
     else return tids
   when (null tasks) $ error' "no build tasks found"
   btasks <- mapM kojiTaskinfoRecursive tasks
-  loopBuildTasks waitdelay btasks
+  loopBuildTasks debug waitdelay btasks
 
 kojiTaskinfoRecursive :: TaskID -> IO BuildTask
 kojiTaskinfoRecursive tid = do
@@ -76,14 +77,14 @@
 type TaskInfoSize = (Struct,Maybe Int)
 type TaskInfoSizes = (Struct,(Maybe Int,Maybe Int))
 
-loopBuildTasks :: Int -> [BuildTask] -> IO ()
-loopBuildTasks _ [] = return ()
-loopBuildTasks waitdelay bts = do
+loopBuildTasks :: Bool -> Int -> [BuildTask] -> IO ()
+loopBuildTasks _ _ [] = return ()
+loopBuildTasks debug waitdelay bts = do
   curs <- filter tasksOpen <$> mapM runProgress bts
   unless (null curs) $ do
     threadDelayMinutes waitdelay
     news <- mapM updateBuildTask curs
-    loopBuildTasks waitdelay news
+    loopBuildTasks debug waitdelay news
   where
     threadDelayMinutes :: Int -> IO ()
     threadDelayMinutes m =
@@ -92,26 +93,28 @@
 
     runProgress :: BuildTask -> IO BuildTask
     runProgress (tid,tasks) =
-      if null tasks then do
-        state <- kojiGetTaskState fedoraKojiHub tid
-        if state `elem` map Just openTaskStates then do
-          threadDelayMinutes waitdelay
-          kojiTaskinfoRecursive tid
-          else return (tid,[])
-      else do
-        putStrLn ""
-        let request = lookupStruct "request" $ fst (head tasks) :: Maybe [Value]
-            nvr = case request of
-                    Just (srpm:_) ->
-                      (takeBaseName . takeBaseName) $
-                      maybeVal "failed to read src rpm" getString srpm
-                    _ -> error "No src rpm found"
-        logMsg $ nvr ++ " (" ++ displayID tid ++ ")"
-        sizes <- mapM buildlogSize tasks
-        printLogSizes waitdelay sizes
-        let news = map (\(t,(s,_)) -> (t,s)) sizes
-            open = filter (\ (t,_) -> getTaskState t `elem` map Just openTaskStates) news
-        return (tid, open)
+      case tasks of
+        [] -> do
+          state <- kojiGetTaskState fedoraKojiHub tid
+          if state `elem` map Just openTaskStates then do
+            threadDelayMinutes waitdelay
+            kojiTaskinfoRecursive tid
+            else return (tid,[])
+        ((task,_):_) -> do
+          putStrLn ""
+          when debug $ print task
+          let request = lookupStruct "request" task :: Maybe [Value]
+          when debug $ print request
+          let mnvr = case request of
+                       Just (srpm:_) ->
+                         takeBaseName . takeBaseName <$> getString srpm
+                       _ -> Nothing
+          logMsg $ fromMaybe "<unknown>" mnvr ++ " (" ++ displayID tid ++ ")"
+          sizes <- mapM buildlogSize tasks
+          printLogSizes waitdelay sizes
+          let news = map (\(t,(s,_)) -> (t,s)) sizes
+              open = filter (\ (t,_) -> getTaskState t `elem` map Just openTaskStates) news
+          return (tid, open)
 
     tasksOpen :: BuildTask -> Bool
     tasksOpen (_,ts) = not (null ts)
@@ -142,49 +145,41 @@
       let few = dropWhile (== '0') $ takeEnd 4 tid in
         if null few then "0" else few
 
-data TaskOutput = TaskOut {_outArch :: Text, outSize :: Text, outSpeed :: Text, _outState :: Text, _method :: Text}
+data TaskOutput = TaskOut {_outArch :: Text, moutSize :: Maybe Int, moutSpeed :: Maybe Int, _outState :: Text, _method :: Text}
 
 printLogSizes :: Int -> [TaskInfoSizes] -> IO ()
 printLogSizes waitdelay tss =
-  mapM_ (T.putStrLn . taskOutList) $ (formatSize . map logSize) tss
+  let (mxsi, mxsp, taskoutputs) = (formatSize . map taskOutput) tss
+  in mapM_ (printTaskOut mxsi mxsp) taskoutputs
   where
-    taskOutList :: TaskOutput -> Text
-    taskOutList (TaskOut a si sp st mth) = T.unwords [a, si, sp, st, mth]
+    printTaskOut :: Int64 -> Int64 -> TaskOutput -> IO ()
+    printTaskOut mxsi mxsp (TaskOut a msi msp st mth) =
+      fprintLn (rpadded 8 ' ' stext % lpadded mxsi ' ' (optioned commas) % "kB" % " " % optioned ("[" % lpadded mxsp ' ' commas % " B/min]") % " " % stext % " " % stext) a ((`div` 1000) <$> msi) ((`div` waitdelay) <$> msp) st (abridgeMethod mth)
 
-    formatSize :: [TaskOutput] -> [TaskOutput]
+    formatSize :: [TaskOutput] -> (Int64, Int64,[TaskOutput])
     formatSize ts =
-      let maxsi = maximum $ 6 : map (T.length . outSize) ts
-                  -- "198,689"
-          maxsp = maximum $ 7 : map (T.length . outSpeed) ts
-      in map (justifyBytes maxsi maxsp) ts
+      let maxsi = maximum $ 0 : mapMaybe moutSize ts
+                  -- was "198,689"
+          maxsp = maximum $ 0 : mapMaybe moutSpeed ts
+      in (decimalLength maxsi, decimalLength maxsp, ts)
+      where
+        decimalLength = fromIntegral . length . show
 
-    justifyBytes :: Int -> Int -> TaskOutput -> TaskOutput
-    justifyBytes maxsi maxsp (TaskOut a si sp st mth) =
-      TaskOut
-      (a  <> T.replicate (7 - T.length a) " ")
-      (T.replicate (maxsi - T.length si) " " <> si <> "kB")
-      (case sp of
-         "" -> ""
-         _ -> "[" <> T.replicate (maxsp - T.length sp) " " <> sp <> " B/min" <> "]")
-      st
-      (case mth of
-         "buildArch" -> ""
-         "buildSRPMFromSCM" -> "SRPM"
-         _ -> mth)
+    abridgeMethod :: Text -> Text
+    abridgeMethod mth =
+      case mth of
+        "buildArch" -> ""
+        "buildSRPMFromSCM" -> "SRPM"
+        _ -> mth
 
-    logSize :: TaskInfoSizes -> TaskOutput
-    logSize (task, (size,old)) =
+    taskOutput :: TaskInfoSizes -> TaskOutput
+    taskOutput (task, (size,old)) =
       let method = maybeVal "method not found" (lookupStruct "method") task :: Text
           arch = maybeVal "arch not found" (lookupStruct "arch") task :: Text
           diff = (-) <$> size <*> old
           state = maybeVal "No state found" getTaskState task
           state' = if state == TaskOpen then "" else T.pack (show state)
-        in TaskOut arch (maybe "" kiloBytes size) (maybe "" speed diff) state' method
-      where
-        kiloBytes s = prettyI (Just ',') (s `div` 1000)
-
-        speed :: Int -> Text
-        speed s = prettyI (Just ',') (s `div` waitdelay)
+        in TaskOut arch size diff state' method
 
 kojiListBuildTasks :: Maybe String -> IO [TaskID]
 kojiListBuildTasks muser = do
diff --git a/src/Tasks.hs b/src/Tasks.hs
--- a/src/Tasks.hs
+++ b/src/Tasks.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE CPP #-}
+{-# LANGUAGE CPP, OverloadedStrings #-}
 
 -- SPDX-License-Identifier: BSD-3-Clause
 
@@ -22,14 +22,12 @@
 import Data.Monoid ((<>))
 #endif
 import Data.RPM.NVR
-import qualified Data.Text as T
-import qualified Data.Text.IO as T
-import Data.Text.Format.Numbers
 import Data.Time.Clock
 import Data.Time.Format
 import Data.Time.LocalTime
 import Distribution.Koji
 import Distribution.Koji.API
+import Formatting hiding (now)
 import Network.HTTP.Directory
 import Network.HTTP.Simple
 import SimpleCmd
@@ -313,9 +311,7 @@
     putStr $ buildlog ++ " "
     msize <- httpFileSize' buildlog
     whenJust msize $ \size -> do
-      putChar '('
-      (T.putStr . kiloBytes) size
-      putStrLn ")"
+      fprintLn ("(" % commas % "kB)") (size `div` 1000)
       -- FIXME check if short build.log ends with srpm
       lastlog <-
         if size < 1500
@@ -325,8 +321,6 @@
         else return BuildTail
       when tail' $
         displayLog taskid lastlog
-  where
-    kiloBytes s = prettyI (Just ',') (fromInteger s `div` 1000) <> T.pack "kB"
 
 kojiMethods :: [String]
 kojiMethods =
