diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5acfaf91..db539c08 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,5 +19,6 @@ jobs:
       - name: xcodegen
         uses: xavierLowmiller/xcodegen-action@1.1.2
       - name: Build
-        run: |
-          ./ci-build.sh
+        run: ./build-and-check-uncommitted-files.sh
+      - name: Tests
+        run: ./run-tests.sh
diff --git a/AeroSpace.xcodeproj/project.pbxproj b/AeroSpace.xcodeproj/project.pbxproj
index 78da4809..153b2b19 100644
--- a/AeroSpace.xcodeproj/project.pbxproj
+++ b/AeroSpace.xcodeproj/project.pbxproj
@@ -26,6 +26,7 @@
 		6317AB471F4C4F5D66A25784 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EEDBFFCA7A77D96B18FB0732 /* Assets.xcassets */; };
 		64A058E536F1EEF7F01043AF /* TOMLKit in Frameworks */ = {isa = PBXBuildFile; productRef = EC8E4F2CA4FF8884F9F59975 /* TOMLKit */; };
 		66E6CDA75DDD5E4B9647EDE2 /* AeroSpaceApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E81623E8954701269A22322 /* AeroSpaceApp.swift */; };
+		6715CC184E718828173DFBD5 /* ConfigTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EA50A972BA714F2BA3BBC1 /* ConfigTest.swift */; };
 		6820E6846AE51B6988B6F673 /* utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD02433B4415EEB163074CE5 /* utils.swift */; };
 		6E4E235FDA41307B19F16182 /* ModeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0CD3C2A0E86CDB9DF312AB /* ModeCommand.swift */; };
 		783B0B965BA45D7A2943F7BF /* TilingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E05FB0C7158C8B6DECBD603 /* TilingContainer.swift */; };
@@ -42,13 +43,25 @@
 		B1E2002BB8F70F2555AAA82D /* TreeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D295CA45172ADBDB1E4DF708 /* TreeNode.swift */; };
 		B3702BB393A9B03CCAE4C60E /* refresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526B113159987FA43EA41120 /* refresh.swift */; };
 		E2FD8E2B2D2BE6B88BF8E8AD /* accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE605CF46DE6377C69B9D49D /* accessibility.swift */; };
+		F74FC5ECBCE9C8A6D09AE9F5 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7910D7B9C0A232187CCA1F10 /* util.swift */; };
 		F982DB924450BBBB4FDF4C2C /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD90FB41FD41602120F67FF5 /* Command.swift */; };
 		FC35D6D0A678CC802972C6FE /* ReloadConfigCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF3C9038C846369FDD71D1D2 /* ReloadConfigCommand.swift */; };
 		FD4386BC632BAA6A4105FFD8 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9752080BBA547C2A0EF076F0 /* Config.swift */; };
 /* End PBXBuildFile section */
 
+/* Begin PBXContainerItemProxy section */
+		B514A502AE8B3770CB8BEA61 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 0B585B3093DA0FC12E7983E2 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = B00BE37A79171B0EE995EB83;
+			remoteInfo = AeroSpace;
+		};
+/* End PBXContainerItemProxy section */
+
 /* Begin PBXFileReference section */
 		09685297933511208058F7CF /* AeroSpace.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AeroSpace.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		12EA50A972BA714F2BA3BBC1 /* ConfigTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigTest.swift; sourceTree = "<group>"; };
 		1A2B673C67B00DBFCC27FFE7 /* LayoutCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutCommand.swift; sourceTree = "<group>"; };
 		1C0D40CBD65704BA9595C2FA /* keysMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = keysMap.swift; sourceTree = "<group>"; };
 		1E81623E8954701269A22322 /* AeroSpaceApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeroSpaceApp.swift; sourceTree = "<group>"; };
@@ -64,9 +77,11 @@
 		4B0CD3C2A0E86CDB9DF312AB /* ModeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModeCommand.swift; sourceTree = "<group>"; };
 		51CE37C1B8D858C81A396F40 /* CollectionEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionEx.swift; sourceTree = "<group>"; };
 		526B113159987FA43EA41120 /* refresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = refresh.swift; sourceTree = "<group>"; };
+		5274C575044C2A7123C57584 /* AeroSpace-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "AeroSpace-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
 		55E2A2BBD8A32FFCD6A88BC7 /* BashCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BashCommand.swift; sourceTree = "<group>"; };
 		67DBAF4ECF8A0B931FC34EAD /* parseConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = parseConfig.swift; sourceTree = "<group>"; };
 		6935AF0A2DB3D186D1C6218F /* NSWorkspaceEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWorkspaceEx.swift; sourceTree = "<group>"; };
+		7910D7B9C0A232187CCA1F10 /* util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = util.swift; sourceTree = "<group>"; };
 		7E6F3930E3BF5D8196A20E9B /* axObservers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = axObservers.swift; sourceTree = "<group>"; };
 		883D7F7F87FBE7D0BDE4E87F /* ArrayEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayEx.swift; sourceTree = "<group>"; };
 		8B7A2DF0D1F72B80B1F04240 /* BundleEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleEx.swift; sourceTree = "<group>"; };
@@ -126,6 +141,7 @@
 			children = (
 				0E0109AE5F7881520B0D2384 /* config-examples */,
 				8338180CE208CBDCD6D8E911 /* src */,
+				A6E1E25AB6F33B13B0633B9C /* test */,
 				62BEA6F49E6648E2EE3C208F /* Products */,
 			);
 			sourceTree = "<group>";
@@ -143,6 +159,7 @@
 		62BEA6F49E6648E2EE3C208F /* Products */ = {
 			isa = PBXGroup;
 			children = (
+				5274C575044C2A7123C57584 /* AeroSpace-Tests.xctest */,
 				09685297933511208058F7CF /* AeroSpace.app */,
 			);
 			name = Products;
@@ -178,6 +195,15 @@
 			path = axWrappers;
 			sourceTree = "<group>";
 		};
+		A6E1E25AB6F33B13B0633B9C /* test */ = {
+			isa = PBXGroup;
+			children = (
+				12EA50A972BA714F2BA3BBC1 /* ConfigTest.swift */,
+				7910D7B9C0A232187CCA1F10 /* util.swift */,
+			);
+			path = test;
+			sourceTree = "<group>";
+		};
 		A711A25EEC57098295B90C17 /* command */ = {
 			isa = PBXGroup;
 			children = (
@@ -235,6 +261,22 @@
 			productReference = 09685297933511208058F7CF /* AeroSpace.app */;
 			productType = "com.apple.product-type.application";
 		};
+		D2B85A91A03DA984483CF473 /* AeroSpace-Tests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = D7CB5528090130BA2AF93D7A /* Build configuration list for PBXNativeTarget "AeroSpace-Tests" */;
+			buildPhases = (
+				101761F785765CD965E9741C /* Sources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				A6E9756DC2D01D6E286DFADF /* PBXTargetDependency */,
+			);
+			name = "AeroSpace-Tests";
+			productName = "AeroSpace-Tests";
+			productReference = 5274C575044C2A7123C57584 /* AeroSpace-Tests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
@@ -266,6 +308,7 @@
 			projectRoot = "";
 			targets = (
 				B00BE37A79171B0EE995EB83 /* AeroSpace */,
+				D2B85A91A03DA984483CF473 /* AeroSpace-Tests */,
 			);
 		};
 /* End PBXProject section */
@@ -283,6 +326,15 @@
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
+		101761F785765CD965E9741C /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6715CC184E718828173DFBD5 /* ConfigTest.swift in Sources */,
+				F74FC5ECBCE9C8A6D09AE9F5 /* util.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		D7A18303C03F2CB26F7BB54B /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -326,6 +378,14 @@
 		};
 /* End PBXSourcesBuildPhase section */
 
+/* Begin PBXTargetDependency section */
+		A6E9756DC2D01D6E286DFADF /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = B00BE37A79171B0EE995EB83 /* AeroSpace */;
+			targetProxy = B514A502AE8B3770CB8BEA61 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
 /* Begin XCBuildConfiguration section */
 		175127AAF914899705FABF12 /* Debug */ = {
 			isa = XCBuildConfiguration;
@@ -468,6 +528,38 @@
 			};
 			name = Release;
 		};
