initial commit

This commit is contained in:
2026-02-09 19:04:19 -08:00
commit 272d92169d
531 changed files with 196294 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
/*
SubMiner - All-in-one sentence mining overlay
Copyright (C) 2024 sudacode
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { WindowGeometry } from "../types";
export type GeometryChangeCallback = (geometry: WindowGeometry) => void;
export type WindowFoundCallback = (geometry: WindowGeometry) => void;
export type WindowLostCallback = () => void;
export abstract class BaseWindowTracker {
protected currentGeometry: WindowGeometry | null = null;
protected windowFound: boolean = false;
public onGeometryChange: GeometryChangeCallback | null = null;
public onWindowFound: WindowFoundCallback | null = null;
public onWindowLost: WindowLostCallback | null = null;
abstract start(): void;
abstract stop(): void;
getGeometry(): WindowGeometry | null {
return this.currentGeometry;
}
isTracking(): boolean {
return this.windowFound;
}
protected updateGeometry(newGeometry: WindowGeometry | null): void {
if (newGeometry) {
if (!this.windowFound) {
this.windowFound = true;
if (this.onWindowFound) this.onWindowFound(newGeometry);
}
if (
!this.currentGeometry ||
this.currentGeometry.x !== newGeometry.x ||
this.currentGeometry.y !== newGeometry.y ||
this.currentGeometry.width !== newGeometry.width ||
this.currentGeometry.height !== newGeometry.height
) {
this.currentGeometry = newGeometry;
if (this.onGeometryChange) this.onGeometryChange(newGeometry);
}
} else {
if (this.windowFound) {
this.windowFound = false;
this.currentGeometry = null;
if (this.onWindowLost) this.onWindowLost();
}
}
}
}

View File

@@ -0,0 +1,114 @@
/*
SubMiner - All-in-one sentence mining overlay
Copyright (C) 2024 sudacode
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as net from "net";
import { execSync } from "child_process";
import { BaseWindowTracker } from "./base-tracker";
import { createLogger } from "../logger";
const log = createLogger("tracker").child("hyprland");
interface HyprlandClient {
class: string;
at: [number, number];
size: [number, number];
}
export class HyprlandWindowTracker extends BaseWindowTracker {
private pollInterval: ReturnType<typeof setInterval> | null = null;
private eventSocket: net.Socket | null = null;
start(): void {
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
this.pollGeometry();
this.connectEventSocket();
}
stop(): void {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
if (this.eventSocket) {
this.eventSocket.destroy();
this.eventSocket = null;
}
}
private connectEventSocket(): void {
const hyprlandSig = process.env.HYPRLAND_INSTANCE_SIGNATURE;
if (!hyprlandSig) {
log.info("HYPRLAND_INSTANCE_SIGNATURE not set, skipping event socket");
return;
}
const xdgRuntime = process.env.XDG_RUNTIME_DIR || "/tmp";
const socketPath = `${xdgRuntime}/hypr/${hyprlandSig}/.socket2.sock`;
this.eventSocket = new net.Socket();
this.eventSocket.on("connect", () => {
log.info("Connected to Hyprland event socket");
});
this.eventSocket.on("data", (data: Buffer) => {
const events = data.toString().split("\n");
for (const event of events) {
if (
event.includes("movewindow") ||
event.includes("windowtitle") ||
event.includes("openwindow") ||
event.includes("closewindow") ||
event.includes("fullscreen")
) {
this.pollGeometry();
}
}
});
this.eventSocket.on("error", (err: Error) => {
log.error("Hyprland event socket error:", err.message);
});
this.eventSocket.on("close", () => {
log.info("Hyprland event socket closed");
});
this.eventSocket.connect(socketPath);
}
private pollGeometry(): void {
try {
const output = execSync("hyprctl clients -j", { encoding: "utf-8" });
const clients: HyprlandClient[] = JSON.parse(output);
const mpvWindow = clients.find((c) => c.class === "mpv");
if (mpvWindow) {
this.updateGeometry({
x: mpvWindow.at[0],
y: mpvWindow.at[1],
width: mpvWindow.size[0],
height: mpvWindow.size[1],
});
} else {
this.updateGeometry(null);
}
} catch (err) {
// hyprctl not available or failed - silent fail
}
}
}

View File

@@ -0,0 +1,86 @@
/*
SubMiner - All-in-one sentence mining overlay
Copyright (C) 2024 sudacode
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { BaseWindowTracker } from "./base-tracker";
import { HyprlandWindowTracker } from "./hyprland-tracker";
import { SwayWindowTracker } from "./sway-tracker";
import { X11WindowTracker } from "./x11-tracker";
import { MacOSWindowTracker } from "./macos-tracker";
import { createLogger } from "../logger";
const log = createLogger("tracker");
export type Compositor = "hyprland" | "sway" | "x11" | "macos" | null;
export type Backend = "auto" | Exclude<Compositor, null>;
export function detectCompositor(): Compositor {
if (process.platform === "darwin") return "macos";
if (process.env.HYPRLAND_INSTANCE_SIGNATURE) return "hyprland";
if (process.env.SWAYSOCK) return "sway";
if (process.env.XDG_SESSION_TYPE === "x11") return "x11";
return null;
}
function normalizeCompositor(value: string): Compositor | null {
const normalized = value.trim().toLowerCase();
if (normalized === "hyprland") return "hyprland";
if (normalized === "sway") return "sway";
if (normalized === "x11") return "x11";
if (normalized === "macos") return "macos";
return null;
}
export function createWindowTracker(
override?: string | null,
): BaseWindowTracker | null {
let compositor = detectCompositor();
if (override && override !== "auto") {
const normalized = normalizeCompositor(override);
if (normalized) {
compositor = normalized;
} else {
log.warn(
`Unsupported backend override "${override}", falling back to auto.`,
);
}
}
log.info(`Detected compositor: ${compositor || "none"}`);
switch (compositor) {
case "hyprland":
return new HyprlandWindowTracker();
case "sway":
return new SwayWindowTracker();
case "x11":
return new X11WindowTracker();
case "macos":
return new MacOSWindowTracker();
default:
log.warn("No supported compositor detected. Window tracking disabled.");
return null;
}
}
export {
BaseWindowTracker,
HyprlandWindowTracker,
SwayWindowTracker,
X11WindowTracker,
MacOSWindowTracker,
};

View File

@@ -0,0 +1,114 @@
/*
subminer - Yomitan integration for mpv
Copyright (C) 2024 sudacode
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { execFile } from "child_process";
import { BaseWindowTracker } from "./base-tracker";
export class MacOSWindowTracker extends BaseWindowTracker {
private pollInterval: ReturnType<typeof setInterval> | null = null;
private pollInFlight = false;
start(): void {
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
this.pollGeometry();
}
stop(): void {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
}
private pollGeometry(): void {
if (this.pollInFlight) {
return;
}
this.pollInFlight = true;
const script = `
set processNames to {"mpv", "MPV", "org.mpv.mpv"}
tell application "System Events"
repeat with procName in processNames
set procList to (every process whose name is procName)
repeat with p in procList
try
if (count of windows of p) > 0 then
set targetWindow to window 1 of p
set windowPos to position of targetWindow
set windowSize to size of targetWindow
return (item 1 of windowPos) & "," & (item 2 of windowPos) & "," & (item 1 of windowSize) & "," & (item 2 of windowSize)
end if
end try
end repeat
end repeat
end tell
return "not-found"
`;
execFile(
"osascript",
["-e", script],
{
encoding: "utf-8",
timeout: 1000,
maxBuffer: 1024 * 1024,
},
(err, stdout) => {
if (err) {
this.updateGeometry(null);
this.pollInFlight = false;
return;
}
const result = (stdout || "").trim();
if (result && result !== "not-found") {
const parts = result.split(",");
if (parts.length === 4) {
const x = parseInt(parts[0], 10);
const y = parseInt(parts[1], 10);
const width = parseInt(parts[2], 10);
const height = parseInt(parts[3], 10);
if (
Number.isFinite(x) &&
Number.isFinite(y) &&
Number.isFinite(width) &&
Number.isFinite(height) &&
width > 0 &&
height > 0
) {
this.updateGeometry({
x,
y,
width,
height,
});
this.pollInFlight = false;
return;
}
}
}
this.updateGeometry(null);
this.pollInFlight = false;
},
);
}
}

View File

@@ -0,0 +1,94 @@
/*
SubMiner - All-in-one sentence mining overlay
Copyright (C) 2024 sudacode
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { execSync } from "child_process";
import { BaseWindowTracker } from "./base-tracker";
interface SwayRect {
x: number;
y: number;
width: number;
height: number;
}
interface SwayNode {
app_id?: string;
window_properties?: { class?: string };
rect?: SwayRect;
nodes?: SwayNode[];
floating_nodes?: SwayNode[];
}
export class SwayWindowTracker extends BaseWindowTracker {
private pollInterval: ReturnType<typeof setInterval> | null = null;
start(): void {
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
this.pollGeometry();
}
stop(): void {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
}
private findMpvWindow(node: SwayNode): SwayNode | null {
if (node.app_id === "mpv" || node.window_properties?.class === "mpv") {
return node;
}
if (node.nodes) {
for (const child of node.nodes) {
const found = this.findMpvWindow(child);
if (found) return found;
}
}
if (node.floating_nodes) {
for (const child of node.floating_nodes) {
const found = this.findMpvWindow(child);
if (found) return found;
}
}
return null;
}
private pollGeometry(): void {
try {
const output = execSync("swaymsg -t get_tree", { encoding: "utf-8" });
const tree: SwayNode = JSON.parse(output);
const mpvWindow = this.findMpvWindow(tree);
if (mpvWindow && mpvWindow.rect) {
this.updateGeometry({
x: mpvWindow.rect.x,
y: mpvWindow.rect.y,
width: mpvWindow.rect.width,
height: mpvWindow.rect.height,
});
} else {
this.updateGeometry(null);
}
} catch (err) {
// swaymsg not available or failed - silent fail
}
}
}

View File

@@ -0,0 +1,73 @@
/*
SubMiner - All-in-one sentence mining overlay
Copyright (C) 2024 sudacode
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { execSync } from "child_process";
import { BaseWindowTracker } from "./base-tracker";
export class X11WindowTracker extends BaseWindowTracker {
private pollInterval: ReturnType<typeof setInterval> | null = null;
start(): void {
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
this.pollGeometry();
}
stop(): void {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
}
private pollGeometry(): void {
try {
const windowIds = execSync("xdotool search --class mpv", {
encoding: "utf-8",
}).trim();
if (!windowIds) {
this.updateGeometry(null);
return;
}
const windowId = windowIds.split("\n")[0];
const winInfo = execSync(`xwininfo -id ${windowId}`, {
encoding: "utf-8",
});
const xMatch = winInfo.match(/Absolute upper-left X:\s*(\d+)/);
const yMatch = winInfo.match(/Absolute upper-left Y:\s*(\d+)/);
const widthMatch = winInfo.match(/Width:\s*(\d+)/);
const heightMatch = winInfo.match(/Height:\s*(\d+)/);
if (xMatch && yMatch && widthMatch && heightMatch) {
this.updateGeometry({
x: parseInt(xMatch[1], 10),
y: parseInt(yMatch[1], 10),
width: parseInt(widthMatch[1], 10),
height: parseInt(heightMatch[1], 10),
});
} else {
this.updateGeometry(null);
}
} catch (err) {
this.updateGeometry(null);
}
}
}