From 0445ea5585e6ef329bb470c79166e0c8c598a245 Mon Sep 17 00:00:00 2001 From: Marcin Radoszewski Date: Sun, 19 Nov 2023 21:14:53 +0100 Subject: [PATCH 1/2] Make loading YAML config a default. --- doc/releasing.md | 4 +- example-config.yaml | 70 +++++++++++++++++++ src/main/kotlin/gh/marad/tiler/Main.kt | 3 +- .../kotlin/gh/marad/tiler/app/internal/App.kt | 19 ++++- .../gh/marad/tiler/config/ConfigFacade.kt | 16 ++++- .../tiler/config/internal/SimpleConfig.kt | 5 ++ .../marad/tiler/config/internal/YamlConfig.kt | 64 ++++++++++++----- src/main/kotlin/gh/marad/tiler/os/OsFacade.kt | 1 + .../gh/marad/tiler/os/internal/WindowsOs.kt | 10 ++- .../gh/marad/tiler/tiler/internal/Tiler.kt | 2 +- 10 files changed, 168 insertions(+), 26 deletions(-) create mode 100644 example-config.yaml diff --git a/doc/releasing.md b/doc/releasing.md index 3c5eba0..c77fb0e 100644 --- a/doc/releasing.md +++ b/doc/releasing.md @@ -1,4 +1,6 @@ -This project uses [axion-release-plugin](https://github.com/allegro/axion-release-plugin) to create releases. It automatically creates patch semantic versions on `./gradlew release`. To bump the major or minor version you need to create new GIT tag which denotes newer version: +This project uses [axion-release-plugin](https://github.com/allegro/axion-release-plugin) to create releases. It +automatically creates patch semantic versions on `./gradlew release`. To bump the major or minor version you need to +create new GIT tag which denotes newer version: ```bash ./gradlew tag v0.1.0 diff --git a/example-config.yaml b/example-config.yaml new file mode 100644 index 0000000..ce2f440 --- /dev/null +++ b/example-config.yaml @@ -0,0 +1,70 @@ +editor: "C:\\Program Files\\Neovim\\bin\\nvim-qt.exe" + +layout: + gap: 0 + ratio: 0.55 + +rules: + - exeName: "nvim-qt.exe" + should: manage + +hotkeys: + # Switch windows + - key: S-C-A-Y + action: SwitchView + value: 0 + - key: S-C-A-U + action: SwitchView + value: 1 + - key: S-C-A-I + action: SwitchView + value: 2 + - key: S-C-A-O + action: SwitchView + value: 3 + - key: S-C-A-P + action: SwitchView + value: 4 + + # Move window to view + - key: S-A-Y + action: MoveActiveWindowToView + value: 0 + - key: S-A-U + action: MoveActiveWindowToView + value: 1 + - key: S-A-I + action: MoveActiveWindowToView + value: 3 + - key: S-A-O + action: MoveActiveWindowToView + value: 4 + - key: S-A-P + action: MoveActiveWindowToView + value: 5 + + # Switch to previous window + - key: S-C-A-F + action: SwitchToPreviousView + + # Window navigation + - key: S-C-A-H + action: MoveWindowLeft + - key: S-C-A-J + action: MoveWindowDown + - key: S-C-A-K + action: MoveWindowUp + - key: S-C-A-L + action: MoveWindowRight + + # Layout ratio change + - key: S-A-L + action: LayoutIncrease + value: 0.05 + - key: S-A-H + action: LayoutDecrease + value: 0.05 + + # Reload config + - key: S-C-A-R + action: ReloadConfig diff --git a/src/main/kotlin/gh/marad/tiler/Main.kt b/src/main/kotlin/gh/marad/tiler/Main.kt index bbbf7ab..d1df76e 100644 --- a/src/main/kotlin/gh/marad/tiler/Main.kt +++ b/src/main/kotlin/gh/marad/tiler/Main.kt @@ -3,6 +3,7 @@ package gh.marad.tiler import gh.marad.tiler.app.AppFacade import gh.marad.tiler.config.ConfigException import gh.marad.tiler.config.ConfigFacade +import gh.marad.tiler.os.OsFacade import org.docopt.Docopt import org.slf4j.LoggerFactory @@ -42,6 +43,6 @@ fun getConfig(data: Map): ConfigFacade { ConfigFacade.loadYamlConfig(configPath) } else { logger.info("Loading default configuration") - ConfigFacade.createConfig() + ConfigFacade.createConfig(OsFacade.createWindowsFacade()) } } \ No newline at end of file diff --git a/src/main/kotlin/gh/marad/tiler/app/internal/App.kt b/src/main/kotlin/gh/marad/tiler/app/internal/App.kt index 6b180e3..8d72618 100644 --- a/src/main/kotlin/gh/marad/tiler/app/internal/App.kt +++ b/src/main/kotlin/gh/marad/tiler/app/internal/App.kt @@ -1,6 +1,7 @@ package gh.marad.tiler.app.internal import gh.marad.tiler.actions.ActionsFacade +import gh.marad.tiler.actions.ReloadConfig import gh.marad.tiler.app.AppFacade import gh.marad.tiler.common.BroadcastingEventHandler import gh.marad.tiler.config.ConfigFacade @@ -8,6 +9,7 @@ import gh.marad.tiler.config.Hotkey import gh.marad.tiler.os.OsFacade import gh.marad.tiler.tiler.TilerFacade import org.slf4j.LoggerFactory +import java.awt.MenuItem import java.awt.SystemTray import java.awt.Toolkit import java.awt.TrayIcon @@ -17,7 +19,7 @@ class App(val config: ConfigFacade, val os: OsFacade, val tiler: TilerFacade, va @Suppress("UNUSED_VARIABLE") override fun start() { - val trayIcon = createTrayIcon(os, tiler) + val trayIcon = createTrayIcon(os, tiler, actions) setupHotkeys(config.getHotkeys()) actions.registerActionListener(ActionHandler(this, os, tiler)) val executor = TilerCommandsExecutorAndWatcher(os, config.getFilteringRules()) @@ -48,7 +50,7 @@ class App(val config: ConfigFacade, val os: OsFacade, val tiler: TilerFacade, va } } - private fun createTrayIcon(os: OsFacade, tiler: TilerFacade): TrayIcon { + private fun createTrayIcon(os: OsFacade, tiler: TilerFacade, actions: ActionsFacade): TrayIcon { val icon = Toolkit.getDefaultToolkit().getImage(AppFacade::class.java.getResource("/icon.png")) val stopped = Toolkit.getDefaultToolkit().getImage(AppFacade::class.java.getResource("/stopped_icon.png")) val trayIcon = TrayIcon(icon, "Tiler") @@ -59,6 +61,7 @@ class App(val config: ConfigFacade, val os: OsFacade, val tiler: TilerFacade, va trayIcon.addMouseListener(object : java.awt.event.MouseAdapter() { override fun mouseClicked(e: java.awt.event.MouseEvent?) { + // TODO: This should be extracted to actions if (e?.button == java.awt.event.MouseEvent.BUTTON1) { if (trayIcon.image == stopped) { trayIcon.image = icon @@ -73,7 +76,17 @@ class App(val config: ConfigFacade, val os: OsFacade, val tiler: TilerFacade, va }) val popupMenu = java.awt.PopupMenu() - popupMenu.add(java.awt.MenuItem("Exit")).addActionListener { + if (config.getConfigPath() != null) { + popupMenu.add(MenuItem("Edit config")).addActionListener { + val editor = ProcessBuilder(config.configEditorPath(), config.getConfigPath()) + editor.start() + } + popupMenu.add(MenuItem("Reload config")).addActionListener { + actions.invokeAction(ReloadConfig) + } + popupMenu.addSeparator() + } + popupMenu.add(MenuItem("Exit")).addActionListener { System.exit(0) } trayIcon.popupMenu = popupMenu diff --git a/src/main/kotlin/gh/marad/tiler/config/ConfigFacade.kt b/src/main/kotlin/gh/marad/tiler/config/ConfigFacade.kt index 98ad06c..05f4168 100644 --- a/src/main/kotlin/gh/marad/tiler/config/ConfigFacade.kt +++ b/src/main/kotlin/gh/marad/tiler/config/ConfigFacade.kt @@ -3,8 +3,10 @@ package gh.marad.tiler.config import gh.marad.tiler.common.assignments.WindowAssignments import gh.marad.tiler.common.filteringrules.FilteringRules import gh.marad.tiler.common.layout.Layout -import gh.marad.tiler.config.internal.SimpleConfig import gh.marad.tiler.config.internal.YamlConfig +import gh.marad.tiler.os.OsFacade +import java.nio.file.Paths +import kotlin.io.path.exists interface ConfigFacade { fun reload() @@ -12,9 +14,19 @@ interface ConfigFacade { fun getHotkeys(): List fun getFilteringRules(): FilteringRules fun getAssignments(): WindowAssignments + fun getConfigPath(): String? + fun configEditorPath(): String companion object { - fun createConfig(): ConfigFacade = SimpleConfig() + fun createConfig(osFacade: OsFacade): ConfigFacade { + val defaultConfigPath = Paths.get(osFacade.userHome(), ".config", "tiler", "config.yaml") + if (!defaultConfigPath.exists()) { + defaultConfigPath.parent.toFile().mkdirs() + defaultConfigPath.toFile().createNewFile() + } + return loadYamlConfig(defaultConfigPath.toString()) + } + fun loadYamlConfig(path: String): ConfigFacade = YamlConfig(path) } diff --git a/src/main/kotlin/gh/marad/tiler/config/internal/SimpleConfig.kt b/src/main/kotlin/gh/marad/tiler/config/internal/SimpleConfig.kt index a43f29f..22fe101 100644 --- a/src/main/kotlin/gh/marad/tiler/config/internal/SimpleConfig.kt +++ b/src/main/kotlin/gh/marad/tiler/config/internal/SimpleConfig.kt @@ -73,4 +73,9 @@ class SimpleConfig : ConfigFacade { override fun getFilteringRules(): FilteringRules = filteringRules override fun getAssignments(): WindowAssignments = assignments + + override fun getConfigPath(): String? = null + override fun configEditorPath(): String { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/src/main/kotlin/gh/marad/tiler/config/internal/YamlConfig.kt b/src/main/kotlin/gh/marad/tiler/config/internal/YamlConfig.kt index d6a85c7..8844441 100644 --- a/src/main/kotlin/gh/marad/tiler/config/internal/YamlConfig.kt +++ b/src/main/kotlin/gh/marad/tiler/config/internal/YamlConfig.kt @@ -25,12 +25,12 @@ class YamlConfig(configPath: String) : ConfigFacade { private var layoutCreator: () -> Layout = { TwoColumnLayout(0.55f) } private val filteringRules: FilteringRules = FilteringRules() private val hotkeys = mutableListOf() + private var configEditorPath: String = "notepad.exe" init { loadConfig() } - @Suppress("UNCHECKED_CAST") fun loadConfig() { logger.info("Loading config from $configPath...") if (Files.notExists(configPath)) { @@ -39,9 +39,12 @@ class YamlConfig(configPath: String) : ConfigFacade { val fileStream = Files.newInputStream(configPath, StandardOpenOption.READ) val yaml = Yaml() val data = yaml.load>(fileStream) - readLayout(data["layout"] as Map) - readFilteringRules(data["rules"] as List>) - readHotkeys(data["hotkeys"] as List>) + if (data != null) { + configEditorPath = data["editor"]?.toString() ?: configEditorPath + readLayout(data["layout"]) + readFilteringRules(data["rules"]) + readHotkeys(data["hotkeys"]) + } } override fun reload() { @@ -61,14 +64,28 @@ class YamlConfig(configPath: String) : ConfigFacade { } override fun getAssignments(): WindowAssignments { - TODO("Not yet implemented") + return WindowAssignments() + } + + override fun getConfigPath(): String { + return configPath.toAbsolutePath().toString() + } + + override fun configEditorPath(): String { + return configEditorPath } @Suppress("UNCHECKED_CAST") - private fun readLayout(data: Map) { - val name = data["name"].toString() - val gap = data["gap"].toString().toInt() - val ratio = data["ratio"].toString().toFloat() + private fun readLayout(section: Any?) { + val data = if (section != null) { + section as Map + } else { + return + } + + val name = data["name"]?.toString() ?: "TwoColumnLayout" + val gap = data["gap"]?.toString()?.toInt() ?: 0 + val ratio = data["ratio"]?.toString()?.toFloat() ?: 0.55f val minSize = data["minSize"] as Map? layoutCreator = when (name) { @@ -78,11 +95,13 @@ class YamlConfig(configPath: String) : ConfigFacade { if (minSize != null) { val wrappedLayout = layoutCreator - layoutCreator = { MinWindowSizeLayoutDecorator( - minimumWidth = minSize["width"]?.toString()?.toInt() ?: 1, - minimumHeight = minSize["height"]?.toString()?.toInt() ?: 1, - wrappedLayout = wrappedLayout() - ) } + layoutCreator = { + MinWindowSizeLayoutDecorator( + minimumWidth = minSize["width"]?.toString()?.toInt() ?: 1, + minimumHeight = minSize["height"]?.toString()?.toInt() ?: 1, + wrappedLayout = wrappedLayout() + ) + } } if (gap != 0) { @@ -91,7 +110,14 @@ class YamlConfig(configPath: String) : ConfigFacade { } } - private fun readFilteringRules(rules: List>) { + private fun readFilteringRules(section: Any?) { + val rules = if (section != null) { + @Suppress("UNCHECKED_CAST") + section as List> + } else { + return + } + filteringRules.clear() rules.forEach { ruleSpec -> val title = ruleSpec["title"]?.toString() @@ -115,7 +141,13 @@ class YamlConfig(configPath: String) : ConfigFacade { } - private fun readHotkeys(hotkeysConfig: List>) { + private fun readHotkeys(section: Any?) { + val hotkeysConfig = if (section != null) { + @Suppress("UNCHECKED_CAST") + section as List> + } else { + return + } hotkeys.clear() hotkeysConfig.forEach { map -> val key = map["key"].toString() diff --git a/src/main/kotlin/gh/marad/tiler/os/OsFacade.kt b/src/main/kotlin/gh/marad/tiler/os/OsFacade.kt index 53ecdb7..c30ba5b 100644 --- a/src/main/kotlin/gh/marad/tiler/os/OsFacade.kt +++ b/src/main/kotlin/gh/marad/tiler/os/OsFacade.kt @@ -15,6 +15,7 @@ interface OsFacade { fun execute(commands: List) fun startEventHandling(handler: WindowEventHandler) fun isWindowAtPosition(windowId: WindowId, position: WindowPosition): Boolean + fun userHome(): String fun windowDebugInfo(window: Window): String companion object { diff --git a/src/main/kotlin/gh/marad/tiler/os/internal/WindowsOs.kt b/src/main/kotlin/gh/marad/tiler/os/internal/WindowsOs.kt index 45f3743..90a94eb 100644 --- a/src/main/kotlin/gh/marad/tiler/os/internal/WindowsOs.kt +++ b/src/main/kotlin/gh/marad/tiler/os/internal/WindowsOs.kt @@ -1,6 +1,7 @@ package gh.marad.tiler.os.internal import com.sun.jna.platform.win32.User32 +import com.sun.jna.platform.win32.WinDef import com.sun.jna.platform.win32.WinDef.RECT import com.sun.jna.platform.win32.WinUser.WINDOWPLACEMENT import gh.marad.tiler.common.* @@ -11,7 +12,9 @@ import gh.marad.tiler.os.internal.winapi.* import gh.marad.tiler.os.internal.winapi.Window as OsWindow class WindowsOs : OsFacade { - private val hotkeys = Hotkeys() + companion object { + private val hotkeys = Hotkeys() + } // private val myU32 = Native.load("user32", MyUser32::class.java, W32APIOptions.DEFAULT_OPTIONS) override fun getDesktopState(): DesktopState { @@ -76,7 +79,7 @@ class WindowsOs : OsFacade { is ShowWindow -> { val hwnd = (command.windowId as WID).handle User32.INSTANCE.ShowWindow(hwnd, User32.SW_SHOWNOACTIVATE) -// User32.INSTANCE.RedrawWindow(hwnd, null, null, WinDef.DWORD(User32.RDW_INVALIDATE.toLong())) + User32.INSTANCE.RedrawWindow(hwnd, null, null, WinDef.DWORD(User32.RDW_INVALIDATE.toLong())) } is ActivateWindow -> { @@ -124,6 +127,9 @@ class WindowsOs : OsFacade { targetPosition.top == actualPosition.top } + override fun userHome(): String = + System.getenv("userprofile") + override fun windowDebugInfo(window: Window): String { val wid = window.id as WID val osWindow = OsWindow(wid.handle) diff --git a/src/main/kotlin/gh/marad/tiler/tiler/internal/Tiler.kt b/src/main/kotlin/gh/marad/tiler/tiler/internal/Tiler.kt index f02ddd5..b25a9a1 100644 --- a/src/main/kotlin/gh/marad/tiler/tiler/internal/Tiler.kt +++ b/src/main/kotlin/gh/marad/tiler/tiler/internal/Tiler.kt @@ -25,7 +25,7 @@ class Tiler( override fun initializeWithOpenWindows(): List { if (!enabled) return emptyList() viewManager.changeCurrentView(0) - os.getDesktopState().getManagableWindows(filteringRules).forEach { + os.getDesktopState().getManagableWindows(filteringRules).reversed().forEach { if (!it.isPopup && !it.isMinimized && it.isVisible) { viewManager.currentView().addWindow(it.id) } From 0c2b20643354ce27df545f910d7fc2a944b6a19c Mon Sep 17 00:00:00 2001 From: Marcin Radoszewski Date: Sun, 19 Nov 2023 21:20:12 +0100 Subject: [PATCH 2/2] Fix tests --- src/test/kotlin/gr/marad/tiler/GraphicalTest.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/gr/marad/tiler/GraphicalTest.kt b/src/test/kotlin/gr/marad/tiler/GraphicalTest.kt index 01113ef..7c5925f 100644 --- a/src/test/kotlin/gr/marad/tiler/GraphicalTest.kt +++ b/src/test/kotlin/gr/marad/tiler/GraphicalTest.kt @@ -64,6 +64,10 @@ val os = object : OsFacade { TODO("Not yet implemented") } + override fun userHome(): String { + TODO("Not yet implemented") + } + override fun execute(command: TilerCommand) { TODO("Not yet implemented") } @@ -99,7 +103,7 @@ val os = object : OsFacade { } } -val config = ConfigFacade.createConfig() +val config = ConfigFacade.createConfig(os) val tiler = TilerFacade.createTiler(config, os) val executor = TilerCommandsExecutorAndWatcher(os, filteringRules) val eventHandler = TilerWindowEventHandler(tiler, filteringRules, os, executor)