+		B3F7FD8336104A3B49E648F4 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				COMBINE_HIDPI_IMAGES = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/../Frameworks",
+				);
+				SDKROOT = macosx;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AeroSpace-Debug.app/Contents/MacOS/AeroSpace-Debug";
+			};
+			name = Release;
+		};
+		CBE2FDFF787AFE0FB33E335B /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				COMBINE_HIDPI_IMAGES = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/../Frameworks",
+				);
+				SDKROOT = macosx;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AeroSpace-Debug.app/Contents/MacOS/AeroSpace-Debug";
+			};
+			name = Debug;
+		};
 		D1D1A9E07F0AB40E14CAC0F6 /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
@@ -516,6 +608,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Debug;
 		};
+		D7CB5528090130BA2AF93D7A /* Build configuration list for PBXNativeTarget "AeroSpace-Tests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				CBE2FDFF787AFE0FB33E335B /* Debug */,
+				B3F7FD8336104A3B49E648F4 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Debug;
+		};
 /* End XCConfigurationList section */
 
 /* Begin XCRemoteSwiftPackageReference section */
diff --git a/ci-build.sh b/build-and-check-uncommitted-files.sh
similarity index 100%
rename from ci-build.sh
rename to build-and-check-uncommitted-files.sh
diff --git a/config-examples/i3-like-config-example.toml b/config-examples/i3-like-config-example.toml
index 39850f87..a7898ab7 100644
--- a/config-examples/i3-like-config-example.toml
+++ b/config-examples/i3-like-config-example.toml
@@ -12,21 +12,24 @@ alt-j = 'focus down'
 alt-k = 'focus up'
 alt-l = 'focus right'
 
-alt-shift-h = 'move_through left'
-alt-shift-j = 'move_through down'
-alt-shift-k = 'move_through up'
-alt-shift-l = 'move_through right'
+#todo support parsing
+#alt-shift-h = 'move_through left'
+#alt-shift-j = 'move_through down'
+#alt-shift-k = 'move_through up'
+#alt-shift-l = 'move_through right'
 
 # alt-h = 'split h' # todo support split command?
 # alt-v = 'split v' # todo support split command?
 
 # alt-f = 'fullscreen' # todo support fullscreen command?
 
-alt-s = 'layout v_accordion' # 'layout stacking' in i3
-alt-w = 'layout h_accordion' # 'layout tabbed' in i3
-alt-e = 'layout h_list v_list' # 'layout toggle list' in i3
+# todo support layout parsing
+#alt-s = 'layout v_accordion' # 'layout stacking' in i3
+#alt-w = 'layout h_accordion' # 'layout tabbed' in i3
+#alt-e = 'layout h_list v_list' # 'layout toggle list' in i3
 
-alt-shift-space = 'layout floating tiling' # 'floating toggle' in i3
+#todo support parsing
+#alt-shift-space = 'layout floating tiling' # 'floating toggle' in i3
 alt-space = 'focus toggle_tiling_floating'
 
 alt-a = 'focus parent'
@@ -42,26 +45,28 @@ alt-8 = 'workspace 8'
 alt-9 = 'workspace 9'
 alt-0 = 'workspace 10'
 
-alt-shift-1 = 'move container to workspace 1'
-alt-shift-2 = 'move container to workspace 2'
-alt-shift-3 = 'move container to workspace 3'
-alt-shift-4 = 'move container to workspace 4'
-alt-shift-5 = 'move container to workspace 5'
-alt-shift-6 = 'move container to workspace 6'
-alt-shift-7 = 'move container to workspace 7'
-alt-shift-8 = 'move container to workspace 8'
-alt-shift-9 = 'move container to workspace 9'
-alt-shift-0 = 'move container to workspace 10'
+#todo support parsing
+#alt-shift-1 = 'move container to workspace 1'
+#alt-shift-2 = 'move container to workspace 2'
+#alt-shift-3 = 'move container to workspace 3'
+#alt-shift-4 = 'move container to workspace 4'
+#alt-shift-5 = 'move container to workspace 5'
+#alt-shift-6 = 'move container to workspace 6'
+#alt-shift-7 = 'move container to workspace 7'
+#alt-shift-8 = 'move container to workspace 8'
+#alt-shift-9 = 'move container to workspace 9'
+#alt-shift-0 = 'move container to workspace 10'
 
 alt-shift-c = 'reload_config'
 
 alt-r = 'mode resize'
 
