diff --git a/AeroSpace.xcodeproj/project.pbxproj b/AeroSpace.xcodeproj/project.pbxproj index 2e641346..de1cea28 100644 --- a/AeroSpace.xcodeproj/project.pbxproj +++ b/AeroSpace.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 1CB4082BE5C95CA8CD52BED9 /* Maybe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345148B22F8A8F85109229AE /* Maybe.swift */; }; 2F8DC074DAB97DC87E07A559 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9CAC977020A08D0227FAFB2 /* Bundle.swift */; }; 4005ECE237BD9230F74CA917 /* TreeNodeEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C39B0C4E4888832129C4C7 /* TreeNodeEx.swift */; }; + 45EA2D1C90430C432E123B51 /* keysMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0D40CBD65704BA9595C2FA /* keysMap.swift */; }; 518B9E5AC031C24C7C84CD70 /* MacWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 243F29F496F7463F3482DD10 /* MacWindow.swift */; }; 6317AB471F4C4F5D66A25784 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EEDBFFCA7A77D96B18FB0732 /* Assets.xcassets */; }; 64A058E536F1EEF7F01043AF /* TOMLKit in Frameworks */ = {isa = PBXBuildFile; productRef = EC8E4F2CA4FF8884F9F59975 /* TOMLKit */; }; @@ -26,6 +27,7 @@ 852F88894A3B9FC385563665 /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = 42BC1E757EF69233C2262FF4 /* HotKey */; }; 920FDF8498DCCB62149D1719 /* Monitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6507EBAA795220FD0C05384 /* Monitor.swift */; }; 96593DF93A69CA2E05189A3F /* axObservers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E6F3930E3BF5D8196A20E9B /* axObservers.swift */; }; + A0765C31043BCFB0420BF1C9 /* parseConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67DBAF4ECF8A0B931FC34EAD /* parseConfig.swift */; }; A2CBF9674964F9083BB198D2 /* ArrayEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 883D7F7F87FBE7D0BDE4E87F /* ArrayEx.swift */; }; A4F66097ADF0FD58C6B715AE /* NSWorkspaceEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6935AF0A2DB3D186D1C6218F /* NSWorkspaceEx.swift */; }; AE76A183D0454E4C8ADCE380 /* SequenceEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE5DCAEC5EE619CE33859E7 /* SequenceEx.swift */; }; @@ -33,13 +35,14 @@ B1E2002BB8F70F2555AAA82D /* TreeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D295CA45172ADBDB1E4DF708 /* TreeNode.swift */; }; B3702BB393A9B03CCAE4C60E /* refresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526B113159987FA43EA41120 /* refresh.swift */; }; C0A88261ECF505FC5648FC0A /* OptionalEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EDFD4A9F45182CA6E0BD7B /* OptionalEx.swift */; }; - C39C2054893A6506C35732D7 /* config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651C5EE18862C252795811B3 /* config.swift */; }; + D4F68F33C6E354856C80E729 /* ConfigModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3C4DD77B74C92C8C01E39 /* ConfigModel.swift */; }; E2FD8E2B2D2BE6B88BF8E8AD /* accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE605CF46DE6377C69B9D49D /* accessibility.swift */; }; F2AFA702961A1D653EB7D269 /* command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 776E3F4EE298A9C69C97EF7F /* command.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 09685297933511208058F7CF /* AeroSpace.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AeroSpace.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1C0D40CBD65704BA9595C2FA /* keysMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = keysMap.swift; sourceTree = ""; }; 1E81623E8954701269A22322 /* AeroSpaceApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeroSpaceApp.swift; sourceTree = ""; }; 243F29F496F7463F3482DD10 /* MacWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacWindow.swift; sourceTree = ""; }; 24F99E8C4FD17A1D939C41F1 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; @@ -50,7 +53,7 @@ 3E05FB0C7158C8B6DECBD603 /* TilingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilingContainer.swift; sourceTree = ""; }; 51CE37C1B8D858C81A396F40 /* CollectionEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionEx.swift; sourceTree = ""; }; 526B113159987FA43EA41120 /* refresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = refresh.swift; sourceTree = ""; }; - 651C5EE18862C252795811B3 /* config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = config.swift; sourceTree = ""; }; + 67DBAF4ECF8A0B931FC34EAD /* parseConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = parseConfig.swift; sourceTree = ""; }; 6935AF0A2DB3D186D1C6218F /* NSWorkspaceEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWorkspaceEx.swift; sourceTree = ""; }; 776E3F4EE298A9C69C97EF7F /* command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = command.swift; sourceTree = ""; }; 7E6F3930E3BF5D8196A20E9B /* axObservers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = axObservers.swift; sourceTree = ""; }; @@ -63,6 +66,7 @@ BEF353340822CD20E9DAB3EC /* AeroSpace.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AeroSpace.entitlements; sourceTree = ""; }; D295CA45172ADBDB1E4DF708 /* TreeNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeNode.swift; sourceTree = ""; }; D6296D5F9AFE5F266EE4B1D0 /* MacApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacApp.swift; sourceTree = ""; }; + D6C3C4DD77B74C92C8C01E39 /* ConfigModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigModel.swift; sourceTree = ""; }; EC2F56249A233EC9806D0F08 /* Bridged-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bridged-Header.h"; sourceTree = ""; }; EE605CF46DE6377C69B9D49D /* accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = accessibility.swift; sourceTree = ""; }; EEDBFFCA7A77D96B18FB0732 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -107,8 +111,10 @@ isa = PBXGroup; children = ( 776E3F4EE298A9C69C97EF7F /* command.swift */, - 651C5EE18862C252795811B3 /* config.swift */, + D6C3C4DD77B74C92C8C01E39 /* ConfigModel.swift */, BA7CD89E786588324DFB5575 /* defaultConfig.swift */, + 1C0D40CBD65704BA9595C2FA /* keysMap.swift */, + 67DBAF4ECF8A0B931FC34EAD /* parseConfig.swift */, ); path = config; sourceTree = ""; @@ -247,6 +253,7 @@ A2CBF9674964F9083BB198D2 /* ArrayEx.swift in Sources */, 2F8DC074DAB97DC87E07A559 /* Bundle.swift in Sources */, 1C46EBB55D401C0D1AFD50F0 /* CollectionEx.swift in Sources */, + D4F68F33C6E354856C80E729 /* ConfigModel.swift in Sources */, B0D0C37BAE7E7F0D0FF1E9FC /* GlobalObserver.swift in Sources */, 7FE92DDAC2F094C83A177914 /* MacApp.swift in Sources */, 518B9E5AC031C24C7C84CD70 /* MacWindow.swift in Sources */, @@ -265,8 +272,9 @@ E2FD8E2B2D2BE6B88BF8E8AD /* accessibility.swift in Sources */, 96593DF93A69CA2E05189A3F /* axObservers.swift in Sources */, F2AFA702961A1D653EB7D269 /* command.swift in Sources */, - C39C2054893A6506C35732D7 /* config.swift in Sources */, 082EECCB2607F31DCBBF3870 /* defaultConfig.swift in Sources */, + 45EA2D1C90430C432E123B51 /* keysMap.swift in Sources */, + A0765C31043BCFB0420BF1C9 /* parseConfig.swift in Sources */, B3702BB393A9B03CCAE4C60E /* refresh.swift in Sources */, 6820E6846AE51B6988B6F673 /* utils.swift in Sources */, ); @@ -352,7 +360,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.0.1; + MARKETING_VERSION = "0.0.1-Alpha"; PRODUCT_BUNDLE_IDENTIFIER = bobko.debug.AeroSpace; PRODUCT_NAME = "AeroSpace-Debug"; SDKROOT = macosx; @@ -434,7 +442,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.0.1; + MARKETING_VERSION = "0.0.1-Alpha"; PRODUCT_BUNDLE_IDENTIFIER = bobko.AeroSpace; PRODUCT_NAME = AeroSpace; SDKROOT = macosx; diff --git a/README.md b/README.md index 65533ccb..0eb090ae 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ AeroSpace is a tiling window manager for macOS. +## Status + +🚧 **Work in progress.** Alpha quality. 🚧 + +- Please expect a lot of bugs +- Please expect that documentation isn't complete +- Please expect that something doesn't work as expected, or doesn't work at all +- Please expect breaking changes in config format in future releases + ## Key features - **Manual** tiling window manager @@ -16,7 +25,9 @@ AeroSpace is a tiling window manager for macOS. ## How to build the project -You would need a mac. Run in terminal: +You would need a mac. + +Firstly, install [xcodegen](https://github.com/yonaskolb/XcodeGen). Then run in terminal: ```bash ./build-debug.sh ``` @@ -28,20 +39,16 @@ You would need a mac. Run in terminal: - select parent -> outline several windows? - OR: outline with "SLSSetWindowOpacity" - OR: "shake" windows -- settings - CLI interface - Check all todos in code -- move vs swap (swap requires position and size proportions decoupling from windows) +- move vs swap - what is src/Assets.xcassets ? - license - unminimize apps automatically +- minimized apps handling +- macOS fullscreen apps handling - unhide apps automatically -## Tests - -- Test main monitor change -- Test monitor add/remove - ## Challenges - Window overlapping diff --git a/config-examples/default-config.toml b/config-examples/default-config.toml new file mode 100644 index 00000000..0a68861f --- /dev/null +++ b/config-examples/default-config.toml @@ -0,0 +1,121 @@ +[config] +after-startup-command = 'bash if [ $hostname == "NVC00112" ]; then aerospace binding nvc; else aerospace binding bobko-macbook; fi' +use-padding-for-nested-containers-with-the-same-orientation = true +auto-flatten-containers = true +# auto-different-orientation-for-nested-containers = true +# start-at-login = true +floating-windows-on-top = true + +[mode.main.binding] +alt-enter = 'bash open /System/Applications/Utilities/Terminal.app' + +alt-shift-quote = 'focus child' +alt-quote = 'focus parent' +alt-slash = 'layout toggle h_list v_list' +alt-comma = 'layout toggle h_accordion v_accordion' + +# todo focus floating binding + +# Focus window +alt-k = ['focus tiling', 'focus up'] +alt-h = ['focus tiling', 'focus left'] +alt-j = ['focus tiling', 'focus down'] +alt-l = ['focus tiling', 'focus right'] +alt-up = ['focus tiling', 'focus up'] +alt-left = ['focus tiling', 'focus left'] +alt-down = ['focus tiling', 'focus down'] +alt-right = ['focus tiling', 'focus right'] + +# Move window +alt-shift-k = 'move_through up' +alt-shift-h = 'move_through left' +alt-shift-j = 'move_through down' +alt-shift-l = 'move_through right' +alt-shift-up = 'move_through up' +alt-shift-left = 'move_through left' +alt-shift-down = 'move_through down' +alt-shift-right = 'move_through right' + +# Resize window +alt-shift-minus = 'resize shrink height 2 px' +alt-shift-equal = 'resize grow height 2 px' +alt-shift-comma = 'resize shrink width 2 px' +alt-shift-period = 'resize grow width 2 px' + +alt-0 = 'workspace 000' +alt-1 = 'workspace 111' +alt-2 = 'workspace 222' +alt-3 = 'workspace 333' +alt-4 = 'workspace 444' +alt-5 = 'workspace 555' +alt-6 = 'workspace 666' +alt-7 = 'workspace 777' +alt-8 = 'workspace 888' +alt-9 = 'workspace 999' +alt-a = 'workspace AAA' +alt-b = 'workspace BBB' +alt-c = 'workspace CCC' +alt-d = 'workspace DDD' +alt-e = 'workspace EEE' +alt-f = 'workspace FFF' +alt-g = 'workspace GGG' +alt-i = 'workspace III' +alt-m = 'workspace MMM' +alt-n = 'workspace NNN' +alt-o = 'workspace OOO' +alt-p = 'workspace PPP' +alt-q = 'workspace QQQ' +alt-r = 'workspace RRR' +alt-s = 'workspace SSS' +alt-t = 'workspace TTT' +alt-u = 'workspace UUU' +alt-v = 'workspace VVV' +alt-w = 'workspace WWW' +alt-x = 'workspace XXX' +alt-y = 'workspace YYY' +alt-z = 'workspace ZZZ' + +alt-shift-0 = 'move container to workspace 000' +alt-shift-1 = 'move container to workspace 111' +alt-shift-2 = 'move container to workspace 222' +alt-shift-3 = 'move container to workspace 333' +alt-shift-4 = 'move container to workspace 444' +alt-shift-5 = 'move container to workspace 555' +alt-shift-6 = 'move container to workspace 666' +alt-shift-7 = 'move container to workspace 777' +alt-shift-8 = 'move container to workspace 888' +alt-shift-9 = 'move container to workspace 999' +alt-shift-a = 'move container to workspace AAA' +alt-shift-b = 'move container to workspace BBB' +alt-shift-c = 'move container to workspace CCC' +alt-shift-d = 'move container to workspace DDD' +alt-shift-e = 'move container to workspace EEE' +alt-shift-f = 'move container to workspace FFF' +alt-shift-g = 'move container to workspace GGG' +alt-shift-i = 'move container to workspace III' +alt-shift-m = 'move container to workspace MMM' +alt-shift-n = 'move container to workspace NNN' +alt-shift-o = 'move container to workspace OOO' +alt-shift-p = 'move container to workspace PPP' +alt-shift-q = 'move container to workspace QQQ' +alt-shift-r = 'move container to workspace RRR' +alt-shift-s = 'move container to workspace SSS' +alt-shift-t = 'move container to workspace TTT' +alt-shift-u = 'move container to workspace UUU' +alt-shift-v = 'move container to workspace VVV' +alt-shift-w = 'move container to workspace WWW' +alt-shift-x = 'move container to workspace XXX' +alt-shift-y = 'move container to workspace YYY' +alt-shift-z = 'move container to workspace ZZZ' + +alt-tab = 'workspace back_and_forth' + +alt-shift-slash.alt-shift-k = 'move_in up' +alt-shift-slash.alt-shift-h = 'move_in left' +alt-shift-slash.alt-shift-j = 'move_in down' +alt-shift-slash.alt-shift-l = 'move_in right' + +# aerospace config set mode.main.binding.alt-l 'focus tiling' 'focus right' +# aerospace config set mode.main.binding.alt-enter 'basn alacritty' +# aerospace config set mode.main.binding.alt-shift-slash.alt-shift-l 'move_in right' +# aerospace config set config.start-at-login false diff --git a/config-examples/i3-like-config-example.toml b/config-examples/i3-like-config-example.toml new file mode 100644 index 00000000..618e9d64 --- /dev/null +++ b/config-examples/i3-like-config-example.toml @@ -0,0 +1,65 @@ +# Reference: https://github.com/sainathadapa/i3-wm-config/blob/master/i3-default-config-backup + +auto-flatten-containers = false + +[mode.main.binding] +alt-enter = 'bash open /System/Applications/Utilities/Terminal.app' + +alt-h = 'focus left' +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' + +# 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 + +alt-shift-space = 'layout floating tiling' # 'floating toggle' in i3 +alt-space = 'focus toggle_tiling_floating' + +alt-a = 'focus parent' + +alt-1 = 'workspace 1' +alt-2 = 'workspace 2' +alt-3 = 'workspace 3' +alt-4 = 'workspace 4' +alt-5 = 'workspace 5' +alt-6 = 'workspace 6' +alt-7 = 'workspace 7' +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' + +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' diff --git a/project.yml b/project.yml index 8f79d703..65f8ed2a 100644 --- a/project.yml +++ b/project.yml @@ -22,8 +22,8 @@ targets: SWIFT_VERSION: 5.8 CODE_SIGN_STYLE: Automatic GENERATE_INFOPLIST_FILE: YES - CURRENT_PROJECT_VERSION: 1 # Build number CFBundleVersion - MARKETING_VERSION: 0.0.1 # User visible version CFBundleShortVersionString + CURRENT_PROJECT_VERSION: 1 # Build number CFBundleVersion + MARKETING_VERSION: 0.0.1-Alpha # User visible version CFBundleShortVersionString SWIFT_OBJC_BRIDGING_HEADER: "src/Bridged-Header.h" # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/uid/20001431-108256 # Specifies whether the app runs as an agent app. If this key is set to YES, Launch Services runs the app as an agent app. diff --git a/src/config/ConfigModel.swift b/src/config/ConfigModel.swift new file mode 100644 index 00000000..0743f65c --- /dev/null +++ b/src/config/ConfigModel.swift @@ -0,0 +1,52 @@ +import HotKey + +struct ConfigRoot { + let config: Config + let modes: [String: Mode] +} + +struct Config { + let afterStartupCommand: Command + let usePaddingForNestedContainersWithTheSameOrientation: Bool + let autoFlattenContainers: Bool + let floatingWindowsOnTop: Bool +} + +struct Mode { + /// User visible name. Optional. todo drop it? + let name: String? + let bindings: [HotkeyBinding] + + func activate() { + for binding in bindings { + binding.activate() + } + } + + func deactivate() { + for binding in bindings { + binding.deactivate() + } + } +} + +class HotkeyBinding { + let modifiers: NSEvent.ModifierFlags + let key: Key + let command: Command + private var hotKey: HotKey? = nil + + init(_ modifiers: NSEvent.ModifierFlags, _ key: Key, _ command: Command) { + self.modifiers = modifiers + self.key = key + self.command = command + } + + func activate() { + hotKey = HotKey(key: key, modifiers: modifiers, keyUpHandler: command.run) + } + + func deactivate() { + hotKey = nil + } +} diff --git a/src/config/command.swift b/src/config/command.swift index b27f1fb4..75487122 100644 --- a/src/config/command.swift +++ b/src/config/command.swift @@ -32,13 +32,11 @@ struct ModeCommand: Command { let idToActivate: String func run() { - for mode in config.modes { - for binding in mode.bindings { - if mode.id == idToActivate { - binding.activate() - } else { - binding.deactivate() - } + for (modeId, mode) in config.modes { + if modeId == idToActivate { + mode.activate() + } else { + mode.deactivate() } } } @@ -55,6 +53,24 @@ struct BashCommand: Command { } } +/// Syntax: +/// layout (main|h_accordion|v_accordion|h_list|v_list|floating|tiling)... +struct LayoutCommand: Command { + let toggleTo: [Layout] + enum Layout { + case main + case h_accordion + case v_accordion + case h_list + case v_list + case floating + } + + func run() { + // todo + } +} + struct FocusCommand: Command { let direction: Direction @@ -75,3 +91,10 @@ struct FocusCommand: Command { // todo } } + +struct ReloadConfigCommand: Command { + func run() { + reloadConfig() + refresh() + } +} diff --git a/src/config/defaultConfig.swift b/src/config/defaultConfig.swift index 44100f15..49f5b4da 100644 --- a/src/config/defaultConfig.swift +++ b/src/config/defaultConfig.swift @@ -6,8 +6,7 @@ let defaultConfig = ConfigRoot( floatingWindowsOnTop: true ), modes: [ - Mode( - id: "main", + mainModeId: Mode( name: nil, bindings: [ HotkeyBinding(.option, .h, FocusCommand(direction: .left)), diff --git a/src/config/keysMap.swift b/src/config/keysMap.swift new file mode 100644 index 00000000..4adb31bb --- /dev/null +++ b/src/config/keysMap.swift @@ -0,0 +1,71 @@ +import HotKey + +let keysMap: [String: Key] = [ + "a": .a, + "b": .b, + "c": .c, + "d": .d, + "e": .e, + "f": .f, + "g": .g, + "h": .h, + "i": .i, + "j": .j, + "k": .k, + "l": .l, + "m": .m, + "n": .n, + "o": .o, + "p": .p, + "q": .q, + "r": .r, + "s": .s, + "t": .t, + "u": .u, + "v": .v, + "w": .w, + "x": .x, + "y": .y, + "z": .z, + + "0": .zero, + "1": .one, + "2": .two, + "3": .three, + "4": .four, + "5": .five, + "6": .six, + "7": .seven, + "8": .eight, + "9": .nine, + + "minus": .minus, + "equal": .equal, + "period": .period, + "comma": .comma, + "slash": .slash, + "backslash": .backslash, + "quote": .quote, + "semicolon": .semicolon, + "backtick": .grave, + "leftSquareBracket": .leftBracket, + "rightSquareBracket": .rightBracket, + "space": .space, + "enter": .return, + "esc": .escape, + "delete": .delete, + "tab": .tab, + + "left": .leftArrow, + "down": .downArrow, + "up": .upArrow, + "right": .rightArrow, +] + +let modifiersMap: [String: NSEvent.ModifierFlags] = [ + "shift": .shift, + "alt": .option, + "ctrl": .control, + "cmd": .command, + "fn": .function, +] diff --git a/src/config/config.swift b/src/config/parseConfig.swift similarity index 71% rename from src/config/config.swift rename to src/config/parseConfig.swift index 73343bf2..0a0de00c 100644 --- a/src/config/config.swift +++ b/src/config/parseConfig.swift @@ -4,25 +4,7 @@ import HotKey // todo convert all `error` during config parsing to returning defaults and reporting errors to where? Some kind of log? -struct Config { - let afterStartupCommand: Command - let usePaddingForNestedContainersWithTheSameOrientation: Bool - let autoFlattenContainers: Bool - let floatingWindowsOnTop: Bool -} - -struct ConfigRoot { - let config: Config - let modes: [Mode] -} - -struct Mode { - let id: String - /// User visible name. Optional. todo drop it? - let name: String? - let bindings: [HotkeyBinding] -} - +let mainModeId = "main" var config: ConfigRoot = defaultConfig func reloadConfig() { @@ -40,7 +22,7 @@ private func parseConfigRoot(_ rawToml: String) -> ConfigRoot { error(e.localizedDescription) } var config: Config? = nil - var modes: [Mode] = [] + var modes: [String: Mode] = defaultConfig.modes for (key, value) in rawTable { switch key { case "config": @@ -48,7 +30,7 @@ private func parseConfigRoot(_ rawToml: String) -> ConfigRoot { case "mode": modes = parseModes(value, .root("mode")) default: - unknownKeyError(key, .root(key)) + unknownKeyError(.root(key)) } } return ConfigRoot( @@ -57,27 +39,56 @@ private func parseConfigRoot(_ rawToml: String) -> ConfigRoot { ) } -private func parseModes(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> [Mode] { - // todo - [] +private func parseModes(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> [String: Mode] { + let rawTable = raw.table ?? expectedActualTypeError(expected: .table, actual: raw.type, backtrace) + var result: [String: Mode] = [:] + for (key, value) in rawTable { + result[key] = parseMode(value, backtrace + .key(key)) + } + if !result.keys.contains(mainModeId) { + error("\(backtrace) is expected to contain 'main' mode") + } + return result } -private func unknownKeyError(_ key: String, _ backtrace: TomlBacktrace) -> Never { - error("\(backtrace): Unknown key '\(key)'") -} +private func parseMode(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> Mode { + let rawTable = raw.table ?? expectedActualTypeError(expected: .table, actual: raw.type, backtrace) -private func expectedActualTypeError(expected: TOMLType, actual: TOMLType, _ backtrace: TomlBacktrace) -> T { - error("\(backtrace): Expected type is \(expected). But actual type is \(actual)") + let key1 = "binding" + var value1: [HotkeyBinding] = [] + + for (key, value) in rawTable { + let keyBacktrace = backtrace + .key(key) + switch key { + case key1: + value1 = parseBindings(value, keyBacktrace) + default: + unknownKeyError(keyBacktrace) + } + } + return Mode( + name: nil, + bindings: value1 + ) } -private func expectedActualTypeError(expected: [TOMLType], actual: TOMLType, _ backtrace: TomlBacktrace) -> T { - if let single = expected.singleOrNil() { - return expectedActualTypeError(expected: single, actual: actual, backtrace) - } else { - error("\(backtrace): Expected types are \(expected.map { $0.description }.joined(separator: " or ")). But actual type is \(actual)") +private func parseBindings(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> [HotkeyBinding] { + let rawTable = raw.table ?? expectedActualTypeError(expected: .table, actual: raw.type, backtrace) + return rawTable.map { (binding: String, value: TOMLValueConvertible) in + let keyBacktrace = backtrace + .key(binding) + let (modifiers, key) = parseBinding(binding, keyBacktrace) + return HotkeyBinding(modifiers, key, parseCommand(value, keyBacktrace)) } } +private func parseBinding(_ raw: String, _ backtrace: TomlBacktrace) -> (NSEvent.ModifierFlags, Key) { + let rawKeys = raw.split(separator: "-") + let modifiers: [NSEvent.ModifierFlags] = rawKeys.dropLast() + .map { modifiersMap[String($0)] ?? errorT("\(backtrace): Can't parse '\(raw)' binding") } + let key = rawKeys.last.flatMap { keysMap[String($0)] } ?? errorT("\(backtrace): Can't parse '\(raw)' binding") + return (NSEvent.ModifierFlags(modifiers), key) +} + private func parseConfig(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> Config { let rawTable = raw.table ?? expectedActualTypeError(expected: .table, actual: raw.type, backtrace) @@ -105,7 +116,7 @@ private func parseConfig(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace case key4: value4 = parseBool(value, keyBacktrace) default: - unknownKeyError(key, backtrace + .key(key)) + unknownKeyError(backtrace + .key(key)) } } @@ -121,51 +132,6 @@ private func parseBool(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) raw.bool ?? expectedActualTypeError(expected: .bool, actual: raw.type, backtrace) } -class HotkeyBinding { - let modifiers: NSEvent.ModifierFlags - let key: Key - let command: Command - private var hotKey: HotKey? = nil - - init(_ modifiers: NSEvent.ModifierFlags, _ key: Key, _ command: Command) { - self.modifiers = modifiers - self.key = key - self.command = command - } - - func activate() { - hotKey = HotKey(key: key, modifiers: modifiers, keyUpHandler: command.run) - } - - func deactivate() { - hotKey = nil - } -} - -private indirect enum TomlBacktrace: CustomStringConvertible { - case root(String) - case key(String) - case index(Int) - case pair((TomlBacktrace, TomlBacktrace)) - - var description: String { - switch self { - case .root(let value): - return value - case .key(let value): - return "." + value - case .index(let index): - return "[\(index)]" - case .pair((let first, let second)): - return first.description + second.description - } - } - - static func +(lhs: TomlBacktrace, rhs: TomlBacktrace) -> TomlBacktrace { - pair((lhs, rhs)) - } -} - private func parseCommand(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> Command { if let rawString = raw.string { return parseSingleCommand(rawString, backtrace) @@ -204,3 +170,43 @@ private func parseSingleCommand(_ raw: String, _ backtrace: TomlBacktrace) -> Co error("\(backtrace): Can't parse '\(raw)' command") } } + +private indirect enum TomlBacktrace: CustomStringConvertible { + case root(String) + case key(String) + case index(Int) + case pair((TomlBacktrace, TomlBacktrace)) + + var description: String { + switch self { + case .root(let value): + return value + case .key(let value): + return "." + value + case .index(let index): + return "[\(index)]" + case .pair((let first, let second)): + return first.description + second.description + } + } + + static func +(lhs: TomlBacktrace, rhs: TomlBacktrace) -> TomlBacktrace { + pair((lhs, rhs)) + } +} + +private func unknownKeyError(_ backtrace: TomlBacktrace) -> Never { + error("Unknown key '\(backtrace)'") +} + +private func expectedActualTypeError(expected: TOMLType, actual: TOMLType, _ backtrace: TomlBacktrace) -> T { + error("\(backtrace): Expected type is \(expected). But actual type is \(actual)") +} + +private func expectedActualTypeError(expected: [TOMLType], actual: TOMLType, _ backtrace: TomlBacktrace) -> T { + if let single = expected.singleOrNil() { + return expectedActualTypeError(expected: single, actual: actual, backtrace) + } else { + error("\(backtrace): Expected types are \(expected.map { $0.description }.joined(separator: " or ")). But actual type is \(actual)") + } +}