diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4014cce2..f951d968 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -65,6 +65,7 @@ boption(SERVICE_UPOWER "UPower" ON)
 boption(SERVICE_NOTIFICATIONS "Notifications" ON)
 
 include(cmake/install-qml-module.cmake)
+include(cmake/util.cmake)
 
 add_compile_options(-Wall -Wextra)
 
@@ -87,9 +88,10 @@ if (NOT CMAKE_BUILD_TYPE)
 	set(CMAKE_BUILD_TYPE Debug)
 endif()
 
-set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Widgets)
 set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets)
 
+include(cmake/pch.cmake)
+
 if (BUILD_TESTING)
 	enable_testing()
 	add_definitions(-DQS_TEST)
@@ -97,56 +99,27 @@ if (BUILD_TESTING)
 endif()
 
 if (SOCKETS)
-	list(APPEND QT_DEPS Qt6::Network)
 	list(APPEND QT_FPDEPS Network)
 endif()
 
 if (WAYLAND)
-	list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate)
 	list(APPEND QT_FPDEPS WaylandClient)
 endif()
 
-if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS)
+if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS)
 	set(DBUS ON)
 endif()
 
 if (DBUS)
-	list(APPEND QT_DEPS Qt6::DBus)
 	list(APPEND QT_FPDEPS DBus)
 endif()
 
 find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
 
+set(CMAKE_AUTOUIC OFF)
 qt_standard_project_setup(REQUIRES 6.6)
 set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
 
-# pch breaks clang-tidy..... somehow
-if (NOT NO_PCH)
-	file(GENERATE
-		OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp
-		CONTENT "// intentionally empty"
-	)
-
-	add_library(qt-pch ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp)
-	target_link_libraries(qt-pch PRIVATE ${QT_DEPS})
-	target_precompile_headers(qt-pch PUBLIC
-		<memory>
-		<qobject.h>
-		<qqmlengine.h>
-		<qlist.h>
-		<qcolor.h>
-		<qquickitem.h>
-		<qevent.h>
-	)
-endif()
-
-function (qs_pch target)
-	if (NOT NO_PCH)
-		target_precompile_headers(${target} REUSE_FROM qt-pch)
-		target_link_libraries(${target} PRIVATE ${QT_DEPS}) # required for gcc to accept the pch on plugin targets
-  endif()
-endfunction()
-
 add_subdirectory(src)
 
 if (USE_JEMALLOC)
diff --git a/cmake/pch.cmake b/cmake/pch.cmake
new file mode 100644
index 00000000..e136015e
--- /dev/null
+++ b/cmake/pch.cmake
@@ -0,0 +1,85 @@
+# pch breaks clang-tidy..... somehow
+if (NOT NO_PCH)
+	file(GENERATE
+		OUTPUT ${CMAKE_BINARY_DIR}/pchstub.cpp
+		CONTENT "// intentionally empty"
+	)
+endif()
+
+function (qs_pch target)
+	if (NO_PCH)
+		return()
+	endif()
+
+	cmake_parse_arguments(PARSE_ARGV 1 arg "" "SET" "")
+
+	if ("${arg_SET}" STREQUAL "")
+		set(arg_SET "common")
+	endif()
+
+	target_precompile_headers(${target} REUSE_FROM "qs-pchset-${arg_SET}")
+endfunction()
+
+function (qs_module_pch target)
+	qs_pch(${target} ${ARGN})
+	qs_pch("${target}plugin" SET plugin)
+	qs_pch("${target}plugin_init" SET plugin)
+endfunction()
+
+function (qs_add_pchset SETNAME)
+	if (NO_PCH)
+		return()
+	endif()
+
+	cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "HEADERS;DEPENDENCIES")
+
+	set(LIBNAME "qs-pchset-${SETNAME}")
+
+	add_library(${LIBNAME} ${CMAKE_BINARY_DIR}/pchstub.cpp)
+	target_link_libraries(${LIBNAME} ${arg_DEPENDENCIES})
+	target_precompile_headers(${LIBNAME} PUBLIC ${arg_HEADERS})
+endfunction()
+
+set(COMMON_PCH_SET
+	<chrono>
+	<memory>
+	<vector>
+	<qdebug.h>
+	<qobject.h>
+	<qmetatype.h>
+	<qstring.h>
+	<qchar.h>
+	<qlist.h>
+	<qabstractitemmodel.h>
+)
+
+qs_add_pchset(common
+	DEPENDENCIES Qt::Quick
+	HEADERS ${COMMON_PCH_SET}
+)
+
+qs_add_pchset(large
+	DEPENDENCIES Qt::Quick
+	HEADERS
+		${COMMON_PCH_SET}
+		<qiodevice.h>
+		<qevent.h>
+		<qcoreapplication.h>
+		<qqmlengine.h>
+		<qquickitem.h>
+		<qquickwindow.h>
+		<qcolor.h>
+		<qdir.h>
+		<qtimer.h>
+		<qabstractitemmodel.h>
+)
+
+
+# including qplugin.h directly will cause required symbols to disappear
+qs_add_pchset(plugin
+	DEPENDENCIES Qt::Qml
+	HEADERS
+		<qobject.h>
+		<qjsonobject.h>
+		<qpointer.h>
+)
diff --git a/cmake/util.cmake b/cmake/util.cmake
new file mode 100644
index 00000000..5d261a40
--- /dev/null
+++ b/cmake/util.cmake
@@ -0,0 +1,20 @@
+function (qs_append_qmldir target text)
+	get_property(qmldir_content TARGET ${target} PROPERTY _qt_internal_qmldir_content)
+
+	if ("${qmldir_content}" STREQUAL "")
+		message(WARNING "qs_append_qmldir depends on private Qt cmake code, which has broken.")
+		return()
+	endif()
+
+	set_property(TARGET ${target} APPEND_STRING PROPERTY _qt_internal_qmldir_content ${text})
+endfunction()
+
+# DEPENDENCIES introduces a cmake dependency which we don't need with static modules.
+# This greatly improves comp speed by not introducing those dependencies.
+function (qs_add_module_deps_light target)
+	foreach (dep IN LISTS ARGN)
+		string(APPEND qmldir_extra "depends ${dep}\n")
+	endforeach()
+
+	qs_append_qmldir(${target} "${qmldir_extra}")
+endfunction()
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5b843543..c518a1c9 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,9 +1,8 @@
 qt_add_executable(quickshell main.cpp)
 
-target_link_libraries(quickshell PRIVATE ${QT_DEPS} quickshell-build)
-
 install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
 
+add_subdirectory(launch)
 add_subdirectory(build)
 add_subdirectory(core)
 add_subdirectory(ipc)
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 62f29425..c75dd588 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -1,5 +1,3 @@
-find_package(CLI11 CONFIG REQUIRED)
-
 qt_add_library(quickshell-core STATIC
 	plugin.cpp
 	shell.cpp
@@ -37,10 +35,9 @@ qt_add_library(quickshell-core STATIC
 	paths.cpp
 	instanceinfo.cpp
 	common.cpp
+	iconprovider.cpp
 )
 
-target_link_libraries(quickshell-core PRIVATE quickshell-build)
-
 qt_add_qml_module(quickshell-core
 	URI Quickshell
 	VERSION 0.1
@@ -51,10 +48,9 @@ qt_add_qml_module(quickshell-core
 
 install_qml_module(quickshell-core)
 
-target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} CLI11::CLI11)
+target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets)
 
-qs_pch(quickshell-core)
-qs_pch(quickshell-coreplugin)
+qs_module_pch(quickshell-core SET large)
 
 target_link_libraries(quickshell PRIVATE quickshell-coreplugin)
 
diff --git a/src/core/clock.cpp b/src/core/clock.cpp
index 75785223..ebb7e92a 100644
--- a/src/core/clock.cpp
+++ b/src/core/clock.cpp
@@ -4,6 +4,7 @@
 #include <qobject.h>
 #include <qtimer.h>
 #include <qtmetamacros.h>
+#include <qtypes.h>
 
 #include "util.hpp"
 
diff --git a/src/core/generation.cpp b/src/core/generation.cpp
index 395f255b..147e2f93 100644
--- a/src/core/generation.cpp
+++ b/src/core/generation.cpp
@@ -8,17 +8,12 @@
 #include <qfileinfo.h>
 #include <qfilesystemwatcher.h>
 #include <qhash.h>
-#include <qicon.h>
-#include <qiconengine.h>
 #include <qlogging.h>
 #include <qloggingcategory.h>
 #include <qobject.h>
-#include <qpixmap.h>
 #include <qqmlcontext.h>
 #include <qqmlengine.h>
 #include <qqmlincubator.h>
-#include <qquickimageprovider.h>
-#include <qsize.h>
 #include <qtmetamacros.h>
 
 #include "iconimageprovider.hpp"
@@ -331,90 +326,6 @@ EngineGeneration* EngineGeneration::currentGeneration() {
 	} else return nullptr;
 }
 
-// QMenu re-calls pixmap() every time the mouse moves so its important to cache it.
-class PixmapCacheIconEngine: public QIconEngine {
-	void paint(
-	    QPainter* /*unused*/,
-	    const QRect& /*unused*/,
-	    QIcon::Mode /*unused*/,
-	    QIcon::State /*unused*/
-	) override {
-		qFatal(
-		) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
-	}
-
-	QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {
-		if (this->lastPixmap.isNull() || size != this->lastSize) {
-			this->lastPixmap = this->createPixmap(size);
-			this->lastSize = size;
-		}
-
-		return this->lastPixmap;
-	}
-
-	virtual QPixmap createPixmap(const QSize& size) = 0;
-
-private:
-	QSize lastSize;
-	QPixmap lastPixmap;
-};
-
-class ImageProviderIconEngine: public PixmapCacheIconEngine {
-public:
-	explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id)
-	    : provider(provider)
-	    , id(std::move(id)) {}
-
-	QPixmap createPixmap(const QSize& size) override {
-		if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) {
-			return this->provider->requestPixmap(this->id, nullptr, size);
-		} else if (this->provider->imageType() == QQmlImageProviderBase::Image) {
-			auto image = this->provider->requestImage(this->id, nullptr, size);
-			return QPixmap::fromImage(image);
-		} else {
-			qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType();
-			return QPixmap(); // never reached, satisfies lint
-		}
-	}
-
-	[[nodiscard]] QIconEngine* clone() const override {
-		return new ImageProviderIconEngine(this->provider, this->id);
-	}
-
-private:
-	QQuickImageProvider* provider;
-	QString id;
-};
-
-QIcon EngineGeneration::iconByUrl(const QUrl& url) const {
-	if (url.isEmpty()) return QIcon();
-
-	auto scheme = url.scheme();
-	if (scheme == "image") {
-		auto providerName = url.authority();
-		auto path = url.path();
-		if (!path.isEmpty()) path = path.sliced(1);
-
-		auto* provider = qobject_cast<QQuickImageProvider*>(this->engine->imageProvider(providerName));
-
-		if (provider == nullptr) {
-			qWarning() << "iconByUrl failed: no provider found for" << url;
-			return QIcon();
-		}
-
-		if (provider->imageType() == QQmlImageProviderBase::Pixmap
-		    || provider->imageType() == QQmlImageProviderBase::Image)
-		{
-			return QIcon(new ImageProviderIconEngine(provider, path));
-		}
-
-	} else {
-		qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url;
-	}
-
-	return QIcon();
-}
-
 EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) {
 	return g_generations.value(engine);
 }
diff --git a/src/core/generation.hpp b/src/core/generation.hpp
index 823ca82a..043d2f70 100644
--- a/src/core/generation.hpp
+++ b/src/core/generation.hpp
@@ -4,13 +4,11 @@
 #include <qdir.h>
 #include <qfilesystemwatcher.h>
 #include <qhash.h>
-#include <qicon.h>
 #include <qobject.h>
 #include <qpair.h>
 #include <qqmlengine.h>
 #include <qqmlincubator.h>
 #include <qtclasshelpermacros.h>
-#include <qurl.h>
 
 #include "incubator.hpp"
 #include "qsintercept.hpp"
@@ -54,8 +52,6 @@ public:
 	// otherwise null.
 	static EngineGeneration* currentGeneration();
 
-	[[nodiscard]] QIcon iconByUrl(const QUrl& url) const;
-
 	RootWrapper* wrapper = nullptr;
 	QDir rootPath;
 	QmlScanner scanner;
