init commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# Laying Out Windows
|
||||
|
||||
The experimental laying out windows command requires that you first enable a tag. You can find an example in the provided settings.talon file, or you can set it yourself like this:
|
||||
|
||||
```
|
||||
tag(): user.experimental_window_layout
|
||||
```
|
||||
|
||||
The `layout` command allows you to lay out multiple windows around the screen in prearranged configurations. With a single command you can arrange multiple windows and if you repeat the same command it will rotate them. Here are some example arrangements:
|
||||
|
||||
Half: Split the screen into two halves. The first window goes to the left half and the second goes to the right.
|
||||
Thirds: Split the screen into thirds, arranging from left to right.
|
||||
Clock: Arrange one window on the left half, and split the right from top to bottom.
|
||||
|
||||
When arranging windows if you specify nothing it will arrange in order of windows from top to bottom-in other words, the most recent three windows that you have interacted with will be snapped into the arrangement. If you want more control you can specify windows by saying an application name or using an ordinal such as 'second' to refer to the second window from the top of the window manager (the second most recently used window). If you want to skip a particular position when arranging windows, you can use the word 'gap' to skip a position. You can also use the word 'all' to refer to the rest of the windows available filling up all available slots. Here are some examples:
|
||||
|
||||
1. `layout clock`: Arrange the most recent three windows in a clockwise layout
|
||||
2. `layout halves chrome slack`: Arrange chrome and slack in a split screen.
|
||||
3. `layout halves gap slack`: Arrange slack on the right (skipping the first placement, which would have been on the left)
|
||||
4. `layout clock second all`: Move these second from the top window to the first position, rearranging all other windows accordingly.
|
||||
|
||||
If you repeat any of these commands without interacting with the window using the mouse, it will rotate the arrangement.
|
||||
@@ -0,0 +1,21 @@
|
||||
from talon import Module, actions, app
|
||||
|
||||
mod = Module()
|
||||
|
||||
|
||||
@mod.action_class
|
||||
class TabActions:
|
||||
def tab_jump(number: int):
|
||||
"""Jumps to the specified tab"""
|
||||
|
||||
def tab_final():
|
||||
"""Jumps to the final tab"""
|
||||
|
||||
def tab_close_wrapper():
|
||||
"""Closes the current tab.
|
||||
Exists so that apps can implement their own delay before running tab_close() to handle repetitions better.
|
||||
"""
|
||||
actions.app.tab_close()
|
||||
|
||||
def tab_duplicate():
|
||||
"""Duplicates the current tab"""
|
||||
@@ -0,0 +1,10 @@
|
||||
tag: user.tabs
|
||||
-
|
||||
tab (open | new): app.tab_open()
|
||||
tab (last | previous): app.tab_previous()
|
||||
tab next: app.tab_next()
|
||||
tab close: user.tab_close_wrapper()
|
||||
tab (reopen | restore): app.tab_reopen()
|
||||
go tab <number>: user.tab_jump(number)
|
||||
go tab final: user.tab_final()
|
||||
tab (duplicate | clone): user.tab_duplicate()
|
||||
@@ -0,0 +1,291 @@
|
||||
import copy
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from talon import Context, Module, actions, settings, ui
|
||||
from talon.ui import UIErr, Window
|
||||
|
||||
from .windows_and_tabs import is_window_valid
|
||||
|
||||
"""Tools for laying out windows in an arrangement """
|
||||
|
||||
SPLIT_POSITIONS = {
|
||||
# Explicit layout names with only one configuration can be easier to force
|
||||
# the desired result:
|
||||
"HALF": ["LEFT", "RIGHT"],
|
||||
"THIRDS": ["LEFT_THIRD", "CENTER_THIRD", "RIGHT_THIRD"],
|
||||
"CLOCK": [
|
||||
"LEFT",
|
||||
"TOP_RIGHT",
|
||||
"BOTTOM_RIGHT",
|
||||
],
|
||||
"COUNTERCLOCK": [
|
||||
"RIGHT",
|
||||
"TOP_LEFT",
|
||||
"BOTTOM_LEFT",
|
||||
],
|
||||
"GRID": [
|
||||
"TOP_LEFT",
|
||||
"TOP_RIGHT",
|
||||
"BOTTOM_LEFT",
|
||||
"BOTTOM_RIGHT",
|
||||
],
|
||||
"BIG_GRID": [
|
||||
"TOP_LEFT_THIRD",
|
||||
"TOP_CENTER_THIRD",
|
||||
"TOP_RIGHT_THIRD",
|
||||
"BOTTOM_LEFT_THIRD",
|
||||
"BOTTOM_CENTER_THIRD",
|
||||
"BOTTOM_RIGHT_THIRD",
|
||||
],
|
||||
}
|
||||
|
||||
# Keys in `windows_snap.py` `_snap_positions`, ie "TopLeft", "BottomCenterThird", etc.
|
||||
SnapPosition = str
|
||||
|
||||
|
||||
@dataclass
|
||||
class WindowLayout:
|
||||
"""Represents a layout of windows on a screen"""
|
||||
|
||||
name: str
|
||||
split_positions: list[SnapPosition]
|
||||
windows: list[Window]
|
||||
can_rotate: bool
|
||||
rotation_count: int
|
||||
finish_time: float
|
||||
|
||||
|
||||
class Gap:
|
||||
"""Users can leave gaps or holes (as in a code snippet) when dictating a layout;
|
||||
this represents such a gap."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Create a union type for Talon windows and Gaps:
|
||||
Window = Union[Window, Gap]
|
||||
|
||||
# The current layout being arranged and the last one arranged, if any.
|
||||
layout_in_progress: Optional[WindowLayout] = None
|
||||
last_layout: Optional[WindowLayout] = None
|
||||
|
||||
|
||||
def snap_next(windows: list[Window], target_layout: SnapPosition) -> Optional[Window]:
|
||||
"""This function snaps a window and returns the window if successful"""
|
||||
while windows:
|
||||
window = windows.pop(0)
|
||||
if isinstance(window, Gap):
|
||||
return window
|
||||
try:
|
||||
actions.user.snap_window_to_position(
|
||||
target_layout,
|
||||
window,
|
||||
)
|
||||
|
||||
return window
|
||||
except (UIErr, AttributeError) as e:
|
||||
print(
|
||||
f'Failed to snap {window.app.name}\'s "{window.title}" window ({type(e).__name__} {e}); this is normal; continuing to the next'
|
||||
)
|
||||
return Gap()
|
||||
|
||||
|
||||
def snap_layout(layout: WindowLayout):
|
||||
"""Split the screen between multiple windows."""
|
||||
try:
|
||||
global layout_in_progress, last_layout
|
||||
layout_in_progress = layout
|
||||
|
||||
# If called multiple times (and the user hasn't focused a window manually since
|
||||
# last time), rotate the offset of the existing windows in the arrangement,
|
||||
# allowing the user to use a repeater to cycle through the windows to get the
|
||||
# desired result.
|
||||
if (
|
||||
layout.can_rotate
|
||||
and last_layout is not None
|
||||
and last_layout.name == layout.name
|
||||
and layout.windows == last_layout.windows
|
||||
):
|
||||
layout.rotation_count = last_layout.rotation_count + 1
|
||||
|
||||
# Copy these data structures so we can mutate them:
|
||||
remaining_windows = [w for w in layout.windows]
|
||||
split_positions = layout.split_positions.copy()
|
||||
|
||||
snapped_windows = []
|
||||
for _ in range(layout.rotation_count):
|
||||
split_positions.append(split_positions.pop(0))
|
||||
|
||||
while len(split_positions) > 0:
|
||||
snapped_window: Window = snap_next(
|
||||
remaining_windows, split_positions.pop(0)
|
||||
)
|
||||
snapped_windows.insert(0, snapped_window)
|
||||
|
||||
if len(snapped_windows) > 0:
|
||||
for _ in range(layout.rotation_count):
|
||||
snapped_windows.append(snapped_windows.pop(0))
|
||||
|
||||
for window in snapped_windows:
|
||||
if isinstance(window, Gap):
|
||||
continue
|
||||
actions.user.switcher_focus_window(window)
|
||||
|
||||
layout_in_progress.finish_time = time.perf_counter()
|
||||
last_layout = layout_in_progress
|
||||
finally:
|
||||
layout_in_progress = None
|
||||
|
||||
|
||||
def filter_nonviable_windows(windows: List[Window]) -> list[Window]:
|
||||
active_window = ui.active_window()
|
||||
|
||||
# Many invisible non-resizable windows are identifiable because they exist above the current window
|
||||
# in the z-index
|
||||
all_windows = ui.windows()
|
||||
active_window_idx = all_windows.index(active_window) # type: ignore
|
||||
return list(
|
||||
filter(
|
||||
lambda w: (isinstance(w, Gap) or is_window_valid(w)),
|
||||
windows,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
mod = Module()
|
||||
mod.list(
|
||||
"window_split_positions",
|
||||
"Predefined window positions when splitting the screen between multiple windows.",
|
||||
)
|
||||
mod.tag(
|
||||
"experimental_window_layout",
|
||||
desc="Tag to enable experimental window layout commands",
|
||||
)
|
||||
|
||||
ctx = Context()
|
||||
|
||||
|
||||
@mod.capture(rule="all")
|
||||
def all_candidate_windows(m) -> list[Window]:
|
||||
return filter_nonviable_windows(ui.windows())
|
||||
|
||||
|
||||
@mod.capture(rule="gap")
|
||||
def skip_window(m) -> list[Window]:
|
||||
return [Gap()]
|
||||
|
||||
|
||||
@mod.capture(rule="<user.running_applications>")
|
||||
def application_windows(m) -> list[Window]:
|
||||
return filter_nonviable_windows(
|
||||
[
|
||||
window
|
||||
for app in m.running_applications_list
|
||||
for window in actions.self.get_running_app(app).windows()
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@mod.capture(
|
||||
rule="<user.application_windows>|<user.numbered_windows>|<user.skip_window>"
|
||||
)
|
||||
def layout_item(m) -> list[Optional[Window]]:
|
||||
attributes = [
|
||||
"application_windows",
|
||||
"numbered_windows",
|
||||
"skip_window",
|
||||
]
|
||||
num_passed = len(list(filter(lambda attrs: hasattr(m, attrs), attributes)))
|
||||
if num_passed > 1:
|
||||
raise ValueError(
|
||||
"Multiple attributes found on 'm'. Only one of 'application_windows', 'numbered_windows', or 'skip_window' should be present."
|
||||
)
|
||||
|
||||
# Return the appropriate list based on which attribute is available
|
||||
if hasattr(m, "application_windows"):
|
||||
return m.application_windows
|
||||
elif hasattr(m, "numbered_windows"):
|
||||
return m.numbered_windows
|
||||
elif hasattr(m, "skip_window"):
|
||||
return m.skip_window
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
@mod.capture(rule="<user.ordinals_small>+")
|
||||
def numbered_windows(m) -> list[Window]:
|
||||
all_windows = filter_nonviable_windows(ui.windows())
|
||||
selected_windows = [
|
||||
all_windows[i - 1] for i in m.ordinals_small_list if i - 1 < len(all_windows)
|
||||
]
|
||||
return selected_windows
|
||||
|
||||
|
||||
@mod.capture(rule="<user.layout_item>+ [<user.all_candidate_windows>]")
|
||||
def target_windows(m) -> list[Window]:
|
||||
windows = []
|
||||
if hasattr(m, "layout_item_list"):
|
||||
windows += [window for sublist in m.layout_item_list for window in sublist]
|
||||
|
||||
if hasattr(m, "all_candidate_windows"):
|
||||
windows += [w for w in m.all_candidate_windows if w not in windows]
|
||||
return windows
|
||||
|
||||
|
||||
def pick_split_arrangement(
|
||||
target_windows: Optional[list[Window]],
|
||||
layout_name: str,
|
||||
) -> list[SnapPosition]:
|
||||
return SPLIT_POSITIONS[layout_name]
|
||||
|
||||
|
||||
@mod.capture(rule="{user.window_split_positions} [<user.target_windows>]")
|
||||
def window_layout(m) -> WindowLayout:
|
||||
global last_layout
|
||||
layout_name = m.window_split_positions
|
||||
window_was_specified = hasattr(m, "target_windows")
|
||||
|
||||
target_windows = (
|
||||
m.target_windows
|
||||
if window_was_specified
|
||||
else filter_nonviable_windows(ui.windows())
|
||||
)
|
||||
|
||||
layout = pick_split_arrangement(target_windows, layout_name)
|
||||
return WindowLayout(
|
||||
name=layout_name,
|
||||
split_positions=layout,
|
||||
windows=target_windows,
|
||||
can_rotate=True,
|
||||
rotation_count=0,
|
||||
finish_time=0,
|
||||
)
|
||||
|
||||
|
||||
@mod.action_class
|
||||
class Actions:
|
||||
def snap_layout(layout: WindowLayout):
|
||||
"""Split the screen between multiple applications."""
|
||||
snap_layout(layout)
|
||||
|
||||
|
||||
def focus_callback(_):
|
||||
global layout_in_progress
|
||||
global last_layout
|
||||
|
||||
# Running a layout will generate focus events, which we don't consider to be manual
|
||||
# / user initiated, so skip in that case.
|
||||
if last_layout is None or layout_in_progress is not None:
|
||||
return
|
||||
|
||||
# Track if the user has manually focused since layout and clear the state if so;
|
||||
# this way we won't rotate if that same layout request is made again.
|
||||
delta = time.perf_counter() - last_layout.finish_time
|
||||
if delta >= 1:
|
||||
last_layout = None
|
||||
|
||||
|
||||
ui.register("app_activate", focus_callback)
|
||||
ui.register("win_focus", focus_callback)
|
||||
@@ -0,0 +1,3 @@
|
||||
tag: user.experimental_window_layout
|
||||
-
|
||||
layout <user.window_layout>: user.snap_layout(window_layout)
|
||||
@@ -0,0 +1,23 @@
|
||||
window (new | open): app.window_open()
|
||||
window next: app.window_next()
|
||||
window last: app.window_previous()
|
||||
window close: app.window_close()
|
||||
window hide: app.window_hide()
|
||||
app (preferences | prefs | settings): app.preferences()
|
||||
focus <user.running_applications>: user.switcher_focus(running_applications)
|
||||
# following only works on windows. Can't figure out how to make it work for mac. No idea what the equivalent for linux would be.
|
||||
focus$: user.switcher_menu()
|
||||
focus last: user.switcher_focus_last()
|
||||
running list: user.switcher_toggle_running()
|
||||
running close: user.switcher_hide_running()
|
||||
launch <user.launch_applications>: user.switcher_launch(launch_applications)
|
||||
|
||||
snap <user.window_snap_position>: user.snap_window(window_snap_position)
|
||||
snap next [screen]: user.move_window_next_screen()
|
||||
snap last [screen]: user.move_window_previous_screen()
|
||||
snap screen <number>: user.move_window_to_screen(number)
|
||||
snap <user.running_applications> <user.window_snap_position>:
|
||||
user.snap_app(running_applications, window_snap_position)
|
||||
|
||||
snap <user.running_applications> [screen] <number>:
|
||||
user.move_app_to_screen(running_applications, number)
|
||||
@@ -0,0 +1,354 @@
|
||||
"""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 logging
|
||||
from typing import Dict, Optional
|
||||
|
||||
from talon import Context, Module, actions, app, registry, settings, ui
|
||||
from talon.ui import Window
|
||||
|
||||
mod = Module()
|
||||
mod.list(
|
||||
"window_snap_positions",
|
||||
"Predefined window positions for the current window. See `RelativeScreenPos`.",
|
||||
)
|
||||
mod.list(
|
||||
"window_split_positions",
|
||||
"Predefined window positions when splitting the screen between three applications.",
|
||||
)
|
||||
mod.setting(
|
||||
"window_snap_screen",
|
||||
type=str,
|
||||
default="proportional",
|
||||
desc="""How to position and size windows when snapping across different physical screens. Options:
|
||||
|
||||
"proportional" (default): Preserve the window's relative position and size proportional to the screen.
|
||||
|
||||
"size aware": Preserve position relative to the screen, but keep absolute size the same, except if window is full-height or -width, keep it so.
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def _set_window_pos(window, x, y, width, height):
|
||||
"""Helper to set the window position."""
|
||||
window.rect = ui.Rect(round(x), round(y), round(width), round(height))
|
||||
|
||||
# on occassion, for whatever reason, it fails to
|
||||
# position correctly on windows the first time
|
||||
if app.platform == "windows" and "user.experimental_window_layout" in registry.tags:
|
||||
actions.sleep("100ms")
|
||||
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 interpolate_interval(w0, w1, s0, s1, d0, d1):
|
||||
"""
|
||||
Interpolates an interval (w0, w1) which is within (s0, s1) so that it lies
|
||||
within (d0, d1). Returns (r0, r1). Tries to preserve absolute interval size,
|
||||
w1 - w0, while maintaining its relative 'position' within (s0, s1). For
|
||||
instance, if w0 == s0 then r0 == d0.
|
||||
|
||||
Use-case: fix a window w, a source screen s, and a destination screen d.
|
||||
Let w0 = w.left, w1 = window.right, s0 = s.left, s1 = s.right, d0 = d.left, d1 = d.right.
|
||||
"""
|
||||
wsize, ssize, dsize = w1 - w0, s1 - s0, d1 - d0
|
||||
assert wsize > 0 and ssize > 0 and dsize > 0
|
||||
before = max(0, (w0 - s0) / ssize)
|
||||
after = max(0, (s1 - w1) / ssize)
|
||||
# If we're within 5% of maximized, preserve this.
|
||||
if before + after <= 0.05:
|
||||
return (d0, d1)
|
||||
# If before is 0 (eg. window is left-aligned), we want to preserve before.
|
||||
# If after is 0 (eg. window is right-aligned), we want to preserve after.
|
||||
# In between, we linearly interpolate.
|
||||
beforeness = before / (before + after)
|
||||
afterness = after / (before + after)
|
||||
a0, b1 = d0 + before * dsize, d1 - after * dsize
|
||||
a1, b0 = a0 + wsize, b1 - wsize
|
||||
r0 = a0 * afterness + b0 * beforeness
|
||||
r1 = a1 * afterness + b1 * beforeness
|
||||
return (max(d0, r0), min(d1, r1)) # clamp to destination
|
||||
|
||||
|
||||
def _move_to_screen(
|
||||
window: ui.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
|
||||
|
||||
if offset:
|
||||
if offset < 0:
|
||||
dest_screen = actions.user.screens_get_previous(src_screen)
|
||||
else:
|
||||
dest_screen = actions.user.screens_get_next(src_screen)
|
||||
else:
|
||||
dest_screen = actions.user.screens_get_by_number(screen_number)
|
||||
|
||||
if src_screen == dest_screen:
|
||||
return
|
||||
|
||||
dest = dest_screen.visible_rect
|
||||
src = src_screen.visible_rect
|
||||
maximized = window.maximized
|
||||
how = settings.get("user.window_snap_screen")
|
||||
if how == "size aware":
|
||||
r = window.rect
|
||||
left, right = interpolate_interval(
|
||||
r.left, r.right, src.left, src.right, dest.left, dest.right
|
||||
)
|
||||
top, bot = interpolate_interval(
|
||||
r.top, r.bot, src.top, src.bot, dest.top, dest.bot
|
||||
)
|
||||
r.x, r.y = left, top
|
||||
r.width = right - left
|
||||
r.height = bot - top
|
||||
window.rect = r
|
||||
if maximized:
|
||||
window.maximized = True
|
||||
return
|
||||
|
||||
# TODO: Test vertical screen with different aspect ratios
|
||||
# Does the orientation between the screens change? (vertical/horizontal)
|
||||
if how != "proportional":
|
||||
logging.warning(
|
||||
f"Unrecognized 'window_snap_screen' setting: {how!r}. Using default 'proportional'."
|
||||
)
|
||||
if (src.width / src.height > 1) != (dest.width / dest.height > 1):
|
||||
# Horizontal -> vertical or vertical -> horizontal
|
||||
# Retain proportional window size, but flip x/y of the vertical monitor to account for the monitors rotation.
|
||||
if src.width / src.height > 1:
|
||||
# horizontal -> vertical
|
||||
width = window.rect.width * dest.height / src.width
|
||||
height = window.rect.height * dest.width / src.height
|
||||
else:
|
||||
# vertical -> horizontal
|
||||
width = window.rect.width * dest.width / src.height
|
||||
height = window.rect.height * dest.height / src.width
|
||||
# Deform window if width or height is bigger than the target monitors while keeping the window area the same.
|
||||
if width > dest.width:
|
||||
over = (width - dest.width) * height
|
||||
width = dest.width
|
||||
height += over / width
|
||||
if height > dest.height:
|
||||
over = (height - dest.height) * width
|
||||
height = dest.height
|
||||
width += over / height
|
||||
# Proportional position:
|
||||
# Since the window size in respect to the monitor size is not proportional (x/y was flipped),
|
||||
# the positioning is more complicated than proportionally scaling the x/y coordinates.
|
||||
# It is computed by keeping the free space to the left of the window proportional to the right
|
||||
# and respectively for the top/bottom free space.
|
||||
# The if conditions account for division by 0. TODO: Refactor positioning without division by 0
|
||||
if src.height == window.rect.height:
|
||||
x = dest.left + (dest.width - width) / 2
|
||||
else:
|
||||
x = dest.left + (window.rect.top - src.top) * (dest.width - width) / (
|
||||
src.height - window.rect.height
|
||||
)
|
||||
if src.width == window.rect.width:
|
||||
y = dest.top + (dest.height - height) / 2
|
||||
else:
|
||||
y = dest.top + (window.rect.left - src.left) * (dest.height - height) / (
|
||||
src.width - window.rect.width
|
||||
)
|
||||
else:
|
||||
# Horizontal -> horizontal or vertical -> vertical
|
||||
# Retain proportional size and position
|
||||
proportional_width = dest.width / src.width
|
||||
proportional_height = dest.height / src.height
|
||||
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
|
||||
_set_window_pos(window, x=x, y=y, width=width, height=height)
|
||||
if maximized:
|
||||
window.maximized = True
|
||||
|
||||
|
||||
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:
|
||||
"""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
|
||||
|
||||
def __str__(self):
|
||||
return f"RelativeScreenPos(left={self.left}, top={self.top}, right={self.right}, bottom={self.bottom})"
|
||||
|
||||
|
||||
_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),
|
||||
# Alternate (simpler) spoken forms for thirds
|
||||
"CENTER_SMALL": RelativeScreenPos(1 / 3, 0, 2 / 3, 1),
|
||||
"LEFT_SMALL": RelativeScreenPos(0, 0, 1 / 3, 1),
|
||||
"RIGHT_SMALL": RelativeScreenPos(2 / 3, 0, 1, 1),
|
||||
"LEFT_LARGE": RelativeScreenPos(0, 0, 2 / 3, 1),
|
||||
"RIGHT_LARGE": 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_LEFT_THIRD": RelativeScreenPos(0, 0, 1 / 3, 0.5),
|
||||
"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),
|
||||
# Alternate (simpler) spoken forms for sixths
|
||||
"TOP_LEFT_SMALL": RelativeScreenPos(0, 0, 1 / 3, 0.5),
|
||||
"TOP_RIGHT_SMALL": RelativeScreenPos(2 / 3, 0, 1, 0.5),
|
||||
"TOP_LEFT_LARGE": RelativeScreenPos(0, 0, 2 / 3, 0.5),
|
||||
"TOP_RIGHT_LARGE": RelativeScreenPos(1 / 3, 0, 1, 0.5),
|
||||
"TOP_CENTER_SMALL": RelativeScreenPos(1 / 3, 0, 2 / 3, 0.5),
|
||||
"BOTTOM_LEFT_SMALL": RelativeScreenPos(0, 0.5, 1 / 3, 1),
|
||||
"BOTTOM_RIGHT_SMALL": RelativeScreenPos(2 / 3, 0.5, 1, 1),
|
||||
"BOTTOM_LEFT_LARGE": RelativeScreenPos(0, 0.5, 2 / 3, 1),
|
||||
"BOTTOM_RIGHT_LARGE": RelativeScreenPos(1 / 3, 0.5, 1, 1),
|
||||
"BOTTOM_CENTER_SMALL": 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(
|
||||
position: RelativeScreenPos, window: Optional[Window] = None
|
||||
) -> None:
|
||||
"""Move a window (defaults to the active window) to a specific position on its current screen, given a `RelativeScreenPos` object."""
|
||||
if window is None:
|
||||
window = ui.active_window()
|
||||
_snap_window_helper(window, position)
|
||||
|
||||
def snap_window_to_position(
|
||||
position_name: str, window: Optional[Window] = None
|
||||
) -> None:
|
||||
"""Move a window (defaults to the active window) to a specifically named position on its current screen, using a key from `_snap_positions`."""
|
||||
position: Optional[RelativeScreenPos] = None
|
||||
if position_name in _snap_positions:
|
||||
position = _snap_positions[position_name]
|
||||
actions.user.snap_window(position, window)
|
||||
else:
|
||||
# Previously this function took a spoken form, but we now have constant identifiers in `_snap_positions`.
|
||||
# If the user passed a previous spoken form instead, see if we can convert it to the new identifier.
|
||||
new_key = actions.user.formatted_text(position_name, "ALL_CAPS,SNAKE_CASE")
|
||||
if new_key in _snap_positions:
|
||||
actions.user.deprecate_action(
|
||||
"2024-12-02",
|
||||
f"snap_window_to_position('{position_name}')",
|
||||
f"snap_window_to_position('{new_key}')",
|
||||
)
|
||||
position = _snap_positions[new_key]
|
||||
actions.user.snap_window(position, window)
|
||||
else:
|
||||
raise KeyError(position_name)
|
||||
|
||||
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, position: RelativeScreenPos):
|
||||
"""Snap a specific application to another screen."""
|
||||
window = _get_app_window(app_name)
|
||||
_bring_forward(window)
|
||||
_snap_window_helper(window, position)
|
||||
|
||||
def move_app_to_screen(app_name: str, screen_number: int):
|
||||
"""Move a specific application to another screen."""
|
||||
window = _get_app_window(app_name)
|
||||
_bring_forward(window)
|
||||
_move_to_screen(
|
||||
window,
|
||||
screen_number=screen_number,
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
list: user.window_snap_positions
|
||||
-
|
||||
|
||||
left: LEFT
|
||||
right: RIGHT
|
||||
top: TOP
|
||||
bottom: BOTTOM
|
||||
center third: CENTER_THIRD
|
||||
left third: LEFT_THIRD
|
||||
right third: RIGHT_THIRD
|
||||
left two thirds: LEFT_TWO_THIRDS
|
||||
right two thirds: RIGHT_TWO_THIRDS
|
||||
center small: CENTER_SMALL
|
||||
left small: LEFT_SMALL
|
||||
right small: RIGHT_SMALL
|
||||
left large: LEFT_LARGE
|
||||
right large: RIGHT_LARGE
|
||||
top left: TOP_LEFT
|
||||
top right: TOP_RIGHT
|
||||
bottom left: BOTTOM_LEFT
|
||||
bottom right: BOTTOM_RIGHT
|
||||
top left third: TOP_LEFT_THIRD
|
||||
top right third: TOP_RIGHT_THIRD
|
||||
top left two thirds: TOP_LEFT_TWO_THIRDS
|
||||
top right two thirds: TOP_RIGHT_TWO_THIRDS
|
||||
top center third: TOP_CENTER_THIRD
|
||||
bottom left third: BOTTOM_LEFT_THIRD
|
||||
bottom right third: BOTTOM_RIGHT_THIRD
|
||||
bottom left two thirds: BOTTOM_LEFT_TWO_THIRDS
|
||||
bottom right two thirds: BOTTOM_RIGHT_TWO_THIRDS
|
||||
bottom center third: BOTTOM_CENTER_THIRD
|
||||
top left small: TOP_LEFT_SMALL
|
||||
top right small: TOP_RIGHT_SMALL
|
||||
top left large: TOP_LEFT_LARGE
|
||||
top right large: TOP_RIGHT_LARGE
|
||||
top center small: TOP_CENTER_SMALL
|
||||
bottom left small: BOTTOM_LEFT_SMALL
|
||||
bottom right small: BOTTOM_RIGHT_SMALL
|
||||
bottom left large: BOTTOM_LEFT_LARGE
|
||||
bottom right large: BOTTOM_RIGHT_LARGE
|
||||
bottom center small: BOTTOM_CENTER_SMALL
|
||||
center: CENTER
|
||||
full: FULL
|
||||
fullscreen: FULLSCREEN
|
||||
@@ -0,0 +1,9 @@
|
||||
list: user.window_split_positions
|
||||
-
|
||||
|
||||
half: HALF
|
||||
thirds: THIRDS
|
||||
clock: CLOCK
|
||||
counterclock: COUNTERCLOCK
|
||||
grid: GRID
|
||||
big grid: BIG_GRID
|
||||
@@ -0,0 +1,45 @@
|
||||
from talon import Context, actions, ui
|
||||
|
||||
ctx = Context()
|
||||
|
||||
|
||||
@ctx.action_class("app")
|
||||
class AppActions:
|
||||
def window_previous():
|
||||
cycle_windows(ui.active_app(), -1)
|
||||
|
||||
def window_next():
|
||||
cycle_windows(ui.active_app(), 1)
|
||||
|
||||
|
||||
def cycle_windows(app: ui.App, diff: int):
|
||||
"""Cycle windows backwards or forwards for the given application"""
|
||||
active = app.active_window
|
||||
windows = [w for w in app.windows() if w == active or is_window_valid(w)]
|
||||
windows.sort(key=lambda w: w.id)
|
||||
current = windows.index(active)
|
||||
i = (current + diff) % len(windows)
|
||||
|
||||
while i != current:
|
||||
try:
|
||||
actions.user.switcher_focus_window(windows[i])
|
||||
break
|
||||
except Exception:
|
||||
i = (i + diff) % len(windows)
|
||||
|
||||
|
||||
def is_window_valid(window: ui.Window) -> bool:
|
||||
"""Returns true if this window is valid for focusing"""
|
||||
try:
|
||||
return (
|
||||
not window.hidden
|
||||
# On Windows, there are many fake windows with empty titles -- this excludes them.
|
||||
and len(window.title) > 0
|
||||
and (window.title not in ("Chrome Legacy Window"))
|
||||
# This excludes many tiny windows that are not actual windows, and is a rough heuristic.
|
||||
and window.rect.width > window.screen.dpi
|
||||
and window.rect.height > window.screen.dpi
|
||||
)
|
||||
except AttributeError:
|
||||
# Handle case where window.rect might not be accessible
|
||||
return False
|
||||
@@ -0,0 +1,44 @@
|
||||
# defines the default app actions for linux
|
||||
|
||||
from talon import Context, actions
|
||||
|
||||
ctx = Context()
|
||||
ctx.matches = r"""
|
||||
os: linux
|
||||
"""
|
||||
|
||||
|
||||
@ctx.action_class("app")
|
||||
class AppActions:
|
||||
def tab_close():
|
||||
actions.key("ctrl-w")
|
||||
|
||||
def tab_next():
|
||||
actions.key("ctrl-tab")
|
||||
|
||||
def tab_open():
|
||||
actions.key("ctrl-t")
|
||||
|
||||
def tab_previous():
|
||||
actions.key("ctrl-shift-tab")
|
||||
|
||||
def tab_reopen():
|
||||
actions.key("ctrl-shift-t")
|
||||
|
||||
def window_close():
|
||||
actions.key("alt-f4")
|
||||
|
||||
def window_hide():
|
||||
actions.key("alt-space n")
|
||||
|
||||
def window_hide_others():
|
||||
actions.key("win-d alt-tab")
|
||||
|
||||
def window_open():
|
||||
actions.key("ctrl-n")
|
||||
|
||||
|
||||
@ctx.action_class("user")
|
||||
class UserActions:
|
||||
def switcher_focus_last():
|
||||
actions.key("alt-tab")
|
||||
@@ -0,0 +1,51 @@
|
||||
from talon import Context, actions
|
||||
|
||||
ctx = Context()
|
||||
ctx.matches = r"""
|
||||
os: mac
|
||||
"""
|
||||
|
||||
|
||||
@ctx.action_class("app")
|
||||
class AppActions:
|
||||
def preferences():
|
||||
actions.key("cmd-,")
|
||||
|
||||
def tab_close():
|
||||
actions.key("cmd-w")
|
||||
|
||||
def tab_next():
|
||||
actions.key("ctrl-tab")
|
||||
|
||||
def tab_open():
|
||||
actions.key("cmd-t")
|
||||
|
||||
def tab_previous():
|
||||
actions.key("ctrl-shift-tab")
|
||||
|
||||
def tab_reopen():
|
||||
actions.key("cmd-shift-t")
|
||||
|
||||
def window_close():
|
||||
actions.key("cmd-w")
|
||||
|
||||
def window_hide():
|
||||
actions.key("cmd-m")
|
||||
|
||||
def window_hide_others():
|
||||
actions.key("cmd-alt-h")
|
||||
|
||||
def window_open():
|
||||
actions.key("cmd-n")
|
||||
|
||||
def window_previous():
|
||||
actions.key("cmd-shift-`")
|
||||
|
||||
def window_next():
|
||||
actions.key("cmd-`")
|
||||
|
||||
|
||||
@ctx.action_class("user")
|
||||
class UserActions:
|
||||
def switcher_focus_last():
|
||||
actions.key("cmd-tab")
|
||||
@@ -0,0 +1,44 @@
|
||||
# defines the default app actions for windows
|
||||
|
||||
from talon import Context, actions
|
||||
|
||||
ctx = Context()
|
||||
ctx.matches = r"""
|
||||
os: windows
|
||||
"""
|
||||
|
||||
|
||||
@ctx.action_class("app")
|
||||
class AppActions:
|
||||
def tab_close():
|
||||
actions.key("ctrl-w")
|
||||
|
||||
def tab_next():
|
||||
actions.key("ctrl-tab")
|
||||
|
||||
def tab_open():
|
||||
actions.key("ctrl-t")
|
||||
|
||||
def tab_previous():
|
||||
actions.key("ctrl-shift-tab")
|
||||
|
||||
def tab_reopen():
|
||||
actions.key("ctrl-shift-t")
|
||||
|
||||
def window_close():
|
||||
actions.key("alt-f4")
|
||||
|
||||
def window_hide():
|
||||
actions.key("alt-space n")
|
||||
|
||||
def window_hide_others():
|
||||
actions.key("win-d alt-tab")
|
||||
|
||||
def window_open():
|
||||
actions.key("ctrl-n")
|
||||
|
||||
|
||||
@ctx.action_class("user")
|
||||
class UserActions:
|
||||
def switcher_focus_last():
|
||||
actions.key("alt-tab")
|
||||
Reference in New Issue
Block a user