pendify 0.1.3__tar.gz → 0.1.4__tar.gz
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.
- {pendify-0.1.3/src/pendify.egg-info → pendify-0.1.4}/PKG-INFO +3 -1
- {pendify-0.1.3 → pendify-0.1.4}/README.md +2 -0
- {pendify-0.1.3 → pendify-0.1.4}/pyproject.toml +1 -1
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/__main__.py +19 -7
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/page.py +2 -0
- pendify-0.1.4/src/pendify/tray.py +310 -0
- {pendify-0.1.3 → pendify-0.1.4/src/pendify.egg-info}/PKG-INFO +3 -1
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify.egg-info/SOURCES.txt +1 -0
- {pendify-0.1.3 → pendify-0.1.4}/LICENSE +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/MANIFEST.in +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/setup.cfg +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/__init__.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/alert.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/autostart.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/client.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/codes.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/config.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/icon.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/pairing.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/pendify.ico +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/plate_almena.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/qr.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/runfile.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/watcher.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify/worker.py +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify.egg-info/dependency_links.txt +0 -0
- {pendify-0.1.3 → pendify-0.1.4}/src/pendify.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pendify
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Pairs this PC with a phone account by a QR on a loopback page and sends it two kinds of alerts.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/ElkinDev/PendiFy
|
|
@@ -61,6 +61,7 @@ El botón de arriba a la derecha cambia entre el tema claro y el oscuro, y el pr
|
|
|
61
61
|
|
|
62
62
|
- Iniciar: el acceso directo `PendiFy`, o `pythonw -m pendify`, que es lo que ejecuta el acceso directo; sin consola, si ya está abierto o no puede iniciarse te lo dice en una ventana. Para verlo en una consola: `python -m pendify`.
|
|
63
63
|
- Detener: pulsa «Salir» en la página del programa; si no la tienes abierta, iniciarlo otra vez la abre. Como último recurso, cierra el proceso `pythonw.exe` en el Administrador de tareas.
|
|
64
|
+
- Icono junto al reloj: un clic en el icono de PendiFy junto al reloj abre un menú para abrir la página, pausar o reanudar los avisos y salir del programa, también con la página y el navegador cerrados.
|
|
64
65
|
- Pausar: «Pausar avisos», en la tarjeta «Este PC» de la página, deja de leer el cliente del juego: no acepta partidas ni avisa a tu teléfono, y la página sigue abierta. «Reanudar avisos» vuelve a leerlo. La pausa no se guarda: cada vez que el programa se inicia, empieza activo.
|
|
65
66
|
- Iniciar con Windows: el interruptor «Iniciar con Windows», en la misma tarjeta, está apagado hasta que lo enciendas. Encendido, el programa empieza solo al iniciar sesión en Windows, solo para tu usuario y sin abrir el navegador ni la página.
|
|
66
67
|
- Actividad: la tarjeta «Actividad» de la página lista lo que el programa hizo desde que empezó: el cliente del juego encontrado y perdido, cada fase, la partida aceptada y el aviso enviado. Guarda las últimas 50 líneas, solo en memoria.
|
|
@@ -123,6 +124,7 @@ The button at the top right switches between the light and the dark theme, and t
|
|
|
123
124
|
|
|
124
125
|
- Start: the `PendiFy` shortcut, or `pythonw -m pendify`, which is what the shortcut runs; with no console, it tells you in a window when it is already running or cannot start. To see it in a console: `python -m pendify`.
|
|
125
126
|
- Stop: press «Quit» on the program's page; if it is not open, starting the program again opens it. As a last resort, end the `pythonw.exe` process in Task Manager.
|
|
127
|
+
- Icon by the clock: a click on the PendiFy icon by the clock opens a menu that opens the page, pauses or resumes the alerts and quits the program, also with the page and the browser closed.
|
|
126
128
|
- Pause: «Pause alerts», on the page's «This PC» card, stops reading the game client: it accepts no match and sends no alert to your phone, and the page stays open. «Resume alerts» reads it again. The pause is not kept: every start of the program is active again.
|
|
127
129
|
- Start with Windows: the «Start with Windows» switch, on the same card, is off until you turn it on. When it is on, the program starts by itself when you sign in to Windows, for your user only, without opening the browser or the page.
|
|
128
130
|
- Activity: the page's «Activity» card lists what the program did since it started: the game client found and lost, each phase, the match accepted and the alert sent. It keeps the last 50 lines, in memory only.
|
|
@@ -45,6 +45,7 @@ El botón de arriba a la derecha cambia entre el tema claro y el oscuro, y el pr
|
|
|
45
45
|
|
|
46
46
|
- Iniciar: el acceso directo `PendiFy`, o `pythonw -m pendify`, que es lo que ejecuta el acceso directo; sin consola, si ya está abierto o no puede iniciarse te lo dice en una ventana. Para verlo en una consola: `python -m pendify`.
|
|
47
47
|
- Detener: pulsa «Salir» en la página del programa; si no la tienes abierta, iniciarlo otra vez la abre. Como último recurso, cierra el proceso `pythonw.exe` en el Administrador de tareas.
|
|
48
|
+
- Icono junto al reloj: un clic en el icono de PendiFy junto al reloj abre un menú para abrir la página, pausar o reanudar los avisos y salir del programa, también con la página y el navegador cerrados.
|
|
48
49
|
- Pausar: «Pausar avisos», en la tarjeta «Este PC» de la página, deja de leer el cliente del juego: no acepta partidas ni avisa a tu teléfono, y la página sigue abierta. «Reanudar avisos» vuelve a leerlo. La pausa no se guarda: cada vez que el programa se inicia, empieza activo.
|
|
49
50
|
- Iniciar con Windows: el interruptor «Iniciar con Windows», en la misma tarjeta, está apagado hasta que lo enciendas. Encendido, el programa empieza solo al iniciar sesión en Windows, solo para tu usuario y sin abrir el navegador ni la página.
|
|
50
51
|
- Actividad: la tarjeta «Actividad» de la página lista lo que el programa hizo desde que empezó: el cliente del juego encontrado y perdido, cada fase, la partida aceptada y el aviso enviado. Guarda las últimas 50 líneas, solo en memoria.
|
|
@@ -107,6 +108,7 @@ The button at the top right switches between the light and the dark theme, and t
|
|
|
107
108
|
|
|
108
109
|
- Start: the `PendiFy` shortcut, or `pythonw -m pendify`, which is what the shortcut runs; with no console, it tells you in a window when it is already running or cannot start. To see it in a console: `python -m pendify`.
|
|
109
110
|
- Stop: press «Quit» on the program's page; if it is not open, starting the program again opens it. As a last resort, end the `pythonw.exe` process in Task Manager.
|
|
111
|
+
- Icon by the clock: a click on the PendiFy icon by the clock opens a menu that opens the page, pauses or resumes the alerts and quits the program, also with the page and the browser closed.
|
|
110
112
|
- Pause: «Pause alerts», on the page's «This PC» card, stops reading the game client: it accepts no match and sends no alert to your phone, and the page stays open. «Resume alerts» reads it again. The pause is not kept: every start of the program is active again.
|
|
111
113
|
- Start with Windows: the «Start with Windows» switch, on the same card, is off until you turn it on. When it is on, the program starts by itself when you sign in to Windows, for your user only, without opening the browser or the page.
|
|
112
114
|
- Activity: the page's «Activity» card lists what the program did since it started: the game client found and lost, each phase, the match accepted and the alert sent. It keeps the last 50 lines, in memory only.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pendify"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Pairs this PC with a phone account by a QR on a loopback page and sends it two kinds of alerts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
unless the start fails
|
|
7
7
|
python -m <package> ping <kind> one alert with the stored pair, one fixed line per answer
|
|
8
8
|
|
|
9
|
-
The page and the watcher stop together on Ctrl+C, on Ctrl+Break
|
|
10
|
-
|
|
11
|
-
and exits 0, and one made while that program is still closing after its quit waits for
|
|
9
|
+
The page and the watcher stop together on Ctrl+C, on Ctrl+Break, on the page's quit button and on the quit of
|
|
10
|
+
the icon by the clock, which remove the run file and exit 0. One instance per config folder: a second start
|
|
11
|
+
opens the running page and exits 0, and one made while that program is still closing after its quit waits for
|
|
12
|
+
it to end and starts.
|
|
12
13
|
Where no console is attached (a pythonw start), the line a start ends on is also shown in a
|
|
13
14
|
message box. `--data-dir`, `--worker` and `--client-lockfile` are the
|
|
14
15
|
test overrides, loopback only, so a test run reads neither the profile, the Worker nor the client's files.
|
|
@@ -24,6 +25,7 @@ from pathlib import Path
|
|
|
24
25
|
|
|
25
26
|
from . import alert, client, config, page, pairing, runfile, watcher, worker
|
|
26
27
|
from .autostart import Autostart
|
|
28
|
+
from .tray import Tray
|
|
27
29
|
|
|
28
30
|
TICK_SECONDS = 1.0
|
|
29
31
|
STOP_SECONDS = 5.0
|
|
@@ -41,6 +43,8 @@ CLAIM_FAILED_LINE = "cannot start: the run file cannot be replaced: {path}"
|
|
|
41
43
|
FOLDER_FAILED_LINE = "cannot start: the config folder cannot be written: {path}"
|
|
42
44
|
_MB_ICONINFORMATION = 0x40
|
|
43
45
|
_MB_SETFOREGROUND = 0x10000
|
|
46
|
+
# The icon by the clock's picture: pendify.ico beside the modules, the file install.ps1 points the shortcuts at.
|
|
47
|
+
ICON_PATH = Path(__file__).with_name("pendify.ico")
|
|
44
48
|
|
|
45
49
|
|
|
46
50
|
def _data_dir(value):
|
|
@@ -150,7 +154,7 @@ def _ends(line, code, show):
|
|
|
150
154
|
return code
|
|
151
155
|
|
|
152
156
|
|
|
153
|
-
def _serve(args, store, base, timeout, opener, stop, delay, beep, show, clock, sleep, autostart):
|
|
157
|
+
def _serve(args, store, base, timeout, opener, stop, delay, beep, show, clock, sleep, autostart, tray):
|
|
154
158
|
# A quiet start is the system's, at logon: it never opens the browser, and a start that ends well shows no box.
|
|
155
159
|
calm = (lambda line: None) if args.quiet else show
|
|
156
160
|
run = runfile.RunFile(store.path.parent, clock=clock, sleep=sleep)
|
|
@@ -192,8 +196,14 @@ def _serve(args, store, base, timeout, opener, stop, delay, beep, show, clock, s
|
|
|
192
196
|
pairing_page = page.PairingPage(state, watch=watch.snapshot, on_quit=quit_page, on_pause=watch.pause,
|
|
193
197
|
on_resume=watch.resume, autostart=autostart, events=watch.events)
|
|
194
198
|
url = pairing_page.start()
|
|
199
|
+
# The icon by the clock, under --quiet as well: the way to stop a copy the Run key started. Its words are
|
|
200
|
+
# the page's, in the language the page resolves with no browser to ask: the kept choice, else Spanish.
|
|
201
|
+
icon = tray(url, on_open=lambda: opener(url), on_pause=watch.pause, on_resume=watch.resume,
|
|
202
|
+
on_quit=quit_page, paused=lambda: watch.paused,
|
|
203
|
+
words=lambda: page.WORDS[pairing_page.language_of(None)], icon_path=ICON_PATH)
|
|
195
204
|
watching = threading.Thread(target=watch.run, daemon=True)
|
|
196
205
|
try:
|
|
206
|
+
icon.start()
|
|
197
207
|
run.publish(pairing_page.port)
|
|
198
208
|
print(f"page: {url}", flush=True)
|
|
199
209
|
if not args.quiet and not opener(url): # a start by a person opens the page, linked or not
|
|
@@ -217,6 +227,7 @@ def _serve(args, store, base, timeout, opener, stop, delay, beep, show, clock, s
|
|
|
217
227
|
watching.join(STOP_SECONDS)
|
|
218
228
|
alerter.flush(STOP_SECONDS)
|
|
219
229
|
pairing_page.close()
|
|
230
|
+
icon.close()
|
|
220
231
|
finally:
|
|
221
232
|
run.release()
|
|
222
233
|
return 0
|
|
@@ -231,10 +242,11 @@ def _break_as_interrupt():
|
|
|
231
242
|
|
|
232
243
|
def main(argv=None, *, opener=webbrowser.open, stop=None, timeout=worker.TIMEOUT_SECONDS, delay=None,
|
|
233
244
|
beep=alert.beep, box=_message_box, autostart=None, console=_console_attached, clock=time.monotonic,
|
|
234
|
-
sleep=time.sleep):
|
|
245
|
+
sleep=time.sleep, tray=Tray):
|
|
235
246
|
"""`beep`, `box` and `autostart` are the seams of the sound, of the message box and of the start with Windows
|
|
236
247
|
(the per-user Run value when None): a test run passes silent ones and a fake. `console` says whether a printed
|
|
237
|
-
line reaches anybody; `clock` and `sleep` time the run file's re-reads.
|
|
248
|
+
line reaches anybody; `clock` and `sleep` time the run file's re-reads. `tray` makes the icon by the clock, the
|
|
249
|
+
seam a test run fills with a fake, so no test shows a real icon."""
|
|
238
250
|
args = _arguments(sys.argv[1:] if argv is None else argv)
|
|
239
251
|
store = config.ConfigStore(getattr(args, "data_dir", None) or config.default_base_dir())
|
|
240
252
|
base = getattr(args, "worker", worker.BASE_URL)
|
|
@@ -250,7 +262,7 @@ def main(argv=None, *, opener=webbrowser.open, stop=None, timeout=worker.TIMEOUT
|
|
|
250
262
|
try:
|
|
251
263
|
return _serve(args, store, base, timeout, opener, stop or threading.Event(),
|
|
252
264
|
delay or watcher.accept_delay, beep, show, clock, sleep,
|
|
253
|
-
autostart if autostart is not None else Autostart())
|
|
265
|
+
autostart if autostart is not None else Autostart(), tray)
|
|
254
266
|
finally:
|
|
255
267
|
if previous is not None:
|
|
256
268
|
signal.signal(signal.SIGBREAK, previous)
|
|
@@ -119,6 +119,7 @@ WORDS = {
|
|
|
119
119
|
"this_pc": "Este PC",
|
|
120
120
|
"pause": "Pausar avisos",
|
|
121
121
|
"resume": "Reanudar avisos",
|
|
122
|
+
"tray_open": "Abrir la página",
|
|
122
123
|
"autostart_label": "Iniciar con Windows",
|
|
123
124
|
"autostart_help": "Al encender el PC el programa empieza solo, sin abrir esta página.",
|
|
124
125
|
"stopped": "El programa se detuvo: ya no vigila el cliente del juego ni envía avisos.",
|
|
@@ -200,6 +201,7 @@ WORDS = {
|
|
|
200
201
|
"this_pc": "This PC",
|
|
201
202
|
"pause": "Pause alerts",
|
|
202
203
|
"resume": "Resume alerts",
|
|
204
|
+
"tray_open": "Open the page",
|
|
203
205
|
"autostart_label": "Start with Windows",
|
|
204
206
|
"autostart_help": "When the PC turns on, the program starts by itself, without opening this page.",
|
|
205
207
|
"stopped": "The program stopped: it no longer watches the game client or sends alerts.",
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""The icon by the clock (lane pftray): an icon in the notification area whose menu opens the page, pauses or
|
|
2
|
+
resumes the alerts and quits the program, so the program can be stopped with the page and the browser closed.
|
|
3
|
+
|
|
4
|
+
Standard library only: user32, shell32 and kernel32 by ctypes, bound for 64-bit Windows. The icon lives on a thread
|
|
5
|
+
of its own, a daemon, with a hidden top-level tool window (never shown, so no taskbar entry and no Alt+Tab slot,
|
|
6
|
+
yet the shell's TaskbarCreated broadcast reaches it) and its message loop; a left or right click on it shows the menu
|
|
7
|
+
in the page's language of that moment. A Win32 call that fails is said in one fixed stderr line, as the page says a
|
|
8
|
+
failed request, and never raises out of the thread: the program runs without an icon rather than dying under
|
|
9
|
+
pythonw. On a platform other than Windows, start() says so in one line and does nothing.
|
|
10
|
+
"""
|
|
11
|
+
import ctypes
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
|
|
16
|
+
TIP = "PendiFy"
|
|
17
|
+
CLASS_NAME = "PendiFyTray"
|
|
18
|
+
THREAD_NAME = "pendify tray"
|
|
19
|
+
CLOSE_SECONDS = 3.0 # how long close() waits for the window to exist, then for the thread to end
|
|
20
|
+
ICON_ID = 1
|
|
21
|
+
|
|
22
|
+
WM_NULL, WM_DESTROY, WM_CLOSE, WM_CONTEXTMENU = 0x0000, 0x0002, 0x0010, 0x007B
|
|
23
|
+
WM_LBUTTONUP, WM_RBUTTONUP, WM_APP = 0x0202, 0x0205, 0x8000
|
|
24
|
+
CALLBACK_MESSAGE = WM_APP + 1
|
|
25
|
+
CLICKS = (WM_LBUTTONUP, WM_RBUTTONUP, WM_CONTEXTMENU)
|
|
26
|
+
WS_OVERLAPPED, WS_EX_TOOLWINDOW = 0x0, 0x80 # the window's style; a tool window takes no taskbar entry
|
|
27
|
+
NIM_ADD, NIM_DELETE = 0, 2
|
|
28
|
+
NIF_MESSAGE, NIF_ICON, NIF_TIP = 0x1, 0x2, 0x4
|
|
29
|
+
IMAGE_ICON, LR_LOADFROMFILE, LR_DEFAULTSIZE = 1, 0x10, 0x40
|
|
30
|
+
MF_STRING, MF_SEPARATOR = 0x0, 0x800
|
|
31
|
+
TPM_RIGHTBUTTON, TPM_NONOTIFY, TPM_RETURNCMD = 0x2, 0x80, 0x100
|
|
32
|
+
OPEN, PAUSE, RESUME, QUIT = 1, 2, 3, 4 # the menu's commands
|
|
33
|
+
|
|
34
|
+
FAILED_LINE = " [tray] {call} failed (error {code})"
|
|
35
|
+
BROKE_LINE = " [tray] the icon broke: {kind}"
|
|
36
|
+
HELD_LINE = " [tray] the icon's thread did not end in {seconds:g} seconds: its icon deleted from close"
|
|
37
|
+
EARLY_LINE = " [tray] the icon's thread did not end in {seconds:g} seconds before its window existed"
|
|
38
|
+
OTHER_PLATFORM_LINE = " [tray] no icon by the clock: this platform is not Windows"
|
|
39
|
+
|
|
40
|
+
# The Win32 types for 64-bit Windows: handles as c_void_p, WPARAM as c_size_t, LPARAM and LRESULT as c_ssize_t.
|
|
41
|
+
HANDLE, UINT, INT, WSTR = ctypes.c_void_p, ctypes.c_uint, ctypes.c_int, ctypes.c_wchar_p
|
|
42
|
+
WPARAM, LPARAM, LRESULT = ctypes.c_size_t, ctypes.c_ssize_t, ctypes.c_ssize_t
|
|
43
|
+
# The window procedure's type, made once: LRESULT (HWND, UINT, WPARAM, LPARAM). WINFUNCTYPE exists on Windows only.
|
|
44
|
+
WNDPROC = getattr(ctypes, "WINFUNCTYPE", ctypes.CFUNCTYPE)(LRESULT, HANDLE, UINT, WPARAM, LPARAM)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class POINT(ctypes.Structure):
|
|
48
|
+
_fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MSG(ctypes.Structure):
|
|
52
|
+
_fields_ = [("hwnd", HANDLE), ("message", UINT), ("wParam", WPARAM), ("lParam", LPARAM), ("time", UINT),
|
|
53
|
+
("pt", POINT), ("lPrivate", UINT)]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class WNDCLASSW(ctypes.Structure):
|
|
57
|
+
_fields_ = [("style", UINT), ("lpfnWndProc", WNDPROC), ("cbClsExtra", INT), ("cbWndExtra", INT),
|
|
58
|
+
("hInstance", HANDLE), ("hIcon", HANDLE), ("hCursor", HANDLE), ("hbrBackground", HANDLE),
|
|
59
|
+
("lpszMenuName", WSTR), ("lpszClassName", WSTR)]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GUID(ctypes.Structure):
|
|
63
|
+
_fields_ = [("Data1", UINT), ("Data2", ctypes.c_ushort), ("Data3", ctypes.c_ushort),
|
|
64
|
+
("Data4", ctypes.c_ubyte * 8)]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class NOTIFYICONDATAW(ctypes.Structure):
|
|
68
|
+
_fields_ = [("cbSize", UINT), ("hWnd", HANDLE), ("uID", UINT), ("uFlags", UINT), ("uCallbackMessage", UINT),
|
|
69
|
+
("hIcon", HANDLE), ("szTip", ctypes.c_wchar * 128), ("dwState", UINT), ("dwStateMask", UINT),
|
|
70
|
+
("szInfo", ctypes.c_wchar * 256), ("uVersion", UINT), ("szInfoTitle", ctypes.c_wchar * 64),
|
|
71
|
+
("dwInfoFlags", UINT), ("guidItem", GUID), ("hBalloonIcon", HANDLE)]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _Win32:
|
|
75
|
+
"""The facade: the Win32 functions the icon calls, each bound with its argtypes and restype, and the last
|
|
76
|
+
error of the calling thread."""
|
|
77
|
+
|
|
78
|
+
def __init__(self):
|
|
79
|
+
user32, shell32, kernel32 = (ctypes.WinDLL(name, use_last_error=True)
|
|
80
|
+
for name in ("user32", "shell32", "kernel32"))
|
|
81
|
+
bound = ((kernel32, "GetModuleHandleW", (WSTR,), HANDLE),
|
|
82
|
+
(user32, "RegisterClassW", (ctypes.POINTER(WNDCLASSW),), ctypes.c_ushort),
|
|
83
|
+
(user32, "UnregisterClassW", (WSTR, HANDLE), INT),
|
|
84
|
+
(user32, "CreateWindowExW", (UINT, WSTR, WSTR, UINT, INT, INT, INT, INT, HANDLE, HANDLE, HANDLE,
|
|
85
|
+
HANDLE), HANDLE),
|
|
86
|
+
(user32, "DestroyWindow", (HANDLE,), INT),
|
|
87
|
+
(user32, "DefWindowProcW", (HANDLE, UINT, WPARAM, LPARAM), LRESULT),
|
|
88
|
+
(user32, "GetMessageW", (ctypes.POINTER(MSG), HANDLE, UINT, UINT), INT),
|
|
89
|
+
(user32, "TranslateMessage", (ctypes.POINTER(MSG),), INT),
|
|
90
|
+
(user32, "DispatchMessageW", (ctypes.POINTER(MSG),), LRESULT),
|
|
91
|
+
(user32, "PostMessageW", (HANDLE, UINT, WPARAM, LPARAM), INT),
|
|
92
|
+
(user32, "PostQuitMessage", (INT,), None),
|
|
93
|
+
(user32, "RegisterWindowMessageW", (WSTR,), UINT),
|
|
94
|
+
(user32, "LoadImageW", (HANDLE, WSTR, UINT, INT, INT, UINT), HANDLE),
|
|
95
|
+
(user32, "DestroyIcon", (HANDLE,), INT),
|
|
96
|
+
(user32, "CreatePopupMenu", (), HANDLE),
|
|
97
|
+
(user32, "AppendMenuW", (HANDLE, UINT, WPARAM, WSTR), INT),
|
|
98
|
+
(user32, "DestroyMenu", (HANDLE,), INT),
|
|
99
|
+
(user32, "GetCursorPos", (ctypes.POINTER(POINT),), INT),
|
|
100
|
+
(user32, "SetForegroundWindow", (HANDLE,), INT),
|
|
101
|
+
(user32, "TrackPopupMenu", (HANDLE, UINT, INT, INT, INT, HANDLE, HANDLE), INT),
|
|
102
|
+
(shell32, "Shell_NotifyIconW", (UINT, ctypes.POINTER(NOTIFYICONDATAW)), INT))
|
|
103
|
+
for library, name, arguments, result in bound:
|
|
104
|
+
function = getattr(library, name)
|
|
105
|
+
function.argtypes, function.restype = arguments, result
|
|
106
|
+
setattr(self, name, function)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def last_error():
|
|
110
|
+
return ctypes.get_last_error()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _windows():
|
|
114
|
+
return os.name == "nt"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _say(line):
|
|
118
|
+
"""One fixed line on stderr, the page's road for a failure; under pythonw there is no stderr and it goes nowhere."""
|
|
119
|
+
print(line, file=sys.stderr)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Tray:
|
|
123
|
+
"""The icon by the clock. `paused` answers the watcher's state and `words` the page's table for the language set
|
|
124
|
+
at that moment; both are read at each click. `win32` is the facade of the Win32 functions, built from ctypes on
|
|
125
|
+
Windows when None; a test passes a fake."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, url, on_open, on_pause, on_resume, on_quit, paused, words, icon_path, win32=None):
|
|
128
|
+
self.url = url
|
|
129
|
+
self._on_open, self._on_pause, self._on_resume, self._on_quit = on_open, on_pause, on_resume, on_quit
|
|
130
|
+
self._paused, self._words, self._icon_path = paused, words, str(icon_path)
|
|
131
|
+
self._win32 = win32
|
|
132
|
+
self._wndproc = WNDPROC(self._proc) # made once and kept here, so the window's procedure is never collected
|
|
133
|
+
self._thread = None
|
|
134
|
+
self._ready = threading.Event() # set once the window exists, or once it cannot
|
|
135
|
+
self._instance = self._hwnd = self._icon = None
|
|
136
|
+
self._registered, self._taskbar_created = False, 0
|
|
137
|
+
self._closing = False # set by close(): an _open still under way then adds no icon
|
|
138
|
+
|
|
139
|
+
def start(self):
|
|
140
|
+
"""The icon on its own thread, a daemon, so a thread held past close() never keeps the process alive."""
|
|
141
|
+
if self._win32 is None:
|
|
142
|
+
if not _windows():
|
|
143
|
+
_say(OTHER_PLATFORM_LINE)
|
|
144
|
+
return
|
|
145
|
+
try:
|
|
146
|
+
self._win32 = _Win32()
|
|
147
|
+
except (OSError, AttributeError) as failure:
|
|
148
|
+
_say(BROKE_LINE.format(kind=type(failure).__name__))
|
|
149
|
+
return
|
|
150
|
+
self._thread = threading.Thread(target=self._run, name=THREAD_NAME, daemon=True)
|
|
151
|
+
self._thread.start()
|
|
152
|
+
|
|
153
|
+
def close(self):
|
|
154
|
+
"""Posts WM_CLOSE to the window when it exists, then waits a few seconds for the thread. A thread still held
|
|
155
|
+
then (in TrackPopupMenu's modal loop, say) leaves no dead icon: close deletes it from the calling thread.
|
|
156
|
+
A thread held before its window existed adds no icon once it goes on, so there is none to delete."""
|
|
157
|
+
self._closing = True
|
|
158
|
+
if self._thread is None:
|
|
159
|
+
return
|
|
160
|
+
self._ready.wait(CLOSE_SECONDS)
|
|
161
|
+
hwnd = self._hwnd
|
|
162
|
+
if hwnd is not None and self._thread.is_alive() and not self._win32.PostMessageW(hwnd, WM_CLOSE, 0, 0):
|
|
163
|
+
self._failed("PostMessageW")
|
|
164
|
+
self._thread.join(CLOSE_SECONDS)
|
|
165
|
+
if self._thread.is_alive():
|
|
166
|
+
if hwnd is not None:
|
|
167
|
+
self._notify(NIM_DELETE, hwnd)
|
|
168
|
+
_say(HELD_LINE.format(seconds=CLOSE_SECONDS))
|
|
169
|
+
else:
|
|
170
|
+
_say(EARLY_LINE.format(seconds=CLOSE_SECONDS))
|
|
171
|
+
|
|
172
|
+
def _failed(self, call):
|
|
173
|
+
_say(FAILED_LINE.format(call=call, code=self._win32.last_error()))
|
|
174
|
+
|
|
175
|
+
def _run(self):
|
|
176
|
+
try:
|
|
177
|
+
if self._open():
|
|
178
|
+
self._loop()
|
|
179
|
+
except Exception as failure: # never out of the thread: the program goes on without its icon
|
|
180
|
+
_say(BROKE_LINE.format(kind=type(failure).__name__))
|
|
181
|
+
finally:
|
|
182
|
+
self._ready.set()
|
|
183
|
+
self._release()
|
|
184
|
+
|
|
185
|
+
def _open(self):
|
|
186
|
+
"""The window class, the hidden top-level tool window, the icon's picture and the TaskbarCreated message;
|
|
187
|
+
then the icon. False when there is no window to serve."""
|
|
188
|
+
win32 = self._win32
|
|
189
|
+
self._instance = win32.GetModuleHandleW(None)
|
|
190
|
+
if not self._instance:
|
|
191
|
+
self._failed("GetModuleHandleW")
|
|
192
|
+
window_class = WNDCLASSW(lpfnWndProc=self._wndproc, hInstance=self._instance, lpszClassName=CLASS_NAME)
|
|
193
|
+
self._registered = bool(win32.RegisterClassW(window_class))
|
|
194
|
+
if not self._registered:
|
|
195
|
+
self._failed("RegisterClassW")
|
|
196
|
+
return False
|
|
197
|
+
hwnd = win32.CreateWindowExW(WS_EX_TOOLWINDOW, CLASS_NAME, TIP, WS_OVERLAPPED, 0, 0, 0, 0, None, None,
|
|
198
|
+
self._instance, None) # top-level, never shown: TaskbarCreated reaches it
|
|
199
|
+
if not hwnd:
|
|
200
|
+
self._failed("CreateWindowExW")
|
|
201
|
+
return False
|
|
202
|
+
self._hwnd = hwnd
|
|
203
|
+
self._icon = win32.LoadImageW(None, self._icon_path, IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE)
|
|
204
|
+
if not self._icon:
|
|
205
|
+
self._failed("LoadImageW")
|
|
206
|
+
self._taskbar_created = win32.RegisterWindowMessageW("TaskbarCreated")
|
|
207
|
+
if not self._taskbar_created:
|
|
208
|
+
self._failed("RegisterWindowMessageW")
|
|
209
|
+
if self._closing: # close() has run: no icon, and _release destroys the window and unregisters the class
|
|
210
|
+
return False
|
|
211
|
+
self._notify(NIM_ADD)
|
|
212
|
+
self._ready.set()
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
def _loop(self):
|
|
216
|
+
win32, message = self._win32, MSG()
|
|
217
|
+
while True:
|
|
218
|
+
got = win32.GetMessageW(message, None, 0, 0)
|
|
219
|
+
if got == 0: # WM_QUIT, posted at WM_DESTROY
|
|
220
|
+
return
|
|
221
|
+
if got == -1:
|
|
222
|
+
self._failed("GetMessageW")
|
|
223
|
+
return
|
|
224
|
+
win32.TranslateMessage(message)
|
|
225
|
+
win32.DispatchMessageW(message)
|
|
226
|
+
|
|
227
|
+
def _release(self):
|
|
228
|
+
"""What the thread made, undone as it ends; the window only when its loop ended without WM_CLOSE."""
|
|
229
|
+
win32 = self._win32
|
|
230
|
+
if self._hwnd is not None:
|
|
231
|
+
self._notify(NIM_DELETE)
|
|
232
|
+
if not win32.DestroyWindow(self._hwnd):
|
|
233
|
+
self._failed("DestroyWindow")
|
|
234
|
+
self._hwnd = None
|
|
235
|
+
if self._icon and not win32.DestroyIcon(self._icon):
|
|
236
|
+
self._failed("DestroyIcon")
|
|
237
|
+
self._icon = None
|
|
238
|
+
if self._registered and not win32.UnregisterClassW(CLASS_NAME, self._instance):
|
|
239
|
+
self._failed("UnregisterClassW")
|
|
240
|
+
self._registered = False
|
|
241
|
+
|
|
242
|
+
def _notify(self, action, hwnd=None):
|
|
243
|
+
"""The icon added or deleted; `hwnd` names the window when the caller holds it (close, off the thread)."""
|
|
244
|
+
data = NOTIFYICONDATAW(cbSize=ctypes.sizeof(NOTIFYICONDATAW), hWnd=self._hwnd if hwnd is None else hwnd,
|
|
245
|
+
uID=ICON_ID)
|
|
246
|
+
if action == NIM_ADD:
|
|
247
|
+
data.uFlags, data.uCallbackMessage = NIF_MESSAGE | NIF_ICON | NIF_TIP, CALLBACK_MESSAGE
|
|
248
|
+
data.hIcon, data.szTip = self._icon, TIP
|
|
249
|
+
if not self._win32.Shell_NotifyIconW(action, data):
|
|
250
|
+
self._failed("Shell_NotifyIconW")
|
|
251
|
+
|
|
252
|
+
def _proc(self, hwnd, message, wparam, lparam):
|
|
253
|
+
try:
|
|
254
|
+
if message == CALLBACK_MESSAGE:
|
|
255
|
+
if (lparam & 0xFFFF) in CLICKS:
|
|
256
|
+
self._menu(hwnd)
|
|
257
|
+
return 0
|
|
258
|
+
if message == WM_CLOSE:
|
|
259
|
+
self._notify(NIM_DELETE)
|
|
260
|
+
if not self._win32.DestroyWindow(hwnd):
|
|
261
|
+
self._failed("DestroyWindow")
|
|
262
|
+
return 0
|
|
263
|
+
if message == WM_DESTROY:
|
|
264
|
+
self._hwnd = None
|
|
265
|
+
self._win32.PostQuitMessage(0)
|
|
266
|
+
return 0
|
|
267
|
+
if self._taskbar_created and message == self._taskbar_created: # Explorer restarted: the icon again
|
|
268
|
+
self._notify(NIM_ADD)
|
|
269
|
+
return 0
|
|
270
|
+
except Exception as failure: # never out of the procedure: the window goes on serving
|
|
271
|
+
_say(BROKE_LINE.format(kind=type(failure).__name__))
|
|
272
|
+
return 0
|
|
273
|
+
return self._win32.DefWindowProcW(hwnd, message, wparam, lparam)
|
|
274
|
+
|
|
275
|
+
def _menu(self, hwnd):
|
|
276
|
+
"""The menu at the cursor, in the words and the pause of this moment, then its command. TrackPopupMenu
|
|
277
|
+
answers 0 when the menu is dismissed, which is no failure."""
|
|
278
|
+
win32, words = self._win32, self._words()
|
|
279
|
+
middle = (RESUME, words["resume"]) if self._paused() else (PAUSE, words["pause"])
|
|
280
|
+
menu = win32.CreatePopupMenu()
|
|
281
|
+
if not menu:
|
|
282
|
+
self._failed("CreatePopupMenu")
|
|
283
|
+
return
|
|
284
|
+
try:
|
|
285
|
+
for item in ((OPEN, words["tray_open"]), middle, None, (QUIT, words["quit"])):
|
|
286
|
+
flags, command, text = (MF_SEPARATOR, 0, None) if item is None else (MF_STRING, *item)
|
|
287
|
+
if not win32.AppendMenuW(menu, flags, command, text):
|
|
288
|
+
self._failed("AppendMenuW")
|
|
289
|
+
cursor = POINT()
|
|
290
|
+
if not win32.GetCursorPos(cursor):
|
|
291
|
+
self._failed("GetCursorPos")
|
|
292
|
+
if not win32.SetForegroundWindow(hwnd): # without it the menu stays open on a click elsewhere
|
|
293
|
+
self._failed("SetForegroundWindow")
|
|
294
|
+
chosen = win32.TrackPopupMenu(menu, TPM_RETURNCMD | TPM_NONOTIFY | TPM_RIGHTBUTTON, cursor.x, cursor.y,
|
|
295
|
+
0, hwnd, None)
|
|
296
|
+
if not win32.PostMessageW(hwnd, WM_NULL, 0, 0): # lets the menu close on a click elsewhere next time
|
|
297
|
+
self._failed("PostMessageW")
|
|
298
|
+
finally:
|
|
299
|
+
if not win32.DestroyMenu(menu):
|
|
300
|
+
self._failed("DestroyMenu")
|
|
301
|
+
if chosen == OPEN:
|
|
302
|
+
self._on_open()
|
|
303
|
+
elif chosen == PAUSE:
|
|
304
|
+
self._on_pause()
|
|
305
|
+
elif chosen == RESUME:
|
|
306
|
+
self._on_resume()
|
|
307
|
+
elif chosen == QUIT:
|
|
308
|
+
self._on_quit()
|
|
309
|
+
if not win32.PostMessageW(hwnd, WM_CLOSE, 0, 0):
|
|
310
|
+
self._failed("PostMessageW")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pendify
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Pairs this PC with a phone account by a QR on a loopback page and sends it two kinds of alerts.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/ElkinDev/PendiFy
|
|
@@ -61,6 +61,7 @@ El botón de arriba a la derecha cambia entre el tema claro y el oscuro, y el pr
|
|
|
61
61
|
|
|
62
62
|
- Iniciar: el acceso directo `PendiFy`, o `pythonw -m pendify`, que es lo que ejecuta el acceso directo; sin consola, si ya está abierto o no puede iniciarse te lo dice en una ventana. Para verlo en una consola: `python -m pendify`.
|
|
63
63
|
- Detener: pulsa «Salir» en la página del programa; si no la tienes abierta, iniciarlo otra vez la abre. Como último recurso, cierra el proceso `pythonw.exe` en el Administrador de tareas.
|
|
64
|
+
- Icono junto al reloj: un clic en el icono de PendiFy junto al reloj abre un menú para abrir la página, pausar o reanudar los avisos y salir del programa, también con la página y el navegador cerrados.
|
|
64
65
|
- Pausar: «Pausar avisos», en la tarjeta «Este PC» de la página, deja de leer el cliente del juego: no acepta partidas ni avisa a tu teléfono, y la página sigue abierta. «Reanudar avisos» vuelve a leerlo. La pausa no se guarda: cada vez que el programa se inicia, empieza activo.
|
|
65
66
|
- Iniciar con Windows: el interruptor «Iniciar con Windows», en la misma tarjeta, está apagado hasta que lo enciendas. Encendido, el programa empieza solo al iniciar sesión en Windows, solo para tu usuario y sin abrir el navegador ni la página.
|
|
66
67
|
- Actividad: la tarjeta «Actividad» de la página lista lo que el programa hizo desde que empezó: el cliente del juego encontrado y perdido, cada fase, la partida aceptada y el aviso enviado. Guarda las últimas 50 líneas, solo en memoria.
|
|
@@ -123,6 +124,7 @@ The button at the top right switches between the light and the dark theme, and t
|
|
|
123
124
|
|
|
124
125
|
- Start: the `PendiFy` shortcut, or `pythonw -m pendify`, which is what the shortcut runs; with no console, it tells you in a window when it is already running or cannot start. To see it in a console: `python -m pendify`.
|
|
125
126
|
- Stop: press «Quit» on the program's page; if it is not open, starting the program again opens it. As a last resort, end the `pythonw.exe` process in Task Manager.
|
|
127
|
+
- Icon by the clock: a click on the PendiFy icon by the clock opens a menu that opens the page, pauses or resumes the alerts and quits the program, also with the page and the browser closed.
|
|
126
128
|
- Pause: «Pause alerts», on the page's «This PC» card, stops reading the game client: it accepts no match and sends no alert to your phone, and the page stays open. «Resume alerts» reads it again. The pause is not kept: every start of the program is active again.
|
|
127
129
|
- Start with Windows: the «Start with Windows» switch, on the same card, is off until you turn it on. When it is on, the program starts by itself when you sign in to Windows, for your user only, without opening the browser or the page.
|
|
128
130
|
- Activity: the page's «Activity» card lists what the program did since it started: the game client found and lost, each phase, the match accepted and the alert sent. It keeps the last 50 lines, in memory only.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|