diff --git a/src/core/iconprovider.cpp b/src/core/iconprovider.cpp
new file mode 100644
index 00000000..99b423ed
--- /dev/null
+++ b/src/core/iconprovider.cpp
@@ -0,0 +1,105 @@
+#include "iconprovider.hpp"
+#include <utility>
+
+#include <qicon.h>
+#include <qiconengine.h>
+#include <qlogging.h>
+#include <qobject.h>
+#include <qpixmap.h>
+#include <qqmlengine.h>
+#include <qquickimageprovider.h>
+#include <qrect.h>
+#include <qsize.h>
+#include <qstring.h>
+
+#include "generation.hpp"
+
+// QMenu re-calls pixmap() every time the mouse moves so its important to cache it.
+class PixmapCacheIconEngine: public QIconEngine {
+	void paint(
+	    QPainter* /*unused*/,
+	    const QRect& /*unused*/,
+	    QIcon::Mode /*unused*/,
+	    QIcon::State /*unused*/
+	) override {
+		qFatal(
+		) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
+	}
+
+	QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {
+		if (this->lastPixmap.isNull() || size != this->lastSize) {
+			this->lastPixmap = this->createPixmap(size);
+			this->lastSize = size;
+		}
+
+		return this->lastPixmap;
+	}
+
+	virtual QPixmap createPixmap(const QSize& size) = 0;
+
+private:
+	QSize lastSize;
+	QPixmap lastPixmap;
+};
+
+class ImageProviderIconEngine: public PixmapCacheIconEngine {
+public:
+	explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id)
+	    : provider(provider)
+	    , id(std::move(id)) {}
+
+	QPixmap createPixmap(const QSize& size) override {
+		if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) {
+			return this->provider->requestPixmap(this->id, nullptr, size);
+		} else if (this->provider->imageType() == QQmlImageProviderBase::Image) {
+			auto image = this->provider->requestImage(this->id, nullptr, size);
+			return QPixmap::fromImage(image);
+		} else {
+			qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType();
+			return QPixmap(); // never reached, satisfies lint
+		}
+	}
+
+	[[nodiscard]] QIconEngine* clone() const override {
+		return new ImageProviderIconEngine(this->provider, this->id);
+	}
+
+private:
+	QQuickImageProvider* provider;
+	QString id;
+};
+
+QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url) {
+	if (!engine || url.isEmpty()) return QIcon();
+
+	auto scheme = url.scheme();
+	if (scheme == "image") {
+		auto providerName = url.authority();
+		auto path = url.path();
+		if (!path.isEmpty()) path = path.sliced(1);
+
+		auto* provider = qobject_cast<QQuickImageProvider*>(engine->imageProvider(providerName));
+
+		if (provider == nullptr) {
+			qWarning() << "iconByUrl failed: no provider found for" << url;
+			return QIcon();
+		}
+
+		if (provider->imageType() == QQmlImageProviderBase::Pixmap
+		    || provider->imageType() == QQmlImageProviderBase::Image)
+		{
+			return QIcon(new ImageProviderIconEngine(provider, path));
+		}
+
+	} else {
+		qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url;
+	}
+
+	return QIcon();
+}
+
+QIcon getCurrentEngineImageAsIcon(const QUrl& url) {
+	auto* generation = EngineGeneration::currentGeneration();
+	if (!generation) return QIcon();
+	return getEngineImageAsIcon(generation->engine, url);
+}
diff --git a/src/core/iconprovider.hpp b/src/core/iconprovider.hpp
new file mode 100644
index 00000000..173d20e6
--- /dev/null
+++ b/src/core/iconprovider.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <qicon.h>
+#include <qqmlengine.h>
+#include <qurl.h>
+
+QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url);
+QIcon getCurrentEngineImageAsIcon(const QUrl& url);
diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp
index 09837ec9..a06575ea 100644
--- a/src/core/platformmenu.cpp
+++ b/src/core/platformmenu.cpp
@@ -19,7 +19,8 @@
 
 #include "../window/proxywindow.hpp"
 #include "../window/windowinterface.hpp"
-#include "generation.hpp"
+#include "iconprovider.hpp"
+#include "platformmenu_p.hpp"
 #include "popupanchor.hpp"
 #include "qsmenu.hpp"
 
@@ -174,8 +175,7 @@ void PlatformMenuEntry::relayout() {
 
 		auto icon = this->menu->icon();
 		if (!icon.isEmpty()) {
-			auto* generation = EngineGeneration::currentGeneration();
-			this->qmenu->setIcon(generation->iconByUrl(this->menu->icon()));
+			this->qmenu->setIcon(getCurrentEngineImageAsIcon(icon));
 		}
 
 		auto children = this->menu->children();
@@ -216,8 +216,7 @@ void PlatformMenuEntry::relayout() {
 
 		auto icon = this->menu->icon();
 		if (!icon.isEmpty()) {
-			auto* generation = EngineGeneration::currentGeneration();
-			this->qaction->setIcon(generation->iconByUrl(this->menu->icon()));
+			this->qaction->setIcon(getCurrentEngineImageAsIcon(icon));
 		}
 
 		this->qaction->setEnabled(this->menu->enabled());
@@ -272,8 +271,7 @@ void PlatformMenuEntry::onIconChanged() {
 	QIcon icon;
 
 	if (!iconName.isEmpty()) {
-		auto* generation = EngineGeneration::currentGeneration();
-		icon = generation->iconByUrl(iconName);
+		icon = getCurrentEngineImageAsIcon(iconName);
 	}
 
 	if (this->qmenu != nullptr) {
diff --git a/src/core/platformmenu.hpp b/src/core/platformmenu.hpp
index 5e8a0afe..5979f90e 100644
--- a/src/core/platformmenu.hpp
+++ b/src/core/platformmenu.hpp
@@ -5,9 +5,7 @@
 #include <qaction.h>
 #include <qactiongroup.h>
 #include <qcontainerfwd.h>
-#include <qmenu.h>
 #include <qobject.h>
-#include <qpoint.h>
 #include <qqmlintegration.h>
 #include <qqmllist.h>
 #include <qtclasshelpermacros.h>
@@ -18,17 +16,7 @@
 
 namespace qs::menu::platform {
 
-class PlatformMenuQMenu: public QMenu {
-public:
-	explicit PlatformMenuQMenu() = default;
-	~PlatformMenuQMenu() override;
-	Q_DISABLE_COPY_MOVE(PlatformMenuQMenu);
-
-	void setVisible(bool visible) override;
-
-	PlatformMenuQMenu* containingMenu = nullptr;
-	QPoint targetPosition;
-};
+class PlatformMenuQMenu;
 
 class PlatformMenuEntry: public QObject {
 	Q_OBJECT;
diff --git a/src/core/platformmenu_p.hpp b/src/core/platformmenu_p.hpp
new file mode 100644
index 00000000..9109959d
--- /dev/null
+++ b/src/core/platformmenu_p.hpp
@@ -0,0 +1,19 @@
+#pragma once
+#include <qmenu.h>
+#include <qpoint.h>
+
+namespace qs::menu::platform {
+
+class PlatformMenuQMenu: public QMenu {
+public:
+	explicit PlatformMenuQMenu() = default;
+	~PlatformMenuQMenu() override;
+	Q_DISABLE_COPY_MOVE(PlatformMenuQMenu);
+
+	void setVisible(bool visible) override;
+
+	PlatformMenuQMenu* containingMenu = nullptr;
+	QPoint targetPosition;
+};
+
+} // namespace qs::menu::platform
diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp
index 0897928a..a0f6353c 100644
--- a/src/core/popupanchor.hpp
+++ b/src/core/popupanchor.hpp
@@ -2,7 +2,6 @@
 
 #include <optional>
 
-#include <QtQmlIntegration/qqmlintegration.h>
 #include <qflags.h>
 #include <qnamespace.h>
 #include <qobject.h>
diff --git a/src/core/test/CMakeLists.txt b/src/core/test/CMakeLists.txt
index 448881a6..c9a82005 100644
--- a/src/core/test/CMakeLists.txt
+++ b/src/core/test/CMakeLists.txt
@@ -1,6 +1,6 @@
 function (qs_test name)
 	add_executable(${name} ${ARGN})
-	target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-core quickshell-window)
+	target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window)
 	add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
 endfunction()
 
diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt
index 49a4a06b..9948ea74 100644
--- a/src/dbus/CMakeLists.txt
+++ b/src/dbus/CMakeLists.txt
@@ -16,8 +16,18 @@ qt_add_library(quickshell-dbus STATIC
 # dbus headers
 target_include_directories(quickshell-dbus PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
 
-target_link_libraries(quickshell-dbus PRIVATE ${QT_DEPS})
+target_link_libraries(quickshell-dbus PRIVATE Qt::Core Qt::DBus)
+# todo: link dbus to quickshell here instead of in modules that use it directly
+# linker does not like this as is
 
-qs_pch(quickshell-dbus)
+qs_add_pchset(dbus
+	DEPENDENCIES Qt::DBus
+	HEADERS
+		<QtDBus>
+		<qdebug.h>
+		<qdbusargument.h>
+)
+
+qs_pch(quickshell-dbus SET dbus)
 
 add_subdirectory(dbusmenu)
diff --git a/src/dbus/dbusmenu/CMakeLists.txt b/src/dbus/dbusmenu/CMakeLists.txt
index f9e4446c..ac50b28a 100644
--- a/src/dbus/dbusmenu/CMakeLists.txt
+++ b/src/dbus/dbusmenu/CMakeLists.txt
@@ -17,15 +17,18 @@ qt_add_library(quickshell-dbusmenu STATIC
 qt_add_qml_module(quickshell-dbusmenu
 	URI Quickshell.DBusMenu
 	VERSION 0.1
-	DEPENDENCIES QtQml Quickshell
+	DEPENDENCIES QtQml
 )
 
+qs_add_module_deps_light(quickshell-dbusmenu Quickshell)
+
 install_qml_module(quickshell-dbusmenu)
 
 # dbus headers
 target_include_directories(quickshell-dbusmenu PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
 
-target_link_libraries(quickshell-dbusmenu PRIVATE ${QT_DEPS})
+target_link_libraries(quickshell-dbusmenu PRIVATE Qt::Quick Qt::DBus quickshell-dbus)
 
-qs_pch(quickshell-dbusmenu)
-qs_pch(quickshell-dbusmenuplugin)
+qs_module_pch(quickshell-dbusmenu SET dbus)
+
+target_link_libraries(quickshell PRIVATE quickshell-dbusmenuplugin)
diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp
index 7dac84ab..1a40ca23 100644
--- a/src/dbus/properties.cpp
+++ b/src/dbus/properties.cpp
@@ -16,7 +16,6 @@
 #include <qloggingcategory.h>
 #include <qmetatype.h>
 #include <qobject.h>
-#include <qpolygon.h>
 #include <qtmetamacros.h>
 #include <qvariant.h>
 
diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt
index 1d936d17..6299b397 100644
--- a/src/io/CMakeLists.txt
+++ b/src/io/CMakeLists.txt
@@ -23,14 +23,12 @@ qt_add_qml_module(quickshell-io
 
 install_qml_module(quickshell-io)
 
-target_link_libraries(quickshell-io PRIVATE ${QT_DEPS})
-target_link_libraries(quickshell-io-init PRIVATE ${QT_DEPS})
+target_link_libraries(quickshell-io PRIVATE Qt::Quick)
+target_link_libraries(quickshell-io-init PRIVATE Qt::Qml)
 
 target_link_libraries(quickshell PRIVATE quickshell-ioplugin quickshell-io-init)
 
-qs_pch(quickshell-io)
-qs_pch(quickshell-ioplugin)
-qs_pch(quickshell-io-init)
+qs_module_pch(quickshell-io)
 
 if (BUILD_TESTING)
 	add_subdirectory(test)
diff --git a/src/io/test/CMakeLists.txt b/src/io/test/CMakeLists.txt
index 0c0cfc55..4ab51739 100644
--- a/src/io/test/CMakeLists.txt
+++ b/src/io/test/CMakeLists.txt
@@ -1,6 +1,6 @@
 function (qs_test name)
 	add_executable(${name} ${ARGN})
-	target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test)
+	target_link_libraries(${name} PRIVATE Qt::Quick Qt::Network Qt::Test)
 	add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
 endfunction()
 
diff --git a/src/ipc/CMakeLists.txt b/src/ipc/CMakeLists.txt
index ff6093c6..e3f9e25f 100644
--- a/src/ipc/CMakeLists.txt
+++ b/src/ipc/CMakeLists.txt
@@ -2,6 +2,8 @@ qt_add_library(quickshell-ipc STATIC
 	ipc.cpp
 )
 
-target_link_libraries(quickshell-ipc PRIVATE ${QT_DEPS})
+qs_pch(quickshell-ipc)
+
+target_link_libraries(quickshell-ipc PRIVATE Qt::Quick Qt::Network)
 
 target_link_libraries(quickshell PRIVATE quickshell-ipc)
diff --git a/src/launch/CMakeLists.txt b/src/launch/CMakeLists.txt
new file mode 100644
index 00000000..4db11bf0
--- /dev/null
+++ b/src/launch/CMakeLists.txt
@@ -0,0 +1,23 @@
+find_package(CLI11 CONFIG REQUIRED)
+
+qt_add_library(quickshell-launch STATIC
+	parsecommand.cpp
+	command.cpp
+	launch.cpp
+	main.cpp
+)
+
+target_link_libraries(quickshell-launch PRIVATE
+	Qt::Quick Qt::Widgets CLI11::CLI11 quickshell-build
+)
+
+qs_add_pchset(launch
+	DEPENDENCIES Qt::Core CLI11::CLI11
+	HEADERS
+		<CLI/App.hpp>
+		<qcoreapplication.h>
+)
+
+qs_pch(quickshell-launch SET launch)
+
+target_link_libraries(quickshell PRIVATE quickshell-launch)
diff --git a/src/launch/command.cpp b/src/launch/command.cpp
new file mode 100644
index 00000000..83001037
--- /dev/null
+++ b/src/launch/command.cpp
@@ -0,0 +1,448 @@
+#include <algorithm>
+#include <array>
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+
+#include <qconfig.h>
+#include <qcontainerfwd.h>
+#include <qcoreapplication.h>
+#include <qcryptographichash.h>
+#include <qdatetime.h>
+#include <qdebug.h>
+#include <qdir.h>
+#include <qfileinfo.h>
+#include <qjsonarray.h>
+#include <qjsondocument.h>
+#include <qjsonobject.h>
+#include <qlogging.h>
+#include <qloggingcategory.h>
+#include <qnamespace.h>
+#include <qstandardpaths.h>
+#include <qtversion.h>
+#include <unistd.h>
+
+#include "../core/instanceinfo.hpp"
+#include "../core/logging.hpp"
+#include "../core/paths.hpp"
+#include "../io/ipccomm.hpp"
+#include "../ipc/ipc.hpp"
+#include "build.hpp"
+#include "launch_p.hpp"
+
+namespace qs::launch {
+
+using qs::ipc::IpcClient;
+
+int readLogFile(CommandState& cmd);
+int listInstances(CommandState& cmd);
+int killInstances(CommandState& cmd);
+int msgInstance(CommandState& cmd);
+int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication);
+int locateConfigFile(CommandState& cmd, QString& path);
+
+int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
+	auto state = CommandState();
+	if (auto ret = parseCommand(argc, argv, state); ret != 0) return ret;
+
+	if (state.misc.checkCompat) {
+		if (strcmp(qVersion(), QT_VERSION_STR) != 0) {
+			QTextStream(stdout) << "\033[31mCOMPATIBILITY WARNING: Quickshell was built against Qt "
+			                    << QT_VERSION_STR << " but the system has updated to Qt " << qVersion()
+			                    << " without rebuilding the package. This is likely to cause crashes, so "
+			                       "you must rebuild the quickshell package.\n";
+			return 1;
+		}
+
+		return 0;
+	}
+
+	// Has to happen before extra threads are spawned.
+	if (state.misc.daemonize) {
+		auto closepipes = std::array<int, 2>();
+		if (pipe(closepipes.data()) == -1) {
+			qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno
+			                   << ": " << qt_error_string();
+		}
+
+		DAEMON_PIPE = closepipes[1];
+
+		pid_t pid = fork(); // NOLINT (include)
+
+		if (pid == -1) {
+			qFatal().nospace() << "Failed to fork daemon with error " << errno << ": "
+			                   << qt_error_string();
+		} else if (pid == 0) {
+			close(closepipes[0]);
+
+			if (setsid() == -1) {
+				qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string();
+			}
+		} else {
+			close(closepipes[1]);
+
+			int ret = 0;
+			if (read(closepipes[0], &ret, sizeof(int)) == -1) {
+				qFatal() << "Failed to wait for daemon launch (it may have crashed)";
+			}
+
+			return ret;
+		}
+	}
+
+	{
+		auto level = state.log.verbosity == 0 ? QtWarningMsg
+		           : state.log.verbosity == 1 ? QtInfoMsg
+		                                      : QtDebugMsg;
+
+		LogManager::init(
+		    !state.log.noColor,
+		    state.log.timestamp,
+		    state.log.sparse,
+		    level,
+		    *state.log.rules,
+		    *state.subcommand.log ? "READER" : ""
+		);
+	}
+
+	if (state.misc.printVersion) {
+		qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION
+		                                    << ", distributed by: " << DISTRIBUTOR;
+
+		if (state.log.verbosity > 1) {
+			qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;
+			qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion();
+			qCInfo(logBare).noquote() << "Compiler:" << COMPILER;
+			qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS;
+		}
+
+		if (state.log.verbosity > 0) {
+			qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE;
+			qCInfo(logBare).noquote() << "Build configuration:";
+			qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION;
+		}
+	} else if (*state.subcommand.log) {
+		return readLogFile(state);
+	} else if (*state.subcommand.list) {
+		return listInstances(state);
+	} else if (*state.subcommand.kill) {
+		return killInstances(state);
+	} else if (*state.subcommand.msg) {
+		return msgInstance(state);
+	} else {
+		if (strcmp(qVersion(), QT_VERSION_STR) != 0) {
+			qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR
+			           << "but the system has updated to Qt" << qVersion()
+			           << "without rebuilding the package. This is likely to cause crashes, so "
+			              "the quickshell package must be rebuilt.\n";
+		}
+
+		return launchFromCommand(state, coreApplication);
+	}
+
+	return 0;
+}
+
+int locateConfigFile(CommandState& cmd, QString& path) {
+	if (!cmd.config.path->isEmpty()) {
+		path = *cmd.config.path;
+	} else {
+		auto manifestPath = *cmd.config.manifest;
+		if (manifestPath.isEmpty()) {
+			auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
+			auto path = configDir.filePath("manifest.conf");
+			if (QFileInfo(path).isFile()) manifestPath = path;
+		}
+
+		if (!manifestPath.isEmpty()) {
+			auto file = QFile(manifestPath);
+			if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+				auto stream = QTextStream(&file);
+				while (!stream.atEnd()) {
+					auto line = stream.readLine();
+					if (line.trimmed().startsWith("#")) continue;
+					if (line.trimmed().isEmpty()) continue;
+
+					auto split = line.split('=');
+					if (split.length() != 2) {
+						qCritical() << "Manifest line not in expected format 'name = relativepath':" << line;
+						return -1;
+					}
+
+					if (split[0].trimmed() == *cmd.config.name) {
+						path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed());
+						break;
+					}
+				}
+
+				if (path.isEmpty()) {
+					qCCritical(logBare) << "Configuration" << *cmd.config.name
+					                    << "not found when searching manifest" << manifestPath;
+					return -1;
+				}
+			} else {
+				qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest;
+				return -1;
+			}
+		} else {
+			auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
+
+			if (cmd.config.name->isEmpty()) {
+				path = configDir.path();
+			} else {
+				path = configDir.filePath(*cmd.config.name);
+			}
+		}
+	}
+
+	if (QFileInfo(path).isDir()) {
+		path = QDir(path).filePath("shell.qml");
+	}
+
+	if (!QFileInfo(path).isFile()) {
+		qCCritical(logBare) << "Could not open config file at" << path;
+		return -1;
+	}
+
+	path = QFileInfo(path).canonicalFilePath();
+
+	return 0;
+}
+
+void sortInstances(QVector<InstanceLockInfo>& list) {
+	std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) {
+		return a.instance.launchTime < b.instance.launchTime;
+	});
+};
+
+int selectInstance(CommandState& cmd, InstanceLockInfo* instance) {
+	auto* basePath = QsPaths::instance()->baseRunDir();
+	if (!basePath) return -1;
+
+	QString path;
+
+	if (cmd.instance.pid != -1) {
+		path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid));
+		if (!QsPaths::checkLock(path, instance)) {
+			qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid;
+			return -1;
+		}
+	} else if (!cmd.instance.id->isEmpty()) {
+		path = basePath->filePath("by-pid");
+		auto instances = QsPaths::collectInstances(path);
+
+		auto itr =
+		    std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) {
+			    return !info.instance.instanceId.startsWith(*cmd.instance.id);
+		    });
+
+		instances.erase(itr, instances.end());
+
+		if (instances.isEmpty()) {
+			qCInfo(logBare) << "No running instances start with" << *cmd.instance.id;
+			return -1;
+		} else if (instances.length() != 1) {
+			qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id;
+
+			for (auto& instance: instances) {
+				qCInfo(logBare).noquote() << " -" << instance.instance.instanceId;
+			}
+
+			return -1;
+		} else {
+			*instance = instances.value(0);
+		}
+	} else {
+		QString configFilePath;
+		auto r = locateConfigFile(cmd, configFilePath);
+		if (r != 0) return r;
+
+		auto pathId =
+		    QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex();
+
+		path = QDir(basePath->filePath("by-path")).filePath(pathId);
+
+		auto instances = QsPaths::collectInstances(path);
+		sortInstances(instances);
+
+		if (instances.isEmpty()) {
+			qCInfo(logBare) << "No running instances for" << configFilePath;
+			return -1;
+		}
+
+		*instance = instances.value(0);
+	}
+
+	return 0;
+}
+
+int readLogFile(CommandState& cmd) {
+	auto path = *cmd.log.file;
+
+	if (path.isEmpty()) {
+		InstanceLockInfo instance;
+		auto r = selectInstance(cmd, &instance);
+		if (r != 0) return r;
+
+		path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog");
+	}
+
+	auto file = QFile(path);
+	if (!file.open(QFile::ReadOnly)) {
+		qCCritical(logBare) << "Failed to open log file" << path;
+		return -1;
+	}
+
+	return qs::log::readEncodedLogs(
+	           &file,
+	           path,
+	           cmd.log.timestamp,
+	           cmd.log.tail,
+	           cmd.log.follow,
+	           *cmd.log.readoutRules
+	       )
+	         ? 0
+	         : -1;
+}
+
+int listInstances(CommandState& cmd) {
+	auto* basePath = QsPaths::instance()->baseRunDir();
+	if (!basePath) return -1; // NOLINT
+
+	QString path;
+	QString configFilePath;
+	if (cmd.instance.all) {
+		path = basePath->filePath("by-pid");
+	} else {
+		auto r = locateConfigFile(cmd, configFilePath);
+
+		if (r != 0) {
+			qCInfo(logBare) << "Use --all to list all instances.";
+			return r;
+		}
+
+		auto pathId =
+		    QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex();
+
+		path = QDir(basePath->filePath("by-path")).filePath(pathId);
+	}
+
+	auto instances = QsPaths::collectInstances(path);
+
+	if (instances.isEmpty()) {
+		if (cmd.instance.all) {
+			qCInfo(logBare) << "No running instances.";
+		} else {
+			qCInfo(logBare) << "No running instances for" << configFilePath;
+			qCInfo(logBare) << "Use --all to list all instances.";
+		}
+	} else {
+		sortInstances(instances);
+
+		if (cmd.output.json) {
+			auto array = QJsonArray();
+
+			for (auto& instance: instances) {
+				auto json = QJsonObject();
+
+				json["id"] = instance.instance.instanceId;
+				json["pid"] = instance.pid;
+				json["shell_id"] = instance.instance.shellId;
+				json["config_path"] = instance.instance.configPath;
+				json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate);
+
+				array.push_back(json);
+			}
+
+			auto document = QJsonDocument(array);
+			QTextStream(stdout) << document.toJson(QJsonDocument::Indented);
+		} else {
+			for (auto& instance: instances) {
+				auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss");
+
+				auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime());
+				auto remSeconds = runSeconds % 60;
+				auto runMinutes = (runSeconds - remSeconds) / 60;
+				auto remMinutes = runMinutes % 60;
+				auto runHours = (runMinutes - remMinutes) / 60;
+				auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds")
+				                      .arg(runHours)
+				                      .arg(remMinutes)
+				                      .arg(remSeconds);
+
+				qCInfo(logBare).noquote().nospace()
+				    << "Instance " << instance.instance.instanceId << ":\n"
+				    << "  Process ID: " << instance.pid << '\n'
+				    << "  Shell ID: " << instance.instance.shellId << '\n'
+				    << "  Config path: " << instance.instance.configPath << '\n'
+				    << "  Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n";
+			}
+		}
+	}
+
+	return 0;
+}
+
+int killInstances(CommandState& cmd) {
+	InstanceLockInfo instance;
+	auto r = selectInstance(cmd, &instance);
+	if (r != 0) return r;
+
+	return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) {
+		client.kill();
+		qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId;
+	});
+}
+
+int msgInstance(CommandState& cmd) {
+	InstanceLockInfo instance;
+	auto r = selectInstance(cmd, &instance);
+	if (r != 0) return r;
+
+	return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) {
+		if (cmd.ipc.info) {
+			return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function);
+		} else {
+			QVector<QString> arguments;
+			for (auto& arg: cmd.ipc.arguments) {
+				arguments += *arg;
+			}
+
+			return qs::io::ipc::comm::callFunction(
+			    &client,
+			    *cmd.ipc.target,
+			    *cmd.ipc.function,
+			    arguments
+			);
+		}
+
+		return -1;
+	});
+}
+
+int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) {
+	QString configPath;
+
+	auto r = locateConfigFile(cmd, configPath);
+	if (r != 0) return r;
+
+	{
+		InstanceLockInfo info;
+		if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) {
+			qCInfo(logBare) << "An instance of this configuration is already running.";
+			return 0;
+		}
+	}
+
+	return launch(
+	    {
+	        .configPath = configPath,
+	        .debugPort = cmd.debug.port,
+	        .waitForDebug = cmd.debug.wait,
+	    },
+	    cmd.exec.argv,
+	    coreApplication
+	);
+}
+
+} // namespace qs::launch
diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp
new file mode 100644
index 00000000..30c87a62
--- /dev/null
+++ b/src/launch/launch.cpp
@@ -0,0 +1,238 @@
+#include <qapplication.h>
+#include <qcoreapplication.h>
+#include <qcryptographichash.h>
+#include <qdebug.h>
+#include <qdir.h>
+#include <qfile.h>
+#include <qguiapplication.h>
+#include <qhash.h>
+#include <qlist.h>
+#include <qlogging.h>
+#include <qnamespace.h>
+#include <qqmldebug.h>
+#include <qquickwindow.h>
+#include <qstring.h>
+#include <qtenvironmentvariables.h>
+#include <qtextstream.h>
+#include <unistd.h>
+
+#include "../core/common.hpp"
+#include "../core/instanceinfo.hpp"
+#include "../core/logging.hpp"
+#include "../core/paths.hpp"
+#include "../core/plugin.hpp"
+#include "../core/rootwrapper.hpp"
+#include "../ipc/ipc.hpp"
+#include "build.hpp"
+#include "launch_p.hpp"
+
+#if CRASH_REPORTER
+#include "../crash/handler.hpp"
+#endif
+
+namespace qs::launch {
+
+template <typename T>
+QString base36Encode(T number) {
+	const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz";
+	QString result;
+
+	do {
+		result.prepend(digits[number % 36]);
+		number /= 36;
+	} while (number > 0);
+
+	for (auto i = 0; i < result.length() / 2; i++) {
+		auto opposite = result.length() - i - 1;
+		auto c = result.at(i);
+		result[i] = result.at(opposite);
+		result[opposite] = c;
+	}
+
+	return result;
+}
+
+int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) {
+	auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex();
+	auto shellId = QString(pathId);
+
+	qInfo() << "Launching config:" << args.configPath;
+
+	auto file = QFile(args.configPath);
+	if (!file.open(QFile::ReadOnly | QFile::Text)) {
+		qCritical() << "Could not open config file" << args.configPath;
+		return -1;
+	}
+
+	struct {
+		bool useQApplication = false;
+		bool nativeTextRendering = false;
+		bool desktopSettingsAware = true;
+		QString iconTheme = qEnvironmentVariable("QS_ICON_THEME");
+		QHash<QString, QString> envOverrides;
+	} pragmas;
+
+	auto stream = QTextStream(&file);
+	while (!stream.atEnd()) {
+		auto line = stream.readLine().trimmed();
+		if (line.startsWith("//@ pragma ")) {
+			auto pragma = line.sliced(11).trimmed();
+
+			if (pragma == "UseQApplication") pragmas.useQApplication = true;
+			else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true;
+			else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false;
+			else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10);
+			else if (pragma.startsWith("Env ")) {
+				auto envPragma = pragma.sliced(4);
+				auto splitIdx = envPragma.indexOf('=');
+
+				if (splitIdx == -1) {
+					qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'";
+					return -1;
+				}
+
+				auto var = envPragma.sliced(0, splitIdx).trimmed();
+				auto val = envPragma.sliced(splitIdx + 1).trimmed();
+				pragmas.envOverrides.insert(var, val);
+			} else if (pragma.startsWith("ShellId ")) {
+				shellId = pragma.sliced(8).trimmed();
+			} else {
+				qCritical() << "Unrecognized pragma" << pragma;
+				return -1;
+			}
+		} else if (line.startsWith("import")) break;
+	}
+
+	file.close();
+
+	if (!pragmas.iconTheme.isEmpty()) {
+		QIcon::setThemeName(pragmas.iconTheme);
+	}
+
+	qInfo() << "Shell ID:" << shellId << "Path ID" << pathId;
+
+	auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch();
+	InstanceInfo::CURRENT = InstanceInfo {
+	    .instanceId = base36Encode(getpid()) + base36Encode(launchTime),
+	    .configPath = args.configPath,
+	    .shellId = shellId,
+	    .launchTime = qs::Common::LAUNCH_TIME,
+	};
+
+#if CRASH_REPORTER
+	auto crashHandler = crash::CrashHandler();
+	crashHandler.init();
+
+	{
+		auto* log = LogManager::instance();
+		crashHandler.setRelaunchInfo({
+		    .instance = InstanceInfo::CURRENT,
+		    .noColor = !log->colorLogs,
+		    .timestamp = log->timestampLogs,
+		    .sparseLogsOnly = log->isSparse(),
+		    .defaultLogLevel = log->defaultLevel(),
+		    .logRules = log->rulesString(),
+		});
+	}
+#endif
+
+	QsPaths::init(shellId, pathId);
+	QsPaths::instance()->linkRunDir();
+	QsPaths::instance()->linkPathDir();
+	LogManager::initFs();
+
+	for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) {
+		qputenv(var.toUtf8(), val.toUtf8());
+	}
+
+	// The qml engine currently refuses to cache non file (qsintercept) paths.
+
+	// if (auto* cacheDir = QsPaths::instance()->cacheDir()) {
+	// 	auto qmlCacheDir = cacheDir->filePath("qml-engine-cache");
+	// 	qputenv("QML_DISK_CACHE_PATH", qmlCacheDir.toLocal8Bit());
+	//
+	// 	if (!qEnvironmentVariableIsSet("QML_DISK_CACHE")) {
+	// 		qputenv("QML_DISK_CACHE", "aot,qmlc");
+	// 	}
+	// }
+
+	// While the simple animation driver can lead to better animations in some cases,
+	// it also can cause excessive repainting at excessively high framerates which can
+	// lead to noticeable amounts of gpu usage, including overheating on some systems.
+	// This gets worse the more windows are open, as repaints trigger on all of them for
+	// some reason. See QTBUG-126099 for details.
+
+	// if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) {
+	// 	qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1");
+	// }
+
+	// Some programs place icons in the pixmaps folder instead of the icons folder.
+	// This seems to be controlled by the QPA and qt6ct does not provide it.
+	{
+		QList<QString> dataPaths;
+
+		if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
+			auto var = qEnvironmentVariable("XDG_DATA_DIRS");
+			dataPaths = var.split(u':', Qt::SkipEmptyParts);
+		} else {
+			dataPaths.push_back("/usr/local/share");
+			dataPaths.push_back("/usr/share");
+		}
+
+		auto fallbackPaths = QIcon::fallbackSearchPaths();
+
+		for (auto& path: dataPaths) {
+			auto newPath = QDir(path).filePath("pixmaps");
+
+			if (!fallbackPaths.contains(newPath)) {
+				fallbackPaths.push_back(newPath);
+			}
+		}
+
+		QIcon::setFallbackSearchPaths(fallbackPaths);
+	}
+
+	QGuiApplication::setDesktopSettingsAware(pragmas.desktopSettingsAware);
+
+	delete coreApplication;
+
+	QGuiApplication* app = nullptr;
+	auto qArgC = 0;
+
+	if (pragmas.useQApplication) {
+		app = new QApplication(qArgC, argv);
+	} else {
+		app = new QGuiApplication(qArgC, argv);
+	}
+
+	if (args.debugPort != -1) {
+		QQmlDebuggingEnabler::enableDebugging(true);
+		auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient
+		                              : QQmlDebuggingEnabler::DoNotWaitForClient;
+		QQmlDebuggingEnabler::startTcpDebugServer(args.debugPort, wait);
+	}
+
+	QuickshellPlugin::initPlugins();
+
+	// Base window transparency appears to be additive.
+	// Use a fully transparent window with a colored rect.
+	QQuickWindow::setDefaultAlphaBuffer(true);
+
+	if (pragmas.nativeTextRendering) {
+		QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
+	}
+
+	qs::ipc::IpcServer::start();
+	QsPaths::instance()->createLock();
+
+	auto root = RootWrapper(args.configPath, shellId);
+	QGuiApplication::setQuitOnLastWindowClosed(false);
+
+	exitDaemon(0);
+
+	auto code = QGuiApplication::exec();
+	delete app;
+	return code;
+}
+
+} // namespace qs::launch
diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp
new file mode 100644
index 00000000..d1916d50
--- /dev/null
+++ b/src/launch/launch_p.hpp
@@ -0,0 +1,103 @@
+#pragma once
+
+#include <string>
+
+#include <CLI/App.hpp>
+#include <qcoreapplication.h>
+#include <qstring.h>
+
+namespace qs::launch {
+
+extern int DAEMON_PIPE; // NOLINT
+
+class QStringOption {
+public:
+	QStringOption() = default;
+	QStringOption& operator=(const std::string& str) {
+		this->str = QString::fromStdString(str);
+		return *this;
+	}
+
+	QString& operator*() { return this->str; }
+	QString* operator->() { return &this->str; }
+
+private:
+	QString str;
+};
+
+struct CommandState {
+	struct {
+		int argc = 0;
+		char** argv = nullptr;
+	} exec;
+
+	struct {
+		bool timestamp = false;
+		bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR");
+		bool sparse = false;
+		size_t verbosity = 0;
+		int tail = 0;
+		bool follow = false;
+		QStringOption rules;
+		QStringOption readoutRules;
+		QStringOption file;
+	} log;
+
+	struct {
+		QStringOption path;
+		QStringOption manifest;
+		QStringOption name;
+	} config;
+
+	struct {
+		int port = -1;
+		bool wait = false;
+	} debug;
+
+	struct {
+		QStringOption id;
+		pid_t pid = -1; // NOLINT (include)
+		bool all = false;
+	} instance;
+
+	struct {
+		bool json = false;
+	} output;
+
+	struct {
+		bool info = false;
+		QStringOption target;
+		QStringOption function;
+		std::vector<QStringOption> arguments;
+	} ipc;
+
+	struct {
+		CLI::App* log = nullptr;
+		CLI::App* list = nullptr;
+		CLI::App* kill = nullptr;
+		CLI::App* msg = nullptr;
+	} subcommand;
+
+	struct {
+		bool checkCompat = false;
+		bool printVersion = false;
+		bool killAll = false;
+		bool noDuplicate = false;
+		bool daemonize = false;
+	} misc;
+};
+
+struct LaunchArgs {
+	QString configPath;
+	int debugPort = -1;
+	bool waitForDebug = false;
+};
+
+void exitDaemon(int code);
+
+int parseCommand(int argc, char** argv, CommandState& state);
+int runCommand(int argc, char** argv, QCoreApplication* coreApplication);
+
+int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication);
+
+} // namespace qs::launch
diff --git a/src/launch/main.cpp b/src/launch/main.cpp
new file mode 100644
index 00000000..3a2b5822
--- /dev/null
+++ b/src/launch/main.cpp
@@ -0,0 +1,116 @@
+#include "main.hpp"
+#include <cerrno>
+
+#include <fcntl.h>
+#include <qcoreapplication.h>
+#include <qdatastream.h>
+#include <qdatetime.h>
+#include <qdebug.h>
+#include <qlogging.h>
+#include <qtenvironmentvariables.h>
+#include <unistd.h>
+
+#include "../core/instanceinfo.hpp"
+#include "../core/logging.hpp"
+#include "../core/paths.hpp"
+#include "build.hpp"
+#include "launch_p.hpp"
+
+#if CRASH_REPORTER
+#include "../crash/main.hpp"
+#endif
+
+namespace qs::launch {
+
+void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication);
+
+int DAEMON_PIPE = -1; // NOLINT
+
+void exitDaemon(int code) {
+	if (DAEMON_PIPE == -1) return;
+
+	if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) {
+		qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": "
+		                      << qt_error_string();
+	}
+
+	close(DAEMON_PIPE);
+
+	close(STDIN_FILENO);
+	close(STDOUT_FILENO);
+	close(STDERR_FILENO);
+
+	if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT
+		qFatal() << "Failed to open /dev/null on stdin";
+	}
+
+	if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT
+		qFatal() << "Failed to open /dev/null on stdout";
+	}
+
+	if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT
+		qFatal() << "Failed to open /dev/null on stderr";
+	}
+}
+
+void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) {
+#if CRASH_REPORTER
+	auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD");
+
+	if (!lastInfoFdStr.isEmpty()) {
+		auto lastInfoFd = lastInfoFdStr.toInt();
+
+		QFile file;
+		file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle);
+		file.seek(0);
+
+		auto ds = QDataStream(&file);
+		RelaunchInfo info;
+		ds >> info;
+
+		LogManager::init(
+		    !info.noColor,
+		    info.timestamp,
+		    info.sparseLogsOnly,
+		    info.defaultLogLevel,
+		    info.logRules
+		);
+
+		qCritical().nospace() << "Quickshell has crashed under pid "
+		                      << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt()
+		                      << " (Coredumps will be available under that pid.)";
+
+		qCritical() << "Further crash information is stored under"
+		            << QsPaths::crashDir(info.instance.instanceId).path();
+
+		if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) {
+			qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid "
+			               "a crash loop.";
+			exit(-1); // NOLINT
+		} else {
+			qCritical() << "Quickshell has been restarted.";
+
+			launch({.configPath = info.instance.configPath}, argv, coreApplication);
+		}
+	}
+#endif
+}
+
+int main(int argc, char** argv) {
+	QCoreApplication::setApplicationName("quickshell");
+
+#if CRASH_REPORTER
+	qsCheckCrash(argc, argv);
+#endif
+
+	auto qArgC = 1;
+	auto* coreApplication = new QCoreApplication(qArgC, argv);
+
+	checkCrashRelaunch(argv, coreApplication);
+	auto code = runCommand(argc, argv, coreApplication);
+
+	exitDaemon(code);
+	return code;
+}
+
+} // namespace qs::launch
diff --git a/src/launch/main.hpp b/src/launch/main.hpp
new file mode 100644
index 00000000..e9d22902
--- /dev/null
+++ b/src/launch/main.hpp
@@ -0,0 +1,7 @@
+#pragma once
+
+namespace qs::launch {
+
+int main(int argc, char** argv);
+
+}
diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp
new file mode 100644
index 00000000..14fd9203
--- /dev/null
+++ b/src/launch/parsecommand.cpp
@@ -0,0 +1,196 @@
+#include <cstddef>
+#include <limits>
+
+#include <CLI/App.hpp>
+#include <CLI/CLI.hpp> // NOLINT: Need to include this for impls of some CLI11 classes
+#include <CLI/Validators.hpp>
+
+#include "launch_p.hpp"
+
+namespace qs::launch {
+
+int parseCommand(int argc, char** argv, CommandState& state) {
+	state.exec = {
+	    .argc = argc,
+	    .argv = argv,
+	};
+
+	auto addConfigSelection = [&](CLI::App* cmd) {
+		auto* group = cmd->add_option_group("Config Selection")
+		                  ->description("If no options in this group are specified,\n"
+		                                "$XDG_CONFIG_HOME/quickshell/shell.qml will be used.");
+
+		auto* path = group->add_option("-p,--path", state.config.path)
+		                 ->description("Path to a QML file.")
+		                 ->envname("QS_CONFIG_PATH");
+
+		group->add_option("-m,--manifest", state.config.manifest)
+		    ->description("Path to a quickshell manifest.\n"
+		                  "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf")
+		    ->envname("QS_MANIFEST")
+		    ->excludes(path);
+
+		group->add_option("-c,--config", state.config.name)
+		    ->description("Name of a quickshell configuration to run.\n"
+		                  "If -m is specified, this is a configuration in the manifest,\n"
+		                  "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.")
+		    ->envname("QS_CONFIG_NAME");
+
+		return group;
+	};
+
+	auto addDebugOptions = [&](CLI::App* cmd) {
+		auto* group = cmd->add_option_group("Debugging", "Options for QML debugging.");
+
+		auto* debug = group->add_option("--debug", state.debug.port)
+		                  ->description("Open the given port for a QML debugger connection.")
+		                  ->check(CLI::Range(0, 65535));
+
+		group->add_flag("--waitfordebug", state.debug.wait)
+		    ->description("Wait for a QML debugger to connect before executing the configuration.")
+		    ->needs(debug);
+
+		return group;
+	};
+
+	auto addLoggingOptions = [&](CLI::App* cmd, bool noGroup, bool noDisplay = false) {
+		auto* group = noGroup ? cmd : cmd->add_option_group(noDisplay ? "" : "Logging");
+
+		group->add_flag("--no-color", state.log.noColor)
+		    ->description("Disables colored logging.\n"
+		                  "Colored logging can also be disabled by specifying a non empty value\n"
+		                  "for the NO_COLOR environment variable.");
+
+		group->add_flag("--log-times", state.log.timestamp)
+		    ->description("Log timestamps with each message.");
+
+		group->add_option("--log-rules", state.log.rules)
+		    ->description("Log rules to apply, in the format of QT_LOGGING_RULES.");
+
+		group->add_flag("-v,--verbose", [&](size_t count) { state.log.verbosity = count; })
+		    ->description("Increases log verbosity.\n"
+		                  "-v will show INFO level internal logs.\n"
+		                  "-vv will show DEBUG level internal logs.");
+
+		auto* hgroup = cmd->add_option_group("");
+		hgroup->add_flag("--no-detailed-logs", state.log.sparse);
+	};
+
+	auto addInstanceSelection = [&](CLI::App* cmd) {
+		auto* group = cmd->add_option_group("Instance Selection");
+
+		group->add_option("-i,--id", state.instance.id)
+		    ->description("The instance id to operate on.\n"
+		                  "You may also use a substring the id as long as it is unique,\n"
+		                  "for example \"abc\" will select \"abcdefg\".");
+
+		group->add_option("--pid", state.instance.pid)
+		    ->description("The process id of the instance to operate on.");
+
+		return group;
+	};
+
+	auto cli = CLI::App();
+
+	// Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands.
+	cli.require_subcommand(0, 1);
+
+	addConfigSelection(&cli);
+	addLoggingOptions(&cli, false);
+	addDebugOptions(&cli);
+
+	{
+		cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat);
+
+		cli.add_flag("-V,--version", state.misc.printVersion)
+		    ->description("Print quickshell's version and exit.");
+
+		cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate)
+		    ->description("Exit immediately if another instance of the given config is running.");
+
+		cli.add_flag("-d,--daemonize", state.misc.daemonize)
+		    ->description("Detach from the controlling terminal.");
+	}
+
+	{
+		auto* sub = cli.add_subcommand("log", "Print quickshell logs.");
+
+		auto* file = sub->add_option("file", state.log.file, "Log file to read.");
+
+		sub->add_option("-t,--tail", state.log.tail)
+		    ->description("Maximum number of lines to print, starting from the bottom.")
+		    ->check(CLI::Range(1, std::numeric_limits<int>::max(), "INT > 0"));
+
+		sub->add_flag("-f,--follow", state.log.follow)
+		    ->description("Keep reading the log until the logging process terminates.");
+
+		sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.")
+		    ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES.");
+
+		auto* instance = addInstanceSelection(sub)->excludes(file);
+		addConfigSelection(sub)->excludes(instance)->excludes(file);
+		addLoggingOptions(sub, false);
+
+		state.subcommand.log = sub;
+	}
+
+	{
+		auto* sub = cli.add_subcommand("list", "List running quickshell instances.");
+
+		auto* all = sub->add_flag("-a,--all", state.instance.all)
+		                ->description("List all instances.\n"
+		                              "If unspecified, only instances of"
+		                              "the selected config will be listed.");
+
+		sub->add_flag("-j,--json", state.output.json, "Output the list as a json.");
+
+		addConfigSelection(sub)->excludes(all);
+		addLoggingOptions(sub, false, true);
+
+		state.subcommand.list = sub;
+	}
+
+	{
+		auto* sub = cli.add_subcommand("kill", "Kill quickshell instances.");
+		//sub->add_flag("-a,--all", "Kill all matching instances instead of just one.");
+		auto* instance = addInstanceSelection(sub);
+		addConfigSelection(sub)->excludes(instance);
+		addLoggingOptions(sub, false, true);
+
+		state.subcommand.kill = sub;
+	}
+
+	{
+		auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option();
+
+		auto* target = sub->add_option("target", state.ipc.target, "The target to message.");
+
+		auto* function = sub->add_option("function", state.ipc.function)
+		                     ->description("The function to call in the target.")
+		                     ->needs(target);
+
+		auto* arguments = sub->add_option("arguments", state.ipc.arguments)
+		                      ->description("Arguments to the called function.")
+		                      ->needs(function)
+		                      ->allow_extra_args();
+
+		sub->add_flag("-s,--show", state.ipc.info)
+		    ->description("Print information about a function or target if given, or all available "
+		                  "targets if not.")
+		    ->excludes(arguments);
+
+		auto* instance = addInstanceSelection(sub);
+		addConfigSelection(sub)->excludes(instance);
+		addLoggingOptions(sub, false, true);
+
+		sub->require_option();
+
+		state.subcommand.msg = sub;
+	}
+
+	CLI11_PARSE(cli, argc, argv);
+
+	return 0;
+}
+
+} // namespace qs::launch
diff --git a/src/main.cpp b/src/main.cpp
index f18c2341..e0ce937f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,1032 +1,3 @@
-#include <algorithm>
-#include <array>
-#include <cerrno>
-#include <cstdio>
-#include <cstdlib>
-#include <cstring>
-#include <limits>
-#include <string>
-#include <vector>
-
-#include <CLI/App.hpp>
-#include <CLI/CLI.hpp> // NOLINT: Need to include this for impls of some CLI11 classes
-#include <CLI/Validators.hpp>
-#include <fcntl.h>
-#include <qapplication.h>
-#include <qconfig.h>
-#include <qcontainerfwd.h>
-#include <qcoreapplication.h>
-#include <qcryptographichash.h>
-#include <qdatastream.h>
-#include <qdatetime.h>
-#include <qdir.h>
-#include <qfileinfo.h>
-#include <qguiapplication.h>
-#include <qhash.h>
-#include <qicon.h>
-#include <qjsonarray.h>
-#include <qjsondocument.h>
-#include <qjsonobject.h>
-#include <qlist.h>
-#include <qlogging.h>
-#include <qloggingcategory.h>
-#include <qnamespace.h>
-#include <qqmldebug.h>
-#include <qquickwindow.h>
-#include <qstandardpaths.h>
-#include <qstring.h>
-#include <qtenvironmentvariables.h>
-#include <qtextstream.h>
-#include <qtversion.h>
-#include <unistd.h>
-
-#include "build.hpp"
-#include "core/common.hpp"
-#include "core/instanceinfo.hpp"
-#include "core/logging.hpp"
-#include "core/paths.hpp"
-#include "core/plugin.hpp"
-#include "core/rootwrapper.hpp"
-#include "io/ipccomm.hpp"
-#include "ipc/ipc.hpp"
-
-#if CRASH_REPORTER
-#include "crash/handler.hpp"
-#include "crash/main.hpp"
-#endif
-
-namespace qs::launch {
-
-using qs::ipc::IpcClient;
-
-void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication);
-int runCommand(int argc, char** argv, QCoreApplication* coreApplication);
-
-int DAEMON_PIPE = -1; // NOLINT
-void exitDaemon(int code) {
-	if (DAEMON_PIPE == -1) return;
-
-	if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) {
-		qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": "
-		                      << qt_error_string();
-	}
-
-	close(DAEMON_PIPE);
-
-	close(STDIN_FILENO);
-	close(STDOUT_FILENO);
-	close(STDERR_FILENO);
-
-	if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT
-		qFatal() << "Failed to open /dev/null on stdin";
-	}
-
-	if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT
-		qFatal() << "Failed to open /dev/null on stdout";
-	}
-
-	if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT
-		qFatal() << "Failed to open /dev/null on stderr";
-	}
-}
-
-int main(int argc, char** argv) {
-	QCoreApplication::setApplicationName("quickshell");
-
-#if CRASH_REPORTER
-	qsCheckCrash(argc, argv);
-#endif
-
-	auto qArgC = 1;
-	auto* coreApplication = new QCoreApplication(qArgC, argv);
-
-	checkCrashRelaunch(argv, coreApplication);
-	auto code = runCommand(argc, argv, coreApplication);
-
-	exitDaemon(code);
-	return code;
-}
-
-class QStringOption {
-public:
-	QStringOption() = default;
-	QStringOption& operator=(const std::string& str) {
-		this->str = QString::fromStdString(str);
-		return *this;
-	}
-
-	QString& operator*() { return this->str; }
-	QString* operator->() { return &this->str; }
-
-private:
-	QString str;
-};
-
-struct CommandState {
-	struct {
-		int argc = 0;
-		char** argv = nullptr;
-	} exec;
-
-	struct {
-		bool timestamp = false;
-		bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR");
-		bool sparse = false;
-		size_t verbosity = 0;
-		int tail = 0;
-		bool follow = false;
-		QStringOption rules;
-		QStringOption readoutRules;
-		QStringOption file;
-	} log;
-
-	struct {
-		QStringOption path;
-		QStringOption manifest;
-		QStringOption name;
-	} config;
-
-	struct {
-		int port = -1;
-		bool wait = false;
-	} debug;
-
-	struct {
-		QStringOption id;
-		pid_t pid = -1; // NOLINT (include)
-		bool all = false;
-	} instance;
-
-	struct {
-		bool json = false;
-	} output;
-
-	struct {
-		bool info = false;
-		QStringOption target;
-		QStringOption function;
-		std::vector<QStringOption> arguments;
-	} ipc;
-
-	struct {
-		CLI::App* log = nullptr;
-		CLI::App* list = nullptr;
-		CLI::App* kill = nullptr;
-		CLI::App* msg = nullptr;
-	} subcommand;
-
-	struct {
-		bool checkCompat = false;
-		bool printVersion = false;
-		bool killAll = false;
-		bool noDuplicate = false;
-		bool daemonize = false;
-	} misc;
-};
-
-int readLogFile(CommandState& cmd);
-int listInstances(CommandState& cmd);
-int killInstances(CommandState& cmd);
-int msgInstance(CommandState& cmd);
-int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication);
-
-struct LaunchArgs {
-	QString configPath;
-	int debugPort = -1;
-	bool waitForDebug = false;
-};
-
-int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication);
-
-int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
-	auto state = CommandState();
-
-	state.exec = {
-	    .argc = argc,
-	    .argv = argv,
-	};
-
-	auto addConfigSelection = [&](CLI::App* cmd) {
-		auto* group = cmd->add_option_group("Config Selection")
-		                  ->description("If no options in this group are specified,\n"
-		                                "$XDG_CONFIG_HOME/quickshell/shell.qml will be used.");
-
-		auto* path = group->add_option("-p,--path", state.config.path)
-		                 ->description("Path to a QML file.")
-		                 ->envname("QS_CONFIG_PATH");
-
-		group->add_option("-m,--manifest", state.config.manifest)
-		    ->description("Path to a quickshell manifest.\n"
-		                  "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf")
-		    ->envname("QS_MANIFEST")
-		    ->excludes(path);
-
-		group->add_option("-c,--config", state.config.name)
-		    ->description("Name of a quickshell configuration to run.\n"
-		                  "If -m is specified, this is a configuration in the manifest,\n"
-		                  "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.")
-		    ->envname("QS_CONFIG_NAME");
-
-		return group;
-	};
-
-	auto addDebugOptions = [&](CLI::App* cmd) {
-		auto* group = cmd->add_option_group("Debugging", "Options for QML debugging.");
-
-		auto* debug = group->add_option("--debug", state.debug.port)
-		                  ->description("Open the given port for a QML debugger connection.")
-		                  ->check(CLI::Range(0, 65535));
-
-		group->add_flag("--waitfordebug", state.debug.wait)
-		    ->description("Wait for a QML debugger to connect before executing the configuration.")
-		    ->needs(debug);
-
-		return group;
-	};
-
-	auto addLoggingOptions = [&](CLI::App* cmd, bool noGroup, bool noDisplay = false) {
-		auto* group = noGroup ? cmd : cmd->add_option_group(noDisplay ? "" : "Logging");
-
-		group->add_flag("--no-color", state.log.noColor)
-		    ->description("Disables colored logging.\n"
-		                  "Colored logging can also be disabled by specifying a non empty value\n"
-		                  "for the NO_COLOR environment variable.");
-
-		group->add_flag("--log-times", state.log.timestamp)
-		    ->description("Log timestamps with each message.");
-
-		group->add_option("--log-rules", state.log.rules)
-		    ->description("Log rules to apply, in the format of QT_LOGGING_RULES.");
-
-		group->add_flag("-v,--verbose", [&](size_t count) { state.log.verbosity = count; })
-		    ->description("Increases log verbosity.\n"
-		                  "-v will show INFO level internal logs.\n"
-		                  "-vv will show DEBUG level internal logs.");
-
-		auto* hgroup = cmd->add_option_group("");
-		hgroup->add_flag("--no-detailed-logs", state.log.sparse);
-	};
-
-	auto addInstanceSelection = [&](CLI::App* cmd) {
-		auto* group = cmd->add_option_group("Instance Selection");
-
-		group->add_option("-i,--id", state.instance.id)
-		    ->description("The instance id to operate on.\n"
-		                  "You may also use a substring the id as long as it is unique,\n"
-		                  "for example \"abc\" will select \"abcdefg\".");
-
-		group->add_option("--pid", state.instance.pid)
-		    ->description("The process id of the instance to operate on.");
-
-		return group;
-	};
-
-	auto cli = CLI::App();
-
-	// Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands.
-	cli.require_subcommand(0, 1);
-
-	addConfigSelection(&cli);
-	addLoggingOptions(&cli, false);
-	addDebugOptions(&cli);
-
-	{
-		cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat);
-
-		cli.add_flag("-V,--version", state.misc.printVersion)
-		    ->description("Print quickshell's version and exit.");
-
-		cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate)
-		    ->description("Exit immediately if another instance of the given config is running.");
-
-		cli.add_flag("-d,--daemonize", state.misc.daemonize)
-		    ->description("Detach from the controlling terminal.");
-	}
-
-	{
-		auto* sub = cli.add_subcommand("log", "Print quickshell logs.");
-
-		auto* file = sub->add_option("file", state.log.file, "Log file to read.");
-
-		sub->add_option("-t,--tail", state.log.tail)
-		    ->description("Maximum number of lines to print, starting from the bottom.")
-		    ->check(CLI::Range(1, std::numeric_limits<int>::max(), "INT > 0"));
-
-		sub->add_flag("-f,--follow", state.log.follow)
-		    ->description("Keep reading the log until the logging process terminates.");
-
-		sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.")
-		    ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES.");
-
-		auto* instance = addInstanceSelection(sub)->excludes(file);
-		addConfigSelection(sub)->excludes(instance)->excludes(file);
-		addLoggingOptions(sub, false);
-
-		state.subcommand.log = sub;
-	}
-
-	{
-		auto* sub = cli.add_subcommand("list", "List running quickshell instances.");
-
-		auto* all = sub->add_flag("-a,--all", state.instance.all)
-		                ->description("List all instances.\n"
-		                              "If unspecified, only instances of"
-		                              "the selected config will be listed.");
-
-		sub->add_flag("-j,--json", state.output.json, "Output the list as a json.");
-
-		addConfigSelection(sub)->excludes(all);
-		addLoggingOptions(sub, false, true);
-
-		state.subcommand.list = sub;
-	}
-
-	{
-		auto* sub = cli.add_subcommand("kill", "Kill quickshell instances.");
-		//sub->add_flag("-a,--all", "Kill all matching instances instead of just one.");
-		auto* instance = addInstanceSelection(sub);
-		addConfigSelection(sub)->excludes(instance);
-		addLoggingOptions(sub, false, true);
-
-		state.subcommand.kill = sub;
-	}
-
-	{
-		auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option();
-
-		auto* target = sub->add_option("target", state.ipc.target, "The target to message.");
-
-		auto* function = sub->add_option("function", state.ipc.function)
-		                     ->description("The function to call in the target.")
-		                     ->needs(target);
-
-		auto* arguments = sub->add_option("arguments", state.ipc.arguments)
-		                      ->description("Arguments to the called function.")
-		                      ->needs(function)
-		                      ->allow_extra_args();
-
-		sub->add_flag("-s,--show", state.ipc.info)
-		    ->description("Print information about a function or target if given, or all available "
-		                  "targets if not.")
-		    ->excludes(arguments);
-
-		auto* instance = addInstanceSelection(sub);
-		addConfigSelection(sub)->excludes(instance);
-		addLoggingOptions(sub, false, true);
-
-		sub->require_option();
-
-		state.subcommand.msg = sub;
-	}
-
-	CLI11_PARSE(cli, argc, argv);
-
-	if (state.misc.checkCompat) {
-		if (strcmp(qVersion(), QT_VERSION_STR) != 0) {
-			QTextStream(stdout) << "\033[31mCOMPATIBILITY WARNING: Quickshell was built against Qt "
-			                    << QT_VERSION_STR << " but the system has updated to Qt " << qVersion()
-			                    << " without rebuilding the package. This is likely to cause crashes, so "
-			                       "you must rebuild the quickshell package.\n";
-			return 1;
-		}
-
-		return 0;
-	}
-
-	// Has to happen before extra threads are spawned.
-	if (state.misc.daemonize) {
-		auto closepipes = std::array<int, 2>();
-		if (pipe(closepipes.data()) == -1) {
-			qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno
-			                   << ": " << qt_error_string();
-		}
-
-		DAEMON_PIPE = closepipes[1];
-
-		pid_t pid = fork(); // NOLINT (include)
-
-		if (pid == -1) {
-			qFatal().nospace() << "Failed to fork daemon with error " << errno << ": "
-			                   << qt_error_string();
-		} else if (pid == 0) {
-			close(closepipes[0]);
-
-			if (setsid() == -1) {
-				qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string();
-			}
-		} else {
-			close(closepipes[1]);
-
-			int ret = 0;
-			if (read(closepipes[0], &ret, sizeof(int)) == -1) {
-				qFatal() << "Failed to wait for daemon launch (it may have crashed)";
-			}
-
-			return ret;
-		}
-	}
-
-	{
-		auto level = state.log.verbosity == 0 ? QtWarningMsg
-		           : state.log.verbosity == 1 ? QtInfoMsg
-		                                      : QtDebugMsg;
-
-		LogManager::init(
-		    !state.log.noColor,
-		    state.log.timestamp,
-		    state.log.sparse,
-		    level,
-		    *state.log.rules,
-		    *state.subcommand.log ? "READER" : ""
-		);
-	}
-
-	if (state.misc.printVersion) {
-		qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION
-		                                    << ", distributed by: " << DISTRIBUTOR;
-
-		if (state.log.verbosity > 1) {
-			qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;
-			qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion();
-			qCInfo(logBare).noquote() << "Compiler:" << COMPILER;
-			qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS;
-		}
-
-		if (state.log.verbosity > 0) {
-			qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE;
-			qCInfo(logBare).noquote() << "Build configuration:";
-			qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION;
-		}
-	} else if (*state.subcommand.log) {
-		return readLogFile(state);
-	} else if (*state.subcommand.list) {
-		return listInstances(state);
-	} else if (*state.subcommand.kill) {
-		return killInstances(state);
-	} else if (*state.subcommand.msg) {
-		return msgInstance(state);
-	} else {
-		if (strcmp(qVersion(), QT_VERSION_STR) != 0) {
-			qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR
-			           << "but the system has updated to Qt" << qVersion()
-			           << "without rebuilding the package. This is likely to cause crashes, so "
-			              "the quickshell package must be rebuilt.\n";
-		}
-
-		return launchFromCommand(state, coreApplication);
-	}
-
-	return 0;
-}
-
-int locateConfigFile(CommandState& cmd, QString& path) {
-	if (!cmd.config.path->isEmpty()) {
-		path = *cmd.config.path;
-	} else {
-		auto manifestPath = *cmd.config.manifest;
-		if (manifestPath.isEmpty()) {
-			auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
-			auto path = configDir.filePath("manifest.conf");
-			if (QFileInfo(path).isFile()) manifestPath = path;
-		}
-
-		if (!manifestPath.isEmpty()) {
-			auto file = QFile(manifestPath);
-			if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
-				auto stream = QTextStream(&file);
-				while (!stream.atEnd()) {
-					auto line = stream.readLine();
-					if (line.trimmed().startsWith("#")) continue;
-					if (line.trimmed().isEmpty()) continue;
-
-					auto split = line.split('=');
-					if (split.length() != 2) {
-						qCritical() << "Manifest line not in expected format 'name = relativepath':" << line;
-						return -1;
-					}
-
-					if (split[0].trimmed() == *cmd.config.name) {
-						path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed());
-						break;
-					}
-				}
-
-				if (path.isEmpty()) {
-					qCCritical(logBare) << "Configuration" << *cmd.config.name
-					                    << "not found when searching manifest" << manifestPath;
-					return -1;
-				}
-			} else {
-				qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest;
-				return -1;
-			}
-		} else {
-			auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
-
-			if (cmd.config.name->isEmpty()) {
-				path = configDir.path();
-			} else {
-				path = configDir.filePath(*cmd.config.name);
-			}
-		}
-	}
-
-	if (QFileInfo(path).isDir()) {
-		path = QDir(path).filePath("shell.qml");
-	}
-
-	if (!QFileInfo(path).isFile()) {
-		qCCritical(logBare) << "Could not open config file at" << path;
-		return -1;
-	}
-
-	path = QFileInfo(path).canonicalFilePath();
-
-	return 0;
-}
-
-void sortInstances(QVector<InstanceLockInfo>& list) {
-	std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) {
-		return a.instance.launchTime < b.instance.launchTime;
-	});
-};
-
-int selectInstance(CommandState& cmd, InstanceLockInfo* instance) {
-	auto* basePath = QsPaths::instance()->baseRunDir();
-	if (!basePath) return -1;
-
-	QString path;
-
-	if (cmd.instance.pid != -1) {
-		path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid));
-		if (!QsPaths::checkLock(path, instance)) {
-			qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid;
-			return -1;
-		}
-	} else if (!cmd.instance.id->isEmpty()) {
-		path = basePath->filePath("by-pid");
-		auto instances = QsPaths::collectInstances(path);
-
-		auto itr =
-		    std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) {
-			    return !info.instance.instanceId.startsWith(*cmd.instance.id);
-		    });
-
-		instances.erase(itr, instances.end());
-
-		if (instances.isEmpty()) {
-			qCInfo(logBare) << "No running instances start with" << *cmd.instance.id;
-			return -1;
-		} else if (instances.length() != 1) {
-			qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id;
-
-			for (auto& instance: instances) {
-				qCInfo(logBare).noquote() << " -" << instance.instance.instanceId;
-			}
-
-			return -1;
-		} else {
-			*instance = instances.value(0);
-		}
-	} else {
-		QString configFilePath;
-		auto r = locateConfigFile(cmd, configFilePath);
-		if (r != 0) return r;
-
-		auto pathId =
-		    QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex();
-
-		path = QDir(basePath->filePath("by-path")).filePath(pathId);
-
-		auto instances = QsPaths::collectInstances(path);
-		sortInstances(instances);
-
-		if (instances.isEmpty()) {
-			qCInfo(logBare) << "No running instances for" << configFilePath;
-			return -1;
-		}
-
-		*instance = instances.value(0);
-	}
-
-	return 0;
-}
-
-int readLogFile(CommandState& cmd) {
-	auto path = *cmd.log.file;
-
-	if (path.isEmpty()) {
-		InstanceLockInfo instance;
-		auto r = selectInstance(cmd, &instance);
-		if (r != 0) return r;
-
-		path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog");
-	}
-
-	auto file = QFile(path);
-	if (!file.open(QFile::ReadOnly)) {
-		qCCritical(logBare) << "Failed to open log file" << path;
-		return -1;
-	}
-
-	return qs::log::readEncodedLogs(
-	           &file,
-	           path,
-	           cmd.log.timestamp,
-	           cmd.log.tail,
-	           cmd.log.follow,
-	           *cmd.log.readoutRules
-	       )
-	         ? 0
-	         : -1;
-}
-
-int listInstances(CommandState& cmd) {
-	auto* basePath = QsPaths::instance()->baseRunDir();
-	if (!basePath) return -1; // NOLINT
-
-	QString path;
-	QString configFilePath;
-	if (cmd.instance.all) {
-		path = basePath->filePath("by-pid");
-	} else {
-		auto r = locateConfigFile(cmd, configFilePath);
-
-		if (r != 0) {
-			qCInfo(logBare) << "Use --all to list all instances.";
-			return r;
-		}
-
-		auto pathId =
-		    QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex();
-
-		path = QDir(basePath->filePath("by-path")).filePath(pathId);
-	}
-
-	auto instances = QsPaths::collectInstances(path);
-
-	if (instances.isEmpty()) {
-		if (cmd.instance.all) {
-			qCInfo(logBare) << "No running instances.";
-		} else {
-			qCInfo(logBare) << "No running instances for" << configFilePath;
-			qCInfo(logBare) << "Use --all to list all instances.";
-		}
-	} else {
-		sortInstances(instances);
-
-		if (cmd.output.json) {
-			auto array = QJsonArray();
-
-			for (auto& instance: instances) {
-				auto json = QJsonObject();
-
-				json["id"] = instance.instance.instanceId;
-				json["pid"] = instance.pid;
-				json["shell_id"] = instance.instance.shellId;
-				json["config_path"] = instance.instance.configPath;
-				json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate);
-
-				array.push_back(json);
-			}
-
-			auto document = QJsonDocument(array);
-			QTextStream(stdout) << document.toJson(QJsonDocument::Indented);
-		} else {
-			for (auto& instance: instances) {
-				auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss");
-
-				auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime());
-				auto remSeconds = runSeconds % 60;
-				auto runMinutes = (runSeconds - remSeconds) / 60;
-				auto remMinutes = runMinutes % 60;
-				auto runHours = (runMinutes - remMinutes) / 60;
-				auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds")
-				                      .arg(runHours)
-				                      .arg(remMinutes)
-				                      .arg(remSeconds);
-
-				qCInfo(logBare).noquote().nospace()
-				    << "Instance " << instance.instance.instanceId << ":\n"
-				    << "  Process ID: " << instance.pid << '\n'
-				    << "  Shell ID: " << instance.instance.shellId << '\n'
-				    << "  Config path: " << instance.instance.configPath << '\n'
-				    << "  Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n";
-			}
-		}
-	}
-
-	return 0;
-}
-
-int killInstances(CommandState& cmd) {
-	InstanceLockInfo instance;
-	auto r = selectInstance(cmd, &instance);
-	if (r != 0) return r;
-
-	return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) {
-		client.kill();
-		qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId;
-	});
-}
-
-int msgInstance(CommandState& cmd) {
-	InstanceLockInfo instance;
-	auto r = selectInstance(cmd, &instance);
-	if (r != 0) return r;
-
-	return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) {
-		if (cmd.ipc.info) {
-			return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function);
-		} else {
-			QVector<QString> arguments;
-			for (auto& arg: cmd.ipc.arguments) {
-				arguments += *arg;
-			}
-
-			return qs::io::ipc::comm::callFunction(
-			    &client,
-			    *cmd.ipc.target,
-			    *cmd.ipc.function,
-			    arguments
-			);
-		}
-
-		return -1;
-	});
-}
-
-template <typename T>
-QString base36Encode(T number) {
-	const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz";
-	QString result;
-
-	do {
-		result.prepend(digits[number % 36]);
-		number /= 36;
-	} while (number > 0);
-
-	for (auto i = 0; i < result.length() / 2; i++) {
-		auto opposite = result.length() - i - 1;
-		auto c = result.at(i);
-		result[i] = result.at(opposite);
-		result[opposite] = c;
-	}
-
-	return result;
-}
-
-void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) {
-#if CRASH_REPORTER
-	auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD");
-
-	if (!lastInfoFdStr.isEmpty()) {
-		auto lastInfoFd = lastInfoFdStr.toInt();
-
-		QFile file;
-		file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle);
-		file.seek(0);
-
-		auto ds = QDataStream(&file);
-		RelaunchInfo info;
-		ds >> info;
-
-		LogManager::init(
-		    !info.noColor,
-		    info.timestamp,
-		    info.sparseLogsOnly,
-		    info.defaultLogLevel,
-		    info.logRules
-		);
-
-		qCritical().nospace() << "Quickshell has crashed under pid "
-		                      << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt()
-		                      << " (Coredumps will be available under that pid.)";
-
-		qCritical() << "Further crash information is stored under"
-		            << QsPaths::crashDir(info.instance.instanceId).path();
-
-		if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) {
-			qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid "
-			               "a crash loop.";
-			exit(-1); // NOLINT
-		} else {
-			qCritical() << "Quickshell has been restarted.";
-
-			launch({.configPath = info.instance.configPath}, argv, coreApplication);
-		}
-	}
-#endif
-}
-
-int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) {
-	QString configPath;
-
-	auto r = locateConfigFile(cmd, configPath);
-	if (r != 0) return r;
-
-	{
-		InstanceLockInfo info;
-		if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) {
-			qCInfo(logBare) << "An instance of this configuration is already running.";
-			return 0;
-		}
-	}
-
-	return launch(
-	    {
-	        .configPath = configPath,
-	        .debugPort = cmd.debug.port,
-	        .waitForDebug = cmd.debug.wait,
-	    },
-	    cmd.exec.argv,
-	    coreApplication
-	);
-}
-
-int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) {
-	auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex();
-	auto shellId = QString(pathId);
-
-	qInfo() << "Launching config:" << args.configPath;
-
-	auto file = QFile(args.configPath);
-	if (!file.open(QFile::ReadOnly | QFile::Text)) {
-		qCritical() << "Could not open config file" << args.configPath;
-		return -1;
-	}
-
-	struct {
-		bool useQApplication = false;
-		bool nativeTextRendering = false;
-		bool desktopSettingsAware = true;
-		QString iconTheme = qEnvironmentVariable("QS_ICON_THEME");
-		QHash<QString, QString> envOverrides;
-	} pragmas;
-
-	auto stream = QTextStream(&file);
-	while (!stream.atEnd()) {
-		auto line = stream.readLine().trimmed();
-		if (line.startsWith("//@ pragma ")) {
-			auto pragma = line.sliced(11).trimmed();
-
-			if (pragma == "UseQApplication") pragmas.useQApplication = true;
-			else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true;
-			else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false;
-			else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10);
-			else if (pragma.startsWith("Env ")) {
-				auto envPragma = pragma.sliced(4);
-				auto splitIdx = envPragma.indexOf('=');
-
-				if (splitIdx == -1) {
-					qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'";
-					return -1;
-				}
-
-				auto var = envPragma.sliced(0, splitIdx).trimmed();
-				auto val = envPragma.sliced(splitIdx + 1).trimmed();
-				pragmas.envOverrides.insert(var, val);
-			} else if (pragma.startsWith("ShellId ")) {
-				shellId = pragma.sliced(8).trimmed();
-			} else {
-				qCritical() << "Unrecognized pragma" << pragma;
-				return -1;
-			}
-		} else if (line.startsWith("import")) break;
-	}
-
-	file.close();
-
-	if (!pragmas.iconTheme.isEmpty()) {
-		QIcon::setThemeName(pragmas.iconTheme);
-	}
-
-	qInfo() << "Shell ID:" << shellId << "Path ID" << pathId;
-
-	auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch();
-	InstanceInfo::CURRENT = InstanceInfo {
-	    .instanceId = base36Encode(getpid()) + base36Encode(launchTime),
-	    .configPath = args.configPath,
-	    .shellId = shellId,
-	    .launchTime = qs::Common::LAUNCH_TIME,
-	};
-
-#if CRASH_REPORTER
-	auto crashHandler = crash::CrashHandler();
-	crashHandler.init();
-
-	{
-		auto* log = LogManager::instance();
-		crashHandler.setRelaunchInfo({
-		    .instance = InstanceInfo::CURRENT,
-		    .noColor = !log->colorLogs,
-		    .timestamp = log->timestampLogs,
-		    .sparseLogsOnly = log->isSparse(),
-		    .defaultLogLevel = log->defaultLevel(),
-		    .logRules = log->rulesString(),
-		});
-	}
-#endif
-
-	QsPaths::init(shellId, pathId);
-	QsPaths::instance()->linkRunDir();
-	QsPaths::instance()->linkPathDir();
-	LogManager::initFs();
-
-	for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) {
-		qputenv(var.toUtf8(), val.toUtf8());
-	}
-
-	// The qml engine currently refuses to cache non file (qsintercept) paths.
-
-	// if (auto* cacheDir = QsPaths::instance()->cacheDir()) {
-	// 	auto qmlCacheDir = cacheDir->filePath("qml-engine-cache");
-	// 	qputenv("QML_DISK_CACHE_PATH", qmlCacheDir.toLocal8Bit());
-	//
-	// 	if (!qEnvironmentVariableIsSet("QML_DISK_CACHE")) {
-	// 		qputenv("QML_DISK_CACHE", "aot,qmlc");
-	// 	}
-	// }
-
-	// While the simple animation driver can lead to better animations in some cases,
-	// it also can cause excessive repainting at excessively high framerates which can
-	// lead to noticeable amounts of gpu usage, including overheating on some systems.
-	// This gets worse the more windows are open, as repaints trigger on all of them for
-	// some reason. See QTBUG-126099 for details.
-
-	// if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) {
-	// 	qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1");
-	// }
-
-	// Some programs place icons in the pixmaps folder instead of the icons folder.
-	// This seems to be controlled by the QPA and qt6ct does not provide it.
-	{
-		QList<QString> dataPaths;
-
-		if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
-			auto var = qEnvironmentVariable("XDG_DATA_DIRS");
-			dataPaths = var.split(u':', Qt::SkipEmptyParts);
-		} else {
-			dataPaths.push_back("/usr/local/share");
-			dataPaths.push_back("/usr/share");
-		}
-
-		auto fallbackPaths = QIcon::fallbackSearchPaths();
-
-		for (auto& path: dataPaths) {
-			auto newPath = QDir(path).filePath("pixmaps");
-
-			if (!fallbackPaths.contains(newPath)) {
-				fallbackPaths.push_back(newPath);
-			}
-		}
-
-		QIcon::setFallbackSearchPaths(fallbackPaths);
-	}
-
-	QGuiApplication::setDesktopSettingsAware(pragmas.desktopSettingsAware);
-
-	delete coreApplication;
-
-	QGuiApplication* app = nullptr;
-	auto qArgC = 0;
-
-	if (pragmas.useQApplication) {
-		app = new QApplication(qArgC, argv);
-	} else {
-		app = new QGuiApplication(qArgC, argv);
-	}
-
-	if (args.debugPort != -1) {
-		QQmlDebuggingEnabler::enableDebugging(true);
-		auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient
-		                              : QQmlDebuggingEnabler::DoNotWaitForClient;
-		QQmlDebuggingEnabler::startTcpDebugServer(args.debugPort, wait);
-	}
-
-	QuickshellPlugin::initPlugins();
-
-	// Base window transparency appears to be additive.
-	// Use a fully transparent window with a colored rect.
-	QQuickWindow::setDefaultAlphaBuffer(true);
-
-	if (pragmas.nativeTextRendering) {
-		QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
-	}
-
-	qs::ipc::IpcServer::start();
-	QsPaths::instance()->createLock();
-
-	auto root = RootWrapper(args.configPath, shellId);
-	QGuiApplication::setQuitOnLastWindowClosed(false);
-
-	exitDaemon(0);
-
-	auto code = QGuiApplication::exec();
-	delete app;
-	return code;
-}
-
-} // namespace qs::launch
+#include "launch/main.hpp"
 
 int main(int argc, char** argv) { return qs::launch::main(argc, argv); }
diff --git a/src/services/greetd/CMakeLists.txt b/src/services/greetd/CMakeLists.txt
index 870f8085..2252f8cf 100644
--- a/src/services/greetd/CMakeLists.txt
+++ b/src/services/greetd/CMakeLists.txt
@@ -11,9 +11,9 @@ qt_add_qml_module(quickshell-service-greetd
 
 install_qml_module(quickshell-service-greetd)
 
-target_link_libraries(quickshell-service-greetd PRIVATE ${QT_DEPS})
+# can't be Qt::Qml because generation.hpp pulls in gui types
+target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick)
 
-qs_pch(quickshell-service-greetd)
-qs_pch(quickshell-service-greetdplugin)
+qs_module_pch(quickshell-service-greetd)
 
 target_link_libraries(quickshell PRIVATE quickshell-service-greetdplugin)
diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt
index 505df7a6..122a0c5c 100644
--- a/src/services/mpris/CMakeLists.txt
+++ b/src/services/mpris/CMakeLists.txt
@@ -30,13 +30,15 @@ target_include_directories(quickshell-service-mpris PRIVATE ${CMAKE_CURRENT_BINA
 qt_add_qml_module(quickshell-service-mpris
 	URI Quickshell.Services.Mpris
   VERSION 0.1
-	DEPENDENCIES QtQml Quickshell
+	DEPENDENCIES QtQml
 )
 
+qs_add_module_deps_light(quickshell-service-mpris Quickshell)
+
 install_qml_module(quickshell-service-mpris)
 
-target_link_libraries(quickshell-service-mpris PRIVATE ${QT_DEPS} quickshell-dbus)
-target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin)
+target_link_libraries(quickshell-service-mpris PRIVATE Qt::Qml Qt::DBus)
 
-qs_pch(quickshell-service-mpris)
-qs_pch(quickshell-service-mprisplugin)
+qs_module_pch(quickshell-service-mpris SET dbus)
+
+target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin)
diff --git a/src/services/notifications/CMakeLists.txt b/src/services/notifications/CMakeLists.txt
index 4ba8d3cc..0cbb42eb 100644
--- a/src/services/notifications/CMakeLists.txt
+++ b/src/services/notifications/CMakeLists.txt
@@ -20,13 +20,13 @@ target_include_directories(quickshell-service-notifications PRIVATE ${CMAKE_CURR
 qt_add_qml_module(quickshell-service-notifications
 	URI Quickshell.Services.Notifications
 	VERSION 0.1
-	DEPENDENCIES QtQml Quickshell
 )
 
+qs_add_module_deps_light(quickshell-service-notifications Quickshell)
+
 install_qml_module(quickshell-service-notifications)
 
-target_link_libraries(quickshell-service-notifications PRIVATE ${QT_DEPS} quickshell-dbus)
+target_link_libraries(quickshell-service-notifications PRIVATE Qt::Quick Qt::DBus)
 target_link_libraries(quickshell PRIVATE quickshell-service-notificationsplugin)
 
-qs_pch(quickshell-service-notifications)
-qs_pch(quickshell-service-notificationsplugin)
+qs_module_pch(quickshell-service-notifications SET dbus)
diff --git a/src/services/pam/CMakeLists.txt b/src/services/pam/CMakeLists.txt
index f9d017e7..c35e74af 100644
--- a/src/services/pam/CMakeLists.txt
+++ b/src/services/pam/CMakeLists.txt
@@ -13,9 +13,8 @@ qt_add_qml_module(quickshell-service-pam
 
 install_qml_module(quickshell-service-pam)
 
-target_link_libraries(quickshell-service-pam PRIVATE ${QT_DEPS} pam ${PAM_LIBRARIES})
+target_link_libraries(quickshell-service-pam PRIVATE Qt::Qml pam ${PAM_LIBRARIES})
 
-qs_pch(quickshell-service-pam)
-qs_pch(quickshell-service-pamplugin)
+qs_module_pch(quickshell-service-pam)
 
 target_link_libraries(quickshell PRIVATE quickshell-service-pamplugin)
diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt
index bb74a078..35aaa137 100644
--- a/src/services/pipewire/CMakeLists.txt
+++ b/src/services/pipewire/CMakeLists.txt
@@ -16,14 +16,17 @@ qt_add_library(quickshell-service-pipewire STATIC
 qt_add_qml_module(quickshell-service-pipewire
 	URI Quickshell.Services.Pipewire
 	VERSION 0.1
-	DEPENDENCIES QtQml Quickshell
+	DEPENDENCIES QtQml
 )
 
+qs_add_module_deps_light(quickshell-service-pipewire Quickshell)
+
 install_qml_module(quickshell-service-pipewire)
 
-target_link_libraries(quickshell-service-pipewire PRIVATE ${QT_DEPS} PkgConfig::pipewire)
+target_link_libraries(quickshell-service-pipewire PRIVATE
+	Qt::Qml PkgConfig::pipewire
+)
 
-qs_pch(quickshell-service-pipewire)
-qs_pch(quickshell-service-pipewireplugin)
+qs_module_pch(quickshell-service-pipewire)
 
 target_link_libraries(quickshell PRIVATE quickshell-service-pipewireplugin)
diff --git a/src/services/status_notifier/CMakeLists.txt b/src/services/status_notifier/CMakeLists.txt
index 20de11a1..7e0bf2b9 100644
--- a/src/services/status_notifier/CMakeLists.txt
+++ b/src/services/status_notifier/CMakeLists.txt
@@ -41,13 +41,14 @@ target_include_directories(quickshell-service-statusnotifier PRIVATE ${CMAKE_CUR
 qt_add_qml_module(quickshell-service-statusnotifier
   URI Quickshell.Services.SystemTray
   VERSION 0.1
-	DEPENDENCIES QtQml Quickshell Quickshell.DBusMenu
+	DEPENDENCIES QtQml
 )
 
+qs_add_module_deps_light(quickshell-service-statusnotifier Quickshell Quickshell.DBusMenu)
+
 install_qml_module(quickshell-service-statusnotifier)
 
-target_link_libraries(quickshell-service-statusnotifier PRIVATE ${QT_DEPS} quickshell-dbus quickshell-dbusmenuplugin)
+target_link_libraries(quickshell-service-statusnotifier PRIVATE Qt::Quick Qt::DBus)
 target_link_libraries(quickshell PRIVATE quickshell-service-statusnotifierplugin)
 
-qs_pch(quickshell-service-statusnotifier)
-qs_pch(quickshell-service-statusnotifierplugin)
+qs_module_pch(quickshell-service-statusnotifier SET dbus)
diff --git a/src/services/upower/CMakeLists.txt b/src/services/upower/CMakeLists.txt
index e913a550..fd0da2af 100644
--- a/src/services/upower/CMakeLists.txt
+++ b/src/services/upower/CMakeLists.txt
@@ -30,13 +30,14 @@ target_include_directories(quickshell-service-upower PRIVATE ${CMAKE_CURRENT_BIN
 qt_add_qml_module(quickshell-service-upower
 	URI Quickshell.Services.UPower
   VERSION 0.1
-	DEPENDENCIES QtQml Quickshell
+	DEPENDENCIES QtQml
 )
 
+qs_add_module_deps_light(quickshell-service-upower Quickshell)
+
 install_qml_module(quickshell-service-upower)
 
-target_link_libraries(quickshell-service-upower PRIVATE ${QT_DEPS} quickshell-dbus)
+target_link_libraries(quickshell-service-upower PRIVATE Qt::Qml Qt::DBus quickshell-dbus)
 target_link_libraries(quickshell PRIVATE quickshell-service-upowerplugin)
 
-qs_pch(quickshell-service-upower)
-qs_pch(quickshell-service-upowerplugin)
+qs_module_pch(quickshell-service-upower SET dbus)
diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt
index 19e74b90..8005a833 100644
--- a/src/wayland/CMakeLists.txt
+++ b/src/wayland/CMakeLists.txt
@@ -20,6 +20,14 @@ execute_process(
 
 message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}")
 
+qs_add_pchset(wayland-protocol
+	DEPENDENCIES Qt::Core Qt::WaylandClient Qt::WaylandClientPrivate
+	HEADERS
+		<wayland-client.h>
+		<qbytearray.h>
+		<qstring.h>
+)
+
 function (wl_proto target name path)
 	set(PROTO_BUILD_PATH ${CMAKE_CURRENT_BINARY_DIR}/wl-proto/${name})
 	make_directory(${PROTO_BUILD_PATH})
@@ -53,13 +61,12 @@ function (wl_proto target name path)
 		DEPENDS Qt6::qtwaylandscanner "${path}"
 	)
 
-	add_library(wl-proto-${name}
-		${WS_CLIENT_HEADER} ${WS_CLIENT_CODE}
-		${QWS_CLIENT_HEADER} ${QWS_CLIENT_CODE}
-	)
+	add_library(wl-proto-${name}-wl STATIC ${WS_CLIENT_HEADER} ${WS_CLIENT_CODE})
+	add_library(wl-proto-${name} STATIC ${QWS_CLIENT_HEADER} ${QWS_CLIENT_CODE})
 
 	target_include_directories(wl-proto-${name} INTERFACE ${PROTO_BUILD_PATH})
-	target_link_libraries(wl-proto-${name} Qt6::WaylandClient Qt6::WaylandClientPrivate)
+	target_link_libraries(wl-proto-${name} wl-proto-${name}-wl Qt6::WaylandClient Qt6::WaylandClientPrivate)
+	qs_pch(wl-proto-${name} SET wayland-protocol)
 
 	target_link_libraries(${target} PRIVATE wl-proto-${name})
 endfunction()
@@ -100,20 +107,24 @@ if (HYPRLAND)
 	add_subdirectory(hyprland)
 endif()
 
-target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS})
-target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS})
+# widgets for qmenu
+target_link_libraries(quickshell-wayland PRIVATE
+	Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate
+)
+
+target_link_libraries(quickshell-wayland-init PRIVATE Qt::Quick)
 
 qt_add_qml_module(quickshell-wayland
 	URI Quickshell.Wayland
 	VERSION 0.1
-	DEPENDENCIES QtQuick Quickshell
+	DEPENDENCIES QtQuick
 	IMPORTS ${WAYLAND_MODULES}
 )
 
+qs_add_module_deps_light(quickshell-wayland Quickshell)
+
 install_qml_module(quickshell-wayland)
 
-qs_pch(quickshell-wayland)
-qs_pch(quickshell-waylandplugin)
-qs_pch(quickshell-wayland-init)
+qs_module_pch(quickshell-wayland SET large)
 
 target_link_libraries(quickshell PRIVATE quickshell-waylandplugin quickshell-wayland-init)
diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt
index 59458fe6..cb375358 100644
--- a/src/wayland/hyprland/CMakeLists.txt
+++ b/src/wayland/hyprland/CMakeLists.txt
@@ -27,7 +27,6 @@ qt_add_qml_module(quickshell-hyprland
 
 install_qml_module(quickshell-hyprland)
 
-qs_pch(quickshell-hyprland)
-qs_pch(quickshell-hyprlandplugin)
+# intentionally no pch as the module is empty
 
 target_link_libraries(quickshell PRIVATE quickshell-hyprlandplugin)
diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt
index 0fd1f85e..04b6e0a9 100644
--- a/src/wayland/hyprland/focus_grab/CMakeLists.txt
+++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt
@@ -7,9 +7,11 @@ qt_add_library(quickshell-hyprland-focus-grab STATIC
 qt_add_qml_module(quickshell-hyprland-focus-grab
 	URI Quickshell.Hyprland._FocusGrab
 	VERSION 0.1
-	DEPENDENCIES QtQml Quickshell
+	DEPENDENCIES QtQml
 )
 
+qs_add_module_deps_light(quickshell-hyprland-focus-grab Quickshell)
+
 install_qml_module(quickshell-hyprland-focus-grab)
 
 wl_proto(quickshell-hyprland-focus-grab
@@ -17,9 +19,10 @@ wl_proto(quickshell-hyprland-focus-grab
 	"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml"
 )
 
-target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client)
+target_link_libraries(quickshell-hyprland-focus-grab PRIVATE
+	Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
+)
 
-qs_pch(quickshell-hyprland-focus-grab)
-qs_pch(quickshell-hyprland-focus-grabplugin)
+qs_module_pch(quickshell-hyprland-focus-grab SET large)
 
 target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin)
diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt
index d2314177..8b2aa94f 100644
--- a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt
+++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt
@@ -17,9 +17,10 @@ wl_proto(quickshell-hyprland-global-shortcuts
 	"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml"
 )
 
-target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE ${QT_DEPS} wayland-client)
+target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE
+	Qt::Qml Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
+)
 
-qs_pch(quickshell-hyprland-global-shortcuts)
-qs_pch(quickshell-hyprland-global-shortcutsplugin)
+qs_module_pch(quickshell-hyprland-global-shortcuts)
 
 target_link_libraries(quickshell PRIVATE quickshell-hyprland-global-shortcutsplugin)
diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt
index 367fa8f4..fd1da674 100644
--- a/src/wayland/hyprland/ipc/CMakeLists.txt
+++ b/src/wayland/hyprland/ipc/CMakeLists.txt
@@ -8,14 +8,15 @@ qt_add_library(quickshell-hyprland-ipc STATIC
 qt_add_qml_module(quickshell-hyprland-ipc
 	URI Quickshell.Hyprland._Ipc
 	VERSION 0.1
-	DEPENDENCIES QtQml Quickshell
+	DEPENDENCIES QtQuick
 )
 
+qs_add_module_deps_light(quickshell-hyprland-ipc Quickshell)
+
 install_qml_module(quickshell-hyprland-ipc)
 
-target_link_libraries(quickshell-hyprland-ipc PRIVATE ${QT_DEPS})
+target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick)
 
-qs_pch(quickshell-hyprland-ipc)
-qs_pch(quickshell-hyprland-ipcplugin)
+qs_module_pch(quickshell-hyprland-ipc SET large)
 
 target_link_libraries(quickshell PRIVATE quickshell-hyprland-ipcplugin)
diff --git a/src/wayland/platformmenu.cpp b/src/wayland/platformmenu.cpp
index 80f9854e..e64e8880 100644
--- a/src/wayland/platformmenu.cpp
+++ b/src/wayland/platformmenu.cpp
@@ -8,6 +8,7 @@
 #include <qwindow.h>
 
 #include "../core/platformmenu.hpp"
+#include "../core/platformmenu_p.hpp"
 
 using namespace qs::menu::platform;
 
diff --git a/src/wayland/session_lock/CMakeLists.txt b/src/wayland/session_lock/CMakeLists.txt
index d6224a8b..63dc1295 100644
--- a/src/wayland/session_lock/CMakeLists.txt
+++ b/src/wayland/session_lock/CMakeLists.txt
@@ -8,6 +8,7 @@ qt_add_library(quickshell-wayland-sessionlock STATIC
 
 wl_proto(quickshell-wayland-sessionlock ext-session-lock-v1 "${WAYLAND_PROTOCOLS}/staging/ext-session-lock/ext-session-lock-v1.xml")
 target_link_libraries(quickshell-wayland-sessionlock PRIVATE ${QT_DEPS} wayland-client)
-qs_pch(quickshell-wayland-sessionlock)
+
+qs_pch(quickshell-wayland-sessionlock SET large)
 
 target_link_libraries(quickshell-wayland PRIVATE quickshell-wayland-sessionlock)
diff --git a/src/wayland/toplevel_management/CMakeLists.txt b/src/wayland/toplevel_management/CMakeLists.txt
index 01c9d756..0db82aae 100644
--- a/src/wayland/toplevel_management/CMakeLists.txt
+++ b/src/wayland/toplevel_management/CMakeLists.txt
@@ -7,7 +7,11 @@ qt_add_library(quickshell-wayland-toplevel-management STATIC
 qt_add_qml_module(quickshell-wayland-toplevel-management
 	URI Quickshell.Wayland._ToplevelManagement
 	VERSION 0.1
-	DEPENDENCIES QtQml Quickshell Quickshell.Wayland
+	DEPENDENCIES QtQml
+)
+
+qs_add_module_deps_light(quickshell-wayland-toplevel-management
+	Quickshell Quickshell.Wayland
 )
 
 install_qml_module(quickshell-wayland-toplevel-management)
@@ -17,9 +21,10 @@ wl_proto(quickshell-wayland-toplevel-management
 	"${CMAKE_CURRENT_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1.xml"
 )
 
-target_link_libraries(quickshell-wayland-toplevel-management PRIVATE ${QT_DEPS} wayland-client)
+target_link_libraries(quickshell-wayland-toplevel-management PRIVATE
+	Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
+)
 
-qs_pch(quickshell-wayland-toplevel-management)
-qs_pch(quickshell-wayland-toplevel-managementplugin)
+qs_module_pch(quickshell-wayland-toplevel-management SET large)
 
 target_link_libraries(quickshell PRIVATE quickshell-wayland-toplevel-managementplugin)
diff --git a/src/wayland/wlr_layershell/CMakeLists.txt b/src/wayland/wlr_layershell/CMakeLists.txt
index 640b7ec2..11bedc6a 100644
--- a/src/wayland/wlr_layershell/CMakeLists.txt
+++ b/src/wayland/wlr_layershell/CMakeLists.txt
@@ -7,17 +7,19 @@ qt_add_library(quickshell-wayland-layershell STATIC
 qt_add_qml_module(quickshell-wayland-layershell
 	URI Quickshell.Wayland._WlrLayerShell
 	VERSION 0.1
-	# Quickshell.Wayland currently creates a dependency cycle, add it here once the main
-	# ls class is moved to this module.
-	DEPENDENCIES QtQuick Quickshell
+	DEPENDENCIES QtQuick
 )
 
+qs_add_module_deps_light(quickshell-wayland-layershell Quickshell Quickshell.Wayland)
+
 install_qml_module(quickshell-wayland-layershell)
 
 wl_proto(quickshell-wayland-layershell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/wlr-layer-shell-unstable-v1.xml")
-target_link_libraries(quickshell-wayland-layershell PRIVATE ${QT_DEPS} wayland-client)
 
-qs_pch(quickshell-wayland-layershell)
-qs_pch(quickshell-wayland-layershellplugin)
+target_link_libraries(quickshell-wayland-layershell PRIVATE
+	Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
+)
+
+qs_module_pch(quickshell-wayland-layershell SET large)
 
 target_link_libraries(quickshell-wayland PRIVATE quickshell-wayland-layershellplugin)
diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt
index 06671b13..226d950d 100644
--- a/src/widgets/CMakeLists.txt
+++ b/src/widgets/CMakeLists.txt
@@ -9,7 +9,6 @@ qt_add_qml_module(quickshell-widgets
 
 install_qml_module(quickshell-widgets)
 
-qs_pch(quickshell-widgets)
-qs_pch(quickshell-widgetsplugin)
+qs_module_pch(quickshell-widgets)
 
 target_link_libraries(quickshell PRIVATE quickshell-widgetsplugin)
diff --git a/src/window/CMakeLists.txt b/src/window/CMakeLists.txt
index e7dd1977..89b2233e 100644
--- a/src/window/CMakeLists.txt
+++ b/src/window/CMakeLists.txt
@@ -9,20 +9,22 @@ qt_add_library(quickshell-window STATIC
 qt_add_qml_module(quickshell-window
 	URI Quickshell._Window
 	VERSION 0.1
-	DEPENDENCIES QtQuick Quickshell
+	DEPENDENCIES QtQuick
 )
 
+qs_add_module_deps_light(quickshell-window Quickshell)
+
 install_qml_module(quickshell-window)
 
 add_library(quickshell-window-init OBJECT init.cpp)
 
-target_link_libraries(quickshell-window PRIVATE ${QT_DEPS} Qt6::QuickPrivate)
-target_link_libraries(quickshell-windowplugin PRIVATE ${QT_DEPS})
-target_link_libraries(quickshell-window-init PRIVATE ${QT_DEPS})
+target_link_libraries(quickshell-window PRIVATE
+	Qt::Core Qt::Gui Qt::Quick Qt6::QuickPrivate
+)
 
-qs_pch(quickshell-window)
-qs_pch(quickshell-windowplugin)
-qs_pch(quickshell-window-init)
+target_link_libraries(quickshell-window-init PRIVATE Qt::Qml)
+
+qs_module_pch(quickshell-window SET large)
 
 target_link_libraries(quickshell PRIVATE quickshell-windowplugin quickshell-window-init)
 
diff --git a/src/window/init.cpp b/src/window/init.cpp
index ef2b8c1d..9930b41b 100644
--- a/src/window/init.cpp
+++ b/src/window/init.cpp
@@ -1,3 +1,6 @@
+#include <qqml.h>
+#include <qstring.h>
+
 #include "../core/plugin.hpp"
 
 namespace {
diff --git a/src/window/test/CMakeLists.txt b/src/window/test/CMakeLists.txt
index ad9e5a0a..4197e4a5 100644
--- a/src/window/test/CMakeLists.txt
+++ b/src/window/test/CMakeLists.txt
@@ -1,6 +1,6 @@
 function (qs_test name)
 	add_executable(${name} ${ARGN})
-	target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-window quickshell-core)
+	target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-window quickshell-core)
 	add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
 endfunction()
 
diff --git a/src/x11/CMakeLists.txt b/src/x11/CMakeLists.txt
index d1079d29..b37b8fbc 100644
--- a/src/x11/CMakeLists.txt
+++ b/src/x11/CMakeLists.txt
@@ -8,17 +8,16 @@ qt_add_library(quickshell-x11 STATIC
 qt_add_qml_module(quickshell-x11
 	URI Quickshell.X11
 	VERSION 0.1
+	DEPENDENCIES QtQuick
 )
 
 install_qml_module(quickshell-x11)
 
 add_library(quickshell-x11-init OBJECT init.cpp)
 
-target_link_libraries(quickshell-x11 PRIVATE ${QT_DEPS} ${XCB_LIBRARIES})
-target_link_libraries(quickshell-x11-init PRIVATE ${QT_DEPS} ${XCB_LIBRARIES})
+target_link_libraries(quickshell-x11 PRIVATE Qt::Quick ${XCB_LIBRARIES})
+target_link_libraries(quickshell-x11-init PRIVATE Qt::Quick Qt::Qml ${XCB_LIBRARIES})
 
-qs_pch(quickshell-x11)
-qs_pch(quickshell-x11plugin)
-qs_pch(quickshell-x11-init)
+qs_module_pch(quickshell-x11 SET large)
 
 target_link_libraries(quickshell PRIVATE quickshell-x11plugin quickshell-x11-init)
diff --git a/src/x11/init.cpp b/src/x11/init.cpp
index 00080036..2e41e761 100644
--- a/src/x11/init.cpp
+++ b/src/x11/init.cpp
@@ -1,5 +1,7 @@
 #include <qguiapplication.h>
+#include <qlist.h>
 #include <qqml.h>
+#include <qstring.h>
 
 #include "../core/plugin.hpp"
 #include "panel_window.hpp"
@@ -8,6 +10,8 @@
 namespace {
 
 class X11Plugin: public QuickshellPlugin {
+	QList<QString> dependencies() override { return {"window"}; }
+
 	bool applies() override { return QGuiApplication::platformName() == "xcb"; }
 
 	void init() override { XAtom::initAtoms(); }