dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

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         )