Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improves-snap-ergonomics #1578

Draft
wants to merge 65 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
9e23058
Make it so the focused window and snap layout is the first one spoken
jaresty Oct 20, 2024
d302098
Make it so you can only speak a valid layout
jaresty Oct 20, 2024
85c4732
Refactor layouts to operate on windows in preparation for window layo…
jaresty Oct 20, 2024
f93cf65
Enable using the trio and pair snap layouts to rotate layouts
jaresty Oct 20, 2024
19f9202
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 20, 2024
8381d32
Merge branch 'main' into improves-snap-ergonomics
jaresty Oct 20, 2024
944f33a
Start counting for layouts at the active window
jaresty Oct 20, 2024
f8d1cb1
Extract _top_n_windows helper function in preparation for adding ordi…
jaresty Oct 20, 2024
04d1dbb
Provide convenience function to pick a specific window to focus
jaresty Oct 20, 2024
f1a94cd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 20, 2024
dfe59e6
Make the error message a little more intuitive
jaresty Oct 21, 2024
f4a41da
Provide away to snap a window by index
jaresty Oct 21, 2024
3c45207
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 21, 2024
f6c08f8
Provide a shortcut to focus an application and snap to a layout at th…
jaresty Oct 21, 2024
461e109
Slightly adjust grammar based on feedback
jaresty Oct 21, 2024
7cdf4dc
Optimized for reducing dfa rules
jaresty Oct 22, 2024
8700e87
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 22, 2024
7dea668
Handle error resizing window
jaresty Oct 22, 2024
f7fb7ee
Gracefully handle snap errors
jaresty Oct 22, 2024
1509807
Implement gap support
jaresty Oct 23, 2024
fb70d72
Refactor some duplicate code
jaresty Oct 23, 2024
898a8da
Merge branch 'main' into improves-snap-ergonomics
jaresty Oct 23, 2024
506d0b4
Cleanup extra print statements
jaresty Oct 23, 2024
77cdeff
Prevent an error if you tried to pop a zero number of snapped windows
jaresty Oct 25, 2024
96b44e7
Respond opal request feedback by making the snapping move to the righ…
jaresty Oct 26, 2024
de71d32
Now it only rotates when focus was done by snap layout first
jaresty Oct 26, 2024
d0705a1
Make it so snap layout only rotates when it was the last thing used t…
jaresty Oct 26, 2024
907315e
Optimized to fix race condition
jaresty Oct 26, 2024
ad792de
Responding to pull request feedback
jaresty Oct 26, 2024
20ab9a0
Make window layout into a data class responding to pull request feedback
jaresty Oct 26, 2024
f01252c
Responding to pull request feedback
jaresty Oct 26, 2024
f56bd10
Merge branch 'main' into improves-snap-ergonomics
jaresty Oct 26, 2024
8b62807
Merge branch 'main' into improves-snap-ergonomics
jaresty Oct 28, 2024
9396c51
Exposed public method for snapping windows to position by name
jaresty Oct 30, 2024
e16c3da
Clarify that the snap failure is normal and provide additional details
jaresty Oct 30, 2024
d411fc2
Remove usage of time delays to ensure snapping does not have a race
jaresty Oct 30, 2024
eeb10ce
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 30, 2024
c2d6dd5
Breakout layout to another file
jaresty Oct 30, 2024
ed9646c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 30, 2024
f869e2b
Refactor window layout code to clarify
jaresty Oct 30, 2024
8fcf263
Handle window focusing errors
jaresty Oct 31, 2024
a7dc2d8
First pass at pull request feedback from today
jaresty Nov 2, 2024
f4600b7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 2, 2024
2bc7570
Removed some debug printing and an exception that I can't make more s…
jaresty Nov 2, 2024
9e27819
Update core/windows_and_tabs/window_snap.py
jaresty Nov 2, 2024
ff51ddf
Fixed typo
jaresty Nov 2, 2024
ffc577a
Removed debug logs
jaresty Nov 2, 2024
d4774d9
Need to pass window from calling function
jaresty Nov 2, 2024
4f8bfa4
Merge branch 'main' into improves-snap-ergonomics
nriley Nov 2, 2024
9fa7926
The active window is not always in the list passed in
jaresty Nov 2, 2024
902de33
Index filtering should be based on the index in all windows not based…
jaresty Nov 2, 2024
601c884
Merge branch 'main' into improves-snap-ergonomics
jaresty Nov 2, 2024
29a0c46
Cleanup imports
jaresty Nov 2, 2024
c20f601
Merge branch 'main' into improves-snap-ergonomics
jaresty Nov 4, 2024
c77aadf
Rescue another exception type that I ran into
jaresty Nov 6, 2024
0c09a45
Merge branch 'main' into improves-snap-ergonomics
jaresty Nov 11, 2024
096a4be
Respond to pull request feedback:
jaresty Nov 11, 2024
23e5093
Extract window snap positions as a talon list
jaresty Nov 11, 2024
b7e5326
Make GapWindow into a proper class
jaresty Nov 15, 2024
0cb17a7
Handle the deep copy earlier
jaresty Nov 15, 2024
2a92d78
Ensure that snap layout finishes by resetting the layout in progress
jaresty Nov 15, 2024
50f449f
Add types to pick_split_arrangement and cleanup snap_layout a bit
jaresty Nov 15, 2024
d6f8172
Skip filtering of application windows
jaresty Nov 15, 2024
463d68a
Remove filter nonviable windows
jaresty Nov 15, 2024
d8054aa
Removed debug logging
jaresty Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions core/windows_and_tabs/window_layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import copy
from dataclasses import dataclass
from time import perf_counter
from typing import List, Optional, Union

