diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,19 @@
 
 # Revision history for nixfmt
 
+## Unreleased
+
+## 0.6.0 -- 2023-10-31
+
+* Fix escaping of interpolations after dollar signs.
+* Fix nixfmt trying to allocate temp files that aren't used.
+* Don't write if files didn't change, fixing treefmt compatibility
+* Nixfmt now accepts the '-' argument to read from stdin.
+* `nixfmt [dir]` now recursively formats nix files in that directory.
+* Float and int literal parsing now matches nix.
+
 ## 0.5.0 -- 2022-03-15
+
 * Add a nix flake to the nixfmt project.
 * Add a --verify flag to check idempotency.
 * Support nix path (`./${foo}.nix`) interpolations.
@@ -15,6 +27,7 @@
 * Fix handling of multiline strings with spaces in the last line.
 
 ## 0.4.0 -- 2020-02-10
+
 * Report non-conforming files on the same line to aid line-oriented processing
 * Fix help, summary, and version flag contents.
 * Fix indentation of leading comments in parens
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -10,8 +10,7 @@
 
 You are encouraged to test this out on your code and submit any undesirable formatting you find as an issue
 
-[![Build Status](https://badge.buildkite.com/b37f73adea391439e63288e8fd3b47f4b98fb9640bb864ccfa.svg)](https://buildkite.com/serokell/nixfmt)
-[![Cachix](https://img.shields.io/badge/cachix-nixfmt-blue.svg)](https://nixfmt.cachix.org)
+![Build Status](https://github.com/serokell/nixfmt/actions/workflows/main.yml/badge.svg?branch=master)
 
 ## Installation
 
@@ -21,8 +20,6 @@
 
 - To get the most recent version, install from master:
 
-      # Optional: use cachix to get cached builds even for master
-      cachix use nixfmt
       nix-env -f https://github.com/serokell/nixfmt/archive/master.tar.gz -i
 
 - Nix with flakes
@@ -47,13 +44,8 @@
 
 ## Usage
 
-* `nixfmt < input.nix` – reads Nix code form `stdin`, formats it, and outputs to `stdout`
+* `nixfmt < input.nix` – reads Nix code from `stdin`, formats it, and outputs to `stdout`
 * `nixfmt file.nix` – format the file in place
-
-
-## For Contributors
-
-We welcome issues and pull requests on GitHub.
 
 
 ## About Serokell
diff --git a/main/Main.hs b/main/Main.hs
--- a/main/Main.hs
+++ b/main/Main.hs
@@ -4,22 +4,25 @@
  - SPDX-License-Identifier: MPL-2.0
  -}
 
-{-# LANGUAGE DeriveDataTypeable, NamedFieldPuns #-}
+{-# LANGUAGE DeriveDataTypeable, NamedFieldPuns, MultiWayIf #-}
 
 module Main where
 
 import Control.Concurrent (Chan, forkIO, newChan, readChan, writeChan)
 import Data.Either (lefts)
 import Data.Text (Text)
+import Data.List (isSuffixOf)
 import Data.Version (showVersion)
 import GHC.IO.Encoding (utf8)
 import Paths_nixfmt (version)
 import System.Console.CmdArgs
   (Data, Typeable, args, cmdArgs, help, summary, typ, (&=))
 import System.Exit (ExitCode(..), exitFailure, exitSuccess)
+import System.FilePath ((</>))
 import System.IO (hPutStrLn, hSetEncoding, stderr)
 import System.Posix.Process (exitImmediately)
 import System.Posix.Signals (Handler(..), installHandler, keyboardSignal)
+import System.Directory (listDirectory, doesDirectoryExist)
 
 import qualified Data.Text.IO as TextIO (getContents, hPutStr, putStr)
 
@@ -39,26 +42,51 @@
     } deriving (Show, Data, Typeable)
 
 options :: Nixfmt
-options = Nixfmt
-    { files = [] &= args &= typ "FILES"
-    , width = 80 &= help "Maximum width in characters"
-    , check = False &= help "Check whether files are formatted"
-    , quiet = False &= help "Do not report errors"
-    , verify = False &= help "Check that the output parses and formats the same as the input"
-    } &= summary ("nixfmt v" ++ showVersion version)
-    &= help "Format Nix source code"
+options =
+  let defaultWidth = 80
+      addDefaultHint value message =
+        message ++ "\n[default: " ++ show value ++ "]"
+   in Nixfmt
+        { files = [] &= args &= typ "FILES"
+        , width =
+            defaultWidth &=
+            help (addDefaultHint defaultWidth "Maximum width in characters")
+        , check = False &= help "Check whether files are formatted"
+        , quiet = False &= help "Do not report errors"
+        , verify =
+            False &=
+            help
+              "Check that the output parses and formats the same as the input"
+        } &=
+      summary ("nixfmt v" ++ showVersion version) &=
+      help "Format Nix source code"
 
 data Target = Target
     { tDoRead :: IO Text
     , tPath :: FilePath
-    , tDoWrite :: Text -> IO ()
+      -- The bool is true when the formatted file differs from the input
+    , tDoWrite :: Bool -> Text -> IO ()
     }
 
+-- | Recursively collect nix files in a directory
+collectNixFiles :: FilePath -> IO [FilePath]
+collectNixFiles path = do
+  dir <- doesDirectoryExist path
+  if | dir -> do
+         files <- listDirectory path
+         concat <$> mapM collectNixFiles ((path </>) <$> files)
+     | ".nix" `isSuffixOf` path -> pure [path]
+     | otherwise -> pure []
+
+-- | Recursively collect nix files in a list of directories
+collectAllNixFiles :: [FilePath] -> IO [FilePath]
+collectAllNixFiles paths = concat <$> mapM collectNixFiles paths
+
 formatTarget :: Formatter -> Target -> IO Result
 formatTarget format Target{tDoRead, tPath, tDoWrite} = do
     contents <- tDoRead
-    let formatted = format tPath contents
-    mapM tDoWrite formatted
+    let formatResult = format tPath contents
+    mapM (\formatted -> tDoWrite (formatted /= contents) formatted) formatResult
 
 -- | Return an error if target could not be parsed or was not formatted
 -- correctly.
@@ -72,19 +100,26 @@
             | otherwise             -> Left $ tPath ++ ": not formatted"
 
 stdioTarget :: Target
-stdioTarget = Target TextIO.getContents "<stdin>" TextIO.putStr
+stdioTarget = Target TextIO.getContents "<stdin>" (const $ TextIO.putStr)
 
 fileTarget :: FilePath -> Target
 fileTarget path = Target (readFileUtf8 path) path atomicWriteFile
   where
-    atomicWriteFile t = withOutputFile path $ \h -> do
+    atomicWriteFile True t = withOutputFile path $ \h -> do
       hSetEncoding h utf8
       TextIO.hPutStr h t
+    -- Don't do anything if the file is already formatted
+    atomicWriteFile False _ = mempty
 
-toTargets :: Nixfmt -> [Target]
-toTargets Nixfmt{ files = [] }    = [stdioTarget]
-toTargets Nixfmt{ files = paths } = map fileTarget paths
+checkFileTarget :: FilePath -> Target
+checkFileTarget path = Target (readFileUtf8 path) path (const $ const $ pure ())
 
+toTargets :: Nixfmt -> IO [Target]
+toTargets Nixfmt{ files = [] }    = pure [stdioTarget]
+toTargets Nixfmt{ files = ["-"] } = pure [stdioTarget]
+toTargets Nixfmt{ check = False, files = paths } = map fileTarget <$> collectAllNixFiles paths
+toTargets Nixfmt{ check = True, files = paths } = map checkFileTarget <$> collectAllNixFiles paths
+
 type Formatter = FilePath -> Text -> Either String Text
 
 toFormatter :: Nixfmt -> Formatter
@@ -101,8 +136,8 @@
 toWriteError Nixfmt{ quiet = False } = hPutStrLn stderr
 toWriteError Nixfmt{ quiet = True } = const $ return ()
 
-toJobs :: Nixfmt -> [IO Result]
-toJobs opts = map (toOperation opts $ toFormatter opts) $ toTargets opts
+toJobs :: Nixfmt -> IO [IO Result]
+toJobs opts = map (toOperation opts $ toFormatter opts) <$> toTargets opts
 
 -- TODO: Efficient parallel implementation. This is just a sequential stub.
 -- This was originally implemented using parallel-io, but it gave a factor two
@@ -141,7 +176,7 @@
     _ <- installHandler keyboardSignal
             (Catch (exitImmediately $ ExitFailure 2)) Nothing
     opts <- cmdArgs options
-    results <- runJobs (toWriteError opts) (toJobs opts)
+    results <- runJobs (toWriteError opts) =<< toJobs opts
     case lefts results of
          [] -> exitSuccess
          _  -> exitFailure
diff --git a/nixfmt.cabal b/nixfmt.cabal
--- a/nixfmt.cabal
+++ b/nixfmt.cabal
@@ -6,7 +6,7 @@
 -- SPDX-License-Identifier: MPL-2.0
 
 name:                nixfmt
-version:             0.5.0
+version:             0.6.0
 synopsis:            An opinionated formatter for Nix
 description:
   A formatter for Nix that ensures consistent and clear formatting by forgetting
@@ -41,11 +41,11 @@
   else
     buildable: True
   build-depends:
-      base             >= 4.12.0 && < 4.17
+      base             >= 4.12.0 && < 4.19
     , cmdargs          >= 0.10.20 && < 0.11
     , nixfmt
-    , unix             >= 2.7.2 && < 2.8
-    , text             >= 1.2.3 && < 1.3
+    , unix             >= 2.7.2 && < 2.9
+    , text             >= 1.2.3 && < 2.2
 
     -- for System.IO.Atomic
     , directory        >= 1.3.3 && < 1.4
@@ -65,26 +65,32 @@
     Nixfmt
     Nixfmt.Lexer
     Nixfmt.Parser
+    Nixfmt.Parser.Float
     Nixfmt.Predoc
     Nixfmt.Pretty
     Nixfmt.Types
     Nixfmt.Util
 
+  default-extensions:
+    PackageImports
+
   other-extensions:
     DeriveFoldable
     DeriveFunctor
     FlexibleInstances
     LambdaCase
     OverloadedStrings
+    ScopedTypeVariables
     StandaloneDeriving
     TupleSections
 
   hs-source-dirs:      src
   build-depends:
-      base             >= 4.12.0 && < 4.17
-    , megaparsec       >= 9.0.1 && < 9.3
+      base             >= 4.12.0 && < 4.19
+    , megaparsec       >= 9.0.1 && < 9.6
     , parser-combinators >= 1.0.3 && < 1.4
-    , text             >= 1.2.3 && < 1.3
+    , scientific       >= 0.3.0 && < 0.4.0
+    , text             >= 1.2.3 && < 2.2
   default-language:    Haskell2010
   ghc-options:
     -Wall
@@ -107,7 +113,7 @@
       -Wredundant-constraints
       -Wno-orphans
     build-depends:
-      base             >= 4.12.0 && < 4.17
+      base             >= 4.12.0 && < 4.19
       , ghcjs-base     >= 0.2.0 && < 0.3
       , nixfmt
     hs-source-dirs:    js/
diff --git a/src/Nixfmt.hs b/src/Nixfmt.hs
--- a/src/Nixfmt.hs
+++ b/src/Nixfmt.hs
@@ -7,6 +7,7 @@
 module Nixfmt
     ( errorBundlePretty
     , ParseErrorBundle
+    , Width
     , format
     , formatVerify
     ) where
diff --git a/src/Nixfmt/Parser.hs b/src/Nixfmt/Parser.hs
--- a/src/Nixfmt/Parser.hs
+++ b/src/Nixfmt/Parser.hs
@@ -22,13 +22,14 @@
   (anySingle, chunk, eof, label, lookAhead, many, notFollowedBy, oneOf,
   optional, satisfy, some, try, (<|>))
 import Text.Megaparsec.Char (char)
-import qualified Text.Megaparsec.Char.Lexer as L (decimal, float)
+import qualified Text.Megaparsec.Char.Lexer as L (decimal)
 
 import Nixfmt.Lexer (lexeme)
 import Nixfmt.Types
   (Ann, Binder(..), Expression(..), File(..), Fixity(..), Leaf, Operator(..),
   ParamAttr(..), Parameter(..), Parser, Path, Selector(..), SimpleSelector(..),
   String, StringPart(..), Term(..), Token(..), operators, tokenText)
+import Nixfmt.Parser.Float (floatParse)
 import Nixfmt.Util
   (commonIndentation, identChar, isSpaces, manyP, manyText, pathChar,
   schemeChar, someP, someText, uriChar)
@@ -65,7 +66,7 @@
 integer = ann Integer L.decimal
 
 float :: Parser (Ann Token)
-float = ann Float L.float
+float = ann Float floatParse
 
 identifier :: Parser (Ann Token)
 identifier = ann Identifier $ do
diff --git a/src/Nixfmt/Parser/Float.hs b/src/Nixfmt/Parser/Float.hs
new file mode 100644
--- /dev/null
+++ b/src/Nixfmt/Parser/Float.hs
@@ -0,0 +1,61 @@
+{- © 2022 Serokell <hi@serokell.io>
+ - © 2022 Lars Jellema <lars.jellema@gmail.com>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# LANGUAGE TypeFamilies, TypeApplications, ScopedTypeVariables, TypeOperators #-}
+
+module Nixfmt.Parser.Float (floatParse) where
+
+import "base" Data.Foldable (foldl')
+import "base" Data.Proxy (Proxy (..))
+import qualified "base" Data.Char as Char
+
+import "base" Control.Monad (void)
+import "megaparsec" Text.Megaparsec (
+    option, chunkToTokens, takeWhile1P, try, notFollowedBy,
+    (<|>), (<?>), MonadParsec, Token,
+  )
+import "megaparsec" Text.Megaparsec.Char.Lexer (decimal, signed)
+import "megaparsec" Text.Megaparsec.Char (char, char', digitChar)
+
+import "scientific" Data.Scientific (toRealFloat, scientific)
+
+-- copied (and modified) from Text.Megaparsec.Char.Lexer
+data SP = SP !Integer {-# UNPACK #-} !Int
+floatParse :: (MonadParsec e s m, Token s ~ Char, RealFloat a) => m a
+floatParse = do
+  notFollowedBy $ (char '0') >> digitChar
+  notFollowedBy $ (char' 'e')
+  c' <- (decimal <?> "decimal") <|> return 0
+  toRealFloat
+    <$> (( do
+              SP c e' <- dotDecimal_ c'
+              e <- option e' (try $ exponent_ e')
+              return (scientific c e)
+         )
+            <|> (scientific c' <$> exponent_ 0)
+        )
+{-# INLINE floatParse #-}
+
+-- copied from Text.Megaparsec.Char.Lexer
+dotDecimal_ :: forall e s m.
+  (MonadParsec e s m, Token s ~ Char) => Integer -> m SP
+dotDecimal_ c' = do
+  void (char '.')
+  let mkNum = foldl' step (SP c' 0) . chunkToTokens @s Proxy
+      step (SP a e') c =
+        SP
+          (a * 10 + fromIntegral (Char.digitToInt c))
+          (e' - 1)
+  mkNum <$> takeWhile1P (Just "digit") Char.isDigit
+{-# INLINE dotDecimal_ #-}
+
+-- copied from Text.Megaparsec.Char.Lexer
+exponent_ :: (MonadParsec e s m, Token s ~ Char) => Int -> m Int
+exponent_ e' = do
+  void (char' 'e')
+  (+ e') <$> signed (return ()) decimal
+{-# INLINE exponent_ #-}
+
diff --git a/src/Nixfmt/Predoc.hs b/src/Nixfmt/Predoc.hs
--- a/src/Nixfmt/Predoc.hs
+++ b/src/Nixfmt/Predoc.hs
@@ -24,6 +24,7 @@
     , hardline
     , emptyline
     , newline
+    , DocE
     , Doc
     , Pretty
     , pretty
diff --git a/src/Nixfmt/Pretty.hs b/src/Nixfmt/Pretty.hs
--- a/src/Nixfmt/Pretty.hs
+++ b/src/Nixfmt/Pretty.hs
@@ -4,7 +4,7 @@
  - SPDX-License-Identifier: MPL-2.0
  -}
 
-{-# LANGUAGE FlexibleInstances, LambdaCase, OverloadedStrings #-}
+{-# LANGUAGE FlexibleInstances, OverloadedStrings #-}
 
 module Nixfmt.Pretty where
 
@@ -14,7 +14,7 @@
 import Data.Maybe (fromMaybe)
 import Data.Text (Text, isPrefixOf, isSuffixOf, stripPrefix)
 import qualified Data.Text as Text
-  (dropEnd, empty, init, isInfixOf, last, null, replace, strip, takeWhile)
+  (dropEnd, empty, init, isInfixOf, last, null, strip, takeWhile)
 
 import Nixfmt.Predoc
   (Doc, Pretty, base, emptyline, group, hardline, hardspace, hcat, line, line',
@@ -377,14 +377,15 @@
 prettySimpleString :: [[StringPart]] -> Doc
 prettySimpleString parts = group $
     text "\""
-    <> (sepBy (text "\\n") (map (prettyLine escape unescapeInterpol) parts))
+    <> sepBy (text "\\n") (map (prettyLine escape unescapeInterpol) parts)
     <> text "\""
-    where escape
-              = Text.replace "$\\${" "$${"
-              . Text.replace "${" "\\${"
-              . Text.replace "\"" "\\\""
-              . Text.replace "\r" "\\r"
-              . Text.replace "\\" "\\\\"
+    where escape = replaceMultiple
+              [ ("$\\${", "$${")
+              , ("${",    "\\${")
+              , ("\"",    "\\\"")
+              , ("\r",    "\\r")
+              , ("\\",    "\\\\")
+              ]
 
           unescapeInterpol t
               | "$" `isSuffixOf` t = Text.init t <> "\\$"
