window_snap.py (7126B)
1 """Tools for voice-driven window management. 2 3 Originally from dweil/talon_community - modified for newapi by jcaw. 4 5 """ 6 7 # TODO: Map keyboard shortcuts to this manager once Talon has key hooks on all 8 # platforms 9 10 import time 11 from operator import xor 12 from typing import Optional 13 14 from talon import ui, Module, Context, actions 15 16 17 def sorted_screens(): 18 """Return screens sorted by their topmost, then leftmost, edge. 19 20 Screens will be sorted left-to-right, then top-to-bottom as a tiebreak. 21 22 """ 23 24 return sorted( 25 sorted(ui.screens(), key=lambda screen: screen.visible_rect.top), 26 key=lambda screen: screen.visible_rect.left, 27 ) 28 29 30 def _set_window_pos(window, x, y, width, height): 31 """Helper to set the window position.""" 32 # TODO: Special case for full screen move - use os-native maximize, rather 33 # than setting the position? 34 35 # 2020/10/01: While the upstream Talon implementation for MS Windows is 36 # settling, this may be buggy on full screen windows. Aegis doesn't want a 37 # hacky solution merged, so for now just repeat the command. 38 # 39 # TODO: Audit once upstream Talon is bug-free on MS Windows 40 window.rect = ui.Rect(round(x), round(y), round(width), round(height)) 41 42 43 def _bring_forward(window): 44 current_window = ui.active_window() 45 try: 46 window.focus() 47 current_window.focus() 48 except Exception as e: 49 # We don't want to block if this fails. 50 print(f"Couldn't bring window to front: {e}") 51 52 53 def _get_app_window(app_name: str) -> ui.Window: 54 return actions.self.get_running_app(app_name).active_window 55 56 57 def _move_to_screen( 58 window, offset: Optional[int] = None, screen_number: Optional[int] = None 59 ): 60 """Move a window to a different screen. 61 62 Provide one of `offset` or `screen_number` to specify a target screen. 63 64 Provide `window` to move a specific window, otherwise the current window is 65 moved. 66 67 """ 68 assert ( 69 screen_number or offset and not (screen_number and offset) 70 ), "Provide exactly one of `screen_number` or `offset`." 71 72 src_screen = window.screen 73 screens = sorted_screens() 74 if offset: 75 screen_number = (screens.index(src_screen) + offset) % len(screens) 76 else: 77 # Human to array index 78 screen_number -= 1 79 80 dest_screen = screens[screen_number] 81 if src_screen == dest_screen: 82 return 83 84 # Retain the same proportional position on the new screen. 85 dest = dest_screen.visible_rect 86 src = src_screen.visible_rect 87 # TODO: Test this on different-sized screens 88 # 89 # TODO: Is this the best behaviour for moving to a vertical screen? Probably 90 # not. 91 proportional_width = dest.width / src.width 92 proportional_height = dest.height / src.height 93 _set_window_pos( 94 window, 95 x=dest.left + (window.rect.left - src.left) * proportional_width, 96 y=dest.top + (window.rect.top - src.top) * proportional_height, 97 width=window.rect.width * proportional_width, 98 height=window.rect.height * proportional_height, 99 ) 100 101 102 def _snap_window_helper(window, pos): 103 screen = window.screen.visible_rect 104 105 _set_window_pos( 106 window, 107 x=screen.x + (screen.width * pos.left), 108 y=screen.y + (screen.height * pos.top), 109 width=screen.width * (pos.right - pos.left), 110 height=screen.height * (pos.bottom - pos.top), 111 ) 112 113 114 class RelativeScreenPos(object): 115 """Represents a window position as a fraction of the screen.""" 116 117 def __init__(self, left, top, right, bottom): 118 self.left = left 119 self.top = top 120 self.bottom = bottom 121 self.right = right 122 123 124 mod = Module() 125 mod.list( 126 "window_snap_positions", 127 "Predefined window positions for the current window. See `RelativeScreenPos`.", 128 ) 129 130 131 _snap_positions = { 132 # Halves 133 # .---.---. .-------. 134 # | | | & |-------| 135 # '---'---' '-------' 136 "left": RelativeScreenPos(0, 0, 0.5, 1), 137 "right": RelativeScreenPos(0.5, 0, 1, 1), 138 "top": RelativeScreenPos(0, 0, 1, 0.5), 139 "bottom": RelativeScreenPos(0, 0.5, 1, 1), 140 # Thirds 141 # .--.--.--. 142 # | | | | 143 # '--'--'--' 144 "center third": RelativeScreenPos(1 / 3, 0, 2 / 3, 1), 145 "left third": RelativeScreenPos(0, 0, 1 / 3, 1), 146 "right third": RelativeScreenPos(2 / 3, 0, 1, 1), 147 "left two thirds": RelativeScreenPos(0, 0, 2 / 3, 1), 148 "right two thirds": RelativeScreenPos(1 / 3, 0, 1, 1,), 149 # Quarters 150 # .---.---. 151 # |---|---| 152 # '---'---' 153 "top left": RelativeScreenPos(0, 0, 0.5, 0.5), 154 "top right": RelativeScreenPos(0.5, 0, 1, 0.5), 155 "bottom left": RelativeScreenPos(0, 0.5, 0.5, 1), 156 "bottom right": RelativeScreenPos(0.5, 0.5, 1, 1), 157 # Sixths 158 # .--.--.--. 159 # |--|--|--| 160 # '--'--'--' 161 "top right third": RelativeScreenPos(2 / 3, 0, 1, 0.5), 162 "top left two thirds": RelativeScreenPos(0, 0, 2 / 3, 0.5), 163 "top right two thirds": RelativeScreenPos(1 / 3, 0, 1, 0.5), 164 "top center third": RelativeScreenPos(1 / 3, 0, 2 / 3, 0.5), 165 "bottom left third": RelativeScreenPos(0, 0.5, 1 / 3, 1), 166 "bottom right third": RelativeScreenPos(2 / 3, 0.5, 1, 1), 167 "bottom left two thirds": RelativeScreenPos(0, 0.5, 2 / 3, 1), 168 "bottom right two thirds": RelativeScreenPos(1 / 3, 0.5, 1, 1), 169 "bottom center third": RelativeScreenPos(1 / 3, 0.5, 2 / 3, 1), 170 # Special 171 "center": RelativeScreenPos(1 / 8, 1 / 6, 7 / 8, 5 / 6), 172 "full": RelativeScreenPos(0, 0, 1, 1), 173 "fullscreen": RelativeScreenPos(0, 0, 1, 1), 174 } 175 176 177 @mod.capture(rule="{user.window_snap_positions}") 178 def window_snap_position(m) -> RelativeScreenPos: 179 return _snap_positions[m.window_snap_positions] 180 181 182 ctx = Context() 183 ctx.lists["user.window_snap_positions"] = _snap_positions.keys() 184 185 186 @mod.action_class 187 class Actions: 188 def snap_window(pos: RelativeScreenPos) -> None: 189 """Move the active window to a specific position on-screen. 190 191 See `RelativeScreenPos` for the structure of this position. 192 193 """ 194 _snap_window_helper(ui.active_window(), pos) 195 196 def move_window_next_screen() -> None: 197 """Move the active window to a specific screen.""" 198 _move_to_screen(ui.active_window(), offset=1) 199 200 def move_window_previous_screen() -> None: 201 """Move the active window to the previous screen.""" 202 _move_to_screen(ui.active_window(), offset=-1) 203 204 def move_window_to_screen(screen_number: int) -> None: 205 """Move the active window leftward by one.""" 206 _move_to_screen(ui.active_window(), screen_number=screen_number) 207 208 def snap_app(app_name: str, pos: RelativeScreenPos): 209 """Snap a specific application to another screen.""" 210 window = _get_app_window(app_name) 211 _bring_forward(window) 212 _snap_window_helper(window, pos) 213 214 def move_app_to_screen(app_name: str, screen_number: int): 215 """Move a specific application to another screen.""" 216 window = _get_app_window(app_name) 217 print(window) 218 _bring_forward(window) 219 _move_to_screen( 220 window, screen_number=screen_number, 221 )