from talon import Context, Module, actions, settings, ui

"""Tools for laying out windows in an arrangement """
jaresty marked this conversation as resolved.
Show resolved Hide resolved

from talon import Context, Module, actions, settings
from talon.ui import UIErr, Window

SPLIT_POSITIONS = {
"split": {
2: ["Left", "Right"],
3: [
"LeftThird",
"CenterThird",
"RightThird",
],
},
"clock": {
3: [
"Left",
"TopRight",
"BottomRight",
],
},
"counterclock": {
3: [
"Right",
"TopLeft",
"BottomLeft",
],
},
}

mod = Module()

mod.list(
"window_split_positions",
"Predefined window positions when splitting the screen between multiple windows.",
)
ctx = Context()

ctx.lists["user.window_split_positions"] = SPLIT_POSITIONS.keys()


def focus_callback(_):
global layout_in_progress
global last_layout
if layout_in_progress is not None:
return

delta = 1
if last_layout is not None:
delta = perf_counter() - last_layout.finish_time
if delta >= 1:
last_layout = None


@dataclass
class WindowLayout:
"""Represents a layout of windows on a screen"""

split_positions: list[str]
windows: list[Window]
should_rotate: bool
finish_time: float


class GapWindow:
pass


layout_in_progress: Optional[WindowLayout] = None
last_layout: Optional[WindowLayout] = None


def snap_next(windows: list[Window], target_layout: str) -> Optional[Window]:
"""This function snaps a window and returns the window if successful"""
while windows:
window = windows.pop(0)
if isinstance(window, GapWindow):
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 GapWindow()


def snap_layout(layout_config: WindowLayout):
"""Split the screen between multiple windows."""
try:
global layout_in_progress, last_layout

layout_in_progress = layout_config
remaining_windows = layout_config.windows

snapped_windows = []
if layout_config.should_rotate:
layout_config.split_positions.append(layout_config.split_positions.pop(0))

while len(layout_config.split_positions) > 0:
snapped_window: Window = snap_next(
remaining_windows, layout_config.split_positions.pop(0)
)
snapped_windows.insert(0, snapped_window)

if layout_config.should_rotate and len(snapped_windows) > 0:
snapped_windows.append(snapped_windows.pop(0))

for window in snapped_windows:
if isinstance(window, GapWindow):
continue
actions.user.switcher_focus_window(window)

layout_in_progress.finish_time = perf_counter()
last_layout = layout_in_progress
finally:
layout_in_progress = None


@mod.capture(rule="all")
def all_candidate_windows(m) -> list[Window]:
return ui.windows()


@mod.capture(rule="gap")
def skip_window(m) -> list[Window]:
return [GapWindow()]


@mod.capture(rule="<user.running_applications>")
def application_windows(m) -> list[Window]:
return [
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[Union[Window, None]]:
# Check for multiple attributes and raise an error if found

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 = 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(
jaresty marked this conversation as resolved.
Show resolved Hide resolved
target_windows: Union[list[Window], None],
window_split_positions: str,
number_small: Union[int, None],
) -> list[str]:
if target_windows is not None:
target_length = len(target_windows)
else:
target_length = len(ui.windows())
if number_small is not None:
return copy.deepcopy(SPLIT_POSITIONS[window_split_positions][number_small])
else:
closest_key = min(
SPLIT_POSITIONS[window_split_positions].keys(),
key=lambda k: abs(k - target_length),
)
return copy.deepcopy(SPLIT_POSITIONS[window_split_positions][closest_key])


@mod.capture(
rule="{user.window_split_positions} [<number_small>] [<user.target_windows>]"
)
def window_layout(m) -> WindowLayout:
global last_layout
window_was_specified = hasattr(m, "target_windows")
specified_layout_count = m.number_small if hasattr(m, "number_small") else None
target_windows = m.target_windows if window_was_specified else ui.windows()

layout = pick_split_arrangement(
target_windows, m.window_split_positions, specified_layout_count
)

return WindowLayout(
layout,
target_windows,
not window_was_specified
and last_layout is not None
and perf_counter() - last_layout.finish_time > 1,
0,
)


@mod.action_class
class Actions:
def snap_layout(
window_layout: WindowLayout,
):
"""Split the screen between multiple applications."""
snap_layout(window_layout)


ui.register("app_activate", focus_callback)
ui.register("win_focus", focus_callback)
5 changes: 2 additions & 3 deletions core/windows_and_tabs/window_management.talon
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ 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)
# <user.running_applications> is here twice to require at least two applications.
snap <user.window_split_position> <user.running_applications> <user.running_applications>+:
user.snap_layout(window_split_position, running_applications_list)
lay <user.window_layout>: user.snap_layout(window_layout)

snap <user.running_applications> [screen] <number>:
user.move_app_to_screen(running_applications, number)
Loading