diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for gtk-sni-tray
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Author name here (c) 2018
+
+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 Author name here 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+gtk-sni-tray
+===============
+
+gtk-sni-tray provides a [StatusNotifiedHost](https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierHost/) widget written using the gtk+3 bindings for haskell provided by [gi-gtk](https://hackage.haskell.org/package/gi-gtk). It also provides a simple standalone executable, `gtk-sni-tray-standalone`, that is configured with command line arguments. This executable will run the aforementioned widget by itself in a strut window, on each monitor for each it is requested.
+
+taffybar
+----------
+It is generally recommeneded that you use this widget with [taffybar](https://github.com/travitch/taffybar), which will allow you to combine it with other useful widgets, and will give more flexibility in configuration.
+
+StatusNotifierWatcher
+--------------------------
+By default, it is assumed that you are running an isolated StatusNotifierWatcher daemon. [status-notifier-item](https://github.com/IvanMalison/status-notifier-item) provides a StatusNotifierWatcher executable that you can use for this purpose. If you get an error like
+
+```
+MethodError {methodErrorName = ErrorName "org.freedesktop.DBus.Error.ServiceUnknown", methodErrorSerial = Serial 7, methodErrorSender = Just (BusName "org.freedesktop.DBus"), methodErrorDestination = Just (BusName ":1.549"), methodErrorBody = [Variant "The name org.kde.StatusNotifierWatcher was not provided by any .service files"]}
+```
+
+when you start `gtk-sni-tray-standalone` it is probably because you have not started a StatusNotifierWatcher on your system. You can solve this problem by passing the `--watcher` flag to `gtk-sni-tray-standalone`, but this is not recommeneded, because many SNI processes do not monitor for new watcher processes, and so may not immediately register when this new watcher is started.
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,210 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Control.Monad
+import           DBus.Client
+import           Data.Int
+import           Data.Maybe
+import           Data.Ratio
+import           Data.Semigroup ((<>))
+import qualified Data.Text as T
+import qualified GI.Gtk as Gtk
+import qualified GI.Gdk as Gdk
+import           Graphics.UI.GIGtkStrut
+import           Options.Applicative
+import qualified StatusNotifier.Host.Service as Host
+import           StatusNotifier.Tray
+import           System.Log.Logger
+import           System.Posix.Process
+import           Text.Printf
+
+positionP :: Parser StrutPosition
+positionP = fromMaybe TopPos <$> optional
+  (   flag' TopPos
+  (  long "top"
+  <> help "Position the bar at the top of the screen"
+  )
+  <|> flag' BottomPos
+  (  long "bottom"
+  <> help "Position the bar at the bottom of the screen"
+  )
+  <|> flag' LeftPos
+  (  long "left"
+  <> help "Position the bar on the left side of the screen"
+  )
+  <|> flag' RightPos
+  (  long "right"
+  <> help "Position the bar on the right side of the screen"
+  ))
+
+alignmentP :: Parser StrutAlignment
+alignmentP = fromMaybe Center <$> optional
+  (   flag' Beginning
+  (  long "beginning"
+  <> help "Use beginning alignment"
+  )
+  <|> flag' Center
+  (  long "center"
+  <> help "Use center alignment"
+  )
+  <|> flag' End
+  (  long "end"
+  <> help "Use end alignment"
+  ))
+
+sizeP :: Parser Int32
+sizeP =
+  option auto
+  (  long "size"
+  <> short 's'
+  <> help "Set the size of the bar"
+  <> value 30
+  <> metavar "SIZE"
+  )
+
+paddingP :: Parser Int32
+paddingP =
+  option auto
+  (  long "padding"
+  <> short 'p'
+  <> help "Set the padding of the bar"
+  <> value 0
+  <> metavar "PADDING"
+  )
+
+monitorNumberP :: Parser [Int32]
+monitorNumberP = many $
+  option auto
+  (  long "monitor"
+  <> short 'm'
+  <> help "Run on the selected monitor"
+  <> metavar "MONITOR"
+  )
+
+logP :: Parser Priority
+logP =
+  option auto
+  (  long "log-level"
+  <> short 'l'
+  <> help "Set the log level"
+  <> metavar "LEVEL"
+  <> value WARNING
+  )
+
+colorP :: Parser String
+colorP =
+  strOption
+  (  long "color"
+  <> short 'c'
+  <> help "Set the background color of the tray"
+  <> metavar "COLOR"
+  <> value "000000"
+  )
+
+expandP :: Parser Bool
+expandP =
+  switch
+  (  long "expand"
+  <> help "Whether to let icons expand into the space allocated to the tray"
+  <> short 'e'
+  )
+
+startWatcherP :: Parser Bool
+startWatcherP =
+  switch
+  (  long "watcher"
+  <> help "Whether to start a Watcher to handle SNI registration"
+  <> short 'w'
+  )
+
+getColor colorString = do
+  rgba <- Gdk.newZeroRGBA
+  colorParsed <- Gdk.rGBAParse rgba (T.pack colorString)
+  unless colorParsed $ do
+    logM "StatusNotifier.Tray" WARNING "Failed to parse provided color"
+    void $ Gdk.rGBAParse rgba "000000"
+  return rgba
+
+buildWindows :: StrutPosition
+             -> StrutAlignment
+             -> Int32
+             -> Int32
+             -> [Int32]
+             -> Priority
+             -> String
+             -> Bool
+             -> Bool
+             -> IO ()
+buildWindows pos align size padding monitors priority colorString expand startWatcher = do
+  Gtk.init Nothing
+  logger <- getLogger "StatusNotifier"
+  saveGlobalLogger $ setLevel priority logger
+  client <- connectSession
+  logger <- getRootLogger
+  pid <- getProcessID
+  -- Okay to use a forced pattern here because we want to die if this fails anyway
+  Just host <- Host.build Host.defaultParams
+    { Host.dbusClient = Just client
+    , Host.uniqueIdentifier = printf "standalone-%s" $ show pid
+    , Host.startWatcher = startWatcher
+    }
+  let c1 = defaultStrutConfig
+           { strutPosition = pos
+           , strutAlignment = align
+           , strutXPadding = padding
+           , strutYPadding = padding
+           }
+      defaultRatio = ScreenRatio (4 % 5)
+      configBase = case pos of
+             TopPos -> c1
+                       { strutHeight = ExactSize size
+                       , strutWidth = defaultRatio
+                       }
+             BottomPos -> c1
+                          { strutHeight = ExactSize size
+                          , strutWidth = defaultRatio
+                          }
+             RightPos -> c1
+                         { strutHeight = defaultRatio
+                         , strutWidth = ExactSize size
+                         }
+             LeftPos -> c1
+                        { strutHeight = defaultRatio
+                        , strutWidth = ExactSize size
+                        }
+      buildWithConfig config = do
+        let orientation =
+              case strutPosition config of
+                TopPos -> Gtk.OrientationHorizontal
+                BottomPos -> Gtk.OrientationHorizontal
+                _ -> Gtk.OrientationVertical
+        tray <- buildTray TrayParams
+                    { trayClient = client
+                    , trayOrientation = orientation
+                    , trayHost = host
+                    , trayImageSize = Expand
+                    , trayIconExpand = expand
+                    }
+        window <- Gtk.windowNew Gtk.WindowTypeToplevel
+        setupStrutWindow config window
+        (Just <$> getColor colorString) >>=
+             Gtk.widgetOverrideBackgroundColor window [Gtk.StateFlagsNormal]
+        Gtk.containerAdd window tray
+        Gtk.widgetShowAll window
+      runForMonitor monitor =
+        buildWithConfig configBase { strutMonitor = Just monitor }
+  if null monitors
+  then buildWithConfig configBase
+  else mapM_ runForMonitor monitors
+  Gtk.main
+
+parser :: Parser (IO ())
+parser = buildWindows <$> positionP <*> alignmentP <*> sizeP <*> paddingP <*>
+         monitorNumberP <*> logP <*> colorP <*> expandP <*> startWatcherP
+
+main :: IO ()
+main = do
+  -- TODO: start watcher if it doesn't exist
+  join $ execParser $ info (parser <**> helper)
+               (  fullDesc
+               <> progDesc "Run a standalone StatusNotifierItem/AppIndicator tray")
diff --git a/gtk-sni-tray.cabal b/gtk-sni-tray.cabal
new file mode 100644
--- /dev/null
+++ b/gtk-sni-tray.cabal
@@ -0,0 +1,76 @@
+-- This file has been generated from package.yaml by hpack version 0.27.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 5b235b7f79be706daee2b55416bfcd57ccbe4b938c9a8946952054d97c72d088
+
+name:           gtk-sni-tray
+version:        0.1.0.0
+synopsis:       A standalone StatusNotifierItem/AppIndicator tray
+description:    Please see the README on Github at <https://github.com/IvanMalison/gtk-sni-tray#readme>
+category:       System
+homepage:       https://github.com/IvanMalison/gtk-sni-tray#readme
+bug-reports:    https://github.com/IvanMalison/gtk-sni-tray/issues
+author:         Ivan Malison
+maintainer:     IvanMalison@gmail.com
+copyright:      2018 Ivan Malison
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+
+extra-source-files:
+    ChangeLog.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/IvanMalison/gtk-sni-tray
+
+library
+  exposed-modules:
+      StatusNotifier.Tray
+  other-modules:
+      Paths_gtk_sni_tray
+  hs-source-dirs:
+      src
+  build-depends:
+      base >=4.7 && <5
+    , bytestring
+    , containers
+    , dbus >=1.0.0 && <2.0.0
+    , directory
+    , gi-dbusmenugtk3
+    , gi-gdk
+    , gi-gdkpixbuf >=2.0.15
+    , gi-glib
+    , gi-gtk >=3.0.21
+    , hslogger
+    , status-notifier-item >=0.2.0.0 && <0.3.0.0
+    , text
+    , transformers
+    , unix
+  pkgconfig-depends:
+      gtk+-3.0
+  default-language: Haskell2010
+
+executable gtk-sni-tray-standalone
+  main-is: Main.hs
+  other-modules:
+      Paths_gtk_sni_tray
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , dbus >=1.0.0 && <2.0.0
+    , gi-gdk
+    , gi-gtk >=3.0.21
+    , gtk-sni-tray
+    , gtk-strut
+    , hslogger
+    , optparse-applicative
+    , status-notifier-item >=0.2.0.0 && <0.3.0.0
+    , text
+    , unix
+  default-language: Haskell2010
diff --git a/src/StatusNotifier/Tray.hs b/src/StatusNotifier/Tray.hs
new file mode 100644
--- /dev/null
+++ b/src/StatusNotifier/Tray.hs
@@ -0,0 +1,261 @@
+{-# LANGUAGE OverloadedLabels #-}
+module StatusNotifier.Tray where
+
+import           Control.Concurrent.MVar as MV
+import           Control.Monad
+import           Control.Monad.Trans.Class
+import           Control.Monad.Trans.Maybe
+import           Control.Monad.Trans.Reader
+import           DBus.Client
+import qualified DBus.Internal.Types as DBusTypes
+import qualified Data.ByteString as BS
+import           Data.ByteString.Unsafe
+import           Data.Coerce
+import           Data.Int
+import           Data.List
+import qualified Data.Map.Strict as Map
+import           Data.Maybe
+import           Data.Ord
+import qualified Data.Text as T
+import           Foreign.Ptr
+import qualified GI.DbusmenuGtk3.Objects.Menu as DM
+import qualified GI.GLib as GLib
+import           GI.GLib.Structs.Bytes
+import qualified GI.Gdk as Gdk
+import           GI.Gdk.Enums
+import           GI.Gdk.Objects.Screen
+import           GI.GdkPixbuf.Callbacks
+import           GI.GdkPixbuf.Enums
+import           GI.GdkPixbuf.Objects.Pixbuf
+import           GI.GdkPixbuf.Structs.Pixdata
+import qualified GI.Gtk as Gtk
+import           GI.Gtk.Flags
+import qualified GI.Gtk.Objects.Box as Gtk
+import qualified GI.Gtk.Objects.HBox as Gtk
+import           GI.Gtk.Objects.IconTheme
+import           StatusNotifier.Host.Service
+import qualified StatusNotifier.Item.Client as IC
+import           StatusNotifier.Util
+import           System.Directory
+import           System.Log.Logger
+import           System.Posix.Process
+import           Text.Printf
+
+trayLogger = logM "StatusNotifier.Tray"
+
+themeLoadFlags = [IconLookupFlagsGenericFallback, IconLookupFlagsUseBuiltin]
+
+getThemeWithDefaultFallbacks :: String -> IO IconTheme
+getThemeWithDefaultFallbacks themePath = do
+  themeForIcon <- iconThemeNew
+  defaultTheme <- iconThemeGetDefault
+
+  runMaybeT $ do
+    screen <- MaybeT screenGetDefault
+    lift $ iconThemeSetScreen themeForIcon screen
+
+  filePaths <- iconThemeGetSearchPath defaultTheme
+  iconThemeAppendSearchPath themeForIcon themePath
+  mapM_ (iconThemeAppendSearchPath themeForIcon) filePaths
+
+  return themeForIcon
+
+getIconPixbufByName :: IsIconTheme it =>  Int32 -> T.Text -> it -> IO (Maybe Pixbuf)
+getIconPixbufByName size name themeForIcon = do
+  trayLogger DEBUG "Getting Pixbuf from name"
+  let panelName = T.pack $ printf "%s-panel" name
+  hasPanelIcon <- iconThemeHasIcon themeForIcon panelName
+  hasIcon <- iconThemeHasIcon themeForIcon name
+  if hasIcon || hasPanelIcon
+  then do
+    let targetName = if hasPanelIcon then panelName else name
+    iconThemeLoadIcon themeForIcon targetName size themeLoadFlags
+  else do
+    -- Try to load the icon as a filepath
+    let nameString = T.unpack name
+    fileExists <- doesFileExist nameString
+    if fileExists
+    then Just <$> pixbufNewFromFile name
+    else return Nothing
+
+getIconPixbufFromByteString :: Int32 -> Int32 -> BS.ByteString -> IO Pixbuf
+getIconPixbufFromByteString width height byteString = do
+  trayLogger DEBUG "Getting Pixbuf from bytestring"
+  bytes <- bytesNew $ Just byteString
+  let bytesPerPixel = 4
+      rowStride = width * bytesPerPixel
+      sampleBits = 8
+  pixbufNewFromBytes bytes ColorspaceRgb True sampleBits width height rowStride
+
+data ItemContext = ItemContext
+  { contextName :: DBusTypes.BusName
+  , contextMenu :: Maybe DM.Menu
+  , contextImage :: Gtk.Image
+  , contextButton :: Gtk.EventBox
+  }
+
+data TrayImageSize = Expand | TrayImageSize Int32
+
+data TrayParams = TrayParams
+  { trayHost :: Host
+  , trayClient :: Client
+  , trayOrientation :: Gtk.Orientation
+  , trayImageSize :: TrayImageSize
+  , trayIconExpand :: Bool
+  }
+
+buildTray :: TrayParams -> IO Gtk.Box
+buildTray TrayParams { trayHost = Host
+                       { itemInfoMap = getInfoMap
+                       , addUpdateHandler = addHandler
+                       , removeUpdateHandler = removeHandler
+                       }
+                     , trayClient = client
+                     , trayOrientation = orientation
+                     , trayImageSize = imageSize
+                     , trayIconExpand = shouldExpand
+                     } = do
+  trayLogger INFO "Building tray"
+
+  trayBox <- Gtk.boxNew orientation 0
+  contextMap <- MV.newMVar Map.empty
+
+  let getContext name = Map.lookup name <$> MV.readMVar contextMap
+
+      getSize rectangle =
+        case orientation of
+          Gtk.OrientationHorizontal ->
+            Gdk.getRectangleHeight rectangle
+          Gtk.OrientationVertical ->
+            Gdk.getRectangleWidth rectangle
+
+      getInfo def name = fromMaybe def . Map.lookup name <$> getInfoMap
+
+      updateIconFromInfo info@ItemInfo { itemServiceName = name } =
+        getContext name >>= updateIcon
+        where updateIcon Nothing = updateHandler ItemAdded info
+              updateIcon (Just ItemContext { contextImage = image } ) = do
+                size <- case imageSize of
+                          TrayImageSize size -> return size
+                          Expand -> Gtk.widgetGetAllocation image >>= getSize
+                getScaledPixBufFromInfo size info >>=
+                                  let handlePixbuf mpbuf =
+                                        if isJust mpbuf
+                                        then Gtk.imageSetFromPixbuf image mpbuf
+                                        else updateHandler ItemRemoved info
+                                  in handlePixbuf
+
+      updateHandler ItemAdded
+                    info@ItemInfo { menuPath = pathForMenu
+                                  , itemServiceName = serviceName
+                                  , itemServicePath = servicePath
+                                  } =
+        do
+          let serviceNameStr = coerce serviceName
+              servicePathStr = coerce servicePath :: String
+              serviceMenuPathStr = coerce <$> pathForMenu
+              logText = printf "Adding widget for %s - %s."
+                        serviceNameStr servicePathStr
+
+          trayLogger INFO logText
+
+          button <- Gtk.eventBoxNew
+
+          image <-
+            case imageSize of
+              Expand -> do
+                traySize <- Gtk.widgetGetAllocation trayBox >>= getSize
+                image <- getScaledPixBufFromInfo traySize info >>= Gtk.imageNewFromPixbuf
+                let setPixbuf rectangle =
+                      do
+                        size <- getSize rectangle
+                        pixBuf <- getInfo info serviceName >>= getScaledPixBufFromInfo size
+                        Gtk.imageSetFromPixbuf image pixBuf
+                Gtk.onWidgetSizeAllocate image setPixbuf
+                return image
+              TrayImageSize size -> do
+                pixBuf <- getScaledPixBufFromInfo size info
+                image <- Gtk.imageNewFromPixbuf pixBuf
+                return image
+
+          Gtk.containerAdd button image
+
+          maybeMenu <- sequenceA $ DM.menuNew (T.pack serviceNameStr) .
+                       T.pack <$> serviceMenuPathStr
+
+          let context =
+                ItemContext { contextName = serviceName
+                            , contextMenu = maybeMenu
+                            , contextImage = image
+                            , contextButton = button
+                            }
+              popupItemForMenu menu =
+                Gtk.menuPopupAtWidget menu image
+                   GravitySouthWest GravityNorthWest Nothing
+              popupItemMenu =
+                maybe activateItem popupItemForMenu maybeMenu >> return False
+              activateItem = void $ IC.activate client serviceName servicePath 0 0
+
+          Gtk.onWidgetButtonPressEvent button $ const popupItemMenu
+
+          MV.modifyMVar_ contextMap $ return . Map.insert serviceName context
+
+          Gtk.widgetShowAll button
+          Gtk.boxPackStart trayBox button shouldExpand True 0
+
+      updateHandler ItemRemoved ItemInfo { itemServiceName = name }
+        = getContext name >>= removeWidget
+        where removeWidget Nothing =
+                trayLogger INFO "Attempt to remove widget with unrecognized service name."
+              removeWidget (Just ItemContext { contextButton = widgetToRemove }) =
+                do
+                  Gtk.containerRemove trayBox widgetToRemove
+                  MV.modifyMVar_ contextMap $ return . Map.delete name
+
+      updateHandler IconUpdated i = updateIconFromInfo i
+
+      updateHandler IconNameUpdated i = updateIconFromInfo i
+
+      updateHandler _ _ = return ()
+
+      logItemInfo info message =
+        trayLogger INFO $ printf "%s - %s pixmap count: %s" message
+               (show $ info { iconPixmaps = []})
+               (show $ length $ iconPixmaps info)
+
+      getScaledPixBufFromInfo size info = runMaybeT $ do
+        pixBuf <- MaybeT $ getPixBufFromInfo size info
+        MaybeT $ pixbufScaleSimple pixBuf size size InterpTypeBilinear
+
+      getPixBufFromInfo size
+                        info@ItemInfo { iconName = name
+                                      , iconThemePath = mpath
+                                      , iconPixmaps = pixmaps
+                                      } = do
+        logItemInfo info "Getting pixbuf"
+        themeForIcon <- maybe iconThemeGetDefault getThemeWithDefaultFallbacks mpath
+        let tooSmall (w, h, _) = w < size || h < size
+            largeEnough = filter (not . tooSmall) pixmaps
+            orderer (w1, h1, _) (w2, h2, _) =
+              case comparing id w1 w2 of
+                EQ -> comparing id h1 h2
+                a -> a
+            selectedPixmap =
+              if null largeEnough
+              then maximumBy orderer pixmaps
+              else minimumBy orderer largeEnough
+            getFromPixmaps (w, h, p) =
+              if BS.length p == 0
+              then Nothing
+              else Just $ getIconPixbufFromByteString w h p
+        if null pixmaps
+        then getIconPixbufByName size (T.pack name) themeForIcon
+        else sequenceA $ getFromPixmaps selectedPixmap
+
+      uiUpdateHandler updateType info =
+        void $ Gdk.threadsAddIdle GLib.PRIORITY_DEFAULT $
+             updateHandler updateType info >> return False
+
+  handlerId <- addHandler uiUpdateHandler
+  _ <- Gtk.onWidgetDestroy trayBox $ removeHandler handlerId
+  return trayBox