-[mode.resize.binding]
-# todo does it work?
-h = 'resize shrink width 10'
-j = 'resize grow height 10'
-k = 'resize shrink height 10'
-l = 'resize grow width 10'
-enter = 'mode main'
-esc = 'mode esc'
+#todo support parsing
+#[mode.resize.binding]
+## todo does it work?
+#h = 'resize shrink width 10'
+#j = 'resize grow height 10'
+#k = 'resize shrink height 10'
+#l = 'resize grow width 10'
+#enter = 'mode main'
+#esc = 'mode esc'
diff --git a/project.yml b/project.yml
index 011c8d76..33550ebe 100644
--- a/project.yml
+++ b/project.yml
@@ -44,3 +44,14 @@ targets:
       properties:
         # Accessibility API doesn't work in sandboxed app
         com.apple.security.app-sandbox: false
+  AeroSpace-Tests:
+    type: bundle.unit-test
+    platform: macOS
+    sources: [test]
+    dependencies:
+      - target: AeroSpace
+    settings:
+      base:
+        # SWIFT_VERSION: 5.8
+        TEST_HOST: "$(BUILT_PRODUCTS_DIR)/AeroSpace-Debug.app/Contents/MacOS/AeroSpace-Debug"
+        GENERATE_INFOPLIST_FILE: YES
diff --git a/run-tests.sh b/run-tests.sh
new file mode 100755
index 00000000..e4123bbd
--- /dev/null
+++ b/run-tests.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -e # Exit if one of commands exit with non-zero exit code
+set -u # Treat unset variables and parameters other than the special parameters ‘@’ or ‘*’ as an error
+set -o pipefail # Any command failed in the pipe fails the whole pipe
+# set -x # Print shell commands as they are executed (or you can try -v which is less verbose)
+
+cd "$(dirname "$0")"
+xcodebuild -scheme AeroSpace_Tests test
diff --git a/test/ConfigTest.swift b/test/ConfigTest.swift
new file mode 100644
index 00000000..872de0f6
--- /dev/null
+++ b/test/ConfigTest.swift
@@ -0,0 +1,16 @@
+//
+//  macos_window_manager_swiftTests.swift
+//  macos-window-manager-swiftTests
+//
+//  Created by Nikita Bobko on 2023-05-20.
+//
+
+import XCTest
+@testable import AeroSpace_Debug
+
+final class ConfigTest: XCTestCase {
+    func testParseI3Config() throws {
+        let toml = try! String(contentsOf: projectRoot.appending(component: "config-examples/i3-like-config-example.toml"))
+        let _ = parseConfig(toml)
+    }
+}
diff --git a/test/util.swift b/test/util.swift
new file mode 100644
index 00000000..d4c696de
--- /dev/null
+++ b/test/util.swift
@@ -0,0 +1,3 @@
+import Foundation
+
+let projectRoot: URL = URL(filePath: #file).appending(component: "../..").standardized
diff --git a/tests/macos_window_manager_swiftTests.swift b/tests/macos_window_manager_swiftTests.swift
deleted file mode 100644
index 7fb525ca..00000000
--- a/tests/macos_window_manager_swiftTests.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-//
-//  macos_window_manager_swiftTests.swift
-//  macos-window-manager-swiftTests
-//
-//  Created by Nikita Bobko on 2023-05-20.
-//
-
-import XCTest
-@testable import AeroSpace
-
-final class macos_window_manager_swiftTests: XCTestCase {
-
-    override func setUpWithError() throws {
-        // Put setup code here. This method is called before the invocation of each test method in the class.
-    }
-
-    override func tearDownWithError() throws {
-        // Put teardown code here. This method is called after the invocation of each test method in the class.
-    }
-
-    func testExample() throws {
-        // This is an example of a functional test case.
-        // Use XCTAssert and related functions to verify your tests produce the correct results.
-        // Any test you write for XCTest can be annotated as throws and async.
-        // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
-        // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
-    }
-
-    func testPerformanceExample() throws {
-        // This is an example of a performance test case.
-        self.measure {
-            // Put the code you want to measure the time of here.
-        }
-    }
-
-}