221 lines
7 KiB
Python
221 lines
7 KiB
Python
"""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,
|
|
)
|