Initial commit
This commit is contained in:
commit
fd68d9f463
10 changed files with 662 additions and 0 deletions
24
LICENSE
Normal file
24
LICENSE
Normal file
|
@ -0,0 +1,24 @@
|
|||
Copyright (c) 2025 Daniel Fichtinger <daniel@ficd.sh>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the author nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
29
README.md
Normal file
29
README.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# niri scripts
|
||||
|
||||
This repository contains scripts and Waybar modules for Niri.
|
||||
|
||||
## picker
|
||||
|
||||
A simple window picker script. Use a `dmenu` compliant picker (`fuzzel` by
|
||||
default) to pick a window and switch focus to it.
|
||||
|
||||
## recorder
|
||||
|
||||
A script and Waybar module to simplify the process of making and sharing screen
|
||||
recordings. Features:
|
||||
|
||||
- Select a screen or region for recording.
|
||||
- Automatically compress and copy recording URI to clipboard for easy pasting
|
||||
into applications like Discord.
|
||||
- Cleans up old recordings.
|
||||
- Waybar module allows starting/stopping recording, displays recording status.
|
||||
|
||||
Check the [README](./recorder/README.md) for usage instructions.
|
||||
|
||||
## windows
|
||||
|
||||
A Waybar module that provides an indicator of open windows in the focused
|
||||
workspace. Helpful for those that take Niri's scrolling a tad too far, and often
|
||||
end up with too many windows in a single workspace.
|
||||
|
||||
Check the [README](./windows/README.md) for usage instructions.
|
64
picker/window-picker.py
Executable file
64
picker/window-picker.py
Executable file
|
@ -0,0 +1,64 @@
|
|||
#!/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
# JSON object to represent a window in Niri
|
||||
type WindowJson = dict[str, int | str | bool]
|
||||
|
||||
# Get a list of open windows from Niri
|
||||
def get_windows():
|
||||
command = "niri msg -j windows"
|
||||
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
data: list[WindowJson] = json.loads(process.communicate()[0])
|
||||
|
||||
return data
|
||||
|
||||
# Generate a string representation for each window.
|
||||
# Map the string to its Niri window ID
|
||||
def get_string_id_mapping(window_list: list[WindowJson]):
|
||||
mapping: dict[str, int] = {}
|
||||
for idx, window in enumerate(window_list):
|
||||
s = f"{idx}: {window.get("app_id")}: {window.get("title")}"
|
||||
id = window.get("id")
|
||||
assert type(id) == int
|
||||
mapping[s] = id
|
||||
return mapping
|
||||
|
||||
# Generate the string to be sent to fuzzel
|
||||
def get_input_string(mapping: dict[str, int]):
|
||||
m = max([len(s) for s in mapping.keys()])
|
||||
return "\n".join(mapping.keys()), m
|
||||
|
||||
def spawn_picker(cmd, input_string, m):
|
||||
# cmd = "fuzzel --dmenu -I --placeholder=Select a window:"
|
||||
cmd = f"{cmd} --width {min(m, 120) + 1}"
|
||||
process = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
selection = process.communicate(input=input_string.encode("UTF-8"))
|
||||
if process.returncode != 2:
|
||||
return selection[0].decode("UTF-8").strip("\n")
|
||||
else:
|
||||
return None
|
||||
|
||||
def switch_window(id: int):
|
||||
cmd = f"niri msg action focus-window --id {id}"
|
||||
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
|
||||
process.communicate()
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("-p", "--picker", required=False, help="Set a picker command. Must take a newline delimited string on stdin and return the selection on stdout (default: %(default)s)", default="fuzzel --dmenu --placeholder=Select a window:", type=str, metavar="COMMAND")
|
||||
args = parser.parse_args()
|
||||
picker_cmd = args.picker
|
||||
# print("picker:", picker_cmd)
|
||||
wl = get_windows()
|
||||
mapping = get_string_id_mapping(wl)
|
||||
input_string, m= get_input_string(mapping)
|
||||
selection = spawn_picker(picker_cmd, input_string, m)
|
||||
if selection is None:
|
||||
exit(1)
|
||||
try:
|
||||
id = mapping[selection]
|
||||
except KeyError:
|
||||
exit(1)
|
||||
switch_window(id)
|
53
recorder/README.md
Normal file
53
recorder/README.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Niri Recorder
|
||||
|
||||
Niri recorder is a shell script and waybar module to for creating screen
|
||||
recordings meant to be easily shared online.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `socat`
|
||||
- `bash`
|
||||
- `wf-recorder`
|
||||
- `niri`
|
||||
- `jq`
|
||||
- `ffmpeg`
|
||||
- `slurp`
|
||||
- `python3` (optional)
|
||||
- `waybar` (optional)
|
||||
- `wl-clipboard`
|
||||
|
||||
## Recorder
|
||||
|
||||
A utility script meant to be triggered via keybinding. It will record your
|
||||
screen, then automatically compress the recording and copy its URI path to your
|
||||
clipboard, so you can easily paste it inside applications like Discord and
|
||||
Matrix. You do not need the Waybar module to use the script.
|
||||
|
||||
**Usage**:
|
||||
|
||||
`recorder.sh`
|
||||
|
||||
1. Run the script once to begin recording. Arguments:
|
||||
|
||||
- `screen` \[default]: record entire screen. Select screen if multi-monitor.
|
||||
- `region`: Select a region to record.
|
||||
|
||||
2. Run the script again to stop recording.
|
||||
|
||||
- Arguments don't matter this time.
|
||||
- Compression and copying to clipboard is done automatically.
|
||||
|
||||
## Waybar Module
|
||||
|
||||
There is an included waybar module. This module shows the current recording
|
||||
state. You can also use it to start/stop the recording with your mouse. Please
|
||||
see `recorder_config.jsonc` for an example of how to setup the custom module.
|
||||
|
||||
You can also use `test_waybar.sh` to test the module without creating any
|
||||
recordings. After you've loaded the Waybar module, simply run the script, and
|
||||
you should see the module responding to socket messages.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Thanks to [Axlefublr](https://axlefublr.github.io/screen-recording/) for the
|
||||
method to optimize the compression of the video.
|
9
recorder/module_config.jsonc
Normal file
9
recorder/module_config.jsonc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"custom/recorder": {
|
||||
"exec": "/path/to/recorder.py",
|
||||
"return-type": "json",
|
||||
"restart-interval": "never",
|
||||
"on-click": "/path/to/recorder.sh screen",
|
||||
"on-click-right": "/path/to/recorder.sh region"
|
||||
}
|
||||
}
|
107
recorder/recorder.py
Executable file
107
recorder/recorder.py
Executable file
|
@ -0,0 +1,107 @@
|
|||
#!/bin/env python
|
||||
|
||||
import os
|
||||
import socket
|
||||
import json
|
||||
import asyncio
|
||||
|
||||
# helper prints dict as json string
|
||||
def p(obj):
|
||||
print(json.dumps(obj), flush=True)
|
||||
|
||||
SOCKET = "/tmp/recorder-sock.sock"
|
||||
# default tooltip
|
||||
DEF_TT = "Click to record screen. Right-click to record region."
|
||||
# default text
|
||||
DEF_TEXT = "rec"
|
||||
|
||||
# Remove socket already exists
|
||||
try:
|
||||
os.unlink(SOCKET)
|
||||
except OSError:
|
||||
# doesn't exist, we're good
|
||||
pass
|
||||
|
||||
# track singleton delayed task
|
||||
delayed_task = None
|
||||
|
||||
# async function to send a message with delay
|
||||
async def delayed_msg(delay, message):
|
||||
try:
|
||||
await asyncio.sleep(delay)
|
||||
p(message)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# function handles message from socket
|
||||
def handle_message(data: str, loop):
|
||||
global delayed_task
|
||||
# always cancel delayed task
|
||||
# if there's new message
|
||||
if delayed_task:
|
||||
delayed_task.cancel()
|
||||
out = {}
|
||||
out_s = ""
|
||||
out_t = ""
|
||||
# process the message type
|
||||
if data:
|
||||
if data == "REC":
|
||||
out_s = "on"
|
||||
out_t = "Recording in progress. Click to stop."
|
||||
elif data == "CMP":
|
||||
out_s = "compressing"
|
||||
out_t = "Recording is being compressed."
|
||||
elif data == "CPD":
|
||||
out_s = "copied"
|
||||
out_t = "Recording has been copied to clipboard."
|
||||
elif data == "STP":
|
||||
out_s = "done"
|
||||
out_t = "Recording has been stopped."
|
||||
elif data == "ERR":
|
||||
out_s = "error"
|
||||
out_t = "Recording has encountered an error."
|
||||
# format the output
|
||||
out["text"] = f"rec: {out_s}" if out_s != "" else DEF_TEXT
|
||||
out["tooltip"] = out_t if out_t != "" else DEF_TT
|
||||
# print to waybar
|
||||
p(out)
|
||||
|
||||
# check if delayed message should be sent afterwards
|
||||
if data in ["ERR", "CPD", "STP"]:
|
||||
# probably redundant but... for good measure lol
|
||||
if delayed_task:
|
||||
delayed_task.cancel()
|
||||
# delayed print of default output
|
||||
delayed_out = {"text": DEF_TEXT, "tooltip": DEF_TT}
|
||||
delayed_task = loop.create_task(delayed_msg(5, delayed_out))
|
||||
|
||||
# start the server
|
||||
async def server():
|
||||
# pointer to this event loop
|
||||
loop = asyncio.get_running_loop()
|
||||
# open our socket
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
|
||||
server.bind(SOCKET)
|
||||
# needed for async
|
||||
server.setblocking(False)
|
||||
# only one connection at a time
|
||||
server.listen(1)
|
||||
# main loop; runs until waybar exits
|
||||
while True:
|
||||
# receive data from socket
|
||||
conn, _ = await loop.sock_accept(server)
|
||||
with conn:
|
||||
# parse string
|
||||
data = (await loop.sock_recv(conn, 1024)).decode().strip()
|
||||
# handle the data
|
||||
handle_message(data, loop)
|
||||
|
||||
# main function
|
||||
async def main():
|
||||
# start by outputing default contents
|
||||
p({"text": DEF_TEXT, "tooltip": DEF_TT})
|
||||
# start the server
|
||||
await server()
|
||||
|
||||
# entry point
|
||||
asyncio.run(main())
|
69
recorder/recorder.sh
Executable file
69
recorder/recorder.sh
Executable file
|
@ -0,0 +1,69 @@
|
|||
#!/bin/env bash
|
||||
|
||||
# depends: wf-recorder, libnotify
|
||||
|
||||
TEMPDIR="/tmp/niri-recorder"
|
||||
mkdir -p "$TEMPDIR"
|
||||
LOCK="$TEMPDIR/lock"
|
||||
RFMT=".mkv"
|
||||
OFMT=".mp4"
|
||||
RAW="$TEMPDIR/raw$RFMT"
|
||||
OUTDIR="$HOME/Videos/niri-recorder"
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
# sends current recording state to socket
|
||||
function sig {
|
||||
echo "$1" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock
|
||||
}
|
||||
|
||||
# function generates unique name for recording
|
||||
function compname {
|
||||
now=$(date +"%y-%m-%d-%H:%M:%S")
|
||||
echo "$OUTDIR/$now$OFMT"
|
||||
}
|
||||
|
||||
# First we need to check if the recording
|
||||
# lockfile exists.
|
||||
if [[ -f "$LOCK" ]]; then
|
||||
# Stop the recording
|
||||
sig "STP"
|
||||
kill "$(cat "$LOCK")"
|
||||
# remove lockfile
|
||||
rm "$LOCK"
|
||||
outpath="$(compname)"
|
||||
sig "CMP"
|
||||
# compress the recording
|
||||
ffmpeg -y -i "$RAW" -c:v libx264 -preset slow -crf 21 -r 30 -b:v 2M -maxrate 3M -bufsize 4M -c:a aac -b:a 96k -movflags +faststart "$outpath" || (sig "ERR"; exit 1)
|
||||
# copy URI path to clipboard
|
||||
wl-copy -t 'text/uri-list' <<<"file://$outpath" || (sig "ERR"; exit 1)
|
||||
sig "CPD"
|
||||
# delete the raw recording
|
||||
rm "$RAW"
|
||||
|
||||
else
|
||||
# count how many monitors are attached
|
||||
num_mon=$(niri msg --json outputs | jq 'keys | length')
|
||||
wf_flags="-Dyf"
|
||||
if [ "$1" = "region" ];then
|
||||
# select a screen region
|
||||
sel=$(slurp) || exit 1
|
||||
wf-recorder -g "$sel" "$wf_flags" "$RAW" &
|
||||
elif [ "$1" = "screen" ] || [ "$1" = "" ] && (( num_mon > 1 )); then
|
||||
# select entire screen
|
||||
sel=$(slurp -o) || exit 1
|
||||
wf-recorder -g "$sel" "$wf_flags" "$RAW" &
|
||||
else
|
||||
# this runs when screen is specified and there's only one monitor
|
||||
# it also runs with no args bc screen is default
|
||||
wf-recorder "$wf_flags" "$RAW" &
|
||||
fi
|
||||
|
||||
sig "REC"
|
||||
# create lockfile
|
||||
touch "$LOCK"
|
||||
|
||||
# save recorder's process id to lockfile
|
||||
PID=$!
|
||||
echo "$PID" > "$LOCK"
|
||||
fi
|
||||
|
14
recorder/test-waybar.sh
Executable file
14
recorder/test-waybar.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
#!/bin/env bash
|
||||
|
||||
echo "REC" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
|
||||
sleep 2; \
|
||||
echo "ERR" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
|
||||
sleep 2; \
|
||||
echo "REC" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
|
||||
sleep 2; \
|
||||
echo "STP" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
|
||||
sleep 2; \
|
||||
echo "CMP" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
|
||||
sleep 2; \
|
||||
echo "CPD" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
|
||||
sleep 2;
|
23
windows/README.md
Normal file
23
windows/README.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# niri-windows
|
||||
|
||||
Waybar module for counting windows open in current workspace.
|
||||
|
||||
## Features
|
||||
|
||||
- Tracks the state of windows per workspace.
|
||||
- Shows number of windows in the focused workspace.
|
||||
- Hover to show the applications and window titles.
|
||||
|
||||
## Usage
|
||||
|
||||
Add the following snippet to the module configuration section of your Waybar
|
||||
config:
|
||||
|
||||
```jsonc
|
||||
"custom/colcount": {
|
||||
"exec": "~/path/to/niri-windows.py",
|
||||
"return-type": "json",
|
||||
"restart-interval": "never",
|
||||
"format": "{} ",
|
||||
},
|
||||
```
|
270
windows/niri-windows.py
Executable file
270
windows/niri-windows.py
Executable file
|
@ -0,0 +1,270 @@
|
|||
#!/bin/env python
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
from typing import override
|
||||
|
||||
|
||||
# helper prints dict as json string
|
||||
def p(obj):
|
||||
print(json.dumps(obj), flush=True)
|
||||
|
||||
|
||||
log = logging.getLogger()
|
||||
log.setLevel(logging.DEBUG)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
# log_level = os.environ["NIRILOG"]
|
||||
if "NIRILOG" in os.environ:
|
||||
handler.setLevel(logging.DEBUG)
|
||||
else:
|
||||
handler.setLevel(logging.ERROR)
|
||||
log.addHandler(handler)
|
||||
SOCKET = os.environ["NIRI_SOCKET"]
|
||||
|
||||
|
||||
class Window:
|
||||
def __init__(self, id: int, title: str, app_id: str) -> None:
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.app_id = app_id
|
||||
|
||||
@override
|
||||
def __str__(self):
|
||||
return f"{self.app_id}: {self.title}"
|
||||
|
||||
|
||||
class Workspace:
|
||||
def __init__(self, id: int, output: str) -> None:
|
||||
self.id = id
|
||||
self.windows: dict[int, Window] = {}
|
||||
# self.windows: set[int] = set()
|
||||
self.output = output
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return str(list(self.windows))
|
||||
|
||||
def add(self, id, title: str, app_id: str):
|
||||
w = Window(id, title, app_id)
|
||||
self.windows[id] = w
|
||||
|
||||
def remove(self, id):
|
||||
self.windows.pop(id)
|
||||
|
||||
def query(self, id) -> bool:
|
||||
return id in self.windows
|
||||
|
||||
def change_output(self, new: str):
|
||||
self.output = new
|
||||
|
||||
def count(self) -> int:
|
||||
return len(self.windows)
|
||||
|
||||
def get_windows(self):
|
||||
return list(self.windows.values())
|
||||
|
||||
|
||||
class State:
|
||||
def __init__(self):
|
||||
self.focused_workspace: int = 0
|
||||
self.active_workspaces: dict[str, int] = {}
|
||||
# output name -> workspace: id -> window count
|
||||
self.workspaces: dict[int, Workspace] = {}
|
||||
self.first_run = True
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
s = ""
|
||||
s += f"focused: {self.focused_workspace}\n"
|
||||
for output, id in self.active_workspaces.items():
|
||||
s += f"{output}: {id}\n"
|
||||
for id, ws in self.workspaces.items():
|
||||
s += f"id: {id}, win: {ws}\n"
|
||||
return s
|
||||
|
||||
def set_activated(self, id: int):
|
||||
output = self.workspaces[id].output
|
||||
self.active_workspaces[output] = id
|
||||
|
||||
def set_focused(self, id: int):
|
||||
self.focused_workspace = id
|
||||
|
||||
def update_workspaces(self, arr: list[dict]):
|
||||
ids = []
|
||||
for ws in arr:
|
||||
id = ws["id"]
|
||||
output = ws["output"]
|
||||
if id not in self.workspaces:
|
||||
self.workspaces[id] = Workspace(id, output)
|
||||
ids.append(id)
|
||||
if ws["is_focused"]:
|
||||
self.focused_workspace = id
|
||||
if ws["is_active"]:
|
||||
self.active_workspaces[output] = id
|
||||
to_pop = []
|
||||
for id in self.workspaces.keys():
|
||||
if id not in ids:
|
||||
to_pop.append(id)
|
||||
for id in to_pop:
|
||||
self.workspaces.pop(id)
|
||||
|
||||
def add_window(
|
||||
self, workspace_id: int, window_id: int, title: str, app_id: str
|
||||
):
|
||||
self.workspaces[workspace_id].add(window_id, title, app_id)
|
||||
|
||||
def remove_window(self, window_id: int):
|
||||
for workspace in self.workspaces.values():
|
||||
if workspace.query(window_id):
|
||||
workspace.remove(window_id)
|
||||
return
|
||||
|
||||
def update_windows(self, arr: list[dict]):
|
||||
for win in arr:
|
||||
window_id: int = win["id"]
|
||||
workspace_id: int = win["workspace_id"]
|
||||
title: str = win["title"]
|
||||
app_id: str = win["app_id"]
|
||||
self.add_window(workspace_id, window_id, title, app_id)
|
||||
|
||||
def get_count(self, output: str | None = None) -> int:
|
||||
if output is None:
|
||||
return self.workspaces[self.focused_workspace].count()
|
||||
else:
|
||||
id = self.active_workspaces[output]
|
||||
return self.workspaces[id].count()
|
||||
|
||||
def get_windows(
|
||||
self, output: str | None = None, ws_id: int | None = None
|
||||
):
|
||||
if ws_id is None:
|
||||
if output is None:
|
||||
ws_id = self.focused_workspace
|
||||
else:
|
||||
ws_id = self.active_workspaces[output]
|
||||
return self.workspaces[ws_id].get_windows()
|
||||
|
||||
|
||||
state = State()
|
||||
|
||||
|
||||
def display():
|
||||
print(generate_message(), flush=True)
|
||||
|
||||
|
||||
def generate_message() -> str:
|
||||
obj = {"text": generate_text(), "tooltip": generate_tooltip()}
|
||||
return json.dumps(obj)
|
||||
|
||||
|
||||
def generate_tooltip(output=None) -> str:
|
||||
windows = state.get_windows(output)
|
||||
s = ""
|
||||
for _, w in enumerate(windows):
|
||||
s += f"{str(w)}\r\n"
|
||||
return s
|
||||
|
||||
|
||||
def generate_text(icon="", output=None):
|
||||
count = state.get_count(output)
|
||||
out = " ".join([icon] * count)
|
||||
if log.level <= logging.DEBUG:
|
||||
log.debug(str(state))
|
||||
return out
|
||||
|
||||
|
||||
# function handles message from socket
|
||||
def handle_message(event: dict):
|
||||
log.debug("Handling message.")
|
||||
log.debug(event)
|
||||
should_display = False
|
||||
match next(iter(event)):
|
||||
case "WorkspacesChanged":
|
||||
workspaces: list[dict] = event["WorkspacesChanged"][
|
||||
"workspaces"
|
||||
]
|
||||
state.update_workspaces(workspaces)
|
||||
log.info("Updated workspaces.")
|
||||
should_display = True
|
||||
case "WindowsChanged":
|
||||
windows: list[dict] = event["WindowsChanged"]["windows"]
|
||||
state.update_windows(windows)
|
||||
log.info("Updated windows.")
|
||||
should_display = True
|
||||
# workspaces : list[dict] = event["WorkspacesChanged"]["workspaces"]
|
||||
# state.process_changed(workspaces)
|
||||
case "WorkspaceActivated":
|
||||
ev = event["WorkspaceActivated"]
|
||||
if ev["focused"]:
|
||||
state.set_focused(ev["id"])
|
||||
log.info("Changed focused workspace.")
|
||||
state.set_activated(ev["id"])
|
||||
should_display = True
|
||||
case "WindowOpenedOrChanged":
|
||||
# This event also handles window moved across workspace
|
||||
window = event["WindowOpenedOrChanged"]["window"]
|
||||
window_id, workspace_id = window["id"], window["workspace_id"]
|
||||
state.remove_window(window_id)
|
||||
title = window["title"]
|
||||
app_id = window["app_id"]
|
||||
state.add_window(workspace_id, window_id, title, app_id)
|
||||
log.info("Updated window.")
|
||||
should_display = True
|
||||
case "WindowClosed":
|
||||
# TODO: update Workspace to track window IDs
|
||||
ev = event["WindowClosed"]
|
||||
id: int = ev["id"]
|
||||
state.remove_window(id)
|
||||
log.info("Removed window.")
|
||||
should_display = True
|
||||
return should_display
|
||||
|
||||
|
||||
# start the server
|
||||
def server():
|
||||
# pointer to this event loop
|
||||
# open our socket
|
||||
log.info(f"Connecting to Niri socket @ {SOCKET}")
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
||||
client.connect(SOCKET)
|
||||
# needed for async
|
||||
client.sendall('"EventStream"'.encode() + b"\n")
|
||||
client.shutdown(socket.SHUT_WR)
|
||||
|
||||
# only one connection at a time
|
||||
log.info("Connection successful, starting event loop.")
|
||||
# main loop; runs until waybar exits
|
||||
while True:
|
||||
# receive data from socket
|
||||
data = client.recv(4096)
|
||||
if not data:
|
||||
log.debug("No data!")
|
||||
for line in data.split(b"\n"):
|
||||
if line.strip():
|
||||
try:
|
||||
event = json.loads(line)
|
||||
if handle_message(event):
|
||||
display()
|
||||
except json.JSONDecodeError:
|
||||
print(
|
||||
"Malformed JSON:",
|
||||
line.decode(errors="replace"),
|
||||
)
|
||||
|
||||
|
||||
# main function
|
||||
def main():
|
||||
# start by outputing default contents
|
||||
# start the server
|
||||
try:
|
||||
server()
|
||||
except Exception as e:
|
||||
print(e, flush=True)
|
||||
log.error(e)
|
||||
|
||||
|
||||
# entry point
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue