From d3ffd85f245da354a367602ee65c2e223a83bd2b Mon Sep 17 00:00:00 2001 From: Evgeniy <38162408+y0zhyck@users.noreply.github.com> Date: Thu, 11 Jul 2024 14:07:24 +0300 Subject: [PATCH] Add files via upload --- resources/config/config.json | 1 + resources/img/icon.ico | Bin 0 -> 4286 bytes resources/img/icon.png | Bin 0 -> 31802 bytes src/__init__.py | 0 src/config/__init__.py | 0 src/config/config.py | 40 +++++++++ src/keyboard_controller/__init__.py | 0 src/keyboard_controller/hotkey_listener.py | 24 ++++++ .../pressed_events_handler.py | 17 ++++ src/main.py | 80 ++++++++++++++++++ src/os_controller/__init__.py | 0 src/os_controller/notifications.py | 26 ++++++ src/os_controller/tray_icon.py | 50 +++++++++++ src/ui/__init__.py | 0 src/ui/overlay_window.py | 27 ++++++ src/ui/update_window.py | 18 ++++ src/util/__init__.py | 0 src/util/lockfile_handler.py | 24 ++++++ src/util/path_util.py | 20 +++++ src/util/update_util.py | 16 ++++ src/util/web_browser_util.py | 18 ++++ 21 files changed, 361 insertions(+) create mode 100644 resources/config/config.json create mode 100644 resources/img/icon.ico create mode 100644 resources/img/icon.png create mode 100644 src/__init__.py create mode 100644 src/config/__init__.py create mode 100644 src/config/config.py create mode 100644 src/keyboard_controller/__init__.py create mode 100644 src/keyboard_controller/hotkey_listener.py create mode 100644 src/keyboard_controller/pressed_events_handler.py create mode 100644 src/main.py create mode 100644 src/os_controller/__init__.py create mode 100644 src/os_controller/notifications.py create mode 100644 src/os_controller/tray_icon.py create mode 100644 src/ui/__init__.py create mode 100644 src/ui/overlay_window.py create mode 100644 src/ui/update_window.py create mode 100644 src/util/__init__.py create mode 100644 src/util/lockfile_handler.py create mode 100644 src/util/path_util.py create mode 100644 src/util/update_util.py create mode 100644 src/util/web_browser_util.py diff --git a/resources/config/config.json b/resources/config/config.json new file mode 100644 index 0000000..c4f61c3 --- /dev/null +++ b/resources/config/config.json @@ -0,0 +1 @@ +{"hotkey": "ctrl+l", "opacity": 0.3, "notificationsEnabled": false} \ No newline at end of file diff --git a/resources/img/icon.ico b/resources/img/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..acbf35b0e8b10fa99e404e7f74ef62141e03cccf GIT binary patch literal 4286 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

6mGe*q#z+qc({Rcs=8 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 P0|&@Ut4LLTG!FhB&gp9l literal 0 HcmV?d00001 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/config/config.py b/src/config/config.py new file mode 100644 index 0000000..24f4a90 --- /dev/null +++ b/src/config/config.py @@ -0,0 +1,40 @@ +import json +import os.path +import shutil + +from src.util.path_util import get_packaged_path, get_config_path +from src.util.web_browser_util import open_about + +BUNDLED_CONFIG_FILE = os.path.join("resources", "config", "config.json") +DEFAULT_HOTKEY = "ctrl+l" + + +def load(): + try: + with open(get_config_path(), "r") as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + shutil.copy(get_packaged_path(BUNDLED_CONFIG_FILE), get_config_path()) + with open(get_config_path(), "r") as f: + return json.load(f) + + +class Config: + def __init__(self) -> None: + config = load() + self.hotkey = config.get("hotkey", DEFAULT_HOTKEY) if config else DEFAULT_HOTKEY + self.opacity = config.get("opacity", 0.3) if config else 0.3 + self.notifications_enabled = config.get("notificationsEnabled", True) if config else True + if not config: + open_about() + self.save() + + def save(self) -> None: + print(f'saving to: {get_config_path()}') + with open(get_config_path(), "w") as f: + config = { + "hotkey": self.hotkey, + "opacity": self.opacity, + "notificationsEnabled": self.notifications_enabled, + } + json.dump(config, f) diff --git a/src/keyboard_controller/__init__.py b/src/keyboard_controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/keyboard_controller/hotkey_listener.py b/src/keyboard_controller/hotkey_listener.py new file mode 100644 index 0000000..be7c1b4 --- /dev/null +++ b/src/keyboard_controller/hotkey_listener.py @@ -0,0 +1,24 @@ +import threading +import time + +import keyboard + + +class HotkeyListener: + def __init__(self, main): + self.main = main + + def start_hotkey_listener_thread(self) -> None: + keyboard.stash_state() + with self.main.hotkey_lock: + self.main.listen_for_hotkey = True + if self.main.hotkey_thread and threading.current_thread() is not self.main.hotkey_thread and self.main.hotkey_thread.is_alive(): + self.main.hotkey_thread.join() + self.main.hotkey_thread = threading.Thread(target=self.hotkey_listener, daemon=True) + self.main.hotkey_thread.start() + + def hotkey_listener(self) -> None: + keyboard.add_hotkey(self.main.config.hotkey, self.main.send_hotkey_signal, suppress=False) + while self.main.listen_for_hotkey: + time.sleep(1) + keyboard.unhook_all_hotkeys() diff --git a/src/keyboard_controller/pressed_events_handler.py b/src/keyboard_controller/pressed_events_handler.py new file mode 100644 index 0000000..c2e8202 --- /dev/null +++ b/src/keyboard_controller/pressed_events_handler.py @@ -0,0 +1,17 @@ +import time + +import keyboard + + +def clear_pressed_events() -> None: + while True: + # Hotkeys stop working after windows locks & unlocks + # https://github.com/boppreh/keyboard/issues/223 + deleted = [] + with keyboard._pressed_events_lock: + for k in list(keyboard._pressed_events.keys()): + item = keyboard._pressed_events[k] + if time.time() - item.time > 2: + deleted.append(item.name) + del keyboard._pressed_events[k] + time.sleep(1) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..538bd10 --- /dev/null +++ b/src/main.py @@ -0,0 +1,80 @@ +import threading +import time +from queue import Queue + +import keyboard + +from src.config.config import Config +from src.keyboard_controller.hotkey_listener import HotkeyListener +from src.keyboard_controller.pressed_events_handler import clear_pressed_events +from src.os_controller.notifications import send_notification_in_thread +from src.os_controller.tray_icon import TrayIcon +from src.ui.overlay_window import OverlayWindow +from src.ui.update_window import UpdateWindow +from src.util.lockfile_handler import check_lockfile, remove_lockfile + + +class CatLockCore: + def __init__(self) -> None: + self.hotkey_thread = None + self.show_overlay_queue = Queue() + self.config = Config() + self.root = None + self.hotkey_lock = threading.Lock() + self.listen_for_hotkey = True + self.program_running = True + self.blocked_keys = set() + self.changing_hotkey_queue = Queue() + self.start_hotkey_listener() + self.clear_pressed_events_thread = threading.Thread(target=clear_pressed_events, daemon=True) + self.clear_pressed_events_thread.start() + self.tray_icon_thread = threading.Thread(target=self.create_tray_icon, daemon=True) + self.tray_icon_thread.start() + + def create_tray_icon(self) -> None: + TrayIcon(main=self).open() + + def start_hotkey_listener(self) -> None: + HotkeyListener(self).start_hotkey_listener_thread() + + def lock_keyboard(self) -> None: + self.blocked_keys.clear() + for i in range(150): + keyboard.block_key(i) + self.blocked_keys.add(i) + send_notification_in_thread(self.config.notifications_enabled) + + def unlock_keyboard(self, event=None) -> None: + for key in self.blocked_keys: + keyboard.unblock_key(key) + self.blocked_keys.clear() + if self.root: + self.root.destroy() + keyboard.stash_state() + + def send_hotkey_signal(self) -> None: + self.show_overlay_queue.put(True) + + def quit_program(self, icon, item) -> None: + remove_lockfile() + self.program_running = False + self.unlock_keyboard() + icon.stop() + + def start(self) -> None: + check_lockfile() + UpdateWindow(self).prompt_update() + # hack to prevent right ctrl sticking + keyboard.remap_key('right ctrl', 'left ctrl') + while self.program_running: + if not self.show_overlay_queue.empty(): + self.show_overlay_queue.get(block=False) + overlay = OverlayWindow(main=self) + keyboard.stash_state() + overlay.open() + time.sleep(.1) + + +if __name__ == "__main__": + core = CatLockCore() + core.start() diff --git a/src/os_controller/__init__.py b/src/os_controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/os_controller/notifications.py b/src/os_controller/notifications.py new file mode 100644 index 0000000..2679180 --- /dev/null +++ b/src/os_controller/notifications.py @@ -0,0 +1,26 @@ +import os +import threading +import time + +import plyer + +from src.util.path_util import get_packaged_path + + +def send_lock_notification() -> None: + path = os.path.join("resources", "img", "icon.ico") + plyer.notification.notify( + app_name="CatLock", + title="Keyboard Locked", + message="Click on screen to unlock", + app_icon=get_packaged_path(path), + timeout=3, + ) + time.sleep(.1) + + +def send_notification_in_thread(notifications_enabled: bool) -> None: + if notifications_enabled: + notification_thread = threading.Thread(target=send_lock_notification, daemon=True) + notification_thread.start() + notification_thread.join() diff --git a/src/os_controller/tray_icon.py b/src/os_controller/tray_icon.py new file mode 100644 index 0000000..fabc9e8 --- /dev/null +++ b/src/os_controller/tray_icon.py @@ -0,0 +1,50 @@ +import os + +from PIL import Image, ImageDraw +from pystray import Icon, Menu, MenuItem + +from src.util.path_util import get_packaged_path +from src.util.web_browser_util import open_about, open_buy_me_a_coffee, open_help + + +class TrayIcon: + def __init__(self, main): + self.main = main + + def set_opacity(self, opacity: float) -> None: + self.main.config.opacity = opacity + self.main.config.save() + + def toggle_notifications(self) -> None: + self.main.config.notifications_enabled = not self.main.config.notifications_enabled + self.main.config.save() + + def is_opacity_checked(self, opacity: float) -> bool: + return self.main.config.opacity == opacity + + def open(self) -> None: + path = os.path.join("resources", "img", "icon.png") + image = Image.open(get_packaged_path(path)) + draw = ImageDraw.Draw(image) + draw.rectangle((16, 16, 48, 48), fill="white") + menu = Menu( + MenuItem( + "Enable/Disable Notifications", + self.toggle_notifications, + checked=lambda item: self.main.config.notifications_enabled, + ), + MenuItem("Set Opacity", Menu( + MenuItem("5%", lambda: self.set_opacity(0.05), checked=lambda item: self.is_opacity_checked(0.05)), + MenuItem("10%", lambda: self.set_opacity(0.1), checked=lambda item: self.is_opacity_checked(0.1)), + MenuItem("30%", lambda: self.set_opacity(0.3), checked=lambda item: self.is_opacity_checked(0.3)), + MenuItem("50%", lambda: self.set_opacity(0.5), checked=lambda item: self.is_opacity_checked(0.5)), + MenuItem("70%", lambda: self.set_opacity(0.7), checked=lambda item: self.is_opacity_checked(0.7)), + MenuItem("90%", lambda: self.set_opacity(0.9), checked=lambda item: self.is_opacity_checked(0.9)), + )), + MenuItem("Help", open_help), + MenuItem("About", open_about), + MenuItem("Support ☕", open_buy_me_a_coffee), + MenuItem("Quit", self.main.quit_program), + ) + tray_icon = Icon("CatLock", image, "CatLock", menu) + tray_icon.run() diff --git a/src/ui/__init__.py b/src/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/overlay_window.py b/src/ui/overlay_window.py new file mode 100644 index 0000000..c615989 --- /dev/null +++ b/src/ui/overlay_window.py @@ -0,0 +1,27 @@ +import tkinter as tk + +from screeninfo import get_monitors + + +class OverlayWindow: + def __init__(self, main): + self.main = main + + def open(self) -> None: + monitors = get_monitors() + + # Calculate combined geometry of all monitors + total_width = sum([monitor.width for monitor in monitors]) + max_height = max([monitor.height for monitor in monitors]) + min_x = min([monitor.x for monitor in monitors]) + min_y = min([monitor.y for monitor in monitors]) + + self.main.root = tk.Tk() + self.main.root.overrideredirect(True) # Remove window decorations + self.main.root.geometry(f'{total_width}x{max_height}+{min_x}+{min_y}') + self.main.root.attributes('-topmost', True) + self.main.root.attributes('-alpha', self.main.config.opacity) + self.main.root.bind('', self.main.unlock_keyboard) + + self.main.lock_keyboard() + self.main.root.mainloop() \ No newline at end of file diff --git a/src/ui/update_window.py b/src/ui/update_window.py new file mode 100644 index 0000000..e8f46d8 --- /dev/null +++ b/src/ui/update_window.py @@ -0,0 +1,18 @@ +import tkinter as tk +from tkinter import messagebox + +from src.util.update_util import is_update_available +from src.util.web_browser_util import open_download + + +class UpdateWindow: + def __init__(self, main): + self.main = main + + def prompt_update(self): + if is_update_available(): + self.main.root = tk.Tk() + self.main.root.withdraw() + if messagebox.askyesno('Update Available', 'A new version of CatLock is available. Do you want to update?'): + open_download() + self.main.root.destroy() diff --git a/src/util/__init__.py b/src/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/util/lockfile_handler.py b/src/util/lockfile_handler.py new file mode 100644 index 0000000..e858539 --- /dev/null +++ b/src/util/lockfile_handler.py @@ -0,0 +1,24 @@ +import os +import signal +from pathlib import Path + +home = str(Path.home()) +LOCKFILE_PATH = os.path.join(home, '.catlock', 'lockfile.lock') + + +def check_lockfile(): + if os.path.exists(LOCKFILE_PATH): + with open(LOCKFILE_PATH, 'r') as f: + pid = int(f.read().strip()) + try: + os.kill(pid, signal.SIGTERM) # Kill the old process + except Exception as e: + # Process not found. It might have already been terminated. + pass + with open(LOCKFILE_PATH, 'w') as f: + f.write(str(os.getpid())) + + +def remove_lockfile(): + if os.path.exists(LOCKFILE_PATH): + os.remove(LOCKFILE_PATH) diff --git a/src/util/path_util.py b/src/util/path_util.py new file mode 100644 index 0000000..1d889be --- /dev/null +++ b/src/util/path_util.py @@ -0,0 +1,20 @@ +import os +import sys +from pathlib import Path + + +def get_packaged_path(path: str) -> str: + try: + wd = sys._MEIPASS + return os.path.abspath(os.path.join(wd, path)) + except: + base = Path(__file__).parent.parent.parent + return os.path.join(base, path) + + +def get_config_path() -> str: + home = str(Path.home()) + config_dir = os.path.join(home, '.catlock', 'config') + if not os.path.exists(config_dir): + os.makedirs(config_dir) + return os.path.join(config_dir, "config.json") diff --git a/src/util/update_util.py b/src/util/update_util.py new file mode 100644 index 0000000..2110e10 --- /dev/null +++ b/src/util/update_util.py @@ -0,0 +1,16 @@ +import requests + +VERSION = 'v1.0.0' + +LATEST_RELEASE_URL = 'https://api.github.com/repos/richiehowelll/cat-lock/releases/latest' + + +def is_update_available() -> bool: + response = requests.get(LATEST_RELEASE_URL) + + if response.status_code == 200: + release_data = response.json() + if release_data.get("name") != VERSION: + return True + + return False diff --git a/src/util/web_browser_util.py b/src/util/web_browser_util.py new file mode 100644 index 0000000..b9bdd3b --- /dev/null +++ b/src/util/web_browser_util.py @@ -0,0 +1,18 @@ +import webbrowser + + +def open_about(): + webbrowser.open("https://catlock.app/about/", new=2) + + +def open_buy_me_a_coffee(): + webbrowser.open("https://buymeacoffee.com/richiehowelll", new=2) + + +def open_help(): + webbrowser.open("https://catlock.app/faq/", new=2) + + +def open_download(): + webbrowser.open("https://catlock.app/download/", new=2) +