Improved output name selection and context scoring

This commit is contained in:
Ezri Brimhall 2025-02-05 14:35:46 -07:00
parent c478a4f7d4
commit c439ad41aa
Signed by: ezri
GPG Key ID: 058A78E5680C6F24
2 changed files with 29 additions and 18 deletions

View File

@ -1,10 +1,8 @@
from sdbus import ( from sdbus import (
DbusInterfaceCommonAsync, DbusInterfaceCommonAsync,
dbus_method_async, dbus_method_async,
dbus_property_async,
dbus_signal_async, dbus_signal_async,
) )
import os
from .workspace_tree import WorkspaceTree from .workspace_tree import WorkspaceTree
from i3ipc.aio import Connection from i3ipc.aio import Connection
import json import json
@ -25,6 +23,15 @@ class ContextMngrInterface(
self.workspace_tree = workspace_tree self.workspace_tree = workspace_tree
self.connection = connection self.connection = connection
@dbus_method_async(input_signature="", result_signature="as")
async def get_available_contexts(self) -> list[str]:
"""
Request a list of contexts compatible with the current monitor configuration, sorted by score.
"""
scores = await self.workspace_tree.score_contexts(self.connection)
contexts = [score[0].name for score in scores if score[1] > 0]
return contexts
@dbus_method_async(input_signature="s", result_signature="s") @dbus_method_async(input_signature="s", result_signature="s")
async def request_context(self, context: str) -> str: async def request_context(self, context: str) -> str:
"""Request a context switch. This will fail if the current monitor configuration is not compatible with the requested context.""" """Request a context switch. This will fail if the current monitor configuration is not compatible with the requested context."""

View File

@ -3,8 +3,8 @@ import json
from i3ipc.replies import OutputReply, WorkspaceReply from i3ipc.replies import OutputReply, WorkspaceReply
from i3ipc.aio import Connection from i3ipc.aio import Connection
import asyncio import asyncio
import subprocess
from .utils import OutputMatch from .utils import OutputMatch
import itertools
DEFAULT_TERMINAL = "alacritty" DEFAULT_TERMINAL = "alacritty"
@ -90,6 +90,7 @@ class WorkspaceGroup:
name: str = None name: str = None
workspaces: list[Workspace] = None workspaces: list[Workspace] = None
reverse: bool = False reverse: bool = False
output_name: str = None
def __init__(self, output_data: dict[str, str]): def __init__(self, output_data: dict[str, str]):
self.name = output_data["group"] self.name = output_data["group"]
@ -156,10 +157,12 @@ class WorkspaceGroup:
mode = f"mode {self.mode}" mode = f"mode {self.mode}"
if self.make and self.model and self.serial: if self.make and self.model and self.serial:
selector = f'"{self.make} {self.model} {self.serial}"' selector = f'"{self.make} {self.model} {self.serial}"'
await self.get_output_name(i3, force=True)
elif len(self.output_names) > 0: elif len(self.output_names) > 0:
for name in self.output_names: for name in self.output_names:
if name in [output.name for output in outputs]: if name in [output.name for output in outputs]:
selector = name selector = name
self.output_name = selector
break break
# Configure the output # Configure the output
await i3.command( await i3.command(
@ -193,8 +196,10 @@ class WorkspaceGroup:
) )
await workspace.relocate(i3, ouput_name) await workspace.relocate(i3, ouput_name)
async def get_output_name(self, i3: Connection) -> str: async def get_output_name(self, i3: Connection, force: bool = False) -> str:
"""Get the name of the output in Sway.""" """Get the name of the output in Sway."""
if not force and self.output_name is not None:
return self.output_name
outputs = await i3.get_outputs() outputs = await i3.get_outputs()
# If we have make, model, and serial, search by those first # If we have make, model, and serial, search by those first
if self.make and self.model and self.serial: if self.make and self.model and self.serial:
@ -208,6 +213,7 @@ class WorkspaceGroup:
f"Found output {output.name} by make, model, and serial for group {self.name}", f"Found output {output.name} by make, model, and serial for group {self.name}",
flush=True, flush=True,
) )
self.output_name = output.name
return output.name return output.name
# If we don't find an exact match for the output, search by name if we have any # If we don't find an exact match for the output, search by name if we have any
if len(self.output_names) > 0: if len(self.output_names) > 0:
@ -217,6 +223,7 @@ class WorkspaceGroup:
f"Found output {output.name} by name for group {self.name}", f"Found output {output.name} by name for group {self.name}",
flush=True, flush=True,
) )
self.output_name = output.name
return output.name return output.name
return None return None
@ -239,9 +246,6 @@ class WorkspaceGroup:
flush=True, flush=True,
) )
return OutputMatch.NAME_MATCH return OutputMatch.NAME_MATCH
print(
f"Match level: NO_MATCH for {output.name} on group {self.name}", flush=True
)
return OutputMatch.NO_MATCH return OutputMatch.NO_MATCH
@ -293,6 +297,7 @@ class WorkspaceContext:
[group.get_match_level(output).value for output in outputs] [group.get_match_level(output).value for output in outputs]
) )
if group_result == 0: if group_result == 0:
print(f"Context {self.name}: failed to match group {group.name}")
return 0 return 0
result += group_result result += group_result
return result return result
@ -308,9 +313,6 @@ class WorkspaceContext:
async def activate(self, i3: Connection): async def activate(self, i3: Connection):
"""Activate the context in Sway.""" """Activate the context in Sway."""
defined_displays = [
f"{group.make} {group.model} {group.serial}" for group in self.groups
]
outputs = await i3.get_outputs() outputs = await i3.get_outputs()
# Configure all displays defined in the context # Configure all displays defined in the context
@ -335,11 +337,13 @@ class WorkspaceContext:
"eww", "eww",
"open", "open",
window, window,
"--id",
f"{group.name}-{window}",
"--screen", "--screen",
await group.get_output_name(i3), await group.get_output_name(i3),
"--arg", "--arg",
f"group={group.name}", f"group={group.name}",
*extra_args, *list(itertools.chain(*extra_args)),
) )
await proc.wait() await proc.wait()
@ -479,19 +483,19 @@ class WorkspaceTree:
if ws not in touched: if ws not in touched:
ws.deactivate() ws.deactivate()
async def update_context(self, i3: Connection, match_context_on_name: bool = False): async def score_contexts(self, i3: Connection):
"""Activates a new context in Sway based on the current display configuration."""
# First, get the current display configuration
outputs = await i3.get_outputs() outputs = await i3.get_outputs()
print(outputs, flush=True)
# Next, calculate match scores for each context
scores = [ scores = [
(context, context.compatability_rank(outputs)) for context in self.contexts (context, context.compatability_rank(outputs)) for context in self.contexts
] ]
print([f"{context.name}: {score}" for context, score in scores], flush=True)
# Sort the scores by rank
scores.sort(key=lambda x: x[1], reverse=True) scores.sort(key=lambda x: x[1], reverse=True)
return scores
async def update_context(self, i3: Connection, match_context_on_name: bool = False):
"""Activates a new context in Sway based on the current display configuration."""
# Get the scores
scores = await self.score_contexts(i3)
# If the top context is the current context, or the rank is 0, do nothing # If the top context is the current context, or the rank is 0, do nothing
if scores[0][1] == 0: if scores[0][1] == 0: