Add files via upload

This commit is contained in:
Evgeniy
2024-07-11 14:07:24 +03:00
committed by GitHub
parent dbae8426d4
commit d3ffd85f24
21 changed files with 361 additions and 0 deletions

View File

@@ -0,0 +1 @@
{"hotkey": "ctrl+l", "opacity": 0.3, "notificationsEnabled": false}

BIN
resources/img/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
resources/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

0
src/__init__.py Normal file
View File

0
src/config/__init__.py Normal file
View File

40
src/config/config.py Normal file
View File

@@ -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)

View File

View File

@@ -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()

View File

@@ -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)

80
src/main.py Normal file
View File

@@ -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()

View File

View File

@@ -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()

View File

@@ -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()

0
src/ui/__init__.py Normal file
View File

27
src/ui/overlay_window.py Normal file
View File

@@ -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('<Button-1>', self.main.unlock_keyboard)
self.main.lock_keyboard()
self.main.root.mainloop()

18
src/ui/update_window.py Normal file
View File

@@ -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()

0
src/util/__init__.py Normal file
View File

View File

@@ -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)

20
src/util/path_util.py Normal file
View File

@@ -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")

16
src/util/update_util.py Normal file
View File

@@ -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

View File

@@ -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)