diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 1d10b7e1..22f233ec 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,7 +1,13 @@ + + \ No newline at end of file diff --git a/AeroSpace.xcodeproj/project.pbxproj b/AeroSpace.xcodeproj/project.pbxproj index 2277a31d..2e641346 100644 --- a/AeroSpace.xcodeproj/project.pbxproj +++ b/AeroSpace.xcodeproj/project.pbxproj @@ -9,7 +9,9 @@ /* Begin PBXBuildFile section */ 07FF7938628995F68DFEB524 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F99E8C4FD17A1D939C41F1 /* ViewModel.swift */; }; 080DAA83BADA766D94A2BD3C /* Workspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A4C333EA5B4D4527DD97D4 /* Workspace.swift */; }; + 082EECCB2607F31DCBBF3870 /* defaultConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7CD89E786588324DFB5575 /* defaultConfig.swift */; }; 115F5CA4BEB80B645E66D198 /* NSScreenEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF3BB3DD434C75536217CB88 /* NSScreenEx.swift */; }; + 1C46EBB55D401C0D1AFD50F0 /* CollectionEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE37C1B8D858C81A396F40 /* CollectionEx.swift */; }; 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 */; }; @@ -18,7 +20,6 @@ 64A058E536F1EEF7F01043AF /* TOMLKit in Frameworks */ = {isa = PBXBuildFile; productRef = EC8E4F2CA4FF8884F9F59975 /* TOMLKit */; }; 66E6CDA75DDD5E4B9647EDE2 /* AeroSpaceApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E81623E8954701269A22322 /* AeroSpaceApp.swift */; }; 6820E6846AE51B6988B6F673 /* utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD02433B4415EEB163074CE5 /* utils.swift */; }; - 7035674CE8D4D0D5D43FAD95 /* Toml.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB0A5C2842534F121A0C430 /* Toml.swift */; }; 783B0B965BA45D7A2943F7BF /* TilingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E05FB0C7158C8B6DECBD603 /* TilingContainer.swift */; }; 78EE0CEF814ABDBA67941B84 /* Rect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B788A95DD3C267878E05B5 /* Rect.swift */; }; 7FE92DDAC2F094C83A177914 /* MacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6296D5F9AFE5F266EE4B1D0 /* MacApp.swift */; }; @@ -32,7 +33,9 @@ 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 */; }; 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 */ @@ -45,13 +48,17 @@ 38A4C333EA5B4D4527DD97D4 /* Workspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Workspace.swift; sourceTree = ""; }; 3C2E5977331398421A4FC168 /* GlobalObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalObserver.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; 883D7F7F87FBE7D0BDE4E87F /* ArrayEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayEx.swift; sourceTree = ""; }; A9EDFD4A9F45182CA6E0BD7B /* OptionalEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalEx.swift; sourceTree = ""; }; AAE5DCAEC5EE619CE33859E7 /* SequenceEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceEx.swift; sourceTree = ""; }; AF3BB3DD434C75536217CB88 /* NSScreenEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSScreenEx.swift; sourceTree = ""; }; + BA7CD89E786588324DFB5575 /* defaultConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = defaultConfig.swift; sourceTree = ""; }; BD02433B4415EEB163074CE5 /* utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = utils.swift; sourceTree = ""; }; 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 = ""; }; @@ -62,7 +69,6 @@ F6507EBAA795220FD0C05384 /* Monitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Monitor.swift; sourceTree = ""; }; F8C39B0C4E4888832129C4C7 /* TreeNodeEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeNodeEx.swift; sourceTree = ""; }; F9CAC977020A08D0227FAFB2 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; - FDB0A5C2842534F121A0C430 /* Toml.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toml.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -97,20 +103,22 @@ ); sourceTree = ""; }; - 62BEA6F49E6648E2EE3C208F /* Products */ = { + 50A41C4CCFC95C514CA1EAD5 /* config */ = { isa = PBXGroup; children = ( - 09685297933511208058F7CF /* AeroSpace.app */, + 776E3F4EE298A9C69C97EF7F /* command.swift */, + 651C5EE18862C252795811B3 /* config.swift */, + BA7CD89E786588324DFB5575 /* defaultConfig.swift */, ); - name = Products; + path = config; sourceTree = ""; }; - 80F16ED76205CA1AC4E0BF3E /* settings */ = { + 62BEA6F49E6648E2EE3C208F /* Products */ = { isa = PBXGroup; children = ( - FDB0A5C2842534F121A0C430 /* Toml.swift */, + 09685297933511208058F7CF /* AeroSpace.app */, ); - path = settings; + name = Products; sourceTree = ""; }; 8338180CE208CBDCD6D8E911 /* src */ = { @@ -126,8 +134,8 @@ 526B113159987FA43EA41120 /* refresh.swift */, 24F99E8C4FD17A1D939C41F1 /* ViewModel.swift */, 89B36C927EF9F6CB9CE830C7 /* axWrappers */, + 50A41C4CCFC95C514CA1EAD5 /* config */, 38E5F06C862662755065FF1D /* model */, - 80F16ED76205CA1AC4E0BF3E /* settings */, C136937AA077E63558E9707C /* util */, ); path = src; @@ -147,6 +155,7 @@ isa = PBXGroup; children = ( 883D7F7F87FBE7D0BDE4E87F /* ArrayEx.swift */, + 51CE37C1B8D858C81A396F40 /* CollectionEx.swift */, 345148B22F8A8F85109229AE /* Maybe.swift */, F6507EBAA795220FD0C05384 /* Monitor.swift */, AF3BB3DD434C75536217CB88 /* NSScreenEx.swift */, @@ -237,6 +246,7 @@ 66E6CDA75DDD5E4B9647EDE2 /* AeroSpaceApp.swift in Sources */, A2CBF9674964F9083BB198D2 /* ArrayEx.swift in Sources */, 2F8DC074DAB97DC87E07A559 /* Bundle.swift in Sources */, + 1C46EBB55D401C0D1AFD50F0 /* CollectionEx.swift in Sources */, B0D0C37BAE7E7F0D0FF1E9FC /* GlobalObserver.swift in Sources */, 7FE92DDAC2F094C83A177914 /* MacApp.swift in Sources */, 518B9E5AC031C24C7C84CD70 /* MacWindow.swift in Sources */, @@ -248,13 +258,15 @@ 78EE0CEF814ABDBA67941B84 /* Rect.swift in Sources */, AE76A183D0454E4C8ADCE380 /* SequenceEx.swift in Sources */, 783B0B965BA45D7A2943F7BF /* TilingContainer.swift in Sources */, - 7035674CE8D4D0D5D43FAD95 /* Toml.swift in Sources */, B1E2002BB8F70F2555AAA82D /* TreeNode.swift in Sources */, 4005ECE237BD9230F74CA917 /* TreeNodeEx.swift in Sources */, 07FF7938628995F68DFEB524 /* ViewModel.swift in Sources */, 080DAA83BADA766D94A2BD3C /* Workspace.swift in Sources */, 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 */, B3702BB393A9B03CCAE4C60E /* refresh.swift in Sources */, 6820E6846AE51B6988B6F673 /* utils.swift in Sources */, ); diff --git a/src/AeroSpaceApp.swift b/src/AeroSpaceApp.swift index f8f0ae3b..bf2c014f 100644 --- a/src/AeroSpaceApp.swift +++ b/src/AeroSpaceApp.swift @@ -17,21 +17,29 @@ struct Setting { let modifiers: NSEvent.ModifierFlags } +//osascript -e 'tell app "Terminal" +//activate +//do script "tail -f ~/log/0.txt" +//end tell' + @main struct AeroSpaceApp: App { var hotKeys: [HotKey] = [] // Keep hotkeys in memory @StateObject var viewModel = ViewModel.shared init() { - checkAccessibilityPermissions() - GlobalObserver.initObserver() - for setting in settings { - hotKeys.append(HotKey(key: setting.hotkey, modifiers: setting.modifiers, keyUpHandler: { - switchToWorkspace(Workspace.get(byName: setting.name)) - })) - } - refresh() - test() + reloadConfig() + + //checkAccessibilityPermissions() + //GlobalObserver.initObserver() + //for setting in settings { + // hotKeys.append(HotKey(key: setting.hotkey, modifiers: setting.modifiers, keyUpHandler: { + // switchToWorkspace(Workspace.get(byName: setting.name)) + // })) + //} + //refresh() + //test() + } var body: some Scene { @@ -44,16 +52,20 @@ struct AeroSpaceApp: App { switchToWorkspace(workspace) } label: { Toggle(isOn: workspace.name == viewModel.focusedWorkspaceTrayText - ? Binding(get: { true }, set: { _, _ in }) - : Binding(get: { false }, set: { _, _ in })) { + ? Binding(get: { true }, set: { _, _ in }) + : Binding(get: { false }, set: { _, _ in })) { let monitor = (workspace.assignedMonitor?.name).flatMap { " - \($0)" } ?? "" Text(workspace.name + monitor).font(.system(.body, design: .monospaced)) } } } Divider() - Button("Quit \(Bundle.appName)") { NSApplication.shared.terminate(nil) } - .keyboardShortcut("Q", modifiers: .command) + Button("Reload config") { + } // todo + Button("Quit \(Bundle.appName)") { + NSApplication.shared.terminate(nil) + } + .keyboardShortcut("Q", modifiers: .command) } label: { // .font(.system(.body, design: .monospaced)) doesn't work unfortunately :( Text(viewModel.focusedWorkspaceTrayText) diff --git a/src/ViewModel.swift b/src/ViewModel.swift index cf49a4da..11d45f64 100644 --- a/src/ViewModel.swift +++ b/src/ViewModel.swift @@ -6,6 +6,6 @@ class ViewModel: ObservableObject { private init() { } - @Published var focusedWorkspaceTrayText: String = currentEmptyWorkspace.name // settings.first?.name ?? "W: 1" + @Published var focusedWorkspaceTrayText: String = currentEmptyWorkspace.name // config.first?.name ?? "W: 1" } diff --git a/src/config/command.swift b/src/config/command.swift new file mode 100644 index 00000000..b27f1fb4 --- /dev/null +++ b/src/config/command.swift @@ -0,0 +1,77 @@ +import Foundation + +protocol Command { + func run() +} + +struct ChainedCommand: Command { + let subCommands: [Command] + + func run() { + for command in subCommands { + command.run() + } + } +} + +enum NoOpCommand: Command { + case instance + + func run() {} // It does nothing +} + +struct WorkspaceCommand : Command { + let workspaceName: String + + func run() { + switchToWorkspace(Workspace.get(byName: workspaceName)) + } +} + +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() + } + } + } + } +} + +struct BashCommand: Command { + let bashCommand: String + + func run() { + do { + try Process.run(URL(filePath: "/bin/bash"), arguments: ["-c", bashCommand]) + } catch { + } + } +} + +struct FocusCommand: Command { + let direction: Direction + + enum Direction { + case up + case down + case left + case right + + case parent + case child + case floating + case tiling + case toggle_tiling_floating + } + + func run() { + // todo + } +} diff --git a/src/config/config.swift b/src/config/config.swift new file mode 100644 index 00000000..07b0755c --- /dev/null +++ b/src/config/config.swift @@ -0,0 +1,203 @@ +import Foundation +import TOMLKit +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] +} + +var config: ConfigRoot = defaultConfig + +func reloadConfig() { + let rawConfig = String.fromUrl(FileManager.default.homeDirectoryForCurrentUser.appending(path: ".aerospace.toml")) + config = parseConfigRoot(rawConfig ?? "") +} + +private func parseConfigRoot(_ rawToml: String) -> ConfigRoot { + let rawTable: TOMLTable + do { + rawTable = try TOMLTable(string: rawToml) + } catch let e as TOMLParseError { + error(e.debugDescription) + } catch let e { + error(e.localizedDescription) + } + let backtrace: TomlBacktrace = .root + var config: Config? = nil + var modes: [Mode] = [] + for (key, value) in rawTable { + switch key { + case "config": + config = parseConfig(value, backtrace + .key("config")) + case "mode": + modes = parseModes(value, backtrace + .key("mode")) + default: + unknownKeyError(key, backtrace + .key(key)) + } + } + return ConfigRoot( + config: config ?? defaultConfig.config, + modes: modes + ) +} + +private func parseModes(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> [Mode] { + // todo + [] +} + +private func unknownKeyError(_ key: String, _ backtrace: TomlBacktrace) -> Never { + error("\(backtrace): Unknown key '\(key)'") +} + +private func expectedActualTypeError(expected: TOMLType, actual: TOMLType, _ backtrace: TomlBacktrace) -> T { + expectedActualTypeError(expected: [expected], actual: actual, backtrace) +} + +private func expectedActualTypeError(expected: [TOMLType], actual: TOMLType, _ backtrace: TomlBacktrace) -> T { + error("\(backtrace): Expected type is \(expected.map { $0.description }.joined(separator: " or ")). But actual type is \(actual)") +} + +private func parseConfig(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> Config { + let rawTable = raw.table ?? expectedActualTypeError(expected: .table, actual: raw.type, backtrace) + + let key1 = "after-startup-command" + var value1: Command = defaultConfig.config.afterStartupCommand + + let key2 = "use-padding-for-nested-containers-with-the-same-orientation" + var value2: Bool = defaultConfig.config.usePaddingForNestedContainersWithTheSameOrientation + + let key3 = "auto-flatten-containers" + var value3: Bool = defaultConfig.config.autoFlattenContainers + + let key4 = "floating-windows-on-top" + var value4: Bool = defaultConfig.config.floatingWindowsOnTop + + for (key, value) in rawTable { + let keyBacktrace = backtrace + .key(key) + switch key { + case key1: + value1 = parseCommand(value, keyBacktrace) + case key2: + value2 = parseBool(value, keyBacktrace) + case key3: + value3 = parseBool(value, keyBacktrace) + case key4: + value4 = parseBool(value, keyBacktrace) + default: + unknownKeyError(key, backtrace + .key(key)) + } + } + + return Config( + afterStartupCommand: value1, + usePaddingForNestedContainersWithTheSameOrientation: value2, + autoFlattenContainers: value3, + floatingWindowsOnTop: value4 + ) +} + +private func parseBool(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> Bool { + 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 + case key(String) + case index(Int) + case pair((TomlBacktrace, TomlBacktrace)) + + var description: String { + switch self { + case .root: + return "" // todo rework + 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) + } else if let rawArray = raw.array { + let commands: [Command] = (0.. Command { + let words = raw.split(separator: " ") + let args = words[1...] + switch words.first { + case "workspace": + let name = args.singleOrNil() ?? errorT( + "\(backtrace): Can't parse 'workspace' command arguments: '\(args.joined())'. Expected: a single arg" + ) + return WorkspaceCommand(workspaceName: String(name)) + case "mode": + let id = args.singleOrNil() ?? errorT( + "\(backtrace): Can't parse 'mode' command arguments: '\(args.joined())'. Expected: a single arg" + ) + return ModeCommand(idToActivate: String(id)) + case "bash": + return BashCommand(bashCommand: raw.removePrefix("bash")) + case "": + error("\(backtrace): Can't parse empty string command") + default: + error("\(backtrace): Can't parse '\(raw)' command") + } +} diff --git a/src/config/defaultConfig.swift b/src/config/defaultConfig.swift new file mode 100644 index 00000000..44100f15 --- /dev/null +++ b/src/config/defaultConfig.swift @@ -0,0 +1,20 @@ +let defaultConfig = ConfigRoot( + config: Config( + afterStartupCommand: NoOpCommand.instance, + usePaddingForNestedContainersWithTheSameOrientation: false, + autoFlattenContainers: true, + floatingWindowsOnTop: true + ), + modes: [ + Mode( + id: "main", + name: nil, + bindings: [ + HotkeyBinding(.option, .h, FocusCommand(direction: .left)), + HotkeyBinding(.option, .j, FocusCommand(direction: .down)), + HotkeyBinding(.option, .k, FocusCommand(direction: .up)), + HotkeyBinding(.option, .l, FocusCommand(direction: .right)), + ] + ) + ] +) diff --git a/src/model/TilingContainer.swift b/src/model/TilingContainer.swift index c2040429..19d15fd4 100644 --- a/src/model/TilingContainer.swift +++ b/src/model/TilingContainer.swift @@ -20,7 +20,7 @@ class TilingContainer: TreeNode { extension TilingContainer { func normalizeWeightsRecursive() { guard let delta = (getWeight(orientation) - children.sumOf { $0.getWeight(orientation) }) - .div(children.count) else { return } + .div(children.count) else { return } for child in children { child.setWeight(orientation, child.getWeight(orientation) + delta) if let tilingChild = child as? TilingContainer { diff --git a/src/model/Workspace.swift b/src/model/Workspace.swift index 4e57b910..1ab14a0a 100644 --- a/src/model/Workspace.swift +++ b/src/model/Workspace.swift @@ -153,15 +153,15 @@ private func rearrangeWorkspacesOnMonitors() { monitorToNotEmptyWorkspace = [:] let monitors = NSScreen.screens.map { $0.monitor }.toSet() let preservedWorkspaces: [Workspace] = oldMonitorToWorkspaces - .filter { oldMonitor, oldWorkspace in monitors.contains(oldMonitor) } - .map { $0.value.valueOrNil } - .filterNotNil() + .filter { oldMonitor, oldWorkspace in monitors.contains(oldMonitor) } + .map { $0.value.valueOrNil } + .filterNotNil() let lostWorkspaces: [Workspace] = oldMonitorToWorkspaces - .filter { oldMonitor, oldWorkspace in !monitors.contains(oldMonitor) } - .map { $0.value.valueOrNil } - .filterNotNil() + .filter { oldMonitor, oldWorkspace in !monitors.contains(oldMonitor) } + .map { $0.value.valueOrNil } + .filterNotNil() var poolOfWorkspaces: [Workspace] = - Workspace.all.reversed() - (preservedWorkspaces + lostWorkspaces) + lostWorkspaces + Workspace.all.reversed() - (preservedWorkspaces + lostWorkspaces) + lostWorkspaces for monitor in monitors { // If monitors change, most likely we will preserve only the main monitor (It always has (0, 0) origin) if let existing = oldMonitorToWorkspaces[monitor] { diff --git a/src/settings/Toml.swift b/src/settings/Toml.swift deleted file mode 100644 index 4250cffe..00000000 --- a/src/settings/Toml.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import TOMLKit - -func foo(table: TOMLTable) { - table["foo"]?.type - //TOMLTable() - //TOMLDecoder().dec -} diff --git a/src/util/ArrayEx.swift b/src/util/ArrayEx.swift index aa0803d0..9967ff81 100644 --- a/src/util/ArrayEx.swift +++ b/src/util/ArrayEx.swift @@ -1,8 +1,4 @@ extension Array { - func singleOrNil() -> Element? { - count == 1 ? first : nil - } - func singleOrNil(where predicate: (Self.Element) throws -> Bool) rethrows -> Self.Element? { var found: Self.Element? = nil for elem in self { diff --git a/src/util/CollectionEx.swift b/src/util/CollectionEx.swift new file mode 100644 index 00000000..956e7e51 --- /dev/null +++ b/src/util/CollectionEx.swift @@ -0,0 +1,5 @@ +extension Collection { + func singleOrNil() -> Element? { + count == 1 ? first : nil + } +} \ No newline at end of file diff --git a/src/util/utils.swift b/src/util/utils.swift index 179b94d8..2a5f9c1d 100644 --- a/src/util/utils.swift +++ b/src/util/utils.swift @@ -37,6 +37,20 @@ extension String? { var isNilOrEmpty: Bool { self == nil || self == "" } } +extension String { + static func fromUrl(_ url: URL) -> String? { + do { + return try String(contentsOf: url) + } catch { + return nil + } + } + + func removePrefix(_ prefix: String) -> String { + hasPrefix(prefix) ? String(dropFirst(prefix.count)) : self + } +} + extension Double { var squared: Double { self * self } } @@ -57,11 +71,11 @@ extension CGPoint { /// Distance to ``Rect`` outline frame func distanceToRectFrame(to rect: Rect) -> CGFloat { let list: [CGFloat] = ((rect.minY..