diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for mischief-sdl
+
+## 0.1.0.0 
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+Copyright (c) 2026, Damon
+
+
+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 the copyright holder nor the names of its
+      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
+HOLDER 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/mischief-sdl.cabal b/mischief-sdl.cabal
new file mode 100644
--- /dev/null
+++ b/mischief-sdl.cabal
@@ -0,0 +1,135 @@
+cabal-version: 3.0
+-- The cabal-version field refers to the version of the .cabal specification,
+-- and can be different from the cabal-install (the tool) version and the
+-- Cabal (the library) version you are using. As such, the Cabal (the library)
+-- version used must be equal or greater than the version stated in this field.
+-- Starting from the specification version 2.2, the cabal-version field must be
+-- the first thing in the cabal file.
+-- Initial package description 'mischief' generated by
+-- 'cabal init'. For further documentation, see:
+--   http://haskell.org/cabal/users-guide/
+--
+-- The name of the package.
+name: mischief-sdl
+-- The package version.
+-- See the Haskell package versioning policy (PVP) for standards
+-- guiding when and how versions should be incremented.
+-- https://pvp.haskell.org
+-- PVP summary:     +-+------- breaking API changes
+--                  | | +----- non-breaking API additions
+--                  | | | +--- code changes with no API change
+version: 0.1.0.0
+-- A short (one-line) description of the package.
+-- synopsis:
+-- A longer description of the package.
+-- description:
+-- The license under which the package is released.
+homepage: https://codeberg.org/mischief/mischief
+synopsis: Library providing SDL integration for the Mischief game engine.
+description: Integrates SDL events into Mischief messages and contains windowing utilities.
+license: BSD-3-Clause
+-- The file containing the license text.
+license-file: LICENSE
+-- The package author(s).
+author: Damon
+-- An email address to which users can send suggestions, bug reports, and patches.
+maintainer: pvdoriginal2019@gmail.com
+-- A copyright notice.
+-- copyright:
+build-type: Simple
+-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.
+extra-doc-files:
+  CHANGELOG.md
+
+-- Extra source files to be distributed with the package, such as examples, or a tutorial module.
+-- extra-source-files:
+common warnings
+  ghc-options:
+    "-Wall,"
+    -Wno-name-shadowing
+
+library
+  -- Import common warning flags.
+  import: warnings
+  -- Modules exported by the library.
+  exposed-modules:
+    Mischief.SDL
+    Mischief.SDL.Events
+    Mischief.SDL.Window
+
+  -- Modules included in this library but not exported.
+  -- other-modules:
+  other-modules:
+  -- LANGUAGE extensions used by modules in this package.z
+  default-extensions:
+    DefaultSignatures
+    DeriveAnyClass
+    DuplicateRecordFields
+    FunctionalDependencies
+    ImportQualifiedPost
+    MagicHash
+    NoFieldSelectors
+    OverloadedRecordDot
+    OverloadedStrings
+    QuasiQuotes
+    RequiredTypeArguments
+    TemplateHaskell
+    TypeFamilyDependencies
+    UndecidableInstances
+
+  -- other-extensions:
+  -- Other library packages from which modules are imported.
+  build-depends:
+    base >=4.21.0 && <4.22,
+    containers >=0.7 && <0.8,
+    data-default >=0.8.0 && <0.9,
+    mischief-ecs >=0.2.0 && <0.3,
+    primitive >=0.9.1 && <0.10,
+    sdl3-bindgen-sys >=0.0.1 && <0.1,
+
+  -- Directories containing source files.
+  hs-source-dirs:
+    src
+
+  -- Base language which the package is written in.
+  default-language:
+    GHC2024
+
+  ghc-options: -fno-omit-yields
+
+test-suite mischief-test
+  -- Import common warning flags.
+  import: warnings
+  ghc-options: -fno-omit-yields
+  -- Base language which the package is written in.
+  default-language: GHC2024
+  -- Modules included in this executable, other than Main.
+  -- other-modules:
+  -- LANGUAGE extensions used by modules in this package.
+  default-extensions:
+    DeriveAnyClass
+    DuplicateRecordFields
+    NoFieldSelectors
+    NoGeneralizedNewtypeDeriving
+    OverloadedRecordDot
+    RequiredTypeArguments
+    TypeFamilyDependencies
+
+  -- other-extensions:
+  -- The interface type and version of the test suite.
+  type: exitcode-stdio-1.0
+  -- Directories containing source files.
+  hs-source-dirs: test
+  -- The entrypoint to the test suite.
+  main-is: Main.hs
+  -- Test dependencies.
+  build-depends:
+    base,
+    containers,
+    data-default,
+    mischief-ecs,
+    mischief-sdl,
+    primitive,
+
+  pkgconfig-depends:
+    sdl3
diff --git a/src/Mischief/SDL.hs b/src/Mischief/SDL.hs
new file mode 100644
--- /dev/null
+++ b/src/Mischief/SDL.hs
@@ -0,0 +1,66 @@
+-- | Provides SDL utlities for Mischief.
+module Mischief.SDL
+  ( -- * What is SDL?
+    -- $whatis
+
+    -- * Installing SDL
+    -- $installing
+    SDLPlugin,
+    module Mischief.SDL.Events,
+    module Mischief.SDL.Window,
+  )
+where
+
+import Mischief.ECS (Plugin (..), dep)
+import Mischief.SDL.Events
+import Mischief.SDL.Window
+
+data SDLPlugin
+
+instance Plugin SDLPlugin where
+  deps = [dep @SDLEventsPlugin, dep @WindowPlugin]
+
+-- $whatis
+-- [SDL](https://wiki.libsdl.org) is a C library offering low-level tools for windows, input, rendering, etc.
+-- A lot of the official Mischief libraries (such as @mischief-input@ and @mischief-render@) use this library,
+-- and therefore require SDL to be installed.
+
+-- $installing
+-- The current official version of SDL used is @sdl-3.4.18@. You should
+-- find this pre-built for most Operating Systems, and installing it is relatively easy.
+--
+-- These install instructions are mostly copied from [sdl3-bindgen-sys](https://hackage.haskell.org/package/sdl3-bindgen-sys)'s readme.
+--
+-- * Debian / Ubuntu
+--
+-- @
+-- apt install libsdl3-dev-3.4.18
+-- @
+--
+-- * MacOS
+--
+-- @
+-- -- uninstall sdl3 if you have it
+-- brew tap-new $USER/local
+-- brew extract --version=3.4.18 sdl3 $USER/local
+-- brew install $USER/local/sdl3@3.4.18
+-- @
+--
+-- * Arch
+--
+-- @
+-- pacman -S sdl3@3.4.18
+-- @
+--
+-- * Fedora
+--
+-- @
+-- dnf install SDL3-devel-3.4.18
+-- @
+--
+-- * Windows
+--
+-- @
+-- pacman -Syyu # repeat/restart terminal if pacman asks you to
+-- pacman -S mingw-w64-ucrt-x86_64-sdl3-3.4.18 mingw-w64-ucrt-x86_64-pkgconf
+-- @
diff --git a/src/Mischief/SDL/Events.hs b/src/Mischief/SDL/Events.hs
new file mode 100644
--- /dev/null
+++ b/src/Mischief/SDL/Events.hs
@@ -0,0 +1,243 @@
+{-# LANGUAGE MultiWayIf #-}
+
+module Mischief.SDL.Events
+  ( -- $intro
+    SDLEventsPlugin,
+    SDLMessage (..),
+    SDLWindow (..),
+    handleEvents,
+  )
+where
+
+import Control.Monad
+import Control.Monad.IO.Class
+import Data.Foldable
+import Data.Primitive.Ptr
+import Foreign (Storable (peek), castPtr)
+import Mischief.ECS
+import Mischief.ECS.App.Schedules (First)
+import Mischief.ECS.Foreign as F
+import Mischief.ECS.Messages (Message)
+import Mischief.ECS.Messages qualified as Messages
+import SDL3.Sys (destroyWindow)
+import SDL3.Sys qualified as SDL3
+import System.Exit
+
+newtype SDLWindow = SDLWindow {ptr :: Ptr SDL3.SDL_Window}
+  deriving anyclass (Component)
+
+-- deriving newtype (Show)
+
+initSdl :: IO ()
+initSdl = do
+  version <- SDL3.getVersion
+  let (major, rest) = fromIntegral version `divMod` (1000000 :: Int)
+      (minor, micro) = rest `divMod` 1000
+  putStrLn
+    ( "sdl3-raw: linked against SDL "
+        <> show major
+        <> "."
+        <> show minor
+        <> "."
+        <> show micro
+    )
+
+  -- The curated layer types the macro constant at the SDL_InitFlags
+  -- newtype (the raw sDL_INIT_VIDEO CUInt stays available under
+  -- SDL3.Sys.Bindgen.Init).
+  ok <- SDL3.init SDL3.SDL_INIT_VIDEO
+  unless ok (die "SDL_Init")
+
+-- window <- withCString "sdl3-raw" $ \title -> SDL3.createWindow (ConstPtr title) 640 360 0
+
+-- when (window == nullPtr) (die "Couldn't create window")
+
+-- pure window
+
+data SDLMessage e = SDLMessage {eventType :: SDL3.SDL_EventType, event :: e}
+  deriving anyclass (Message)
+  deriving stock (Show)
+
+data SDLEventsPlugin
+
+instance Plugin SDLEventsPlugin where
+  init = do
+    liftIO initSdl
+
+    systems handleEvents
+      & schedule @First
+
+    systems (handleQuit, handleWindowClose)
+      & after handleEvents
+      & schedule @First
+
+-- addMessage @(SDLMessage SDL3.SDL_DisplayEvent)
+-- addMessage @(SDLMessage SDl3.SDL_WindowEvent)
+-- addMessage @(SDLMessage SDL3.SDL_KeyboardEvent)
+-- addMessage @(SDLMessage SDLMouseButtonEvent)
+-- addMessage @(SDLMessage SDLMouseMotionEvent)
+-- addMessage @(SDLMessage SDLQuitEvent)
+
+-- addSystems First handleEvents
+-- addSystems First $ handleQuit `after` handleEvents
+
+handleEvents :: System ()
+handleEvents = F.alloca @SDL3.SDL_Event $ \event -> do
+  pending <- liftIO $ SDL3.pollEvent event
+  when pending $ do
+    e <- liftIO $ peek event
+    eventType <- liftIO $ peek (castPtr event :: Ptr SDL3.SDL_EventType)
+    handleEvent eventType e
+    handleEvents
+
+-- alloca @SDL3.SDL_Event $ \event -> do
+--   liftIO SDL3.pollEventSafe
+--   case event of
+--     Nothing -> do
+--       return ()
+--     Just event -> do
+--       handleEvent event
+--       handleEvents
+
+-- these correspond to https://wiki.libsdl.org/SDL3/SDL_Event
+handleEvent :: SDL3.SDL_EventType -> SDL3.SDL_Event -> System ()
+handleEvent t e = case t of
+  -- audio
+  SDL3.SDL_EVENT_AUDIO_DEVICE_ADDED -> s t e.adevice
+  SDL3.SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED -> s t e.adevice
+  SDL3.SDL_EVENT_AUDIO_DEVICE_REMOVED -> s t e.adevice
+  -- TODO: camera
+  -- TODO: clipboard
+  -- display
+  SDL3.SDL_EVENT_DISPLAY_ADDED -> s t e.display
+  SDL3.SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED -> s t e.display
+  SDL3.SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED -> s t e.display
+  SDL3.SDL_EVENT_DISPLAY_DESKTOP_MODE_CHANGED -> s t e.display
+  SDL3.SDL_EVENT_DISPLAY_FIRST -> s t e.display
+  SDL3.SDL_EVENT_DISPLAY_LAST -> s t e.display
+  SDL3.SDL_EVENT_DISPLAY_MOVED -> s t e.display
+  SDL3.SDL_EVENT_DISPLAY_ORIENTATION -> s t e.display
+  SDL3.SDL_EVENT_DISPLAY_REMOVED -> s t e.display
+  SDL3.SDL_EVENT_DISPLAY_USABLE_BOUNDS_CHANGED -> s t e.display
+  -- TODO: gamepad
+  -- TODO: gamepad axis
+  -- TODO: gamepad button
+  -- TODO: gamepad touchpad
+  -- TODO: gamepad sensor
+  -- TODO: drop
+  -- TODO: finger
+  -- TODO: pinch
+  -- keyboard
+  SDL3.SDL_EVENT_KEYBOARD_ADDED -> s t e.kdevice
+  SDL3.SDL_EVENT_KEYBOARD_REMOVED -> s t e.kdevice
+  -- key
+  SDL3.SDL_EVENT_KEY_DOWN -> s t e.key
+  SDL3.SDL_EVENT_KEY_UP -> s t e.key
+  -- TODO: joystick
+  -- TODO: joystick axis
+  -- TODO: joystick ball
+  -- TODO: joystick hat
+  -- TODO: joystick battery
+  -- TODO: joystick button
+  -- mouse
+  SDL3.SDL_EVENT_MOUSE_ADDED -> s t e.mdevice
+  SDL3.SDL_EVENT_MOUSE_REMOVED -> s t e.mdevice
+  -- mouse motion
+  SDL3.SDL_EVENT_MOUSE_MOTION -> s t e.motion
+  -- mouse button
+  SDL3.SDL_EVENT_MOUSE_BUTTON_DOWN -> s t e.button
+  SDL3.SDL_EVENT_MOUSE_BUTTON_UP -> s t e.button
+  -- mouse wheel
+  SDL3.SDL_EVENT_MOUSE_WHEEL -> s t e.wheel
+  -- TODO: pen proximity
+  -- TODO: pen touch
+  -- TODO: pen motion
+  -- TODO: pen button
+  -- TODO: pen axis
+  -- quit
+  SDL3.SDL_EVENT_QUIT -> s t e.quit
+  -- TODO: sensor
+  -- TODO: edit
+  -- TODO: edit candidates
+  -- text
+  SDL3.SDL_EVENT_TEXT_INPUT -> s t e.text
+  -- user
+  SDL3.SDL_EVENT_USER -> s t e.user
+  -- window
+  SDL3.SDL_EVENT_WINDOW_CLOSE_REQUESTED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_DESTROYED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_DISPLAY_CHANGED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_ENTER_FULLSCREEN -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_EXPOSED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_FIRST -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_FOCUS_GAINED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_FOCUS_LOST -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_HDR_STATE_CHANGED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_HIDDEN -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_HIT_TEST -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_ICCPROF_CHANGED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_LAST -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_LEAVE_FULLSCREEN -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_MAXIMIZED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_METAL_VIEW_RESIZED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_MINIMIZED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_MOUSE_ENTER -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_MOUSE_LEAVE -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_MOVED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_OCCLUDED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_RESIZED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_RESTORED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_SAFE_AREA_CHANGED -> s t e.window
+  SDL3.SDL_EVENT_WINDOW_SHOWN -> s t e.window
+  -- common
+  t -> s t e.common
+  where
+    s t a = Messages.write $ SDLMessage t a
+
+handleQuit :: System ()
+handleQuit = do
+  messages <- Messages.read @(SDLMessage SDL3.SDL_QuitEvent)
+  unless (null messages) $ liftIO exitSuccess
+
+handleWindowClose :: System ()
+handleWindowClose = do
+  messages <- filter (\(SDLMessage t _) -> t == SDL3.SDL_EVENT_WINDOW_CLOSE_REQUESTED) <$> Messages.read @(SDLMessage SDL3.SDL_WindowEvent)
+
+  for_ messages $ \(SDLMessage _ SDL3.SDL_WindowEvent {windowID}) -> do
+    window <- liftIO $ SDL3.getWindowFromID windowID
+    unless (window == nullPtr) $ do
+      liftIO $ destroyWindow window
+
+      a <-
+        [q|(Entity, SDLWindow) / With SDLWindow|]
+          & qfilter (\(_, SDLWindow p) -> p == window)
+          & qmap fst
+          & single
+
+      traverse_ despawn a
+
+-- $intro
+-- This package provides the 'sdlPlugin' for @Mischief@, along with a few components.
+--
+-- These are necessary to run any SDL-based packages, such as @MischiefInput@, and @MischiefRender@.
+--
+-- In order to properly run this, you'll need to build / install SDL locally. This can be done via:
+--
+-- @
+-- git clone --recurse-submodules https://github.com/PVDoriginal/mischief.git
+-- cd mischief\/external\/SDL3
+-- mkdir -p build && cd build
+-- cmake -DCMAKE_BUILD_TYPE=Release -GNinja ..
+-- cmake --build . --config Release --parallel
+-- sudo cmake --install . --config Release   # omit sudo on Windows
+-- @
+--
+-- On @Windows@, ensure @SDL3.dll@ is available in @PATH@ (or next to your .exe). If @pkg-config@ cannot find SDL3, set:
+--
+-- @
+-- set PKG_CONFIG_PATH=path\to\SDL3\lib\pkgconfig
+-- # or
+-- \$env:PKG_CONFIG_PATH="path\to\SDL3\lib\pkgconfig"
+-- @
diff --git a/src/Mischief/SDL/Window.hs b/src/Mischief/SDL/Window.hs
new file mode 100644
--- /dev/null
+++ b/src/Mischief/SDL/Window.hs
@@ -0,0 +1,122 @@
+module Mischief.SDL.Window where
+
+import Control.Monad.IO.Class
+import Data.Bits ((.|.))
+import Data.Default
+import Data.Foldable (for_)
+import Foreign.C
+import Foreign.C.ConstPtr (ConstPtr (..))
+import Mischief.ECS
+import Mischief.ECS.App.Schedules (First)
+import Mischief.ECS.Messages qualified as Messages
+import Mischief.SDL.Events
+import SDL3.Sys qualified as SDL3
+import SDL3.Sys.Video
+
+-- | A component that will create a new window when added to an entity, and destroy it when removed.
+--
+-- Windows can be configured by inserting a 'WindowSize', 'WindowTitle', 'WindowFlags' alongside this component.
+data Window = Window
+
+instance Component Window where
+  onAdd = [hook handleNewWindow]
+  onRemove = [hook handleWindowRemove]
+  required = require @(WindowSize, WindowTitle, WindowFlags)
+
+-- | The size of a window in pixels.
+data WindowSize = WindowSize {width :: Int, height :: Int} deriving (Component)
+
+instance Default WindowSize where
+  def = WindowSize 600 400
+
+-- | The displayed title (or name) of a window.
+newtype WindowTitle = WindowTitle String deriving anyclass (Component)
+
+instance Default WindowTitle where
+  def = WindowTitle "Mischief Window"
+
+handleNewWindow :: HookContext -> System ()
+handleNewWindow (HookContext entity) = do
+  Just (WindowSize w h, WindowTitle title, flags) <- single [q|entity. WindowSize, WindowTitle, WindowFlags|]
+  window <- liftIO $ withCString title $ \title -> SDL3.createWindow (ConstPtr title) (fromIntegral w) (fromIntegral h) (flagsToBit flags)
+  insert (SDLWindow window) entity
+
+handleWindowRemove :: HookContext -> System ()
+handleWindowRemove (HookContext entity) = do
+  Just (SDLWindow p) <- single [q|entity. SDLWindow|]
+  liftIO $ SDL3.destroyWindow p
+
+-- | These correspond to the [SDL3 Window Flags](https://wiki.libsdl.org/SDL3/SDL_WindowFlags).
+--
+-- The most important ones are 'WindowFullscreen' and 'WindowResizable'.
+data WindowFlag
+  = WindowFullscreen
+  | WindowHidden
+  | WindowBorderless
+  | WindowResizable
+  | WindowMinimized
+  | WindowMaximized
+  | WindowModal
+  | WindowHighPixelDensity
+  | WindowMouseRelativeMode
+  | WindowAlwaysOnTop
+  | WindowUtility
+  | WindowTooltip
+  | WindowPopupMenu
+  | WindowTransparent
+  | WindowNotFocuable
+  | WindowMetal
+
+flagToBit :: WindowFlag -> SDL_WindowFlags
+flagToBit WindowFullscreen = SDL_WINDOW_FULLSCREEN
+flagToBit WindowHidden = SDL_WINDOW_HIDDEN
+flagToBit WindowBorderless = SDL_WINDOW_BORDERLESS
+flagToBit WindowResizable = SDL_WINDOW_RESIZABLE
+flagToBit WindowMinimized = SDL_WINDOW_MINIMIZED
+flagToBit WindowMaximized = SDL_WINDOW_MAXIMIZED
+flagToBit WindowModal = SDL_WINDOW_MODAL
+flagToBit WindowHighPixelDensity = SDL_WINDOW_HIGH_PIXEL_DENSITY
+flagToBit WindowMouseRelativeMode = SDL_WINDOW_MOUSE_RELATIVE_MODE
+flagToBit WindowAlwaysOnTop = SDL_WINDOW_ALWAYS_ON_TOP
+flagToBit WindowUtility = SDL_WINDOW_UTILITY
+flagToBit WindowTooltip = SDL_WINDOW_TOOLTIP
+flagToBit WindowPopupMenu = SDL_WINDOW_POPUP_MENU
+flagToBit WindowTransparent = SDL_WINDOW_TRANSPARENT
+flagToBit WindowNotFocuable = SDL_WINDOW_NOT_FOCUSABLE
+flagToBit WindowMetal = SDL_WINDOW_METAL
+
+-- | A list of 'WindowFlag's specifying various configurations for the window. The only one set by default is 'WindowResizable'.
+newtype WindowFlags = WindowFlags [WindowFlag] deriving anyclass (Component)
+
+instance Default WindowFlags where
+  def = WindowFlags [WindowResizable]
+
+flagsToBit :: WindowFlags -> SDL_WindowFlags
+flagsToBit (WindowFlags a) = foldr (\a b -> b .|. flagToBit a) 0 a
+
+-- | Plugin adding windowing functionality.
+data WindowPlugin
+
+instance Plugin WindowPlugin where
+  init = do
+    systems handleWindowEvents
+      & after handleEvents
+      & schedule @First
+
+handleWindowEvents :: System ()
+handleWindowEvents = do
+  messages <- Messages.read @(SDLMessage SDL3.SDL_WindowEvent)
+  for_ messages handleWindowEvent
+
+handleWindowEvent :: SDLMessage SDL3.SDL_WindowEvent -> System ()
+handleWindowEvent (SDLMessage SDL3.SDL_EVENT_WINDOW_RESIZED w) = do
+  let width :: Int = fromIntegral w.data1
+  let height :: Int = fromIntegral w.data2
+
+  window <- liftIO $ SDL3.getWindowFromID w.windowID
+
+  [q|(Entity, SDLWindow) / With SDLWindow|]
+    & qfilter (\(_, SDLWindow p) -> p == window)
+    & qinsert (const $ WindowSize width height)
+    & query_
+handleWindowEvent _ = pure ()
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,31 @@
+module Main where
+
+import Data.Maybe
+import Mischief.ECS
+import Mischief.ECS.Components.Spawn
+import Mischief.ECS.Systems qualified as Systems
+import Mischief.SDL
+import Mischief.SDL.Window
+
+main :: IO ()
+main = do
+  app <- newApp
+  addPlugin @MainPlugin app
+  runApp app
+
+data MainPlugin = MainPlugin deriving (Eq)
+
+instance Plugin MainPlugin where
+  init = do
+    schedule @Update $ systems (pure () :: System ())
+    schedule @Startup $ systems setup
+
+  deps = [dep @SDLPlugin]
+
+setup :: System ()
+setup = do
+  w1 <- spawn (Window, WindowSize 100 300)
+  w2 <- spawn (Window, WindowTitle "Second Window", WindowSize 1000 40)
+  w3 <- spawn Window
+
+  pure ()
