feat: round bar on smart corners
also take in the i3 ipc code to support custom messages
This commit is contained in:
21
sims/cli.py
21
sims/cli.py
@@ -72,6 +72,15 @@ def _cmd_screenrec(ns: argparse.Namespace) -> None:
|
|||||||
invoke_action(mapping[ns.screenrec_cmd])
|
invoke_action(mapping[ns.screenrec_cmd])
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_corners(ns: argparse.Namespace) -> None:
|
||||||
|
mapping = {
|
||||||
|
"rounded": "set-bar-corners-rounded",
|
||||||
|
"flat": "set-bar-corners-flat",
|
||||||
|
"toggle": "toggle-bar-corners",
|
||||||
|
}
|
||||||
|
invoke_action(mapping[ns.corners_cmd])
|
||||||
|
|
||||||
|
|
||||||
def _cmd_list(ns: argparse.Namespace) -> None:
|
def _cmd_list(ns: argparse.Namespace) -> None:
|
||||||
actions = list_actions()
|
actions = list_actions()
|
||||||
if ns.json:
|
if ns.json:
|
||||||
@@ -108,6 +117,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
rec_sub.add_parser(sub_name, help=sub_help)
|
rec_sub.add_parser(sub_name, help=sub_help)
|
||||||
rec.set_defaults(func=_cmd_screenrec)
|
rec.set_defaults(func=_cmd_screenrec)
|
||||||
|
|
||||||
|
corners = sub.add_parser("corners", help="bar bottom-corner rounding")
|
||||||
|
corners_sub = corners.add_subparsers(
|
||||||
|
dest="corners_cmd", required=True, metavar="STATE"
|
||||||
|
)
|
||||||
|
for sub_name, sub_help in [
|
||||||
|
("rounded", "round the bar's bottom corners"),
|
||||||
|
("flat", "remove rounding (current default look)"),
|
||||||
|
("toggle", "flip the current rounding state"),
|
||||||
|
]:
|
||||||
|
corners_sub.add_parser(sub_name, help=sub_help)
|
||||||
|
corners.set_defaults(func=_cmd_corners)
|
||||||
|
|
||||||
lst = sub.add_parser("list", help="list registered actions")
|
lst = sub.add_parser("list", help="list registered actions")
|
||||||
lst.add_argument("--json", action="store_true", help="emit JSON array")
|
lst.add_argument("--json", action="store_true", help="emit JSON array")
|
||||||
lst.set_defaults(func=_cmd_list)
|
lst.set_defaults(func=_cmd_list)
|
||||||
|
|||||||
25
sims/main.py
25
sims/main.py
@@ -12,7 +12,7 @@ else:
|
|||||||
logger.configure(handlers=[{"sink": sys.stderr, "level": LOG_LEVEL, "format": "{time} | {level} | {name}:{function}:{line} - {message}"}])
|
logger.configure(handlers=[{"sink": sys.stderr, "level": LOG_LEVEL, "format": "{time} | {level} | {name}:{function}:{line} - {message}"}])
|
||||||
|
|
||||||
from fabric import Application
|
from fabric import Application
|
||||||
from fabric.i3 import I3, I3MessageType
|
from sims.services.i3 import I3, I3MessageType
|
||||||
from fabric.system_tray.widgets import SystemTray
|
from fabric.system_tray.widgets import SystemTray
|
||||||
from fabric.widgets.wayland import WaylandWindow as Window
|
from fabric.widgets.wayland import WaylandWindow as Window
|
||||||
from fabric.utils import (
|
from fabric.utils import (
|
||||||
@@ -105,13 +105,34 @@ def screenrec_stop():
|
|||||||
if screenrec_service is not None:
|
if screenrec_service is not None:
|
||||||
screenrec_service.stop()
|
screenrec_service.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def _set_all_bars_rounded(rounded: bool):
|
||||||
|
for bar in bar_windows:
|
||||||
|
bar.set_corners_rounded(rounded)
|
||||||
|
|
||||||
|
|
||||||
|
@Application.action()
|
||||||
|
def set_bar_corners_rounded():
|
||||||
|
_set_all_bars_rounded(True)
|
||||||
|
|
||||||
|
|
||||||
|
@Application.action()
|
||||||
|
def set_bar_corners_flat():
|
||||||
|
_set_all_bars_rounded(False)
|
||||||
|
|
||||||
|
|
||||||
|
@Application.action()
|
||||||
|
def toggle_bar_corners():
|
||||||
|
new_state = not any(bar.corners_rounded for bar in bar_windows)
|
||||||
|
_set_all_bars_rounded(new_state)
|
||||||
|
|
||||||
# Load CSS - use Stylix if enabled, otherwise use default
|
# Load CSS - use Stylix if enabled, otherwise use default
|
||||||
if STYLIX.get("enable", False):
|
if STYLIX.get("enable", False):
|
||||||
stylix_css_path = get_stylix_css_path()
|
stylix_css_path = get_stylix_css_path()
|
||||||
if stylix_css_path:
|
if stylix_css_path:
|
||||||
logger.info("[Bar] Using Stylix CSS")
|
logger.info("[Bar] Using Stylix CSS")
|
||||||
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
|
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
|
||||||
app.set_stylesheet_from_file(stylix_css_path)
|
app.set_stylesheet_from_file(stylix_css_path, append=True)
|
||||||
else:
|
else:
|
||||||
logger.warning("[Bar] Stylix enabled but CSS generation failed, falling back to default")
|
logger.warning("[Bar] Stylix enabled but CSS generation failed, falling back to default")
|
||||||
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
|
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from fabric.system_tray.widgets import SystemTray
|
|||||||
from sims.widgets.fenster import FensterWorkspaces, FensterWorkspaceButton, FensterActiveWindow
|
from sims.widgets.fenster import FensterWorkspaces, FensterWorkspaceButton, FensterActiveWindow
|
||||||
from sims.services.fenster import get_i3_connection
|
from sims.services.fenster import get_i3_connection
|
||||||
from sims.services.screenrec import ScreenrecService
|
from sims.services.screenrec import ScreenrecService
|
||||||
|
from sims.services.smart_corners import get_smart_corners_service
|
||||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||||
from sims.services.system_stats import SystemStatsService
|
from sims.services.system_stats import SystemStatsService
|
||||||
|
|
||||||
@@ -41,6 +42,8 @@ class StatusBar(Window):
|
|||||||
all_visible=False,
|
all_visible=False,
|
||||||
monitor=monitor,
|
monitor=monitor,
|
||||||
)
|
)
|
||||||
|
self.output = display
|
||||||
|
self._corners_rounded = False
|
||||||
|
|
||||||
self.workspaces = FensterWorkspaces(
|
self.workspaces = FensterWorkspaces(
|
||||||
output=display,
|
output=display,
|
||||||
@@ -139,7 +142,7 @@ class StatusBar(Window):
|
|||||||
if WINDOW_TITLE["enable"]:
|
if WINDOW_TITLE["enable"]:
|
||||||
center_children.append(self.active_window)
|
center_children.append(self.active_window)
|
||||||
|
|
||||||
self.children = CenterBox(
|
self.inner = CenterBox(
|
||||||
name="sims-inner",
|
name="sims-inner",
|
||||||
start_children=Box(
|
start_children=Box(
|
||||||
name="start-container",
|
name="start-container",
|
||||||
@@ -163,6 +166,7 @@ class StatusBar(Window):
|
|||||||
children=end_container_children,
|
children=end_container_children,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
self.children = self.inner
|
||||||
|
|
||||||
# Create system stats service with signal-based updates
|
# Create system stats service with signal-based updates
|
||||||
self.system_stats_service = SystemStatsService(update_interval=3000)
|
self.system_stats_service = SystemStatsService(update_interval=3000)
|
||||||
@@ -171,6 +175,10 @@ class StatusBar(Window):
|
|||||||
# Set the bar height
|
# Set the bar height
|
||||||
self.set_size_request(-1, BAR_HEIGHT)
|
self.set_size_request(-1, BAR_HEIGHT)
|
||||||
|
|
||||||
|
smart_corners = get_smart_corners_service()
|
||||||
|
smart_corners.connect("state-changed", self._on_smart_corners_changed)
|
||||||
|
self.set_corners_rounded(not smart_corners.get(display))
|
||||||
|
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
@@ -178,6 +186,24 @@ class StatusBar(Window):
|
|||||||
if hasattr(self, 'calendar_service'):
|
if hasattr(self, 'calendar_service'):
|
||||||
self.calendar_service.stop_monitoring()
|
self.calendar_service.stop_monitoring()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def corners_rounded(self) -> bool:
|
||||||
|
return self._corners_rounded
|
||||||
|
|
||||||
|
def set_corners_rounded(self, rounded: bool) -> None:
|
||||||
|
if rounded == self._corners_rounded:
|
||||||
|
return
|
||||||
|
if rounded:
|
||||||
|
self.inner.add_style_class("rounded-bottom")
|
||||||
|
else:
|
||||||
|
self.inner.remove_style_class("rounded-bottom")
|
||||||
|
self._corners_rounded = rounded
|
||||||
|
|
||||||
|
def _on_smart_corners_changed(self, _service, output: str, active: bool):
|
||||||
|
if output != self.output:
|
||||||
|
return
|
||||||
|
self.set_corners_rounded(not active)
|
||||||
|
|
||||||
def update_progress_bars(self, service, cpu_percent, memory_percent):
|
def update_progress_bars(self, service, cpu_percent, memory_percent):
|
||||||
"""Update progress bars when system stats change"""
|
"""Update progress bars when system stats change"""
|
||||||
self.cpu_progress_bar.value = cpu_percent
|
self.cpu_progress_bar.value = cpu_percent
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from fabric.i3 import I3, I3MessageType
|
from sims.services.i3 import I3, I3MessageType
|
||||||
from fabric.widgets.box import Box
|
from fabric.widgets.box import Box
|
||||||
from fabric.widgets.label import Label
|
from fabric.widgets.label import Label
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ def generate_stylix_css():
|
|||||||
border-bottom: solid 2px;
|
border-bottom: solid 2px;
|
||||||
border-color: #{colors["base02"]};
|
border-color: #{colors["base02"]};
|
||||||
background-color: #{colors["base00"]};
|
background-color: #{colors["base00"]};
|
||||||
|
border-radius: 0;
|
||||||
|
transition: border-radius 200ms ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#sims-inner.rounded-bottom {{
|
||||||
|
border-radius: 0 0 28px 28px;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
#center-container {{
|
#center-container {{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Provides a singleton I3 connection configured for Fenster's SWAYSOCK.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from fabric.i3 import I3
|
from sims.services.i3 import I3
|
||||||
|
|
||||||
|
|
||||||
_connection: I3 | None = None
|
_connection: I3 | None = None
|
||||||
|
|||||||
245
sims/services/i3.py
Normal file
245
sims/services/i3.py
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
"""Vendored i3/sway IPC client (originally from fabric.i3).
|
||||||
|
|
||||||
|
Maintained in-tree so we can extend `I3MessageType` with fenster-specific
|
||||||
|
event types without monkey-patching upstream fabric. To add a new event:
|
||||||
|
|
||||||
|
1. Add a new `*_EVENT` member to `I3MessageType` with the wire type number
|
||||||
|
(`0x80000000 | <event_id>`).
|
||||||
|
2. Make sure fenster broadcasts it under the matching atom and accepts the
|
||||||
|
subscription string (the auto-derived name = enum-name lowercased without
|
||||||
|
`_event`).
|
||||||
|
3. Subscribers connect to `event::<name>` (or `event::<name>::<change>` for
|
||||||
|
sub-events).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
from enum import IntEnum
|
||||||
|
from loguru import logger
|
||||||
|
from typing import ParamSpec
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from fabric.core.service import Service, Signal, Property
|
||||||
|
from fabric.utils.helpers import exec_shell_command, idle_add
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
P = ParamSpec("P")
|
||||||
|
|
||||||
|
SOCKET_MAGIC = b"i3-ipc"
|
||||||
|
|
||||||
|
|
||||||
|
# exceptions
|
||||||
|
class I3Error(Exception): ...
|
||||||
|
|
||||||
|
|
||||||
|
class I3SocketError(I3Error): ...
|
||||||
|
|
||||||
|
|
||||||
|
class I3SocketNotFoundError(I3SocketError): ...
|
||||||
|
|
||||||
|
|
||||||
|
class I3MessageType(IntEnum):
|
||||||
|
# commands
|
||||||
|
COMMAND = 0
|
||||||
|
GET_WORKSPACES = 1
|
||||||
|
SUBSCRIBE = 2
|
||||||
|
GET_OUTPUTS = 3
|
||||||
|
GET_TREE = 4
|
||||||
|
GET_MARKS = 5
|
||||||
|
GET_BAR_CONFIG = 6
|
||||||
|
GET_VERSION = 7
|
||||||
|
GET_BINDING_MODES = 8
|
||||||
|
GET_CONFIG = 9
|
||||||
|
SEND_TICK = 10
|
||||||
|
SYNC = 11
|
||||||
|
GET_BINDING_STATE = 12
|
||||||
|
# sway only
|
||||||
|
GET_INPUTS = 100
|
||||||
|
GET_SEATS = 101
|
||||||
|
|
||||||
|
# events
|
||||||
|
WORKSPACE_EVENT = 0x80000000
|
||||||
|
OUTPUT_EVENT = 0x80000001
|
||||||
|
MODE_EVENT = 0x80000002
|
||||||
|
WINDOW_EVENT = 0x80000003
|
||||||
|
BARCONFIG_UPDATE_EVENT = 0x80000004
|
||||||
|
BINDING_EVENT = 0x80000005
|
||||||
|
SHUTDOWN_EVENT = 0x80000006
|
||||||
|
TICK_EVENT = 0x80000007
|
||||||
|
# sway only
|
||||||
|
BAR_STATE_UPDATE_EVENT = 0x80000014
|
||||||
|
INPUT_EVENT = 0x80000015
|
||||||
|
# fenster extensions (event id 100+)
|
||||||
|
SMART_CORNERS_EVENT = 0x80000064
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class I3Event:
|
||||||
|
name: str
|
||||||
|
"the name of the received event"
|
||||||
|
data: dict
|
||||||
|
"the json data gotten from event's body"
|
||||||
|
raw_data: bytes
|
||||||
|
"the raw json data"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class I3Reply:
|
||||||
|
command: str
|
||||||
|
"the passed in command"
|
||||||
|
reply: dict | list
|
||||||
|
"the raw reply from i3/sway as a dict or list"
|
||||||
|
is_ok: bool
|
||||||
|
"this indicates if the ran command has returned a success message"
|
||||||
|
|
||||||
|
|
||||||
|
class I3(Service):
|
||||||
|
"""
|
||||||
|
A connection to the i3/Sway's IPC socket.
|
||||||
|
This can be used for sending commands and receiving events.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SOCKET_PATH: str | None = None
|
||||||
|
|
||||||
|
@Property(bool, "readable", "is-ready", default_value=False)
|
||||||
|
def ready(self) -> bool:
|
||||||
|
return self._ready
|
||||||
|
|
||||||
|
@Signal("event", flags="detailed")
|
||||||
|
def event(self, event: object): ...
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self._ready = False
|
||||||
|
self.lookup_socket()
|
||||||
|
|
||||||
|
self.event_socket_thread = GLib.Thread.new(
|
||||||
|
"i3-socket-service",
|
||||||
|
self.event_socket_task, # type: ignore
|
||||||
|
self.SOCKET_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._ready = True
|
||||||
|
self.notify("ready")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def lookup_socket() -> str:
|
||||||
|
if I3.SOCKET_PATH:
|
||||||
|
return I3.SOCKET_PATH
|
||||||
|
|
||||||
|
for cmd in ("sway", "i3"):
|
||||||
|
path = exec_shell_command(f"{cmd} --get-socketpath")
|
||||||
|
if not path or not (path := path.strip()) or not os.path.exists(path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
I3.SOCKET_PATH = path
|
||||||
|
|
||||||
|
return I3.SOCKET_PATH
|
||||||
|
|
||||||
|
raise I3SocketNotFoundError(
|
||||||
|
"Couldn't find i3 or Sway socket, is either of them running?"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def pack(message_type: I3MessageType, payload: str = "") -> bytes:
|
||||||
|
payload_bytes = payload.encode()
|
||||||
|
header = struct.pack("<II", len(payload_bytes), message_type.value)
|
||||||
|
return SOCKET_MAGIC + header + payload_bytes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unpack(connection: socket.socket) -> tuple[int, str]:
|
||||||
|
header = connection.recv(14)
|
||||||
|
if len(header) != 14:
|
||||||
|
raise I3SocketError("Failed to read IPC header")
|
||||||
|
|
||||||
|
magic, length, message_type = struct.unpack("<6sII", header)
|
||||||
|
if magic != SOCKET_MAGIC:
|
||||||
|
raise I3SocketError(f"Invalid IPC magic string ({magic}). Report this!")
|
||||||
|
|
||||||
|
return message_type, connection.recv(length).decode()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_command(
|
||||||
|
command: str, message_type: I3MessageType = I3MessageType.COMMAND
|
||||||
|
) -> I3Reply:
|
||||||
|
"""
|
||||||
|
Sends a command to the i3/sway socket.
|
||||||
|
|
||||||
|
example usage:
|
||||||
|
```python
|
||||||
|
# next workspace...
|
||||||
|
I3.send_command("workspace next")
|
||||||
|
```
|
||||||
|
:param command: The command to send.
|
||||||
|
:type command: str
|
||||||
|
:param message_type: The type of message to send.
|
||||||
|
:type message_type: I3MessageType, optional
|
||||||
|
:return: A reply object containing the data from i3/sway.
|
||||||
|
:rtype: I3Reply
|
||||||
|
"""
|
||||||
|
reply_data = {}
|
||||||
|
is_ok = False
|
||||||
|
try:
|
||||||
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
||||||
|
sock.connect(I3.lookup_socket())
|
||||||
|
sock.sendall(I3.pack(message_type, command))
|
||||||
|
|
||||||
|
_, payload = I3.unpack(sock)
|
||||||
|
reply_data = json.loads(payload)
|
||||||
|
|
||||||
|
# results for any GET_* command is considered ok
|
||||||
|
# other commands a success reply is a list of dicts with {"success": True}
|
||||||
|
if (message_type != I3MessageType.COMMAND) or (
|
||||||
|
isinstance(reply_data, list)
|
||||||
|
and reply_data
|
||||||
|
and reply_data[0].get("success")
|
||||||
|
):
|
||||||
|
is_ok = True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"[I3Service] got error while sending command via socket ({e})"
|
||||||
|
)
|
||||||
|
|
||||||
|
return I3Reply(command=command, reply=reply_data, is_ok=is_ok)
|
||||||
|
|
||||||
|
def event_socket_task(self, socket_addr: str) -> bool:
|
||||||
|
try:
|
||||||
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
||||||
|
sock.connect(socket_addr)
|
||||||
|
|
||||||
|
# subscribe to all events
|
||||||
|
sock.sendall(
|
||||||
|
self.pack(
|
||||||
|
I3MessageType.SUBSCRIBE,
|
||||||
|
json.dumps(
|
||||||
|
[
|
||||||
|
evnt_name.replace("_event", "")
|
||||||
|
for mt in I3MessageType
|
||||||
|
if (evnt_name := mt.name.lower()).endswith("_event")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.unpack(sock) # success reply
|
||||||
|
|
||||||
|
while True:
|
||||||
|
idle_add(self.handle_raw_event, *self.unpack(sock))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[I3Service] events socket thread ended with an error: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle_raw_event(self, message_type: int, payload: str):
|
||||||
|
event_data = json.loads(payload)
|
||||||
|
event_name = I3MessageType(message_type).name.lower().replace("_event", "")
|
||||||
|
|
||||||
|
if "change" in event_data: # subevents
|
||||||
|
event_name = f"{event_name}::{event_data['change']}"
|
||||||
|
|
||||||
|
return self.emit(
|
||||||
|
f"event::{event_name}",
|
||||||
|
I3Event(event_name, event_data, payload.encode()),
|
||||||
|
)
|
||||||
@@ -12,7 +12,7 @@ from datetime import datetime
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from fabric.core.service import Service, Signal
|
from fabric.core.service import Service, Signal
|
||||||
from fabric.i3 import I3, I3MessageType
|
from sims.services.i3 import I3, I3MessageType
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|||||||
48
sims/services/smart_corners.py
Normal file
48
sims/services/smart_corners.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""Smart-corners IPC subscriber.
|
||||||
|
|
||||||
|
Listens to fenster's `:smart_corners` event and emits a per-output signal
|
||||||
|
when the WM's smart-corners state flips. Caches the latest state per output
|
||||||
|
so widgets created after the event can ask for the current value.
|
||||||
|
"""
|
||||||
|
from fabric.core.service import Service, Signal
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from sims.services.fenster import get_i3_connection
|
||||||
|
|
||||||
|
|
||||||
|
_service: "SmartCornersService | None" = None
|
||||||
|
|
||||||
|
|
||||||
|
class SmartCornersService(Service):
|
||||||
|
@Signal
|
||||||
|
def state_changed(self, output: str, active: bool) -> None:
|
||||||
|
"""Emitted when a fenster output flips smart-corners state."""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._state: dict[str, bool] = {}
|
||||||
|
i3 = get_i3_connection()
|
||||||
|
i3.connect("event::smart_corners", self._on_event)
|
||||||
|
|
||||||
|
def get(self, output: str) -> bool:
|
||||||
|
"""Latest known state for an output, or False if unseen."""
|
||||||
|
return self._state.get(output, False)
|
||||||
|
|
||||||
|
def _on_event(self, _i3, event):
|
||||||
|
change = event.data.get("change")
|
||||||
|
output = event.data.get("output")
|
||||||
|
if not isinstance(output, str) or change not in ("active", "inactive"):
|
||||||
|
logger.warning(f"[SmartCorners] unexpected event payload: {event.data!r}")
|
||||||
|
return
|
||||||
|
active = change == "active"
|
||||||
|
if self._state.get(output) == active:
|
||||||
|
return
|
||||||
|
self._state[output] = active
|
||||||
|
self.state_changed(output, active)
|
||||||
|
|
||||||
|
|
||||||
|
def get_smart_corners_service() -> SmartCornersService:
|
||||||
|
global _service
|
||||||
|
if _service is None:
|
||||||
|
_service = SmartCornersService()
|
||||||
|
return _service
|
||||||
@@ -3,6 +3,12 @@
|
|||||||
border-bottom: solid 2px;
|
border-bottom: solid 2px;
|
||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
background-color: var(--window-bg);
|
background-color: var(--window-bg);
|
||||||
|
border-radius: 0;
|
||||||
|
transition: border-radius 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sims-inner.rounded-bottom {
|
||||||
|
border-radius: 0 0 28px 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#center-container {
|
#center-container {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Fenster widgets for workspace and window management via sway IPC.
|
|||||||
|
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
|
|
||||||
from fabric.i3 import I3, I3Event, I3MessageType
|
from sims.services.i3 import I3, I3Event, I3MessageType
|
||||||
from fabric.utils.helpers import bulk_connect
|
from fabric.utils.helpers import bulk_connect
|
||||||
from fabric.widgets.box import Box
|
from fabric.widgets.box import Box
|
||||||
from fabric.widgets.button import Button
|
from fabric.widgets.button import Button
|
||||||
|
|||||||
Reference in New Issue
Block a user