diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for cabal-detailed-quickcheck
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2022 Anselm Schüler
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/cabal-detailed-quickcheck.cabal b/cabal-detailed-quickcheck.cabal
new file mode 100644
--- /dev/null
+++ b/cabal-detailed-quickcheck.cabal
@@ -0,0 +1,32 @@
+cabal-version:      2.4
+name:               cabal-detailed-quickcheck
+version:            0.1.0.0
+synopsis:           QuickCheck for Cabal tests
+description:        Turn QuickCheck properties into detailed Cabal tests
+homepage:           https://github.com/schuelermine/cabal-detailed-quickcheck
+bug-reports:        https://github.com/schuelermine/cabal-detailed-quickcheck/issues
+license:            MIT
+license-file:       LICENSE
+author:             Anselm Schüler
+maintainer:         mail@anselmschueler.com
+copyright:          Ⓒ Anselm Schüler 2022
+category:           Testing
+extra-source-files: CHANGELOG.md
+
+library
+    exposed-modules:    Distribution.TestSuite.QuickCheck
+    build-depends:      base >=4.14&&<4.18,
+                        QuickCheck ^>=2.14.2,
+                        Cabal ^>=3.6.3
+    hs-source-dirs:     lib
+    default-language:   Haskell2010
+
+source-repository head
+    type:               git
+    location:           https://github.com/schuelermine/cabal-detailed-quickcheck.git
+    branch:             b0
+
+source-repository this
+    type:               git
+    location:           https://github.com/schuelermine/cabal-detailed-quickcheck.git
+    tag:                0.1.0.0
diff --git a/lib/Distribution/TestSuite/QuickCheck.hs b/lib/Distribution/TestSuite/QuickCheck.hs
new file mode 100644
--- /dev/null
+++ b/lib/Distribution/TestSuite/QuickCheck.hs
@@ -0,0 +1,322 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- NoFieldSelectors is implemented in GHC 9.2.2, but HLS doesn’t support it
+-- {-# LANGUAGE NoFieldSelectors #-}
+
+-- |
+-- Module:       Distribution.TestSuite.QuickCheck
+-- Description:  Convert QuickCheck properties into Cabal tests
+-- Copyright:    ⓒ Anselm Schüler 2022
+-- License:      MIT
+-- Maintainer:   Anselm Schüler <mail@anselmschueler.com>
+-- Stability:    stable
+-- Portability:  Portable
+--
+-- This module allows you to easily make Cabal tests for the @detailed-0.9@ interface.
+-- It sets sensible option declarations for the tests.
+--
+-- This module re-uses record names from 'Distribution.TestSuite' and 'Test.QuickCheck'.
+-- It is recommended that you enable the @DisambiguateRecordFields@ extension in GHC and/or import the module qualified.
+-- For basic tests, you don’t need to import 'Distribution.TestSuite'.
+module Distribution.TestSuite.QuickCheck
+  ( -- * Create tests
+    getPropertyTest,
+    getPropertyTestWith,
+    getPropertyTestUsing,
+    getPropertyTestWithUsing,
+
+    -- * Argument data types
+    PropertyTest (..),
+    TestArgs (..),
+    Verbosity (..),
+
+    -- * Functions for using arguments
+    argsToTestArgs,
+    testArgsToArgs,
+    stdTestArgs,
+  )
+where
+
+import Data.Bool (bool)
+import Data.Functor ((<&>))
+import qualified Distribution.TestSuite as T
+import qualified Test.QuickCheck as QC
+import Text.Read (readMaybe)
+
+-- | Datatype for setting the verbosity of tests
+data Verbosity
+  = -- | QuickCheck prints nothing
+    Silent
+  | -- | Print basic statistics
+    Chatty
+  | -- | Print every test case
+    Verbose
+  deriving
+    ( Eq,
+      -- | 'Silent' < 'Chatty' < 'Verbose'
+      Ord,
+      Show,
+      Read,
+      Enum,
+      Bounded
+    )
+
+-- ! [PARTIAL] This function fails when passed Silent
+switchVerbosity :: Verbosity -> Bool -> Verbosity -> Verbosity
+switchVerbosity v' q v = bool max min q v $ bool id pred q v'
+
+-- | Arguments for altering property test behaviour.
+--   These can be altered in the final Cabal 'T.Test' using 'T.setOption'.
+data TestArgs = TestArgs
+  { verbosity :: Verbosity,
+    verboseShrinking :: Bool,
+    maxDiscardRatio :: Int,
+    noShrinking :: Bool,
+    maxShrinks :: Int,
+    maxSuccess :: Int,
+    maxSize :: Int,
+    sizeScale :: Int
+  }
+
+-- | Transform a QuickCheck 'QC.Args' value to a 'TestArgs' value, defaulting all missing properties
+argsToTestArgs :: QC.Args -> TestArgs
+argsToTestArgs QC.Args {..} =
+  TestArgs
+    { verbosity = if chatty then Chatty else Silent,
+      verboseShrinking = False,
+      maxDiscardRatio,
+      noShrinking = False,
+      maxShrinks,
+      maxSuccess,
+      maxSize,
+      sizeScale = 1
+    }
+
+-- | Default arguments for property tests
+stdTestArgs :: TestArgs
+stdTestArgs = argsToTestArgs QC.stdArgs
+
+-- | Recover arguments passed to 'QC.quickCheck' from a 'TestArgs'
+testArgsToArgs :: TestArgs -> QC.Args
+testArgsToArgs
+  TestArgs
+    { verbosity,
+      maxDiscardRatio,
+      maxShrinks,
+      maxSuccess,
+      maxSize
+    } =
+    QC.Args
+      { replay = Nothing,
+        maxSuccess,
+        maxDiscardRatio,
+        maxSize,
+        chatty = verbosity >= Chatty,
+        maxShrinks
+      }
+
+useModifiers :: QC.Testable a => TestArgs -> a -> QC.Property
+useModifiers TestArgs {verbosity, noShrinking, verboseShrinking, sizeScale} =
+  foldr (.) QC.property $
+    snd
+      <$> filter
+        fst
+        [ (verbosity == Verbose, QC.verbose),
+          (verboseShrinking, QC.verboseShrinking),
+          (noShrinking, QC.noShrinking),
+          (sizeScale /= 1, QC.mapSize (* sizeScale))
+        ]
+
+qcTestArgs :: QC.Testable a => TestArgs -> a -> IO QC.Result
+qcTestArgs args property = QC.quickCheckWithResult (testArgsToArgs args) (useModifiers args property)
+
+switchVIn :: Verbosity -> Bool -> TestArgs -> TestArgs
+switchVIn v' q args@TestArgs {verbosity} = args {verbosity = switchVerbosity v' q verbosity}
+
+setArgStr :: String -> String -> Maybe (TestArgs -> TestArgs)
+setArgStr "silent" str =
+  readMaybe str <&> \val args@TestArgs {verbosity} ->
+    if val
+      then args {verbosity = Silent}
+      else args {verbosity = max Chatty verbosity}
+setArgStr "chatty" str = readMaybe str <&> switchVIn Chatty
+setArgStr "verbose" str = readMaybe str <&> switchVIn Verbose
+setArgStr "verboseShrinking" str =
+  readMaybe str <&> \val args ->
+    args {verboseShrinking = val}
+setArgStr "verbosity" str =
+  readMaybe str <&> \val args ->
+    args {verbosity = val}
+setArgStr "maxDiscardRatio" str =
+  readMaybe str <&> \val args ->
+    args {maxDiscardRatio = val}
+setArgStr "noShrinking" str =
+  readMaybe str <&> \val args ->
+    args {noShrinking = val}
+setArgStr "shrinking" str =
+  readMaybe str <&> \val args ->
+    args {noShrinking = not val}
+setArgStr "maxShrinks" str =
+  readMaybe str <&> \val args ->
+    args {maxShrinks = val}
+setArgStr "maxSuccess" str =
+  readMaybe str <&> \val args ->
+    args {maxSuccess = val}
+setArgStr "maxSize" str =
+  readMaybe str <&> \val args ->
+    args {maxSize = val}
+setArgStr "sizeScale" str =
+  readMaybe str <&> \val args ->
+    args {sizeScale = val}
+setArgStr _ _ = Nothing
+
+positiveIntType :: T.OptionType
+positiveIntType =
+  T.OptionNumber
+    { optionNumberIsInt = True,
+      optionNumberBounds = (Just "1", Nothing)
+    }
+
+getOptionDescrs :: TestArgs -> [T.OptionDescr]
+getOptionDescrs TestArgs {..} =
+  [ T.OptionDescr
+      { optionName = "silent",
+        optionDescription = "Suppress QuickCheck output",
+        optionType = T.OptionBool,
+        optionDefault = Just . show $ verbosity == Silent
+      },
+    T.OptionDescr
+      { optionName = "chatty",
+        optionDescription = "Print QuickCheck output",
+        optionType = T.OptionBool,
+        optionDefault = Just . show $ verbosity > Chatty
+      },
+    T.OptionDescr
+      { optionName = "verbose",
+        optionDescription = "Print checked values",
+        optionType = T.OptionBool,
+        optionDefault = Just . show $ verbosity > Verbose
+      },
+    T.OptionDescr
+      { optionName = "verboseShrinking",
+        optionDescription = "Print all checked and shrunk values",
+        optionType = T.OptionBool,
+        optionDefault = Just . show $ verboseShrinking
+      },
+    T.OptionDescr
+      { optionName = "verbosity",
+        optionDescription = "Verbosity level",
+        optionType = T.OptionEnum ["Silent", "Chatty", "Verbose", "VerboseShrinking"],
+        optionDefault = Just $ show verbosity
+      },
+    T.OptionDescr
+      { optionName = "maxDiscardRatio",
+        optionDescription = "Maximum number of discarded tests per successful test before giving up",
+        optionType = positiveIntType,
+        optionDefault = Just $ show maxDiscardRatio
+      },
+    T.OptionDescr
+      { optionName = "noShrinking",
+        optionDescription = "Disable shrinking",
+        optionType = T.OptionBool,
+        optionDefault = Just $ show noShrinking
+      },
+    T.OptionDescr
+      { optionName = "shrinking",
+        optionDescription = "Enable shrinking",
+        optionType = T.OptionBool,
+        optionDefault = Just . show $ not noShrinking
+      },
+    T.OptionDescr
+      { optionName = "maxShrinks",
+        optionDescription = "Maximum number of shrinks to before giving up or zero to disable shrinking",
+        optionType = positiveIntType,
+        optionDefault = Just $ show maxShrinks
+      },
+    T.OptionDescr
+      { optionName = "maxSuccess",
+        optionDescription = "Maximum number of successful tests before succeeding",
+        optionType = positiveIntType,
+        optionDefault = Just $ show maxSuccess
+      },
+    T.OptionDescr
+      { optionName = "maxSize",
+        optionDescription = "Size to use for the biggest test cases",
+        optionType = positiveIntType,
+        optionDefault = Just $ show maxSize
+      },
+    T.OptionDescr
+      { optionName = "sizeScale",
+        optionDescription = "Scale all sizes by a number",
+        optionType = positiveIntType,
+        optionDefault = Just $ show sizeScale
+      }
+  ]
+
+-- | Property test declaration with metadata
+data PropertyTest prop = PropertyTest
+  { -- | Name of the test, for Cabal. See See Cabal’s 'T.name'.
+    name :: String,
+    -- | Tags of the test, for Cabal. See Cabal’s 'T.tags'.
+    tags :: [String],
+    -- | Property to check. This should usually be or return an instance of 'QC.Testable'.
+    property :: prop
+  }
+
+-- | Get a Cabal 'T.Test' with custom 'TestArgs' from a 'PropertyTest' that takes the test arguments and returns a 'QC.testable' value
+getPropertyTestWithUsing ::
+  QC.Testable prop =>
+  -- | The arguments for the test
+  TestArgs ->
+  -- | A property test whose 'property' takes a 'TestArgs' argument
+  PropertyTest (TestArgs -> prop) ->
+  T.Test
+getPropertyTestWithUsing originalArgs PropertyTest {..} =
+  let withArgs args =
+        T.TestInstance
+          { run = do
+              result <- qcTestArgs args (property args)
+              let resultStr = "\n" ++ show result
+              return $ T.Finished case result of
+                QC.Success {} -> T.Pass
+                QC.GaveUp {} -> T.Error $ "GaveUp: QuickCheck gave up" ++ resultStr
+                QC.Failure {} -> T.Fail $ "Failure: A property failed" ++ resultStr
+                QC.NoExpectedFailure {} ->
+                  T.Fail $
+                    "NoExpectedFailure: A property that should have failed did not"
+                      ++ "\n"
+                      ++ show result,
+            name,
+            tags,
+            options = getOptionDescrs originalArgs,
+            setOption = \opt str -> case setArgStr opt str of
+              Nothing -> Left "Parse error"
+              Just f -> Right . withArgs $ f args
+          }
+   in T.Test $ withArgs originalArgs
+
+discardingTestArgs :: PropertyTest prop -> PropertyTest (TestArgs -> prop)
+discardingTestArgs test@PropertyTest {property} = test {property = const property}
+
+-- | Get a Cabal 'T.Test' from a 'PropertyTest' that takes the test arguments and returns a 'QC.Testable' value
+getPropertyTestUsing ::
+  QC.Testable prop =>
+  -- | A property test whose 'property' takes a 'TestArgs' argument
+  PropertyTest (TestArgs -> prop) ->
+  T.Test
+getPropertyTestUsing = getPropertyTestWithUsing stdTestArgs
+
+-- | Get a Cabal 'T.Test' from a 'PropertyTest' with custom 'TestArgs'
+getPropertyTestWith ::
+  QC.Testable prop =>
+  -- | The arguments for the test
+  TestArgs ->
+  PropertyTest prop ->
+  T.Test
+getPropertyTestWith args = getPropertyTestWithUsing args . discardingTestArgs
+
+-- | Get a Cabal 'T.Test' from a 'PropertyTest'
+getPropertyTest :: QC.Testable prop => PropertyTest prop -> T.Test
+getPropertyTest = getPropertyTestWithUsing stdTestArgs . discardingTestArgs
