@freely01/opencode-notify 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,29 @@
1
1
  /**
2
- * Terminator 子屏幕最大化检测
2
+ * Terminator 子屏幕遮挡检测
3
3
  *
4
- * 当用户在 Terminator 中最大化某个子屏幕时(Ctrl+Shift+X),
5
- * 其他子屏幕被遮挡。通过 DBus 查询焦点终端 UUID,
6
- * 与本进程的 TERMINATOR_UUID 比较,判定本屏是否被遮挡。
4
+ * 检测当前子屏幕是否被真正遮挡(不可见),用于判定是否需要
5
+ * 强制通知(即使用户在该会话中活跃)。
7
6
  *
8
- * 检测策略(依次尝试):
9
- * 1. busctl(systemd 自带,Ubuntu 预装)
10
- * 2. python3-dbus(备选)
11
- * 3. gdbus(GLib 备选)
12
- * 4. xdotool 窗口类检测(回退,仅判断是否在 Terminator 窗口中)
7
+ * 检测策略(两级):
8
+ * 1. X 窗口级别:Terminator 是否是当前活跃窗口(xdotool + xprop)
9
+ * → 用户在别的应用中 → 遮挡
10
+ * 2. 子屏级别:用户当前聚焦的是哪个子屏(DBus get_focused_terminal)
11
+ * 聚焦 != 本屏 用户在另一个子屏上 → 遮挡
12
+ *
13
+ * 注意:此版本 Terminator 未暴露 get_maximized_terminal 接口,
14
+ * 无法区分"分屏可见"和"最大化遮挡"。两级结合覆盖核心场景:
15
+ * - Terminator 不活跃(浏览器/IDE):强制通知
16
+ * - 本屏聚焦:不通知
17
+ * - 另一子屏聚焦(分屏或最大化):强制通知
13
18
  */
14
19
 
15
20
  import { execSync } from "node:child_process"
16
- import { warn, debug } from "./log.js"
21
+ import { debug } from "./log.js"
17
22
 
18
23
  /** 当前进程的 TERMINATOR_UUID */
19
24
  const MY_UUID = process.env.TERMINATOR_UUID ?? null
20
25
 
21
- /** Terminator DBus 信息(每实例唯一) */
26
+ /** Terminator DBus 信息 */
22
27
  const DBUS_NAME = process.env.TERMINATOR_DBUS_NAME ?? null
23
28
  const DBUS_PATH = process.env.TERMINATOR_DBUS_PATH ?? null
24
29
 
@@ -26,10 +31,10 @@ const DBUS_PATH = process.env.TERMINATOR_DBUS_PATH ?? null
26
31
  let insideTerminator: boolean | null = null
27
32
 
28
33
  /**
29
- * 检测当前子屏幕是否被遮挡(用户在其他子屏幕上操作)
34
+ * 检测当前子屏幕是否被遮挡(不可见)
30
35
  *
31
- * @returns true → 本子屏幕被遮挡,不应抑制通知
32
- * false → 本子屏幕可见,正常抑制逻辑
36
+ * @returns true → 本子屏幕被遮挡(用户看不到,应强制通知)
37
+ * false → 本子屏幕可见(正常抑制逻辑)
33
38
  * null → 无法确定(不在 Terminator 中或检测失败)
34
39
  */
35
40
  export function isTerminalOccluded(): boolean | null {
@@ -40,57 +45,38 @@ export function isTerminalOccluded(): boolean | null {
40
45
 
41
46
  insideTerminator = true
42
47
 
43
- // 如果没有 DBus 名称/路径信息,直接回退到 xdotool
44
- if (!DBUS_NAME || !DBUS_PATH) {
45
- debug(`Terminator DBus 环境变量不全: name=${DBUS_NAME} path=${DBUS_PATH},回退到 xdotool`)
46
- return callXdotool()
47
- }
48
+ // ─── 第一级:X 窗口级别 ────────────────────────────────────────────────
49
+ // Terminator 窗口是否是当前 X 活跃窗口?
50
+ const terminatorActive = isTerminatorWindowActive()
48
51
 
49
- // 策略 1: busctl
50
- try {
51
- const focused = callBusctl()
52
- if (focused !== null) {
53
- const occluded = !uuidEqual(focused, MY_UUID)
54
- debug(`Terminator 焦点检测(busctl): 本屏=${shortId(MY_UUID)} 焦点=${shortId(focused)} 遮挡=${occluded}`)
55
- return occluded
56
- }
57
- } catch (e) {
58
- debug(`Terminator busctl 失败: ${e instanceof Error ? e.message : String(e)}`)
52
+ if (terminatorActive === false) {
53
+ // 用户在其他应用中(浏览器、IDE 等),本屏不可见
54
+ debug(`Terminator 窗口非活跃(用户在其他应用中),判定为遮挡`)
55
+ return true
59
56
  }
60
57
 
61
- // 策略 2: python3-dbus
62
- try {
63
- const focused = callPythonDBus()
64
- if (focused !== null) {
65
- const occluded = !uuidEqual(focused, MY_UUID)
66
- debug(`Terminator 焦点检测(python): 本屏=${shortId(MY_UUID)} 焦点=${shortId(focused)} 遮挡=${occluded}`)
67
- return occluded
68
- }
69
- } catch (e) {
70
- debug(`Terminator python-dbus 失败: ${e instanceof Error ? e.message : String(e)}`)
71
- }
58
+ if (terminatorActive === true) {
59
+ // ─── 第二级:聚焦终端检测 ──────────────────────────────────────────
60
+ // Terminator 窗口活跃,检查用户聚焦的是不是本屏
61
+ const focused = queryFocusedTerminal()
72
62
 
73
- // 策略 3: gdbus
74
- try {
75
- const focused = callGDBus()
76
63
  if (focused !== null) {
77
- const occluded = !uuidEqual(focused, MY_UUID)
78
- debug(`Terminator 焦点检测(gdbus): 本屏=${shortId(MY_UUID)} 焦点=${shortId(focused)} 遮挡=${occluded}`)
79
- return occluded
64
+ if (focused === MY_UUID) {
65
+ debug(`Terminator 本屏聚焦,判定为可见`)
66
+ return false
67
+ }
68
+ // 用户聚焦在另一个子屏上 → 本屏不可见(无论分屏还是最大化)
69
+ debug(`Terminator 用户聚焦在其他子屏(${shortId(focused)}),判定为遮挡`)
70
+ return true
80
71
  }
81
- } catch (e) {
82
- debug(`Terminator gdbus 失败: ${e instanceof Error ? e.message : String(e)}`)
83
- }
84
72
 
85
- // 策略 4: xdotool 窗口类检测(回退)
86
- try {
87
- const result = callXdotool()
88
- debug(`Terminator 窗口检测(xdotool): ${result}`)
89
- return result
90
- } catch (e) {
91
- warn(`Terminator 所有检测策略均失败,最后错误: ${e instanceof Error ? e.message : String(e)}`)
92
- return null
73
+ // 无法查询焦点状态,保守假设可见
74
+ debug(`Terminator 窗口活跃,无法查询焦点状态,保守假设不遮挡`)
75
+ return false
93
76
  }
77
+
78
+ // 无法确定 X 窗口状态
79
+ return null
94
80
  }
95
81
 
96
82
  /**
@@ -104,61 +90,62 @@ export function isInsideTerminator(): boolean {
104
90
 
105
91
  // ─── 工具函数 ───────────────────────────────────────────────────────────────
106
92
 
107
- /** 比较两个 UUID(忽略 urn:uuid: 前缀) */
108
- function uuidEqual(a: string, b: string): boolean {
109
- const strip = (s: string) => s.replace(/^urn:uuid:/i, "")
110
- return strip(a) === strip(b)
111
- }
112
-
113
93
  /** 取 UUID 前 8 位用于日志 */
114
94
  function shortId(uuid: string): string {
115
95
  return uuid.replace(/^urn:uuid:/i, "").slice(0, 8)
116
96
  }
117
97
 
118
- // ─── 检测策略实现 ───────────────────────────────────────────────────────────
119
-
120
- /** 策略 1: busctl(systemd) */
121
- function callBusctl(): string | null {
122
- const out = execSync(
123
- `busctl --user call ${DBUS_NAME} ${DBUS_PATH} ${DBUS_NAME} get_focused_terminal`,
124
- { encoding: "utf-8", timeout: 3000 },
125
- ).trim()
126
- // 输出格式: s "uuid-string"
127
- const m = out.match(/s\s+"([^"]+)"/)
128
- return m ? m[1] : null
129
- }
98
+ // ─── 第一级:X 窗口检测 ──────────────────────────────────────────────────
130
99
 
131
- /** 策略 2: python3-dbus */
132
- function callPythonDBus(): string | null {
133
- const out = execSync(
134
- `python3 -c "
135
- import dbus
136
- bus = dbus.SessionBus()
137
- proxy = bus.get_object('${DBUS_NAME}', '${DBUS_PATH}')
138
- focused = proxy.get_focused_terminal(dbus_interface='${DBUS_NAME}')
139
- print(focused)
140
- "`,
141
- { encoding: "utf-8", timeout: 5000 },
142
- ).trim()
143
- return out || null
100
+ /**
101
+ * 检测 Terminator 窗口是否是当前 X 活跃窗口
102
+ * @returns true=Terminator 是活跃窗口, false=不是, null=检测失败
103
+ */
104
+ function isTerminatorWindowActive(): boolean | null {
105
+ try {
106
+ const out = execSync(
107
+ `xprop -id $(xdotool getactivewindow) WM_CLASS 2>/dev/null`,
108
+ { encoding: "utf-8", timeout: 5000 },
109
+ ).trim()
110
+ return out.includes('"Terminator"')
111
+ } catch (e) {
112
+ debug(`xprop/xdotool 检测 Terminator 窗口失败: ${e instanceof Error ? e.message : String(e)}`)
113
+ return null
114
+ }
144
115
  }
145
116
 
146
- /** 策略 3: gdbus(GLib) */
147
- function callGDBus(): string | null {
148
- const out = execSync(
149
- `gdbus call --session --dest ${DBUS_NAME} --object-path ${DBUS_PATH} --method ${DBUS_NAME}.get_focused_terminal`,
150
- { encoding: "utf-8", timeout: 3000 },
151
- ).trim()
152
- // 输出格式: ('uuid-string',)
153
- const m = out.match(/\('([^']+)'/)
154
- return m ? m[1] : null
155
- }
117
+ // ─── 第二级:聚焦终端检测 ───────────────────────────────────────────────
118
+
119
+ /**
120
+ * 通过 DBus 查询当前聚焦的子屏 UUID
121
+ * @returns UUID 字符串,或 null(查询失败)
122
+ */
123
+ function queryFocusedTerminal(): string | null {
124
+ if (!DBUS_NAME || !DBUS_PATH) return null
125
+
126
+ // 策略 1: busctl(无 stderr 输出,避免未知方法报错)
127
+ try {
128
+ const out = execSync(
129
+ `busctl --user call ${DBUS_NAME} ${DBUS_PATH} ${DBUS_NAME} get_focused_terminal 2>/dev/null`,
130
+ { encoding: "utf-8", timeout: 3000 },
131
+ ).trim()
132
+ const m = out.match(/s\s+"([^"]+)"/)
133
+ if (m) return m[1]
134
+ } catch {
135
+ // 静默忽略
136
+ }
137
+
138
+ // 策略 2: gdbus(备选)
139
+ try {
140
+ const out = execSync(
141
+ `gdbus call --session --dest ${DBUS_NAME} --object-path ${DBUS_PATH} --method ${DBUS_NAME}.get_focused_terminal 2>/dev/null`,
142
+ { encoding: "utf-8", timeout: 3000 },
143
+ ).trim()
144
+ const m = out.match(/\('([^']+)'/)
145
+ if (m) return m[1]
146
+ } catch {
147
+ // 静默忽略
148
+ }
156
149
 
157
- /** 策略 4: xdotool 窗口类检测 */
158
- function callXdotool(): boolean | null {
159
- const cls = execSync(
160
- `xdotool getactivewindow getwindowclassname`,
161
- { encoding: "utf-8", timeout: 3000 },
162
- ).trim()
163
- return cls === "Terminator" ? true : null
150
+ return null
164
151
  }