Notes/src/Python/Scripts/smart-window-position.py

289 lines
9.3 KiB
Python

#!/usr/bin/env python3
# Python Imports
import sys
import enum
import argparse
# Lib Imports
from Xlib import display
from Xlib.ext import randr, xinerama
try:
from . import __version__
except ImportError:
__version__ = "N/A"
class Position(enum.Enum):
TOP = enum.auto()
BOTTOM = enum.auto()
LEFT = enum.auto()
RIGHT = enum.auto()
def __str__(self):
return self.name.lower()
@staticmethod
def from_str(pos):
try:
return Position[pos.upper()]
except KeyError:
raise ValueError(f"value '{pos}' not part of the Pos enum")
class Shape:
def __init__(self, major, minor):
if max(major, minor) > 1.0 or min(major, minor) < 0.0:
raise ValueError(f"Shape out of range [0,1]: major={major}, minor={minor}")
self.major = major
self.minor = minor
class Monitor:
def __init__(self, name, x, y, width, height):
self.name = name
self.x = x
self.y = y
self.width = width
self.height = height
DEFAULTS = {
"name": "Smart Position",
"shape": (1.0, 0.4),
"position": str(Position.LEFT),
}
class SmartPositioner:
def __init__(self, name: str, shape: Shape, pos: Position, positioner_argv: list = None):
self.positioner_argv = positioner_argv
self.display = display.Display()
self.screen = self.display.screen()
self.root = self.screen.root
# self.fakeWindow = self.screen.root.create_window(0, 0, 1, 1, 1, self.screen.root_depth)
self.name = name
self.shape = shape
self.pos = pos
self.id = None
self.monitors = []
self.printXineramaVersion()
self.generateMonitorList()
self.getActiveMonitor()
def on_keybind(self, _, be):
pass
# if be.binding.command == f"nop {self.name}":
# self.toggle()
def _get_instance(self):
pass
# tree = self.i3.get_tree()
# if self.id is None:
# matches = tree.find_instanced(self.name)
# instance = {m.window_instance: m for m in matches}.get(self.name, None)
# if instance is not None:
# self.id = instance.id
# else:
# instance = tree.find_by_id(self.id)
# return instance
def toggle(self):
scrSize = self.getScreenSize()
mPos = self.getMousePosition()
self.display.flush() # Needed in some instances to prompt XWindows
# kitty = self._get_instance()
# if kitty is None:
# self.spawn()
# else:
# focused_ws = self._get_focused_workspace()
# if focused_ws is None:
# print("no focused workspaces; ignoring toggle request")
# return
# if kitty.workspace().name == focused_ws.name: # kitty present on current WS; hide
# self.i3.command(f"[con_id={self.id}] floating enable, move scratchpad")
# else:
# self.fetch(focused_ws)
def getMousePosition(self):
data = self.screen.root.query_pointer()._data
return data["root_x"], data["root_y"]
def getActiveMonitor(self):
pos = self.getMousePosition()
for mon in self.monitors:
pass
def getScreenSize(self):
w = self.display.screen().width_in_pixels
h = self.display.screen().height_in_pixels
return w, h
def printXineramaVersion(self):
xinerama_version = self.display.xinerama_query_version()
print('Found XINERAMA version %s.%s' % (
xinerama_version.major_version,
xinerama_version.minor_version,
), file=sys.stderr)
def generateMonitorList(self):
print('Available screens:')
screens = xinerama.query_screens(self.root).screens
i = 1
for (idx, screen) in enumerate(screens):
x, y, w, h = screen.x, screen.y, screen.width, screen.height
print('screen %d: %s' % (idx, screen))
self.monitors.append( Monitor( "Monitor " + str(i), x, y, w, x ) )
i += 1
# resources = randr.get_screen_resources(self.root)
# for output in resources.outputs:
# params = self.display.xrandr_get_output_info(output, resources.config_timestamp)
# if not params.crtc:
# continue
# crtc = self.display.xrandr_get_crtc_info(params.crtc, resources.config_timestamp)
# self.monitors.append( Monitor(params.name, crtc.width, crtc.height) )
def spawn(self):
pass
# cmd_base = f"exec --no-startup-id kitty --name {self.name}"
# if self.positioner_argv is None:
# cmd = cmd_base
# else:
# argv = " ".join(self.positioner_argv)
# cmd = f"{cmd_base} {argv}"
# self.i3.command(cmd)
def on_spawned(self, _, we):
pass
# if we.container.window_instance == self.name:
# self.id = we.container.id
# self.i3.command(f"[con_id={we.container.id}] "
# "floating enable, "
# "border none, "
# "move scratchpad")
# self.fetch(self._get_focused_workspace())
def on_moved(self, _, we):
pass
# # Con is floating wrapper; the Kitty window/container is a child
# is_kitty = we.container.find_by_id(self.id)
# if not is_kitty:
# return
# focused_ws = self._get_focused_workspace()
# if focused_ws is None:
# return
# kitty = self._get_instance() # need "fresh" instance to capture destination WS
# kitty_ws = kitty.workspace()
# if (kitty_ws is None or kitty_ws.name == focused_ws.name or
# kitty_ws.name == "__i3_scratch"): # FIXME: fragile way to check if hidden?
# return
# self.fetch(kitty_ws, retrieve=False)
def _get_focused_workspace(self):
pass
# focused_workspaces = [w for w in self.i3.get_workspaces() if w.focused]
# if not len(focused_workspaces):
# return None
# return focused_workspaces[0]
def fetch(self, ws, retrieve=True):
pass
# if self.id is None:
# raise RuntimeError("Kitty instance ID not yet assigned")
#
# if self.pos in (Position.TOP, Position.BOTTOM):
# width = round(ws.rect.width * self.shape.major)
# height = round(ws.rect.height * self.shape.minor)
# x = ws.rect.x
# y = ws.rect.y if self.pos is Position.TOP else ws.rect.y + ws.rect.height - height
# else: # LEFT || RIGHT
# width = round(ws.rect.width * self.shape.minor)
# height = round(ws.rect.height * self.shape.major)
# x = ws.rect.x if self.pos is Position.LEFT else ws.rect.x + ws.rect.width - width
# y = ws.rect.y
#
# self.i3.command(f"[con_id={self.id}] "
# f"resize set {width}px {height}px, "
# f"{', move scratchpad, scratchpad show' if retrieve else ''}"
# f"move absolute position {x}px {y}px")
@staticmethod
def on_shutdown(_, se):
exit(0)
def _split_args(args):
try:
split = args.index("--")
return args[:split], args[split + 1:]
except ValueError:
return args, None
def _simple_fraction(arg):
arg = float(arg)
if not 0 <= arg <= 1:
raise argparse.ArgumentError("Argument needs to be a simple fraction, within"
"[0, 1]")
return arg
def _parse_args(argv, defaults):
ap = argparse.ArgumentParser(
description="SmartPositioner: Window position wrapper for programs. "
"Arguments following '--' are forwarded to the program instance")
ap.set_defaults(**defaults)
ap.add_argument("-v", "--version",
action="version",
version=f"%(prog)s {__version__}",
help="show %(prog)s's version number and exit")
ap.add_argument("-n", "--name",
help="name/tag connecting a Kitti3 bindsym with a Kitty instance. "
"Forwarded to Kitty on spawn and scanned for on i3 binding "
"events")
ap.add_argument("-p", "--position",
type=Position.from_str,
choices=list(Position),
help="Along which edge of the screen to align the Kitty window")
ap.add_argument("-s", "--shape",
type=_simple_fraction,
nargs=2,
help="shape of the terminal window major and minor dimensions as a "
"fraction [0, 1] of the screen size (note: i3bar is accounted "
"for such that a 1.0 1.0 shaped terminal would not overlap it)")
args = ap.parse_args(argv)
return args
def cli():
argv_positioner3, argv_positioner = _split_args(sys.argv[1:])
args = _parse_args(argv_positioner3, DEFAULTS)
positioner = SmartPositioner(
name=args.name,
shape=Shape(*args.shape),
pos=args.position,
positioner_argv=argv_positioner,
)
# positioner.loop()
if __name__ == "__main__":
cli()