Repository: talonhub/community

Issue:
window management: `window move screen {number}`
for moving a window to a different screen: https://github.com/dwiel/talon_community/blob/master/misc/window_snap.py


Discussion:

Resolving patch:
diff --git a/code/switcher.py b/code/switcher.py
--- a/code/switcher.py
+++ b/code/switcher.py
@@ -2,7 +2,7 @@
 import re
 import time
 
-from talon import Context, Module, app, imgui, ui, fs
+from talon import Context, Module, app, imgui, ui, fs, actions
 
 # Construct at startup a list of overides for application names (similar to how homophone list is managed)
 # ie for a given talon recognition word set  `one note`, recognized this in these switcher functions as `ONENOTE`
@@ -137,45 +137,42 @@ def update_overrides(name, flags):
 
 @mod.action_class
 class Actions:
-    def switcher_focus(name: str):
-        """Focus a new application by  name"""
-
-        wanted_app = name
-
-        # we should use the capture result directly if it's already in the
-        # list of running applications
-        # otherwise, name is from <user.text> and we can be a bit fuzzier
-        if name not in running_application_dict:
-
-            # don't process silly things like "focus i"
+    def get_running_app(name: str) -> ui.App:
+        """Get the first available running app with `name`."""
+        # We should use the capture result directly if it's already in the list
+        # of running applications. Otherwise, name is from <user.text> and we
+        # can be a bit fuzzier
+        if name in running_application_dict:
+            for app in ui.apps():
+                if app.name == name and not app.background:
+                    return app
+            raise RuntimeError(f'App not running: "{name}"')
+        else:
+            # Don't process silly things like "focus i"
             if len(name) < 3:
-                print("switcher_focus skipped: len({}) < 3".format(name))
-                return
-
-            running = ctx.lists["self.running"]
-            wanted_app = None
-
-            for running_name in running.keys():
+                raise RuntimeError(
+                    f'Skipped getting app: "{name}" has less than 3 chars.'
+                )
 
+            for running_name, app in ctx.lists["self.running"].items():
                 if running_name == name or running_name.lower().startswith(
                     name.lower()
                 ):
-                    wanted_app = running[running_name]
-                    break
-
-            if wanted_app is None:
-                return
+                    return app
 
-        for cur_app in ui.apps():
-            if cur_app.name == wanted_app and not cur_app.background:
-                cur_app.focus()
+            raise RuntimeError(f'Could not find app "{name}"')
 
-                # there is currently only a reliable way to do this on mac
-                if app.platform == "mac":
-                    while cur_app != ui.active_app():
-                        time.sleep(0.1)
-
-                break
+    def switcher_focus(name: str):
+        """Focus a new application by  name"""
+        app = actions.self.get_running_app(name)
+        app.focus()
+
+        # Hacky solution to do this reliably on Mac.
+        timeout = 5
+        t1 = time.monotonic()
+        if app.platform == "mac":
+            while ui.active_app() != app and time.monotonic() - t1 < timeout:
+                time.sleep(0.1)
 
     def switcher_launch(path: str):
         """Launch a new application by path"""
diff --git a/code/window_snap.py b/code/window_snap.py
new file mode 100644
--- /dev/null
+++ b/code/window_snap.py
@@ -0,0 +1,221 @@
+"""Tools for voice-driven window management.
+
+Originally from dweil/talon_community - modified for newapi by jcaw.
+
+"""
+
+# TODO: Map keyboard shortcuts to this manager once Talon has key hooks on all
+#   platforms
+
+import time
+from operator import xor
+from typing import Optional
+
+from talon import ui, Module, Context, actions
+
+
+def sorted_screens():
+    """Return screens sorted by their topmost, then leftmost, edge.
+
+    Screens will be sorted left-to-right, then top-to-bottom as a tiebreak.
+
+    """
+
+    return sorted(
+        sorted(ui.screens(), key=lambda screen: screen.visible_rect.top),
+        key=lambda screen: screen.visible_rect.left,
+    )
+
+
+def _set_window_pos(window, x, y, width, height):
+    """Helper to set the window position."""
+    # TODO: Special case for full screen move - use os-native maximize, rather
+    #   than setting the position?
+
+    # 2020/10/01: While the upstream Talon implementation for MS Windows is
+    #   settling, this may be buggy on full screen windows. Aegis doesn't want a
+    #   hacky solution merged, so for now just repeat the command.
+    #
+    # TODO: Audit once upstream Talon is bug-free on MS Windows
+    window.rect = ui.Rect(round(x), round(y), round(width), round(height))
+
+
+def _bring_forward(window):
+    current_window = ui.active_window()
+    try:
+        window.focus()
+        current_window.focus()
+    except Exception as e:
+        # We don't want to block if this fails.
+        print(f"Couldn't bring window to front: {e}")
+
+
+def _get_app_window(app_name: str) -> ui.Window:
+    return actions.self.get_running_app(app_name).active_window
+
+
+def _move_to_screen(
+    window, offset: Optional[int] = None, screen_number: Optional[int] = None
+):
+    """Move a window to a different screen.
+
+    Provide one of `offset` or `screen_number` to specify a target screen.
+
+    Provide `window` to move a specific window, otherwise the current window is
+    moved.
+
+    """
+    assert (
+        screen_number or offset and not (screen_number and offset)
+    ), "Provide exactly one of `screen_number` or `offset`."
+
+    src_screen = window.screen
+    screens = sorted_screens()
+    if offset:
+        screen_number = (screens.index(src_screen) + offset) % len(screens)
+    else:
+        # Human to array index
+        screen_number -= 1
+
+    dest_screen = screens[screen_number]
+    if src_screen == dest_screen:
+        return
+
+    # Retain the same proportional position on the new screen.
+    dest = dest_screen.visible_rect
+    src = src_screen.visible_rect
+    # TODO: Test this on different-sized screens
+    #
+    # TODO: Is this the best behaviour for moving to a vertical screen? Probably
+    #   not.
+    proportional_width = dest.width / src.width
+    proportional_height = dest.height / src.height
+    _set_window_pos(
+        window,
+        x=dest.left + (window.rect.left - src.left) * proportional_width,
+        y=dest.top + (window.rect.top - src.top) * proportional_height,
+        width=window.rect.width * proportional_width,
+        height=window.rect.height * proportional_height,
+    )
+
+
+def _snap_window_helper(window, pos):
+    screen = window.screen.visible_rect
+
+    _set_window_pos(
+        window,
+        x=screen.x + (screen.width * pos.left),
+        y=screen.y + (screen.height * pos.top),
+        width=screen.width * (pos.right - pos.left),
+        height=screen.height * (pos.bottom - pos.top),
+    )
+
+
+class RelativeScreenPos(object):
+    """Represents a window position as a fraction of the screen."""
+
+    def __init__(self, left, top, right, bottom):
+        self.left = left
+        self.top = top
+        self.bottom = bottom
+        self.right = right
+
+
+mod = Module()
+mod.list(
+    "window_snap_positions",
+    "Predefined window positions for the current window. See `RelativeScreenPos`.",
+)
+
+
+_snap_positions = {
+    # Halves
+    # .---.---.     .-------.
+    # |   |   |  &  |-------|
+    # '---'---'     '-------'
+    "left": RelativeScreenPos(0, 0, 0.5, 1),
+    "right": RelativeScreenPos(0.5, 0, 1, 1),
+    "top": RelativeScreenPos(0, 0, 1, 0.5),
+    "bottom": RelativeScreenPos(0, 0.5, 1, 1),
+    # Thirds
+    # .--.--.--.
+    # |  |  |  |
+    # '--'--'--'
+    "center third": RelativeScreenPos(1 / 3, 0, 2 / 3, 1),
+    "left third": RelativeScreenPos(0, 0, 1 / 3, 1),
+    "right third": RelativeScreenPos(2 / 3, 0, 1, 1),
+    "left two thirds": RelativeScreenPos(0, 0, 2 / 3, 1),
+    "right two thirds": RelativeScreenPos(1 / 3, 0, 1, 1,),
+    # Quarters
+    # .---.---.
+    # |---|---|
+    # '---'---'
+    "top left": RelativeScreenPos(0, 0, 0.5, 0.5),
+    "top right": RelativeScreenPos(0.5, 0, 1, 0.5),
+    "bottom left": RelativeScreenPos(0, 0.5, 0.5, 1),
+    "bottom right": RelativeScreenPos(0.5, 0.5, 1, 1),
+    # Sixths
+    # .--.--.--.
+    # |--|--|--|
+    # '--'--'--'
+    "top right third": RelativeScreenPos(2 / 3, 0, 1, 0.5),
+    "top left two thirds": RelativeScreenPos(0, 0, 2 / 3, 0.5),
+    "top right two thirds": RelativeScreenPos(1 / 3, 0, 1, 0.5),
+    "top center third": RelativeScreenPos(1 / 3, 0, 2 / 3, 0.5),
+    "bottom left third": RelativeScreenPos(0, 0.5, 1 / 3, 1),
+    "bottom right third": RelativeScreenPos(2 / 3, 0.5, 1, 1),
+    "bottom left two thirds": RelativeScreenPos(0, 0.5, 2 / 3, 1),
+    "bottom right two thirds": RelativeScreenPos(1 / 3, 0.5, 1, 1),
+    "bottom center third": RelativeScreenPos(1 / 3, 0.5, 2 / 3, 1),
+    # Special
+    "center": RelativeScreenPos(1 / 8, 1 / 6, 7 / 8, 5 / 6),
+    "full": RelativeScreenPos(0, 0, 1, 1),
+    "fullscreen": RelativeScreenPos(0, 0, 1, 1),
+}
+
+
+@mod.capture(rule="{user.window_snap_positions}")
+def window_snap_position(m) -> RelativeScreenPos:
+    return _snap_positions[m.window_snap_positions]
+
+
+ctx = Context()
+ctx.lists["user.window_snap_positions"] = _snap_positions.keys()
+
+
+@mod.action_class
+class Actions:
+    def snap_window(pos: RelativeScreenPos) -> None:
+        """Move the active window to a specific position on-screen.
+
+        See `RelativeScreenPos` for the structure of this position.
+
+        """
+        _snap_window_helper(ui.active_window(), pos)
+
+    def move_window_next_screen() -> None:
+        """Move the active window to a specific screen."""
+        _move_to_screen(ui.active_window(), offset=1)
+
+    def move_window_previous_screen() -> None:
+        """Move the active window to the previous screen."""
+        _move_to_screen(ui.active_window(), offset=-1)
+
+    def move_window_to_screen(screen_number: int) -> None:
+        """Move the active window leftward by one."""
+        _move_to_screen(ui.active_window(), screen_number=screen_number)
+
+    def snap_app(app_name: str, pos: RelativeScreenPos):
+        """Snap a specific application to another screen."""
+        window = _get_app_window(app_name)
+        _bring_forward(window)
+        _snap_window_helper(window, pos)
+
+    def move_app_to_screen(app_name: str, screen_number: int):
+        """Move a specific application to another screen."""
+        window = _get_app_window(app_name)
+        print(window)
+        _bring_forward(window)
+        _move_to_screen(
+            window, screen_number=screen_number,
+        )


Test patch: