├── src ├── _internal │ └── Data │ │ ├── icon.ico │ │ ├── Active │ │ └── AutoHotkey Interception │ │ │ ├── Lib │ │ │ ├── Unblocker.ps1 │ │ │ ├── x64 │ │ │ │ ├── interception.dll │ │ │ │ └── interception.lib │ │ │ ├── x86 │ │ │ │ ├── interception.dll │ │ │ │ └── interception.lib │ │ │ ├── AutoHotInterception.dll │ │ │ ├── CLR.ahk │ │ │ └── AutoHotInterception.ahk │ │ │ ├── AutoHotInterception.dll │ │ │ ├── find_device.ahk │ │ │ └── Monitor.ahk │ │ ├── icon │ │ ├── dark │ │ │ ├── exit.svg │ │ │ ├── plus.svg │ │ │ ├── next.svg │ │ │ ├── arrow.svg │ │ │ ├── prev.svg │ │ │ ├── run.svg │ │ │ ├── search.svg │ │ │ ├── import.svg │ │ │ ├── delete.svg │ │ │ ├── show_stored.svg │ │ │ ├── question.svg │ │ │ ├── show_stored_fill.svg │ │ │ ├── on_top.svg │ │ │ ├── store.svg │ │ │ ├── copy.svg │ │ │ ├── filter.svg │ │ │ ├── edit.svg │ │ │ ├── file_search.svg │ │ │ ├── on_top_fill.svg │ │ │ ├── thumbtack.svg │ │ │ ├── thumbtack_fill.svg │ │ │ ├── rocket.svg │ │ │ ├── rocket_fill.svg │ │ │ └── setting.svg │ │ └── light │ │ │ ├── exit.svg │ │ │ ├── plus.svg │ │ │ ├── arrow.svg │ │ │ ├── next.svg │ │ │ ├── prev.svg │ │ │ ├── run.svg │ │ │ ├── search.svg │ │ │ ├── import.svg │ │ │ ├── delete.svg │ │ │ ├── show_stored.svg │ │ │ ├── question.svg │ │ │ ├── show_stored_fill.svg │ │ │ ├── store.svg │ │ │ ├── on_top.svg │ │ │ ├── copy.svg │ │ │ ├── filter.svg │ │ │ ├── edit.svg │ │ │ ├── file_search.svg │ │ │ ├── on_top_fill.svg │ │ │ ├── thumbtack.svg │ │ │ ├── thumbtack_fill.svg │ │ │ ├── rocket.svg │ │ │ ├── rocket_fill.svg │ │ │ └── setting.svg │ │ ├── ahk_install.bat │ │ ├── inter_uninstall.bat │ │ └── inter_install.bat ├── utility │ ├── icon.py │ ├── utils.py │ └── constant.py ├── ui │ ├── edit_script │ │ ├── select_device.py │ │ ├── parse_script.py │ │ ├── edit_script_main.py │ │ ├── select_program.py │ │ ├── edit_script_logic.py │ │ └── choose_key.py │ ├── welcome.py │ └── setting.py └── logic │ └── logic.py ├── .github ├── Preview │ ├── main dark.png │ ├── text mode.png │ ├── KeyTik Icon.png │ ├── default mode.png │ ├── key format.gif │ ├── main light.png │ ├── Pro │ │ ├── slideshow.gif │ │ ├── auto clicker.png │ │ ├── files opener.png │ │ ├── window manager.png │ │ ├── multi copy paste.png │ │ └── always on top manager.png │ ├── choosing key1.png │ ├── choosing key2.png │ ├── select device.png │ └── select program.png ├── ISSUE_TEMPLATE │ ├── question.md │ ├── windows-warning-report.md │ ├── supported-key-suggestion.md │ ├── automation-tool-suggestion.md │ ├── feature_suggestion.md │ └── bug-report.md ├── workflows │ └── sponsors.yml └── FUNDING.yml ├── requirements.txt ├── .gitignore ├── CONTRIBUTING.md ├── SECURITY.md ├── LICENSE └── README.md /src/_internal/Data/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/src/_internal/Data/icon.ico -------------------------------------------------------------------------------- /.github/Preview/main dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/main dark.png -------------------------------------------------------------------------------- /.github/Preview/text mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/text mode.png -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/Lib/Unblocker.ps1: -------------------------------------------------------------------------------- 1 | Get-ChildItem -Path '.' -Recurse | Unblock-File -------------------------------------------------------------------------------- /.github/Preview/KeyTik Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/KeyTik Icon.png -------------------------------------------------------------------------------- /.github/Preview/default mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/default mode.png -------------------------------------------------------------------------------- /.github/Preview/key format.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/key format.gif -------------------------------------------------------------------------------- /.github/Preview/main light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/main light.png -------------------------------------------------------------------------------- /.github/Preview/Pro/slideshow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/Pro/slideshow.gif -------------------------------------------------------------------------------- /.github/Preview/choosing key1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/choosing key1.png -------------------------------------------------------------------------------- /.github/Preview/choosing key2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/choosing key2.png -------------------------------------------------------------------------------- /.github/Preview/select device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/select device.png -------------------------------------------------------------------------------- /.github/Preview/select program.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/select program.png -------------------------------------------------------------------------------- /.github/Preview/Pro/auto clicker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/Pro/auto clicker.png -------------------------------------------------------------------------------- /.github/Preview/Pro/files opener.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/Pro/files opener.png -------------------------------------------------------------------------------- /.github/Preview/Pro/window manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/Pro/window manager.png -------------------------------------------------------------------------------- /.github/Preview/Pro/multi copy paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/Pro/multi copy paste.png -------------------------------------------------------------------------------- /.github/Preview/Pro/always on top manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/.github/Preview/Pro/always on top manager.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keyboard 2 | markdown 3 | psutil 4 | pynput 5 | PySide6 6 | pywin32; platform_system == "Windows" 7 | urllib3==2.6.0 8 | requests 9 | winshell 10 | 11 | -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/AutoHotInterception.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/src/_internal/Data/Active/AutoHotkey Interception/AutoHotInterception.dll -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/Lib/x64/interception.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/src/_internal/Data/Active/AutoHotkey Interception/Lib/x64/interception.dll -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/Lib/x64/interception.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/src/_internal/Data/Active/AutoHotkey Interception/Lib/x64/interception.lib -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/Lib/x86/interception.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/src/_internal/Data/Active/AutoHotkey Interception/Lib/x86/interception.dll -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/Lib/x86/interception.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/src/_internal/Data/Active/AutoHotkey Interception/Lib/x86/interception.lib -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/Lib/AutoHotInterception.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fajar-RahmadJaya/KeyTik/HEAD/src/_internal/Data/Active/AutoHotkey Interception/Lib/AutoHotInterception.dll -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/exit.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/exit.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/plus.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/plus.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/next.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/arrow.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/prev.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/run.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/search.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/arrow.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/next.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/prev.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/run.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/search.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask any question about KeyTik 4 | title: Question 5 | labels: Question 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Question 11 | If you have any question about KeyTik, don't hesitate to ask it. We will answer it as much as we can. 12 | 13 | ## Template 14 | * Specify your question. 15 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/import.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/import.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/delete.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/show_stored.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/delete.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/show_stored.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/question.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/question.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/show_stored_fill.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/show_stored_fill.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/on_top.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/store.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/store.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/on_top.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python related 2 | __pycache__/ 3 | *.py[cod] 4 | *.so 5 | .Python 6 | env/ 7 | venv/ 8 | .env/ 9 | .venv/ 10 | build/ 11 | dist/ 12 | eggs/ 13 | *.egg-info/ 14 | 15 | # IDE specific files 16 | .idea/ 17 | .vscode/ 18 | *.swp 19 | *.swo 20 | .vs/ 21 | 22 | # Operating System Files 23 | .DS_Store 24 | Thumbs.db 25 | 26 | # Files 27 | exit_keys.json 28 | dont_show.json 29 | build.bat 30 | 31 | # Folder 32 | Active/ 33 | backup/ 34 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/copy.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/filter.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/copy.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/filter.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/edit.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/edit.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/file_search.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/file_search.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/on_top_fill.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/on_top_fill.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/windows-warning-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Windows Warning Report 3 | about: Report any Windows warnings, such as untrusted author notifications or false 4 | positives 5 | title: Windows Warning Report 6 | labels: Windows Warning Report 7 | assignees: '' 8 | 9 | --- 10 | 11 | # Windows Warning Report 12 | Report windows warning such as untrusted author or false positive so we can apply warning removal to Windows. 13 | 14 | ## Template 15 | * If possible, include warning screenshots. 16 | * Specify your device (Example: Windows 11). 17 | * Specify KeyTik version you use. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/supported-key-suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Supported Key Suggestion 3 | about: Suggest a key to include in the list if you find some keys aren’t working 4 | title: Supported Key Suggestion 5 | labels: Supported Key Suggestion 6 | assignees: '' 7 | 8 | --- 9 | 10 | #Key Request 11 | If you find key that can not work you can request to add it in key list. If it's possible we can add it to made that key work. 12 | 13 | ## Template 14 | * Specify Key [Example, Mouse Button X]. 15 | * Specify Device Used By Key [Example, Mouse]. 16 | * If possible, specify AutoHotkey error using screenshots. 17 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/thumbtack.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/thumbtack.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/automation-tool-suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Automation Tool Suggestion 3 | about: " Suggest additions to KeyTik’s built-in automation tools" 4 | title: Automation Tool Suggestion 5 | labels: Automation Tool Suggestion, Supported Key Suggestion 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Automation Tool Suggestion 11 | Suggest KeyTik automation tool to include in the download. If possible, we will add it or give you alternative or solution. 12 | 13 | ## Template 14 | * Specify your automation tool suggestion. 15 | * Specify how is your automation tool work. 16 | * If there any, specify some program or software that can do the same. 17 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/thumbtack_fill.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/thumbtack_fill.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/ahk_install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | set "AHK_URL=https://www.autohotkey.com/download/ahk-v2.exe" 5 | set "AHK_EXE=%TEMP%\ahk-v2_%RANDOM%.exe" 6 | 7 | echo Downloading AutoHotkey v2 from %AHK_URL% ... 8 | powershell -Command "try { Invoke-WebRequest -Uri '%AHK_URL%' -OutFile '%AHK_EXE%' -UseBasicParsing; Write-Host 'Download completed.' } catch { Write-Host 'Download failed.'; exit 1 }" 9 | 10 | if not exist "%AHK_EXE%" ( 11 | echo Download failed! 12 | pause 13 | exit /b 1 14 | ) 15 | 16 | echo Running AutoHotkey installer: %AHK_EXE% 17 | start /wait "" "%AHK_EXE%" 18 | 19 | echo. 20 | del /f /q "%AHK_EXE%" 21 | 22 | echo Done. 23 | pause 24 | 25 | endlocal 26 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/rocket.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/rocket.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/rocket_fill.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/rocket_fill.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Suggestion 3 | about: Suggest an idea for a new feature or improvement for the project 4 | title: Feature Suggestion 5 | labels: Feature Suggestion 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report any bugs or issues you’ve encountered to help us improve 4 | title: Bug Report 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Version (please complete the following information):** 27 | - Version [e.g. 22] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/workflows/sponsors.yml: -------------------------------------------------------------------------------- 1 | name: Generate Sponsors README 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: 30 15 * * 0-6 6 | permissions: 7 | contents: write 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 🛎️ 13 | uses: actions/checkout@v2 14 | 15 | - name: Generate Sponsors 💖 16 | uses: JamesIves/github-sponsors-readme-action@v1 17 | with: 18 | token: ${{ secrets.TOK }} 19 | file: 'README.md' 20 | template: '* [{{ name }}]({{ url }}) - {{ avatarUrl }}}' 21 | 22 | # ⚠️ Note: You can use any deployment step here to automatically push the README 23 | # changes back to your branch. 24 | - name: Deploy to GitHub Pages 🚀 25 | uses: JamesIves/github-pages-deploy-action@v4 26 | with: 27 | branch: main 28 | folder: '.' 29 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Fajar-RahmadJaya 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/dark/setting.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/_internal/Data/icon/light/setting.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to KeyTik 2 | 3 | Pull requests are welcome! 4 | We welcome contributions of all kinds, including bug fixes, features, improvements, documentation improvement and more. 5 | 6 | ## Setting up Workplace 7 | 1. Download and install Python on your machine. 8 | 2. Clone the repository. 9 | 4. Install requirements with ```pip install -r requirements.txt``` 10 | 11 | ## Folder Structures 12 | All code is stored inside the src folder. 13 | ``` 14 | src/ ; Source Floder 15 | ├── _internal/ ; Data Folder 16 | ├── logic/ 17 | └── logic.py 18 | ├── ui/ 19 | ├── edit_script/ 20 | ├── choose_key.py 21 | ├── edit_frame_row.py 22 | ├── edit_script_logic.py 23 | ├── edit_script_main.py 24 | ├── parse_script.py 25 | ├── select_device.py 26 | ├── select_program.py 27 | └── write_script.py 28 | ├── setting.py 29 | └── welcome.py 30 | ├── utility/ 31 | ├── constant.py 32 | ├── icon.py 33 | └── utils.py 34 | └── main.py ; Initialization Code 35 | ``` 36 | 37 | ## License 38 | By contributing, you agree that your contributions will be licensed under the terms of the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). 39 | -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/find_device.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance force 2 | Persistent 3 | #include Lib\AutoHotInterception.ahk ; Include your AutoHotInterception library 4 | 5 | ; Initialize AutoHotInterception library 6 | AHI := AutoHotInterception() 7 | 8 | ; Define the file path (no folder, same directory as the script) 9 | filePath := A_ScriptDir "\shared_device_info.txt" 10 | 11 | ; Clear the contents of the file before writing new data (if it exists) 12 | if FileExist(filePath) 13 | FileDelete(filePath) 14 | 15 | ; Get the list of devices 16 | DeviceList := AHI.GetDeviceList() 17 | 18 | ; Loop through each device in the DeviceList 19 | for index, device in DeviceList { 20 | ; Check if the device is a keyboard or a mouse 21 | if (device.isMouse || !device.isMouse) { ; Modify this condition if you need specific filtering 22 | ; Prepare the device information string 23 | devInfo := "Device ID: " device.Id "`n" 24 | devInfo .= "VID: 0x" Format("{:04X}", device.Vid) "`n" 25 | devInfo .= "PID: 0x" Format("{:04X}", device.Pid) "`n" 26 | devInfo .= "Handle: " device.Handle "`n" 27 | devInfo .= "Is Mouse: " (device.isMouse ? "Yes" : "No") "`n`n" ; Added isMouse information 28 | 29 | ; Append the device information to the file 30 | FileAppend(devInfo, filePath) 31 | } 32 | } 33 | 34 | ExitApp 35 | -------------------------------------------------------------------------------- /src/_internal/Data/inter_uninstall.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | :: Ensure the script is running as Administrator 3 | set "downloadURL=https://github.com/oblitum/Interception/releases/download/v1.0.1/Interception.zip" 4 | set "zipFile=Interception.zip" 5 | set "extractFolder=Interception" 6 | set "installerExe=install-interception.exe" 7 | 8 | :: Check for Administrator Privileges 9 | net session >nul 2>&1 10 | if %errorLevel% NEQ 0 ( 11 | echo This script requires Administrator privileges. Please run as Administrator. 12 | pause 13 | exit /b 14 | ) 15 | 16 | :: Step 1: Download the Interception.zip file 17 | echo Downloading Interception.zip... 18 | powershell -Command "Invoke-WebRequest -Uri %downloadURL% -OutFile %zipFile%" 19 | if %errorLevel% NEQ 0 ( 20 | echo Failed to download the file. 21 | pause 22 | exit /b 23 | ) 24 | 25 | :: Step 2: Extract the downloaded zip file 26 | echo Extracting Interception.zip... 27 | powershell -Command "Expand-Archive -Path %zipFile% -DestinationPath . -Force" 28 | if %errorLevel% NEQ 0 ( 29 | echo Failed to extract the file. 30 | pause 31 | exit /b 32 | ) 33 | 34 | :: Step 3: Find the installer folder dynamically 35 | echo Locating installer folder... 36 | set "installerPath=" 37 | for /d %%d in (%extractFolder%\*) do ( 38 | if exist "%%d\%installerExe%" ( 39 | set "installerPath=%%d" 40 | ) 41 | ) 42 | 43 | if "%installerPath%"=="" ( 44 | echo Installer folder not found. Please check the extracted files. 45 | pause 46 | exit /b 47 | ) 48 | 49 | :: Step 4: Install Interception 50 | cd "%installerPath%" 51 | echo Installing Interception driver... 52 | "%installerExe%" /uninstall 53 | if %errorLevel% NEQ 0 ( 54 | echo Failed to install the Interception driver. 55 | pause 56 | exit /b 57 | ) 58 | 59 | :: Step 5: Clean up downloaded and extracted files 60 | cd ../.. 61 | rmdir /s /q "%extractFolder%" 62 | del /q %zipFile% 63 | 64 | echo Uninstall completed successfully! 65 | -------------------------------------------------------------------------------- /src/utility/icon.py: -------------------------------------------------------------------------------- 1 | import os 2 | from utility.utils import theme 3 | from utility.constant import script_dir 4 | from PySide6.QtGui import QIcon 5 | 6 | icon_cache = {} 7 | 8 | 9 | def get_icon(path): 10 | if path not in icon_cache: 11 | icon_cache[path] = QIcon(path) 12 | return icon_cache[path] 13 | 14 | 15 | icon_dir = os.path.join(script_dir, '_internal', 'Data', 'icon') 16 | if theme == "dark": 17 | icon_dir = os.path.join(script_dir, '_internal', 'Data', 'icon', 'dark') 18 | else: 19 | icon_dir = os.path.join(script_dir, '_internal', 'Data', 'icon', 'light') 20 | 21 | # Profile Icon 22 | icon_run = os.path.join(icon_dir, "run.svg") 23 | icon_exit = os.path.join(icon_dir, "exit.svg") 24 | icon_edit = os.path.join(icon_dir, "edit.svg") 25 | icon_rocket = os.path.join(icon_dir, "rocket.svg") 26 | icon_rocket_fill = os.path.join(icon_dir, "rocket_fill.svg") 27 | icon_copy = os.path.join(icon_dir, "copy.svg") 28 | icon_store = os.path.join(icon_dir, "store.svg") 29 | icon_delete = os.path.join(icon_dir, "delete.svg") 30 | icon_pin = os.path.join(icon_dir, "thumbtack.svg") 31 | icon_pin_fill = os.path.join(icon_dir, "thumbtack_fill.svg") 32 | 33 | # Main Window Icon 34 | icon_plus = os.path.join(icon_dir, "plus.svg") 35 | icon_next = os.path.join(icon_dir, "next.svg") 36 | icon_prev = os.path.join(icon_dir, "prev.svg") 37 | icon_setting = os.path.join(icon_dir, "setting.svg") 38 | icon_import = os.path.join(icon_dir, "import.svg") 39 | icon_on_top = os.path.join(icon_dir, "on_top.svg") 40 | icon_on_top_fill = os.path.join(icon_dir, "on_top_fill.svg") 41 | icon_show_stored = os.path.join(icon_dir, "show_stored.svg") 42 | icon_show_stored_fill = os.path.join(icon_dir, "show_stored_fill.svg") 43 | 44 | # Edit Window Icon 45 | icon_arrow = os.path.join(icon_dir, "arrow.svg") 46 | icon_filter = os.path.join(icon_dir, "filter.svg") 47 | icon_search = os.path.join(icon_dir, "search.svg") 48 | icon_file_search = os.path.join(icon_dir, "file_search.svg") 49 | icon_question = os.path.join(icon_dir, "question.svg") 50 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you believe you have discovered a security vulnerability in this project, please **open an issue** in this repository. We encourage you to provide as much detail as possible so that we can assess and address the problem quickly. 6 | 7 | **Steps to report a security vulnerability:** 8 | 1. **Create a new issue** in the repository using the "Security" label (we recommend creating a new issue instead of using existing ones). 9 | 2. **Do not share sensitive information** publicly in the issue. If needed, you can describe the issue with enough detail without sharing the exact exploit or sensitive data. 10 | 3. In the issue description, include: 11 | - A brief description of the vulnerability. 12 | - Steps to reproduce the issue. 13 | - Any possible solutions or mitigation steps you suggest. 14 | - If possible, share the version of the software you are using. 15 | 16 | Please ensure that the issue is marked as a "Security" issue so we can prioritize and address it promptly. 17 | 18 | ## Responsible Disclosure 19 | 20 | We take security vulnerabilities seriously and follow a responsible disclosure process: 21 | - We will investigate the vulnerability and provide feedback as soon as possible. 22 | - We will notify the reporter and discuss potential fixes privately. 23 | - After addressing the issue, we will release an update, patch, or workaround and notify users accordingly. 24 | 25 | ## Supported Versions 26 | 27 | We commit to providing security updates and patches for **Latest Version** of this project. This ensures that security vulnerabilities are addressed for current or future version. 28 | 29 | - For any major, minor, or patch versions of the project, security updates will be made available to fix reported vulnerabilities. 30 | 31 | To receive the latest security fixes, we strongly encourage users to upgrade to the latest stable version of the software. 32 | 33 | ## Security Best Practices 34 | 35 | We encourage all users and contributors to: 36 | - Regularly update dependencies and software. 37 | - Use secure protocols for communication (e.g., HTTPS). 38 | - Implement strong authentication and authorization methods. 39 | - Follow security guidelines and the [OWASP Top Ten](https://owasp.org/www-project-top-ten/) to minimize risks. 40 | -------------------------------------------------------------------------------- /src/_internal/Data/inter_install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | :: Ensure the script is running as Administrator 3 | set "downloadURL=https://github.com/oblitum/Interception/releases/download/v1.0.1/Interception.zip" 4 | set "zipFile=Interception.zip" 5 | set "extractFolder=Interception" 6 | set "installerExe=install-interception.exe" 7 | 8 | :: Check for Administrator Privileges 9 | net session >nul 2>&1 10 | if %errorLevel% NEQ 0 ( 11 | echo This script requires Administrator privileges. Please run as Administrator. 12 | pause 13 | exit /b 14 | ) 15 | 16 | :: Step 1: Download the Interception.zip file 17 | echo Downloading Interception.zip... 18 | powershell -Command "Invoke-WebRequest -Uri %downloadURL% -OutFile %zipFile%" 19 | if %errorLevel% NEQ 0 ( 20 | echo Failed to download the file. 21 | pause 22 | exit /b 23 | ) 24 | 25 | :: Step 2: Extract the downloaded zip file 26 | echo Extracting Interception.zip... 27 | powershell -Command "Expand-Archive -Path %zipFile% -DestinationPath . -Force" 28 | if %errorLevel% NEQ 0 ( 29 | echo Failed to extract the file. 30 | pause 31 | exit /b 32 | ) 33 | 34 | :: Step 3: Find the installer folder dynamically 35 | echo Locating installer folder... 36 | set "installerPath=" 37 | for /d %%d in (%extractFolder%\*) do ( 38 | if exist "%%d\%installerExe%" ( 39 | set "installerPath=%%d" 40 | ) 41 | ) 42 | 43 | if "%installerPath%"=="" ( 44 | echo Installer folder not found. Please check the extracted files. 45 | pause 46 | exit /b 47 | ) 48 | 49 | :: Step 4: Install Interception 50 | cd "%installerPath%" 51 | echo Installing Interception driver... 52 | "%installerExe%" /install 53 | if %errorLevel% NEQ 0 ( 54 | echo Failed to install the Interception driver. 55 | pause 56 | exit /b 57 | ) 58 | 59 | :: Step 5: Unblock interception.dll using Unblocker.ps1 60 | cd /d "%~dp0" 61 | echo Unblocking interception.dll... 62 | powershell -Command "Start-Process powershell -ArgumentList '-ExecutionPolicy Bypass -File \"Active\AutoHotkey Interception\Lib\Unblocker.ps1\"' -Verb RunAs -Wait" 63 | if %errorLevel% NEQ 0 ( 64 | echo Failed to run Unblocker.ps1. 65 | pause 66 | exit /b 67 | ) 68 | 69 | :: Step 6: Clean up downloaded and extracted files 70 | cd /d "%~dp0" 71 | rmdir /s /q "%extractFolder%" 72 | del /q %zipFile% 73 | 74 | echo Installation completed successfully! 75 | -------------------------------------------------------------------------------- /src/ui/edit_script/select_device.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from PySide6.QtWidgets import ( 4 | QDialog, QPushButton, 5 | QVBoxLayout, QHBoxLayout, QMessageBox, QTreeWidget, QTreeWidgetItem 6 | ) 7 | from PySide6.QtCore import Qt 8 | from PySide6.QtGui import QIcon 9 | from utility.constant import (icon_path) 10 | from utility.utils import (device_list_path, device_finder_path) 11 | 12 | 13 | class SelectDevice: 14 | def select_device(self, tree, entry, window): 15 | selected_items = tree.selectedItems() 16 | if selected_items: 17 | device = [selected_items[0].text(i) 18 | for i in range(tree.columnCount())] 19 | device_type = device[0] 20 | vid_pid = device[3] 21 | 22 | entry.setText(f"{device_type}, {vid_pid}") 23 | 24 | window.accept() 25 | 26 | def open_device_selection(self): 27 | if not self.check_interception_driver(): 28 | return 29 | self.device_selection_window = None 30 | 31 | if (self.device_selection_window 32 | and self.device_selection_window.isVisible()): 33 | self.device_selection_window.raise_() 34 | return 35 | 36 | parent_window = (self.create_profile_window 37 | if self.create_profile_window 38 | and self.create_profile_window.isVisible() 39 | else self.edit_window) 40 | if not parent_window or not parent_window.isVisible(): 41 | QMessageBox.critical(self, "Error", "Parent window no longer exists.") # noqa 42 | return 43 | 44 | self.device_selection_window = QDialog(parent_window) 45 | self.device_selection_window.setWindowTitle("Select Device") 46 | self.device_selection_window.setWindowIcon(QIcon(icon_path)) 47 | self.device_selection_window.setFixedSize(600, 300) 48 | self.device_selection_window.setModal(True) 49 | self.device_selection_window.setAttribute( 50 | Qt.WidgetAttribute.WA_DeleteOnClose) 51 | 52 | main_layout = QVBoxLayout(self.device_selection_window) 53 | 54 | self.device_tree = QTreeWidget(self.device_selection_window) 55 | self.device_tree.setHeaderLabels( 56 | ["Device Type", "VID", "PID", "Handle"]) 57 | main_layout.addWidget(self.device_tree) 58 | 59 | button_layout = QHBoxLayout() 60 | main_layout.addLayout(button_layout) 61 | 62 | select_button = QPushButton("Select", self.device_selection_window) 63 | select_button.clicked.connect( 64 | lambda: self.select_device( 65 | self.device_tree, 66 | self.keyboard_entry, 67 | self.device_selection_window)) 68 | button_layout.addWidget(select_button) 69 | 70 | monitor_button = QPushButton( 71 | "Open AHI Monitor To Test Device", self.device_selection_window) 72 | monitor_button.clicked.connect(self.run_monitor) 73 | button_layout.addWidget(monitor_button) 74 | 75 | refresh_button = QPushButton("Refresh", self.device_selection_window) 76 | refresh_button.clicked.connect(lambda: self.update_treeview( 77 | self.refresh_device_list(device_list_path), self.device_tree)) 78 | button_layout.addWidget(refresh_button) 79 | 80 | devices = self.refresh_device_list(device_list_path) 81 | self.update_treeview(devices, self.device_tree) 82 | 83 | self.device_selection_window.exec() 84 | 85 | def update_treeview(self, devices, tree): 86 | tree.clear() 87 | 88 | for device in devices: 89 | if (device.get('VID') 90 | and device.get('PID') 91 | and device.get('Handle')): 92 | 93 | device_type = ("Mouse" 94 | if device['Is Mouse'] == "Yes" 95 | else "Keyboard") 96 | item = QTreeWidgetItem( 97 | [device_type, device['VID'], 98 | device['PID'], device['Handle']]) 99 | tree.addTopLevelItem(item) 100 | 101 | def refresh_device_list(self, file_path): 102 | os.startfile(device_finder_path) 103 | time.sleep(1) 104 | devices = self.parse_device_info(file_path) 105 | return devices 106 | -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/Lib/CLR.ahk: -------------------------------------------------------------------------------- 1 | ; ========================================================== 2 | ; .NET Framework Interop 3 | ; https://www.autohotkey.com/boards/viewtopic.php?t=4633 4 | ; ========================================================== 5 | ; 6 | ; Author: Lexikos 7 | ; Version: 2.0 8 | ; Requires: AutoHotkey v2.0-beta.1 9 | ; 10 | 11 | CLR_LoadLibrary(AssemblyName, AppDomain:=0) 12 | { 13 | if !AppDomain 14 | AppDomain := CLR_GetDefaultDomain() 15 | try 16 | return AppDomain.Load_2(AssemblyName) 17 | static null := ComValue(13,0) 18 | args := ComObjArray(0xC, 1), args[0] := AssemblyName 19 | typeofAssembly := AppDomain.GetType().Assembly.GetType() 20 | try 21 | return typeofAssembly.InvokeMember_3("LoadWithPartialName", 0x158, null, null, args) 22 | catch 23 | return typeofAssembly.InvokeMember_3("LoadFrom", 0x158, null, null, args) 24 | } 25 | 26 | CLR_CreateObject(Assembly, TypeName, Args*) 27 | { 28 | if !(argCount := Args.Length) 29 | return Assembly.CreateInstance_2(TypeName, true) 30 | 31 | vargs := ComObjArray(0xC, argCount) 32 | Loop argCount 33 | vargs[A_Index-1] := Args[A_Index] 34 | 35 | static Array_Empty := ComObjArray(0xC,0), null := ComValue(13,0) 36 | 37 | return Assembly.CreateInstance_3(TypeName, true, 0, null, vargs, null, Array_Empty) 38 | } 39 | 40 | CLR_CompileCS(Code, References:="", AppDomain:=0, FileName:="", CompilerOptions:="") 41 | { 42 | return CLR_CompileAssembly(Code, References, "System", "Microsoft.CSharp.CSharpCodeProvider", AppDomain, FileName, CompilerOptions) 43 | } 44 | 45 | CLR_CompileVB(Code, References:="", AppDomain:=0, FileName:="", CompilerOptions:="") 46 | { 47 | return CLR_CompileAssembly(Code, References, "System", "Microsoft.VisualBasic.VBCodeProvider", AppDomain, FileName, CompilerOptions) 48 | } 49 | 50 | CLR_StartDomain(&AppDomain, BaseDirectory:="") 51 | { 52 | static null := ComValue(13,0) 53 | args := ComObjArray(0xC, 5), args[0] := "", args[2] := BaseDirectory, args[4] := ComValue(0xB,false) 54 | AppDomain := CLR_GetDefaultDomain().GetType().InvokeMember_3("CreateDomain", 0x158, null, null, args) 55 | } 56 | 57 | ; ICorRuntimeHost::UnloadDomain 58 | CLR_StopDomain(AppDomain) => ComCall(20, CLR_Start(), "ptr", ComObjValue(AppDomain)) 59 | 60 | ; NOTE: IT IS NOT NECESSARY TO CALL THIS FUNCTION unless you need to load a specific version. 61 | CLR_Start(Version:="") ; returns ICorRuntimeHost* 62 | { 63 | static RtHst := 0 64 | ; The simple method gives no control over versioning, and seems to load .NET v2 even when v4 is present: 65 | ; return RtHst ? RtHst : (RtHst:=COM_CreateObject("CLRMetaData.CorRuntimeHost","{CB2F6722-AB3A-11D2-9C40-00C04FA30A3E}"), DllCall(NumGet(NumGet(RtHst+0)+40),"uint",RtHst)) 66 | if RtHst 67 | return RtHst 68 | if Version = "" 69 | Loop Files EnvGet("SystemRoot") "\Microsoft.NET\Framework" (A_PtrSize=8?"64":"") "\*","D" 70 | if (FileExist(A_LoopFilePath "\mscorlib.dll") && StrCompare(A_LoopFileName, Version) > 0) 71 | Version := A_LoopFileName 72 | static CLSID_CorRuntimeHost := CLR_GUID("{CB2F6723-AB3A-11D2-9C40-00C04FA30A3E}") 73 | static IID_ICorRuntimeHost := CLR_GUID("{CB2F6722-AB3A-11D2-9C40-00C04FA30A3E}") 74 | DllCall("mscoree\CorBindToRuntimeEx", "wstr", Version, "ptr", 0, "uint", 0 75 | , "ptr", CLSID_CorRuntimeHost, "ptr", IID_ICorRuntimeHost 76 | , "ptr*", &RtHst:=0, "hresult") 77 | ComCall(10, RtHst) ; Start 78 | return RtHst 79 | } 80 | 81 | ; 82 | ; INTERNAL FUNCTIONS 83 | ; 84 | 85 | CLR_GetDefaultDomain() 86 | { 87 | ; ICorRuntimeHost::GetDefaultDomain 88 | static defaultDomain := ( 89 | ComCall(13, CLR_Start(), "ptr*", &p:=0), 90 | ComObjFromPtr(p) 91 | ) 92 | return defaultDomain 93 | } 94 | 95 | CLR_CompileAssembly(Code, References, ProviderAssembly, ProviderType, AppDomain:=0, FileName:="", CompilerOptions:="") 96 | { 97 | if !AppDomain 98 | AppDomain := CLR_GetDefaultDomain() 99 | 100 | asmProvider := CLR_LoadLibrary(ProviderAssembly, AppDomain) 101 | codeProvider := asmProvider.CreateInstance(ProviderType) 102 | codeCompiler := codeProvider.CreateCompiler() 103 | 104 | asmSystem := (ProviderAssembly="System") ? asmProvider : CLR_LoadLibrary("System", AppDomain) 105 | 106 | ; Convert | delimited list of references into an array. 107 | Refs := References is String ? StrSplit(References, "|", " `t") : References 108 | aRefs := ComObjArray(8, Refs.Length) 109 | Loop Refs.Length 110 | aRefs[A_Index-1] := Refs[A_Index] 111 | 112 | ; Set parameters for compiler. 113 | prms := CLR_CreateObject(asmSystem, "System.CodeDom.Compiler.CompilerParameters", aRefs) 114 | , prms.OutputAssembly := FileName 115 | , prms.GenerateInMemory := FileName="" 116 | , prms.GenerateExecutable := SubStr(FileName,-4)=".exe" 117 | , prms.CompilerOptions := CompilerOptions 118 | , prms.IncludeDebugInformation := true 119 | 120 | ; Compile! 121 | compilerRes := codeCompiler.CompileAssemblyFromSource(prms, Code) 122 | 123 | if error_count := (errors := compilerRes.Errors).Count 124 | { 125 | error_text := "" 126 | Loop error_count 127 | error_text .= ((e := errors.Item[A_Index-1]).IsWarning ? "Warning " : "Error ") . e.ErrorNumber " on line " e.Line ": " e.ErrorText "`n`n" 128 | throw Error("Compilation failed",, "`n" error_text) 129 | } 130 | ; Success. Return Assembly object or path. 131 | return FileName="" ? compilerRes.CompiledAssembly : compilerRes.PathToAssembly 132 | } 133 | 134 | ; Usage 1: pGUID := CLR_GUID(&GUID, "{...}") 135 | ; Usage 2: GUID := CLR_GUID("{...}"), pGUID := GUID.Ptr 136 | CLR_GUID(a, b:=unset) 137 | { 138 | DllCall("ole32\IIDFromString" 139 | , "wstr", sGUID := IsSet(b) ? b : a 140 | , "ptr", GUID := Buffer(16,0), "hresult") 141 | return IsSet(b) ? GUID.Ptr : GUID 142 | } 143 | -------------------------------------------------------------------------------- /src/utility/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import tempfile 4 | import sys 5 | import winreg 6 | from utility.constant import (appdata_dir, pinned_file, condition_path, 7 | theme_path) 8 | 9 | 10 | def load_condition(): 11 | try: 12 | if os.path.exists(condition_path): 13 | with open(condition_path, "r") as f: 14 | content = f.read().strip() 15 | if content: 16 | data = json.loads(content) 17 | if isinstance(data, dict) and "path" in data: 18 | return data["path"] 19 | else: 20 | print("Condition file is empty. Returning None.") 21 | except json.JSONDecodeError: 22 | print("Error: Condition file is not in valid JSON format. Resetting condition.") # noqa 23 | except Exception as e: 24 | print(f"An error occurred while loading condition: {e}") 25 | return None 26 | 27 | 28 | path_from_condition = load_condition() 29 | 30 | 31 | if path_from_condition: 32 | active_dir = os.path.join(path_from_condition, 'Active') 33 | store_dir = os.path.join(path_from_condition, 'Store') 34 | else: 35 | 36 | active_dir = os.path.join(appdata_dir, 'Active') 37 | store_dir = os.path.join(appdata_dir, 'Store') 38 | 39 | 40 | if not os.path.exists(active_dir): 41 | os.makedirs(active_dir) 42 | 43 | if not os.path.exists(store_dir): 44 | os.makedirs(store_dir) 45 | 46 | 47 | SCRIPT_DIR = active_dir 48 | 49 | 50 | def load_pinned_profiles(): 51 | try: 52 | if os.path.exists(pinned_file): 53 | with open(pinned_file, "r") as f: 54 | content = f.read().strip() 55 | if content: 56 | data = json.loads(content) 57 | if isinstance(data, list): 58 | return data 59 | else: 60 | print("Pinned profiles file is empty. Returning an empty list.") # noqa 61 | except json.JSONDecodeError: 62 | print("Error: Pinned profiles file is not in valid JSON format. Resetting pinned profiles.") # noqa 63 | except Exception as e: 64 | print(f"An error occurred while loading pinned profiles: {e}") 65 | return [] 66 | 67 | 68 | def save_pinned_profiles(pinned_profiles): 69 | with open(pinned_file, "w") as f: 70 | json.dump(pinned_profiles, f) 71 | 72 | 73 | if not os.path.exists(appdata_dir): 74 | os.makedirs(appdata_dir) 75 | 76 | 77 | if not os.path.exists(condition_path): 78 | with open(condition_path, "w") as f: 79 | json.dump({"path": ""}, f) 80 | 81 | 82 | if not os.path.exists(pinned_file): 83 | with open(pinned_file, "w") as f: 84 | json.dump([ 85 | "Multiple Files Opener.ahk", 86 | "Take Coordinate And Copy It For Screen Clicker.ahk", 87 | "Screen Clicker.ahk", 88 | "Auto Clicker.ahk" 89 | ], f) 90 | 91 | device_list_path = os.path.join( 92 | active_dir, "Autohotkey Interception", "shared_device_info.txt") 93 | device_finder_path = os.path.join( 94 | active_dir, "Autohotkey Interception", "find_device.ahk") 95 | coordinate_path = os.path.join( 96 | active_dir, "Autohotkey Interception", "Coordinate.ahk") 97 | 98 | TEMP_RUNNING_FILE = os.path.join(tempfile.gettempdir(), "running_scripts.tmp") 99 | 100 | 101 | def read_running_scripts_temp(): 102 | scripts = set() 103 | if os.path.exists(TEMP_RUNNING_FILE): 104 | try: 105 | with open(TEMP_RUNNING_FILE, "r", encoding="utf-8") as f: 106 | scripts = set(line.strip() for line in f if line.strip()) 107 | except Exception as e: 108 | print(f"[read_running_scripts_temp] Error: {e}") 109 | return scripts 110 | 111 | 112 | def write_running_scripts_temp(scripts): 113 | try: 114 | with open(TEMP_RUNNING_FILE, "w", encoding="utf-8") as f: 115 | for s in scripts: 116 | f.write(s + "\n") 117 | except Exception as e: 118 | print(f"[write_running_scripts_temp] Error: {e}") 119 | 120 | 121 | def add_script_to_temp(script_name): 122 | scripts = read_running_scripts_temp() 123 | scripts.add(script_name) 124 | write_running_scripts_temp(scripts) 125 | 126 | 127 | def remove_script_from_temp(script_name): 128 | scripts = read_running_scripts_temp() 129 | scripts.discard(script_name) 130 | write_running_scripts_temp(scripts) 131 | 132 | 133 | def detect_system_theme(): 134 | if sys.platform == "win32": 135 | try: 136 | import winreg 137 | registry = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) 138 | key = winreg.OpenKey(registry, r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize") # noqa 139 | value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme") 140 | winreg.CloseKey(key) 141 | return "dark" if value == 0 else "light" 142 | except Exception: 143 | return "light" 144 | 145 | 146 | def get_theme(): 147 | try: 148 | if os.path.exists(theme_path): 149 | with open(theme_path, 'r') as f: 150 | theme = f.read().strip().lower() 151 | if theme in ("dark", "light"): 152 | return theme 153 | return detect_system_theme() 154 | except Exception: 155 | return detect_system_theme() 156 | 157 | 158 | theme = get_theme() 159 | 160 | 161 | def get_ahk_install_dir(): 162 | reg_paths = [ 163 | r"SOFTWARE\AutoHotkey", 164 | r"SOFTWARE\WOW6432Node\AutoHotkey" 165 | ] 166 | for reg_path in reg_paths: 167 | try: 168 | with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path) as key: 169 | install_dir, _ = winreg.QueryValueEx(key, "InstallDir") 170 | return install_dir 171 | except FileNotFoundError: 172 | continue 173 | except Exception: 174 | continue 175 | return None 176 | 177 | ahk_uninstall_path = os.path.join(get_ahk_install_dir() or r"C:\Program Files\AutoHotkey\UX\ui-uninstall.ahk", "UX", "ui-uninstall.ahk") # noqa 178 | ahkv2_dir = os.path.join(get_ahk_install_dir() or r"C:\Program Files\AutoHotkey", "v2") # noqa 179 | -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/Monitor.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | Script to show data flowing from Interception 3 | */ 4 | #SingleInstance force 5 | Persistent 6 | #include Lib\AutoHotInterception.ahk 7 | 8 | OutputDebug("DBGVIEWCLEAR") 9 | 10 | monitorGui := Gui("", "AutoHotInterception Monitor") 11 | monitorGui.MarginX := 0 12 | monitorGui.MarginY := 0 13 | monitorGui.OnEvent("Close", GuiClosed) 14 | 15 | DeviceList := {} 16 | filterMouseMove := 1 17 | filterKeyPress := 0 18 | 19 | AHI := AutoHotInterception() 20 | 21 | ; Device List 22 | DeviceList := AHI.GetDeviceList() 23 | 24 | marginX := 10 25 | marginY := 10 26 | idW := 50 ; Width of the ID text 27 | vhOff := 7 ; Width to space VIDPID / Handle above/below ID row 28 | copyW := 40 ; Width of the Copy buttons 29 | outputH := 350 ; Height of the Output boxes 30 | rowH := 35 ; Distance between each row of devices 31 | 32 | maxWidths := Map("K", 0, "M", 0) ; Max Width of device entries for each column 33 | totalWidths := Map("K", 0, "M", 0) ; Total Width of each column 34 | tW := Map("K", 0, "M", 0) 35 | devTypes := ["K", "M"] ; Lookup table for device type 36 | starts := Map("K", 0, "M", 10) ; Start IDs for each device type 37 | columnTitles := Map("K", "Keyboards", "M", "Mice") ; Column Titles 38 | columnX := Map("K", 0, "M", 0) 39 | 40 | 41 | Loop 2 { 42 | strings := Map() 43 | devType := devTypes[A_Index] 44 | columnX[devType] := GetColX(devType) 45 | 46 | start := starts[devType] 47 | UpdateWidth(0, 1) ; Reset max width 48 | 49 | ; Add device entries 50 | Loop 10 { 51 | i := start + A_Index 52 | if (!DeviceList.Has(i)){ 53 | continue 54 | } 55 | dev := DeviceList[i] 56 | rowY := (marginY * 3) + ((A_Index - 1) * rowH) 57 | 58 | chkDevice := monitorGui.Add("Checkbox", "x" columnX[devType] " y" rowY " w" idW, "ID: " dev.id) 59 | chkDevice.OnEvent("Click", CheckboxChanged.Bind(dev.id)) 60 | 61 | lowest := UpdateLowest(chkDevice) 62 | strings[A_index] := {vid:FormatHex(dev.VID), pid: FormatHex(dev.PID), handle: dev.Handle} 63 | 64 | textVidPid := monitorGui.Add("Text", "x" columnX[devType] + idW " y" rowY - vhOff, "VID / PID:`t0x" strings[A_index].vid ", 0x" strings[A_index].pid) 65 | maxWidths[devType] := UpdateWidth(textVidPid) 66 | 67 | textHandle := monitorGui.Add("Text", "x" columnX[devType] + idW " y" rowY + vhOff, "Handle:`t`t" StrReplace(strings[A_index].Handle, "&", "&&")) 68 | maxWidths[devType] := UpdateWidth(textHandle) 69 | } 70 | 71 | ; Add copy buttons 72 | Loop 10 { 73 | i := start + A_Index 74 | if (!DeviceList.Has(i)){ 75 | continue 76 | } 77 | dev := DeviceList[i] 78 | rowY := (marginY * 3) + ((A_Index - 1) * rowH) 79 | xpos := columnX[devType] + idW + maxWidths[devType] 80 | 81 | btnCopyVidPid := monitorGui.Add("Button", "x" xpos " y" rowY - vhOff " h14 w" copyW, "Copy") 82 | btnCopyVidPid.OnEvent("Click", CopyClipboard.Bind("0x" strings[A_index].vid ", 0x" strings[A_index].pid)) 83 | 84 | btnCopyHandle := monitorGui.Add("Button", "x" xpos " y" rowY + vhOff " h14 w" copyW, "Copy") 85 | btnCopyHandle.OnEvent("Click", CopyClipboard.Bind(strings[A_index].handle)) 86 | } 87 | 88 | totalWidths[devType] := idW + maxWidths[devType] + copyW 89 | monitorGui.Add("Text", "x" columnX[devType] " y5 w" totalWidths[devType] " Center", columnTitles[devType]) 90 | } 91 | 92 | lowest += 2 * MarginY 93 | 94 | ; Options 95 | chkFilterPress := monitorGui.Add("CheckBox", "x" columnX["K"] " y" lowest, "Only show key releases") 96 | chkFilterPress.OnEvent("Click", FilterPress) 97 | 98 | chkFilterMove := monitorGui.Add("CheckBox", "x" columnX["M"] " w" totalWidths[devType] " yp Checked", "Filter Movement (Warning: Turning off can cause crashes)") 99 | chkFilterMove.OnEvent("Click", FilterMove) 100 | 101 | lowest += 2 * MarginY 102 | 103 | btnClearKeyboard := monitorGui.Add("Button", "x" columnX["K"] " y" lowest " w" totalWidths["K"] " Center", "Clear") 104 | btnClearKeyboard.OnEvent("Click", ClearKeyboard) 105 | 106 | btnClearMouse := monitorGui.Add("Button", "x" columnX["M"] " yp w" totalWidths["M"] " Center", "Clear") 107 | btnClearMouse.OnEvent("Click", ClearMouse) 108 | 109 | lowest += 30 110 | 111 | ; Output 112 | lvKeyboard := monitorGui.Add("ListView", "x" columnX["K"] " y" lowest " w" totalWidths["K"] " h" outputH, ["ID", "Code", "State", "Key Name"]) 113 | lvKeyboard.ModifyCol(4, 100) 114 | 115 | lvMouse := monitorGui.Add("ListView", "x" columnX["M"] " yp w" totalWidths["M"] " h" outputH, ["ID", "Code", "State", "X", "Y", "Info"]) 116 | lvMouse.ModifyCol(6, 200) 117 | 118 | lowest += outputH 119 | 120 | monitorGui.Show("w" (marginX * 3) + totalWidths["K"] + totalWidths["M"] " h" marginY + lowest) 121 | return 122 | 123 | 124 | GetColX(devType){ 125 | global marginX, idW, maxWidths, copyW 126 | if (devType == "K") 127 | return marginX 128 | else 129 | return (marginX * 2) + idW + maxWidths["K"] + copyW 130 | } 131 | 132 | UpdateLowest(ctrl){ 133 | static max := 0 134 | ctrl.GetPos(&cpX, &cpY, &cpW, &cpH) 135 | pos := cpY + cpH 136 | if (pos > max){ 137 | max := pos 138 | } 139 | return max 140 | } 141 | 142 | UpdateWidth(ctrl, reset := 0){ 143 | static max := 0 144 | if (reset){ 145 | max := 0 146 | return 147 | } 148 | ctrl.GetPos(&cpX, &cpY, &cpW, &cpH) 149 | if (cpW > max){ 150 | max := cpW 151 | } 152 | return max 153 | } 154 | 155 | CheckboxChanged(id, ctrl, info){ 156 | global AHI 157 | if (ctrl.Value){ 158 | if (id < 11){ 159 | AHI.SubscribeKeyboard(id, false, KeyboardEvent.Bind(id)) 160 | } else { 161 | AHI.SubscribeMouseButtons(id, false, MouseButtonEvent.Bind(id)) 162 | AHI.SubscribeMouseMoveRelative(id, false, MouseAxisEvent.Bind(id, "Relative Move")) 163 | AHI.SubscribeMouseMoveAbsolute(id, false, MouseAxisEvent.Bind(id, "Absolute Move")) 164 | } 165 | } else { 166 | if (id < 11){ 167 | AHI.UnsubscribeKeyboard(id) 168 | } else { 169 | AHI.UnsubscribeMouseButtons(id) 170 | AHI.UnsubscribeMouseMoveRelative(id) 171 | AHI.UnsubscribeMouseMoveAbsolute(id) 172 | } 173 | } 174 | } 175 | 176 | FilterMove(ctrl, info){ 177 | global filterMouseMove 178 | filterMouseMove := ctrl.Value 179 | } 180 | 181 | FilterPress(ctrl, info){ 182 | global filterKeyPress 183 | filterKeyPress := ctrl.Value 184 | } 185 | 186 | ClearKeyboard(ctrl, info){ 187 | global lvKeyboard 188 | lvKeyboard.Delete() 189 | } 190 | 191 | ClearMouse(ctrl, info){ 192 | global lvMouse 193 | lvMouse.Delete() 194 | } 195 | 196 | FormatHex(num){ 197 | return Format("{:04X}", num) 198 | } 199 | 200 | 201 | KeyboardEvent(id, code, state){ 202 | global lvKeyboard, filterKeyPress 203 | if (filterKeyPress && state) 204 | return 205 | scanCode := Format("{:x}", code) 206 | keyName := GetKeyName("SC" scanCode) 207 | row := lvKeyboard.Add(, id, code, state, keyName) 208 | lvKeyboard.Modify(row, "Vis") 209 | } 210 | 211 | MouseButtonEvent(id, code, state){ 212 | global lvMouse 213 | row := lvMouse.Add(, id, code, state, "", "", "Button") 214 | lvMouse.Modify(row, "Vis") 215 | } 216 | 217 | MouseAxisEvent(id, info, x, y){ 218 | global lvMouse, filterMouseMove 219 | if (filterMouseMove) 220 | return 221 | row := lvMouse.Add(, id, "", "", x, y, info) 222 | lvMouse.Modify(row, "Vis") 223 | } 224 | 225 | CopyClipboard(str, ctrl, info){ 226 | A_Clipboard := str 227 | Tooltip("Copied to Clipboard") 228 | SetTimer(ClearTooltip, 1000) 229 | } 230 | 231 | ClearTooltip(){ 232 | ToolTip 233 | } 234 | 235 | GuiClosed(gui){ 236 | ExitApp 237 | } 238 | 239 | ^Esc:: 240 | { 241 | ExitApp 242 | } 243 | -------------------------------------------------------------------------------- /src/logic/logic.py: -------------------------------------------------------------------------------- 1 | import os 2 | import winshell 3 | from win32com.client import Dispatch 4 | import win32gui 5 | import win32process 6 | import json 7 | from utility.constant import (script_dir, keylist_path) 8 | 9 | 10 | class Logic: 11 | def list_scripts(self): 12 | all_scripts = [f for f in os.listdir(self.SCRIPT_DIR) 13 | if f.endswith('.ahk') or f.endswith('.py')] 14 | 15 | pinned = [script for script in all_scripts 16 | if script in self.pinned_profiles] 17 | unpinned = [script for script in all_scripts 18 | if script not in self.pinned_profiles] 19 | 20 | self.scripts = pinned + unpinned 21 | return self.scripts 22 | 23 | def prev_page(self): 24 | if self.current_page > 0: 25 | self.current_page -= 1 26 | self.update_script_list() 27 | 28 | def next_page(self): 29 | if (self.current_page + 1) * 6 < len(self.scripts): 30 | self.current_page += 1 31 | self.update_script_list() 32 | 33 | def add_ahk_to_startup(self, script_name): 34 | script_path = os.path.join(self.SCRIPT_DIR, script_name) 35 | 36 | startup_folder = winshell.startup() 37 | 38 | shortcut_name = os.path.splitext(script_name)[0] 39 | shortcut_path = os.path.join(startup_folder, f"{shortcut_name}.lnk") 40 | 41 | shell = Dispatch("WScript.Shell") 42 | shortcut = shell.CreateShortcut(shortcut_path) 43 | shortcut.TargetPath = script_path 44 | shortcut.WorkingDirectory = os.path.dirname(script_path) 45 | shortcut.IconLocation = script_path 46 | shortcut.save() 47 | 48 | del shell 49 | 50 | self.update_script_list() 51 | return shortcut_path 52 | 53 | def remove_ahk_from_startup(self, script_name): 54 | shortcut_name = os.path.splitext(script_name)[0] 55 | startup_folder = winshell.startup() 56 | shortcut_path = os.path.join(startup_folder, f"{shortcut_name}.lnk") 57 | 58 | try: 59 | if os.path.exists(shortcut_path): 60 | os.remove(shortcut_path) 61 | print(f"Removed {shortcut_path} from startup.") 62 | else: 63 | print(f"{shortcut_path} does not exist in startup.") 64 | 65 | self.update_script_list() 66 | 67 | except Exception as e: 68 | print(f"Error removing {shortcut_path}: {e}") 69 | 70 | def parse_device_info(self, file_path): 71 | devices = [] 72 | try: 73 | with open(file_path, 'r') as file: 74 | lines = file.readlines() 75 | 76 | lines = [line.strip() for line in lines if line.strip()] 77 | 78 | device_info = {} 79 | for line in lines: 80 | line = line.strip() 81 | if line.startswith("Device ID"): 82 | if device_info: 83 | if (device_info.get('VID') and 84 | device_info.get('PID') and 85 | device_info.get('Handle')): 86 | devices.append(device_info) 87 | device_info = {'Device ID': line.split(":")[1].strip()} 88 | elif line.startswith("VID:"): 89 | device_info['VID'] = line.split(":")[1].strip() 90 | elif line.startswith("PID:"): 91 | device_info['PID'] = line.split(":")[1].strip() 92 | elif line.startswith("Handle:"): 93 | device_info['Handle'] = line.split(":")[1].strip() 94 | elif line.startswith("Is Mouse:"): 95 | device_info['Is Mouse'] = line.split(":")[1].strip() 96 | 97 | if (device_info.get('VID') and 98 | device_info.get('PID') and 99 | device_info.get('Handle')): 100 | devices.append(device_info) 101 | 102 | except Exception as e: 103 | print(f"Error reading device info: {e}") 104 | 105 | return devices 106 | 107 | def is_visible_application(self, pid): 108 | try: 109 | def callback(hwnd, pid_list): 110 | _, process_pid = win32process.GetWindowThreadProcessId(hwnd) 111 | if process_pid == pid and win32gui.IsWindowVisible(hwnd): 112 | pid_list.append(pid) 113 | 114 | visible_pids = [] 115 | win32gui.EnumWindows(callback, visible_pids) 116 | return len(visible_pids) > 0 117 | except Exception: 118 | return False 119 | 120 | def run_monitor(self): 121 | script_path = os.path.join(script_dir, "_internal", "Data", "Active", 122 | "AutoHotkey Interception", "Monitor.ahk") 123 | if os.path.exists(script_path): 124 | os.startfile(script_path) 125 | else: 126 | print(f"Error: The script at {script_path} does not exist.") 127 | 128 | def load_key_translations(self): 129 | key_translations = {} 130 | try: 131 | with open(keylist_path, 'r', encoding='utf-8') as file: 132 | data = json.load(file) 133 | for category_dict in data: 134 | for _, keys in category_dict.items(): 135 | for key, info in keys.items(): 136 | readable_key = key.strip().lower() 137 | translation = info.get("translate", "").strip() 138 | if translation: 139 | key_translations[readable_key] = translation 140 | 141 | except Exception as e: 142 | print(f"Error reading key translations: {e}") 143 | return key_translations 144 | 145 | def translate_key(self, key, key_translations): 146 | keys = key.split('+') 147 | translated_keys = [] 148 | 149 | for single_key in keys: 150 | translated_key = key_translations.get(single_key.strip().lower(), 151 | single_key.strip()) 152 | translated_keys.append(translated_key) 153 | 154 | return " & ".join(translated_keys) 155 | 156 | def load_key_list(self): 157 | key_map = {} 158 | try: 159 | with open(keylist_path, 'r', encoding='utf-8') as file: 160 | data = json.load(file) 161 | for category_dict in data: 162 | for _, keys in category_dict.items(): 163 | for key, info in keys.items(): 164 | readable = key 165 | raw = info.get("translate", "") 166 | if raw: 167 | key_map[raw] = readable 168 | except Exception as e: 169 | print(f"Error reading key list: {e}") 170 | return key_map 171 | 172 | def load_key_values(self): 173 | key_values = [] 174 | try: 175 | with open(keylist_path, 'r', encoding='utf-8') as file: 176 | data = json.load(file) 177 | for category_dict in data: 178 | for _, keys in category_dict.items(): 179 | for key in keys.keys(): 180 | key_values.append(key) 181 | except Exception as e: 182 | print(f"Error reading key_list.json: {e}") 183 | return key_values 184 | -------------------------------------------------------------------------------- /src/ui/welcome.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests 4 | from markdown import markdown 5 | from PySide6.QtWidgets import ( 6 | QDialog, QVBoxLayout, QHBoxLayout, QCheckBox, QPushButton, 7 | QTextBrowser, QWidget, QFrame 8 | ) 9 | from PySide6.QtGui import QIcon 10 | from PySide6.QtCore import Qt 11 | from utility.constant import (icon_path, dont_show_path, welcome_cache) 12 | 13 | 14 | class Welcome: 15 | def load_welcome_condition(self): 16 | try: 17 | if os.path.exists(dont_show_path): 18 | with open(dont_show_path, "r") as f: 19 | config = json.load(f) 20 | return config.get("welcome_condition", True) 21 | except Exception as e: 22 | print(f"Error loading condition file: {e}") 23 | return True 24 | 25 | def save_welcome_condition(self): 26 | try: 27 | with open(dont_show_path, "w") as f: 28 | json.dump({"welcome_condition": self.welcome_condition}, f) 29 | except Exception as e: 30 | print(f"Error saving condition file: {e}") 31 | 32 | def show_welcome_window(self): 33 | try: 34 | self.welcome_files = [] 35 | i = 1 36 | while True: 37 | url = f"https://keytik.com/normal-md/{i}.txt" 38 | try: 39 | response = requests.get(url, timeout=5) 40 | if response.status_code == 404: 41 | break 42 | response.raise_for_status() 43 | self.welcome_files.append(url) 44 | i += 1 45 | except Exception: 46 | break 47 | self.current_welcome_index = 0 48 | 49 | welcome_dialog = QDialog(self) 50 | welcome_dialog.setWindowTitle("Announcement") 51 | welcome_dialog.setFixedSize(525, 290) 52 | welcome_dialog.setWindowIcon(QIcon(icon_path)) 53 | welcome_dialog.setModal(True) 54 | welcome_dialog.setWindowModality(Qt.WindowModality.WindowModal) 55 | welcome_dialog.setFixedSize(525, 290) 56 | 57 | main_layout = QVBoxLayout(welcome_dialog) 58 | main_layout.setContentsMargins(10, 10, 10, 10) 59 | 60 | app_palette = welcome_dialog.palette() 61 | bg_color = app_palette.window().color().name() 62 | text_color = app_palette.windowText().color().name() 63 | 64 | html_frame = QFrame() 65 | html_frame.setFrameShape(QFrame.Shape.StyledPanel) 66 | html_frame.setFrameShadow(QFrame.Shadow.Sunken) 67 | html_frame.setFixedSize(500, 230) 68 | html_frame.setStyleSheet(f""" 69 | QFrame {{ 70 | border: 1px solid {text_color}; 71 | border-radius: 3px; 72 | background-color: {bg_color}; 73 | }} 74 | """) 75 | html_layout = QVBoxLayout(html_frame) 76 | html_layout.setContentsMargins(5, 5, 5, 5) 77 | 78 | html_label = QTextBrowser(html_frame) 79 | html_label.setOpenExternalLinks(True) 80 | html_label.setStyleSheet(f""" 81 | QTextBrowser {{ 82 | background-color: {bg_color}; 83 | color: {text_color}; 84 | border: none; 85 | font-family: 'Segoe UI'; 86 | padding: 2px; 87 | }} 88 | """) 89 | html_layout.addWidget(html_label) 90 | main_layout.addWidget( 91 | html_frame, alignment=Qt.AlignmentFlag.AlignHCenter) 92 | 93 | button_frame = QWidget() 94 | button_layout = QHBoxLayout(button_frame) 95 | button_layout.setContentsMargins(0, 0, 0, 0) 96 | 97 | prev_button = QPushButton("Previous") 98 | prev_button.setFixedWidth(100) 99 | next_button = QPushButton("Next") 100 | next_button.setFixedWidth(100) 101 | 102 | dont_show_checkbox = QCheckBox("Don't show again") 103 | dont_show_checkbox.setChecked(not self.welcome_condition) 104 | button_layout.addWidget(prev_button) 105 | button_layout.addWidget(next_button) 106 | button_layout.addWidget(dont_show_checkbox) 107 | main_layout.addWidget( 108 | button_frame, alignment=Qt.AlignmentFlag.AlignHCenter) 109 | 110 | def load_content(index): 111 | try: 112 | url = self.welcome_files[index] 113 | if url in welcome_cache: 114 | md_content = welcome_cache[url] 115 | else: 116 | response = requests.get(url, timeout=5) 117 | response.raise_for_status() 118 | md_content = response.text 119 | welcome_cache[url] = md_content 120 | html_content = markdown(md_content) 121 | styling = """ 122 | 126 | """ 127 | html_label.setHtml(styling + html_content) 128 | except requests.HTTPError: 129 | html_label.setHtml( 130 | f"
File not found.
" # noqa 131 | ) 132 | 133 | def update_buttons(): 134 | prev_button.setEnabled(self.current_welcome_index > 0) 135 | next_button.setEnabled( 136 | self.current_welcome_index < len(self.welcome_files) - 1) 137 | 138 | def next_doc(): 139 | if self.current_welcome_index < len(self.welcome_files) - 1: 140 | self.current_welcome_index += 1 141 | load_content(self.current_welcome_index) 142 | update_buttons() 143 | 144 | def prev_doc(): 145 | if self.current_welcome_index > 0: 146 | self.current_welcome_index -= 1 147 | load_content(self.current_welcome_index) 148 | update_buttons() 149 | 150 | prev_button.clicked.connect(prev_doc) 151 | next_button.clicked.connect(next_doc) 152 | 153 | def toggle_dont_show(): 154 | self.welcome_condition = not dont_show_checkbox.isChecked() 155 | self.save_welcome_condition() 156 | 157 | dont_show_checkbox.stateChanged.connect(toggle_dont_show) 158 | 159 | def on_dialog_close(event): 160 | self.welcome_condition = not dont_show_checkbox.isChecked() 161 | self.save_welcome_condition() 162 | event.accept() 163 | welcome_dialog.closeEvent = on_dialog_close 164 | 165 | if self.welcome_files: 166 | load_content(self.current_welcome_index) 167 | update_buttons() 168 | else: 169 | html_label.setHtml( 170 | "Unable to load announcements. Please check your internet connection.
" # noqa 171 | ) 172 | 173 | welcome_dialog.raise_() 174 | welcome_dialog.activateWindow() 175 | welcome_dialog.exec() 176 | 177 | except Exception as e: 178 | print(f"Error displaying welcome window: {e}") 179 | -------------------------------------------------------------------------------- /src/_internal/Data/Active/AutoHotkey Interception/Lib/AutoHotInterception.ahk: -------------------------------------------------------------------------------- 1 | #include %A_LineFile%\..\CLR.ahk 2 | 3 | class AutoHotInterception { 4 | _contextManagers := Map() 5 | 6 | __New() { 7 | bitness := A_PtrSize == 8 ? "x64" : "x86" 8 | dllName := "interception.dll" 9 | if (A_IsCompiled) { 10 | dllFile := A_LineFile "\..\Lib\" bitness "\" dllName 11 | DirCreate("Lib") 12 | FileInstall("Lib\AutoHotInterception.dll", "Lib\AutoHotInterception.dll", 1) 13 | if (bitness == "x86") { 14 | DirCreate("Lib\x86") 15 | FileInstall("Lib\x86\interception.dll", "Lib\x86\interception.dll", 1) 16 | } else { 17 | DirCreate("Lib\x64") 18 | FileInstall("Lib\x64\interception.dll", "Lib\x64\interception.dll", 1) 19 | } 20 | } else { 21 | dllFile := A_LineFile "\..\" bitness "\" dllName 22 | } 23 | if (!FileExist(dllFile)) { 24 | MsgBox("Unable to find " dllFile ", exiting...`nYou should extract both x86 and x64 folders from the library folder in interception.zip into AHI's lib folder.") 25 | ExitApp 26 | } 27 | 28 | hModule := DllCall("LoadLibrary", "Str", dllFile, "Ptr") 29 | if (hModule == 0) { 30 | this_bitness := A_PtrSize == 8 ? "64-bit" : "32-bit" 31 | other_bitness := A_PtrSize == 4 ? "64-bit" : "32-bit" 32 | MsgBox("Bitness of " dllName " does not match bitness of AHK.`nAHK is " this_bitness ", but " dllName " is " other_bitness ".") 33 | ExitApp 34 | } 35 | DllCall("FreeLibrary", "Ptr", hModule) 36 | 37 | dllName := "AutoHotInterception.dll" 38 | if (A_IsCompiled) { 39 | dllFile := A_LineFile "\..\Lib\" dllName 40 | } else { 41 | dllFile := A_LineFile "\..\" dllName 42 | } 43 | hintMessage := "Try right-clicking " dllFile ", select Properties, and if there is an 'Unblock' checkbox, tick it`nAlternatively, running Unblocker.ps1 in the lib folder (ideally as admin) can do this for you." 44 | if (!FileExist(dllFile)) { 45 | MsgBox("Unable to find " dllFile ", exiting...") 46 | ExitApp 47 | } 48 | 49 | asm := CLR_LoadLibrary(dllFile) 50 | try { 51 | this.Instance := asm.CreateInstance("AutoHotInterception.Manager") 52 | } 53 | catch { 54 | MsgBox(dllName " failed to load`n`n" hintMessage) 55 | ExitApp 56 | } 57 | if (this.Instance.OkCheck() != "OK") { 58 | MsgBox(dllName " loaded but check failed!`n`n" hintMessage) 59 | ExitApp 60 | } 61 | } 62 | 63 | GetInstance() { 64 | return this.Instance 65 | } 66 | 67 | ; --------------- Input Synthesis ---------------- 68 | SendKeyEvent(id, code, state) { 69 | this.Instance.SendKeyEvent(id, code, state) 70 | } 71 | 72 | SendMouseButtonEvent(id, btn, state) { 73 | this.Instance.SendMouseButtonEvent(id, btn, state) 74 | } 75 | 76 | SendMouseButtonEventAbsolute(id, btn, state, x, y) { 77 | this.Instance.SendMouseButtonEventAbsolute(id, btn, state, x, y) 78 | } 79 | 80 | SendMouseMove(id, x, y) { 81 | this.Instance.SendMouseMove(id, x, y) 82 | } 83 | 84 | SendMouseMoveRelative(id, x, y) { 85 | this.Instance.SendMouseMoveRelative(id, x, y) 86 | } 87 | 88 | SendMouseMoveAbsolute(id, x, y) { 89 | this.Instance.SendMouseMoveAbsolute(id, x, y) 90 | } 91 | 92 | SetState(state) { 93 | this.Instance.SetState(state) 94 | } 95 | 96 | MoveCursor(x, y, cm := "Screen", mouseId := -1) { 97 | if (mouseId == -1) 98 | mouseId := 11 ; Use 1st found mouse 99 | oldMode := A_CoordModeMouse 100 | CoordMode("Mouse", cm) 101 | Loop { 102 | MouseGetPos(&cx, &cy) 103 | dx := this.GetDirection(cx, x) 104 | dy := this.GetDirection(cy, y) 105 | if (dx == 0 && dy == 0) 106 | break 107 | this.SendMouseMove(mouseId, dx, dy) 108 | } 109 | CoordMode("Mouse", oldMode) 110 | } 111 | 112 | GetDirection(cp, dp) { 113 | d := dp - cp 114 | if (d > 0) 115 | return 1 116 | if (d < 0) 117 | return -1 118 | return 0 119 | } 120 | 121 | ; --------------- Querying ------------------------ 122 | GetDeviceId(IsMouse, VID, PID, instance := 1) { 123 | static devType := Map(0, "Keyboard", 1, "Mouse") 124 | dev := this.Instance.GetDeviceId(IsMouse, VID, PID, instance) 125 | if (dev == 0) { 126 | MsgBox("Could not get " devType[isMouse] " with VID " VID ", PID " PID ", Instance " instance) 127 | ExitApp 128 | } 129 | return dev 130 | } 131 | 132 | GetDeviceIdFromHandle(isMouse, handle, instance := 1) { 133 | static devType := Map(0, "Keyboard", 1, "Mouse") 134 | dev := this.Instance.GetDeviceIdFromHandle(IsMouse, handle, instance) 135 | if (dev == 0) { 136 | MsgBox("Could not get " devType[isMouse] " with Handle " handle ", Instance " instance) 137 | ExitApp 138 | } 139 | return dev 140 | } 141 | 142 | GetKeyboardId(VID, PID, instance := 1) { 143 | return this.GetDeviceId(false, VID, PID, instance) 144 | } 145 | 146 | GetMouseId(VID, PID, instance := 1) { 147 | return this.GetDeviceId(true, VID, PID, instance) 148 | } 149 | 150 | GetKeyboardIdFromHandle(handle, instance := 1) { 151 | return this.GetDeviceIdFromHandle(false, handle, instance) 152 | } 153 | 154 | GetMouseIdFromHandle(handle, instance := 1) { 155 | return this.GetDeviceIdFromHandle(true, handle, instance) 156 | } 157 | 158 | GetDeviceList() { 159 | DeviceList := Map() 160 | arr := this.Instance.GetDeviceList() 161 | for v in arr { 162 | ; ToDo: Return a class, so code completion works? 163 | DeviceList[v.id] := { ID: v.id, VID: v.vid, PID: v.pid, IsMouse: v.IsMouse, Handle: v.Handle } 164 | } 165 | return DeviceList 166 | } 167 | 168 | ; ---------------------- Subscription Mode ---------------------- 169 | SubscribeKey(id, code, block, callback, concurrent := false) { 170 | this.Instance.SubscribeKey(id, code, block, callback, concurrent) 171 | } 172 | 173 | UnsubscribeKey(id, code) { 174 | this.Instance.UnsubscribeKey(id, code) 175 | } 176 | 177 | SubscribeKeyboard(id, block, callback, concurrent := false) { 178 | this.Instance.SubscribeKeyboard(id, block, callback, concurrent) 179 | } 180 | 181 | UnsubscribeKeyboard(id) { 182 | this.Instance.UnsubscribeKeyboard(id) 183 | } 184 | 185 | SubscribeMouseButton(id, btn, block, callback, concurrent := false) { 186 | this.Instance.SubscribeMouseButton(id, btn, block, callback, concurrent) 187 | } 188 | 189 | UnsubscribeMouseButton(id, btn) { 190 | this.Instance.UnsubscribeMouseButton(id, btn) 191 | } 192 | 193 | SubscribeMouseButtons(id, block, callback, concurrent := false) { 194 | this.Instance.SubscribeMouseButtons(id, block, callback, concurrent) 195 | } 196 | 197 | UnsubscribeMouseButtons(id) { 198 | this.Instance.UnsubscribeMouseButtons(id) 199 | } 200 | 201 | SubscribeMouseMove(id, block, callback, concurrent := false) { 202 | this.Instance.SubscribeMouseMove(id, block, callback, concurrent) 203 | } 204 | 205 | UnsubscribeMouseMove(id) { 206 | this.Instance.UnsubscribeMouseMove(id) 207 | } 208 | 209 | SubscribeMouseMoveRelative(id, block, callback, concurrent := false) { 210 | this.Instance.SubscribeMouseMoveRelative(id, block, callback, concurrent) 211 | } 212 | 213 | UnsubscribeMouseMoveRelative(id) { 214 | this.Instance.UnsubscribeMouseMoveRelative(id) 215 | } 216 | 217 | SubscribeMouseMoveAbsolute(id, block, callback, concurrent := false) { 218 | this.Instance.SubscribeMouseMoveAbsolute(id, block, callback, concurrent) 219 | } 220 | 221 | UnsubscribeMouseMoveAbsolute(id) { 222 | this.Instance.UnsubscribeMouseMoveAbsolute(id) 223 | } 224 | 225 | ; ------------- Context Mode ---------------- 226 | ; Creates a context class to make it easy to turn on/off the hotkeys 227 | CreateContextManager(id) { 228 | if (this._contextManagers.Has(id)) { 229 | Msgbox("ID " id " already has a Context Manager") 230 | ExitApp 231 | } 232 | cm := AutoHotInterception.ContextManager(this, id) 233 | this._contextManagers[id] := cm 234 | return cm 235 | } 236 | 237 | RemoveContextManager(id) { 238 | if (!this._contextManagers.Has(id)) { 239 | Msgbox("ID " id " does not have a Context Manager") 240 | ExitApp 241 | } 242 | this._contextManagers[id].Remove() 243 | this._contextManagers.Delete(id) 244 | } 245 | 246 | ; Helper class for dealing with context mode 247 | class ContextManager { 248 | IsActive := 0 249 | __New(parent, id) { 250 | this.parent := parent 251 | this.id := id 252 | result := this.parent.Instance.SetContextCallback(id, this.OnContextCallback.Bind(this)) 253 | } 254 | 255 | OnContextCallback(state) { 256 | Sleep 0 257 | this.IsActive := state 258 | } 259 | 260 | Remove() { 261 | this.parent.Instance.RemoveContextCallback(this.id) 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/ui/edit_script/parse_script.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class ParseScript: 5 | def parse_device(self, lines): 6 | device_id = None 7 | device_type = "Keyboard" 8 | for line in lines: 9 | if ("AHI.GetDeviceId" in line 10 | or "AHI.GetDeviceIdFromHandle" in line): 11 | start = line.find("(") + 1 12 | end = line.find(")") 13 | params = line[start:end].split(",") 14 | if "false" in params[0].strip(): 15 | device_type = "Keyboard" 16 | elif "true" in params[0].strip(): 17 | device_type = "Mouse" 18 | device_id = ", ".join( 19 | param.strip().replace('"', '') for param in params) 20 | device_id = device_id.replace("false", device_type).replace( 21 | "true", device_type) 22 | break 23 | return device_id 24 | 25 | def parse_program(self, lines): 26 | programs = [] 27 | for line in lines: 28 | line = line.strip() 29 | if line.startswith("#HotIf"): 30 | matches = re.findall( 31 | r'WinActive\("ahk_(exe|class)\s+([^"]+)"\)', line) 32 | for match in matches: 33 | program_type, program_name = match 34 | if program_type == "exe": 35 | programs.append(f"Process - {program_name}") 36 | elif program_type == "class": 37 | programs.append(f"Class - {program_name}") 38 | return ", ".join(programs) 39 | 40 | def parse_shortcuts(self, lines, key_map): 41 | shortcuts = [] 42 | in_hotif_block = False 43 | for line in lines[3:]: 44 | line = line.strip() 45 | if line.startswith("#HotIf"): 46 | in_hotif_block = not in_hotif_block 47 | if 'GetKeyState("CapsLock", "T")' in line: 48 | shortcuts.append("CapsLock ON") 49 | elif '!GetKeyState("CapsLock", "T")' in line: 50 | shortcuts.append("CapsLock OFF") 51 | elif 'GetKeyState("NumLock", "T")' in line: 52 | shortcuts.append("NumLock ON") 53 | elif '!GetKeyState("NumLock", "T")' in line: 54 | shortcuts.append("NumLock OFF") 55 | continue 56 | if ":: ; Shortcuts" in line and not in_hotif_block: 57 | parts = line.split("::") 58 | shortcuts_key = parts[0].strip() 59 | shortcuts_key = (self.replace_raw_keys(shortcuts_key, key_map) 60 | .replace("~", "") 61 | .replace(" & ", "+") 62 | .replace("*", "")) 63 | shortcuts.append(shortcuts_key) 64 | 65 | return shortcuts 66 | 67 | def parse_default_mode(self, lines, key_map): 68 | shortcuts = self.parse_shortcuts(lines, key_map) 69 | remaps = [] 70 | in_block = False 71 | current_block = [] 72 | default_key = "" 73 | 74 | for line in lines[3:]: 75 | line = line.strip() 76 | if not line or line.startswith(";"): 77 | continue 78 | 79 | if line.startswith("#HotIf"): 80 | continue 81 | 82 | if in_block: 83 | if line == "}": 84 | in_block = False 85 | block_text = " ".join(current_block) 86 | self.parse_double_click(default_key, block_text, remaps) 87 | current_block = [] 88 | continue 89 | 90 | current_block.append(line) 91 | continue 92 | 93 | if line.startswith("*") and "::{" in line: 94 | default_key = line[1:line.index("::{")] 95 | in_block = True 96 | current_block = [] 97 | continue 98 | 99 | if ("::" in line and "::{" not in line and ":: ; Shortcuts" 100 | not in line): 101 | self.parse_remap_key(line, key_map, remaps) 102 | 103 | return shortcuts, remaps 104 | 105 | def parse_default_key(self, default_key, key_map): 106 | return (self.replace_raw_keys(default_key, key_map) 107 | .replace("~", "") 108 | .replace(" & ", " + ") 109 | .replace("*", "")) 110 | 111 | def parse_remap_key(self, line, key_map, remaps): 112 | parts = line.split("::") 113 | default_key = parts[0].strip() 114 | remap_or_action = parts[1].strip() if len(parts) > 1 else "" 115 | 116 | default_key = self.parse_default_key(default_key, key_map) 117 | 118 | if remap_or_action: 119 | is_text_format = False 120 | is_hold_format = False 121 | remap_key = "" 122 | hold_interval = "10" 123 | 124 | if remap_or_action.startswith('SendText'): 125 | remap_key = self.parse_text_format(remap_or_action) 126 | is_text_format = True 127 | elif 'SetTimer' in remap_or_action: 128 | remap_key, hold_interval = self.parse_hold_format( 129 | remap_or_action, default_key) 130 | is_hold_format = True 131 | elif (remap_or_action.startswith('Send') or 132 | remap_or_action.startswith('SendInput')): 133 | remap_key = self.parse_send_remap(remap_or_action, default_key) 134 | else: 135 | remap_key = remap_or_action 136 | 137 | remaps.append((default_key, remap_key, is_text_format, 138 | is_hold_format, hold_interval)) 139 | 140 | def get_unicode(self, text): 141 | def chr_replacer(match): 142 | code = int(match.group(1)) 143 | return chr(code) 144 | text = re.sub(r'"', '', text) 145 | text = re.sub(r'\s*\+\s*', '', text) 146 | text = re.sub(r'Chr\((\d+)\)', chr_replacer, text) 147 | return text 148 | 149 | def parse_hold_format(self, remap_or_action, default_key): 150 | remap_key = "" 151 | hold_interval = "10" 152 | 153 | send_match = re.search(r'Send(?:Input)?\((.+)\)', remap_or_action) 154 | if send_match: 155 | down_sequence = send_match.group(1) 156 | down_sequence = self.get_unicode(down_sequence) 157 | down_keys = re.findall(r'{(.*?) Down}', down_sequence) 158 | if down_keys: 159 | remap_key = " + ".join(down_keys) 160 | interval_match = re.search(r'-\s*(\d+)', remap_or_action) 161 | if interval_match: 162 | hold_interval = str(int(interval_match.group(1)) / 1000) 163 | 164 | return remap_key, hold_interval 165 | 166 | def parse_send_remap(self, remap_or_action, default_key): 167 | if remap_or_action.startswith("SendInput("): 168 | key_sequence = remap_or_action[len("SendInput("):-1] 169 | elif remap_or_action.startswith("Send("): 170 | key_sequence = remap_or_action[len("Send("):-1] 171 | else: 172 | key_sequence = remap_or_action.split(" ", 1)[1] 173 | key_sequence = self.get_unicode(key_sequence) 174 | keys = [] 175 | remap_key = "" 176 | 177 | matches = re.findall(r'{(.*?)( down| up)}', key_sequence) 178 | if matches: 179 | seen_keys = set() 180 | for match in matches: 181 | key = match[0] 182 | if key not in seen_keys: 183 | seen_keys.add(key) 184 | keys.append(key) 185 | remap_key = " + ".join(keys) 186 | else: 187 | remap_key = key_sequence.strip('"{}"') 188 | 189 | return remap_key 190 | 191 | def parse_text_format(self, block_text): 192 | text_match = re.search(r'SendText\("(.+?)"\)', block_text) 193 | remap_key = "" 194 | if text_match: 195 | remap_key = text_match.group(1) 196 | return remap_key 197 | 198 | def parse_double_click(self, default_key, block_text, remaps): 199 | is_text_format = False 200 | is_hold_format = False 201 | hold_interval = "10" 202 | remap_key = "" 203 | 204 | if ('A_PriorHotkey' in block_text and 205 | 'A_TimeSincePriorHotkey < 400' in block_text): 206 | 207 | if 'SendText' in block_text: 208 | remap_key = self.parse_text_format(block_text) 209 | is_text_format = True 210 | elif 'SetTimer' in block_text: 211 | remap_key, hold_interval = self.parse_hold_format( 212 | block_text, default_key) 213 | is_hold_format = True 214 | else: 215 | send_match = re.search( 216 | r'Send(?:Input)?\("(.+?)"\)', block_text) 217 | if send_match: 218 | remap_key = self.parse_send_remap( 219 | send_match.group(0), default_key) 220 | else: 221 | remap_key = "" 222 | 223 | remaps.append((f"{default_key} + {default_key}", 224 | remap_key, is_text_format, 225 | is_hold_format, hold_interval)) 226 | -------------------------------------------------------------------------------- /src/ui/edit_script/edit_script_main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PySide6.QtWidgets import ( 3 | QWidget, QDialog, QLabel, QLineEdit, QPushButton, QTextEdit, QScrollArea, 4 | QVBoxLayout, QSpacerItem, QSizePolicy, QComboBox, QGridLayout 5 | ) 6 | from PySide6.QtCore import Qt 7 | from PySide6.QtGui import QIcon 8 | from utility.constant import (icon_path) 9 | 10 | 11 | class EditScriptMain: 12 | def edit_script(self, script_name): 13 | self.shortcut_row_widgets = [] 14 | self.mapping_row_widgets = [] 15 | self.copas_rows = [] 16 | 17 | self.is_text_mode = False 18 | 19 | is_new_profile = not script_name 20 | if is_new_profile: 21 | script_path = None 22 | lines = ["; default\n"] 23 | else: 24 | script_path = os.path.join(self.SCRIPT_DIR, script_name) 25 | with open(script_path, 'r', encoding='utf-8') as file: 26 | lines = file.readlines() 27 | first_line = file.readline().strip() 28 | if not lines: 29 | return 30 | 31 | first_line = lines[0].strip() 32 | key_map = self.load_key_list() 33 | mode_line = lines[0].strip() if lines else "; default" 34 | 35 | self.edit_window = QDialog(self) 36 | if is_new_profile: 37 | self.edit_window.setWindowTitle("Create New Profile") 38 | else: 39 | self.edit_window.setWindowTitle("Edit Profile") 40 | self.edit_window.setWindowIcon(QIcon(icon_path)) 41 | self.edit_window.setFixedSize(600, 460) 42 | 43 | edit_layout = QGridLayout(self.edit_window) 44 | edit_layout.setContentsMargins(30, 10, 30, 10) 45 | 46 | top_widget = QWidget(self.edit_window) 47 | top_layout = QGridLayout(top_widget) 48 | top_layout.setContentsMargins(40, 0, 40, 5) 49 | 50 | script_name_label = QLabel("Profile Name", top_widget) 51 | script_name_label.setFixedWidth(90) 52 | script_name_entry = QLineEdit(top_widget) 53 | if script_name: 54 | script_name_without_extension = script_name.replace('.ahk', '') 55 | script_name_entry.setText(script_name_without_extension) 56 | script_name_entry.setReadOnly(True) 57 | else: 58 | script_name_entry.setText("") 59 | script_name_entry.setReadOnly(False) 60 | self.script_name_entry = script_name_entry 61 | top_layout.addWidget(script_name_label, 0, 0, 1, 1) 62 | top_layout.addWidget(script_name_entry, 0, 1, 1, 3) 63 | 64 | program_label = QLabel("Program", top_widget) 65 | program_label.setFixedWidth(90) 66 | program_entry = QLineEdit(top_widget) 67 | program_select_button = QPushButton("Select Program", top_widget) 68 | program_select_button.setToolTip("Choose program and bind profile to it") # noqa 69 | program_select_button.clicked.connect(lambda: self.program_window( 70 | self.program_entry)) 71 | self.program_entry = program_entry 72 | top_layout.addWidget(program_label, 1, 0, 1, 1) 73 | top_layout.addWidget(program_entry, 1, 1, 1, 2) 74 | top_layout.addWidget(program_select_button, 1, 3, 1, 1) 75 | 76 | keyboard_label = QLabel("Device ID", top_widget) 77 | keyboard_label.setFixedWidth(90) 78 | keyboard_entry = QLineEdit(top_widget) 79 | keyboard_select_button = QPushButton("Select Device", top_widget) 80 | keyboard_select_button.setToolTip("Choose device and bind profile to it") # noqa 81 | keyboard_select_button.clicked.connect(self.open_device_selection) 82 | self.keyboard_entry = keyboard_entry 83 | top_layout.addWidget(keyboard_label, 2, 0, 1, 1) 84 | top_layout.addWidget(keyboard_entry, 2, 1, 1, 2) 85 | top_layout.addWidget(keyboard_select_button, 2, 3, 1, 1) 86 | 87 | device_id = self.parse_device(lines) 88 | if device_id: 89 | keyboard_entry.setText(" " + device_id) 90 | 91 | program_entry_value = self.parse_program(lines) 92 | if program_entry_value: 93 | program_entry.setText(" " + program_entry_value) 94 | 95 | edit_layout.addWidget(top_widget, 0, 0, 1, 4) 96 | 97 | self.edit_scroll = QScrollArea(self.edit_window) 98 | self.edit_scroll.setFixedSize(535, 305) 99 | self.edit_scroll.setWidgetResizable(True) 100 | self.edit_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 101 | edit_layout.addWidget(self.edit_scroll, 1, 0, 1, 4) 102 | 103 | self.edit_frame = QWidget() 104 | self.edit_scroll.setWidget(self.edit_frame) 105 | 106 | self.edit_frame_layout = QVBoxLayout(self.edit_frame) 107 | self.edit_frame.setLayout(self.edit_frame_layout) 108 | 109 | self.key_rows = [] 110 | self.shortcut_rows = [] 111 | 112 | shortcuts = [] 113 | remaps = [] 114 | 115 | if mode_line == "; default": 116 | shortcuts, remaps = self.parse_default_mode(lines, key_map) 117 | 118 | self.shortcut_title() 119 | 120 | if not shortcuts: 121 | self.shortcut_row() 122 | else: 123 | for shortcut in shortcuts: 124 | self.shortcut_row(shortcut) 125 | 126 | self.remap_title() 127 | 128 | if not remaps: 129 | self.remap_row() 130 | else: 131 | for (default_key, remap_key, is_text_format, 132 | is_hold_format, hold_interval) in remaps: 133 | self.remap_row( 134 | default_key, 135 | remap_key, 136 | is_text_format=is_text_format, 137 | is_hold_format=is_hold_format, 138 | hold_interval=hold_interval 139 | ) 140 | 141 | self.update_plus_visibility('shortcut') 142 | self.update_plus_visibility('remap') 143 | 144 | elif mode_line == "; text": 145 | self.is_text_mode = True 146 | self.text_block = QTextEdit(self.edit_frame) 147 | self.text_block.setLineWrapMode(QTextEdit.WidgetWidth) 148 | self.text_block.setFixedHeight(14 * self.fontMetrics().height()) 149 | self.text_block.setFontPointSize(10) 150 | self.edit_frame_layout.addWidget(self.text_block) 151 | 152 | shortcuts = self.parse_shortcuts(lines, key_map) 153 | 154 | self.row_num += 1 155 | 156 | text_content = self.extract_and_filter_content(lines) 157 | self.text_block.setPlainText(text_content.strip()) 158 | 159 | if not shortcuts: 160 | self.shortcut_row() 161 | else: 162 | for shortcut in shortcuts: 163 | self.shortcut_row(shortcut) 164 | 165 | self.update_plus_visibility('shortcut') 166 | 167 | self.edit_frame_layout.addItem(QSpacerItem(20, 40, QSizePolicy.Minimum, 168 | QSizePolicy.Expanding)) 169 | 170 | bottom_widget = QWidget(self.edit_window) 171 | bottom_layout = QGridLayout(bottom_widget) 172 | bottom_layout.setContentsMargins(0, 5, 0, 0) 173 | bottom_layout.setHorizontalSpacing(225) 174 | 175 | save_button = QPushButton("Save Changes", self.edit_window) 176 | save_button.clicked.connect(lambda: self.save_changes(script_name)) 177 | bottom_layout.addWidget(save_button, 0, 0, 1, 1) 178 | 179 | mode_combobox = QComboBox(self.edit_window) 180 | mode_combobox.addItems([ 181 | "Default Mode", 182 | "Text Mode", 183 | ]) 184 | mode_combobox.setEditable(True) 185 | mode_combobox.lineEdit().setAlignment(Qt.AlignmentFlag.AlignCenter) 186 | mode_combobox.lineEdit().setReadOnly(True) 187 | mode_combobox.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) 188 | self.mode_combobox = mode_combobox 189 | bottom_layout.addWidget(mode_combobox, 0, 3, 1, 1) 190 | 191 | edit_layout.addWidget(bottom_widget, 2, 0, 1, 4) 192 | 193 | first_line_lower = first_line.lower() 194 | mode_map = { 195 | "; default": 0, 196 | "; text": 1, 197 | } 198 | default_index = mode_map.get(first_line_lower, 0) 199 | mode_combobox.setCurrentIndex(default_index) 200 | 201 | on_mode_changed = self.handle_mode_changed 202 | mode_combobox.currentIndexChanged.connect(on_mode_changed) 203 | 204 | self.edit_window.setLayout(edit_layout) 205 | self.edit_window.exec() 206 | 207 | def handle_mode_changed(self, index): 208 | while self.edit_frame_layout.count(): 209 | item = self.edit_frame_layout.takeAt(0) 210 | widget = item.widget() 211 | if widget: 212 | widget.setParent(None) 213 | 214 | self.key_rows = [] 215 | self.shortcut_rows = [] 216 | if hasattr(self, "files_opener_rows"): 217 | self.files_opener_rows = [] 218 | if hasattr(self, "files_opener_row_widgets"): 219 | self.files_opener_row_widgets = [] 220 | if hasattr(self, "text_block"): 221 | self.text_block = None 222 | self.is_text_mode = False 223 | 224 | if index == 0: 225 | self.is_text_mode = False 226 | self.shortcut_title() 227 | self.shortcut_row() 228 | self.remap_title() 229 | self.remap_row() 230 | self.edit_frame_layout.addItem(QSpacerItem(20, 40, 231 | QSizePolicy.Minimum, 232 | QSizePolicy.Expanding)) 233 | elif index == 1: 234 | self.is_text_mode = True 235 | self.shortcut_title() 236 | self.shortcut_row() 237 | self.edit_frame_layout.addItem(QSpacerItem(20, 40, 238 | QSizePolicy.Minimum, 239 | QSizePolicy.Expanding)) 240 | -------------------------------------------------------------------------------- /src/ui/setting.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from PySide6.QtWidgets import ( 4 | QDialog, QVBoxLayout, QGridLayout, QGroupBox, QPushButton, 5 | QMessageBox, QFileDialog, QInputDialog, QHBoxLayout, QCheckBox 6 | ) 7 | from PySide6.QtGui import QIcon 8 | from PySide6.QtCore import Qt 9 | import webbrowser 10 | import json 11 | import subprocess 12 | import ctypes 13 | from utility.constant import (icon_path, condition_path, theme_path, 14 | driver_path, interception_install_path, 15 | interception_uninstall_path) 16 | from utility.utils import (active_dir, store_dir, ahkv2_dir, # noqa 17 | ahk_uninstall_path) 18 | 19 | 20 | class Setting: 21 | def open_settings_window(self): 22 | settings_window = QDialog(self) 23 | settings_window.setWindowTitle("Settings") 24 | settings_window.setFixedSize(400, 250) 25 | settings_window.setWindowIcon(QIcon(icon_path)) 26 | settings_window.setModal(True) 27 | settings_window.setWindowFlag( 28 | Qt.WindowType.WindowStaysOnTopHint, 29 | getattr(self, "is_on_top", False)) 30 | 31 | main_layout = QVBoxLayout(settings_window) 32 | main_layout.setContentsMargins(10, 10, 10, 10) 33 | 34 | group_box = QGroupBox() 35 | group_layout = QGridLayout(group_box) 36 | group_layout.setHorizontalSpacing(20) 37 | group_layout.setContentsMargins(10, 10, 10, 10) 38 | 39 | theme_button = QPushButton("Change Theme") 40 | theme_button.setFixedHeight(40) 41 | theme_button.clicked.connect(self.change_theme_dialog) 42 | group_layout.addWidget(theme_button, 0, 0, 1, 1) 43 | 44 | change_path_button = QPushButton("Change Profile Location") 45 | change_path_button.setFixedHeight(40) 46 | change_path_button.clicked.connect(self.change_data_location) 47 | group_layout.addWidget(change_path_button, 0, 1, 1, 1) 48 | 49 | installation_button = QPushButton("Check Installation") 50 | installation_button.setFixedHeight(40) 51 | installation_button.clicked.connect(self.show_installation_dialog) 52 | group_layout.addWidget(installation_button, 1, 0, 1, 1) 53 | 54 | check_update_button = QPushButton("Check For Update") 55 | check_update_button.setFixedHeight(40) 56 | check_update_button.clicked.connect( 57 | self.check_update_and_show_messagebox) 58 | group_layout.addWidget(check_update_button, 1, 1, 1, 1) 59 | 60 | pro_upgrade_button = QPushButton("Get KeyTik Pro") 61 | pro_upgrade_button.setFixedHeight(40) 62 | pro_upgrade_button.clicked.connect( 63 | lambda: webbrowser.open( 64 | "https://fajarrahmadjaya.gumroad.com/l/keytik-pro")) 65 | group_layout.addWidget(pro_upgrade_button, 2, 0, 1, 1) 66 | 67 | readme_button = QPushButton("Announcement") 68 | readme_button.setFixedHeight(40) 69 | readme_button.clicked.connect(self.show_welcome_window) 70 | group_layout.addWidget(readme_button, 2, 1, 1, 1) 71 | 72 | group_layout.setRowStretch(0, 1) 73 | group_layout.setRowStretch(1, 1) 74 | group_layout.setRowStretch(2, 1) 75 | group_layout.setColumnStretch(0, 1) 76 | group_layout.setColumnStretch(1, 1) 77 | 78 | main_layout.addWidget(group_box) 79 | settings_window.exec() 80 | 81 | def change_data_location(self): 82 | global active_dir, store_dir 83 | 84 | new_path = QFileDialog.getExistingDirectory( 85 | self, "Select a New Path for Active and Store Folders" 86 | ) 87 | 88 | if not new_path: 89 | print("No directory selected. Operation canceled.") 90 | return 91 | 92 | try: 93 | if not os.path.exists(new_path): 94 | print(f"The selected path does not exist: {new_path}") 95 | return 96 | 97 | new_active_dir = os.path.join(new_path, 'Active') 98 | new_store_dir = os.path.join(new_path, 'Store') 99 | 100 | if os.path.exists(active_dir): 101 | shutil.move(active_dir, new_active_dir) 102 | print(f"Moved Active folder to {new_active_dir}") 103 | else: 104 | print(f"Active folder does not exist at {active_dir}") 105 | 106 | if os.path.exists(store_dir): 107 | shutil.move(store_dir, new_store_dir) 108 | print(f"Moved Store folder to {new_store_dir}") 109 | else: 110 | print(f"Store folder does not exist at {store_dir}") 111 | 112 | new_condition_data = {"path": new_path} 113 | with open(condition_path, 'w') as f: 114 | json.dump(new_condition_data, f) 115 | print(f"Updated condition.json with the new path: {new_path}") 116 | 117 | active_dir = new_active_dir 118 | store_dir = new_store_dir 119 | print(f"Global active_dir updated to: {active_dir}") 120 | print(f"Global store_dir updated to: {store_dir}") 121 | 122 | self.SCRIPT_DIR = active_dir 123 | self.scripts = self.list_scripts() 124 | self.update_script_list() 125 | 126 | QMessageBox.information( 127 | self, "Change Profile Location", 128 | "Profile location changed successfully!") 129 | except Exception as e: 130 | print(f"An error occurred: {e}") 131 | QMessageBox.critical(self, "Error", f"An error occurred: {e}") 132 | 133 | def update_messagebox(self, latest_version, show_no_update_message=False): # noqa 134 | if latest_version: 135 | reply = QMessageBox.question( 136 | self, "Update Available", 137 | f"New update available: KeyTik {latest_version}\n\nWould you like to go to the update page?", # noqa 138 | QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No # noqa 139 | ) 140 | if reply == QMessageBox.StandardButton.Yes: 141 | webbrowser.open("https://github.com/Fajar-RahmadJaya/KeyTik/releases") # noqa 142 | else: 143 | if show_no_update_message: 144 | QMessageBox.information( 145 | self, "Check For Update", 146 | "You are using the latest version of KeyTik.") 147 | 148 | def check_update_and_show_messagebox(self): 149 | latest_version = self.check_for_update() 150 | self.update_messagebox(latest_version, show_no_update_message=True) 151 | 152 | def change_theme_dialog(self): 153 | options = ["Light", "Dark", "System"] 154 | current_theme = self.read_theme() 155 | if current_theme == "dark": 156 | current_index = 1 157 | elif current_theme == "light": 158 | current_index = 0 159 | else: 160 | current_index = 2 161 | theme, ok = QInputDialog.getItem( 162 | self, "Change Theme", "Select theme:", 163 | options, current_index, False) 164 | if ok: 165 | self.save_theme(theme.lower()) 166 | QMessageBox.information(self, "Theme Changed", "Theme will be applied after restarting the app.") # noqa 167 | 168 | def read_theme(self): 169 | try: 170 | if os.path.exists(theme_path): 171 | with open(theme_path, 'r') as f: 172 | theme = f.read().strip().lower() 173 | if theme in ("dark", "light"): 174 | return theme 175 | return "system" 176 | except Exception: 177 | return "system" 178 | 179 | def save_theme(self, theme): 180 | try: 181 | with open(theme_path, 'w') as f: 182 | if theme == "system": 183 | f.write("") 184 | else: 185 | f.write(theme) 186 | except Exception as e: 187 | print(f"Failed to save theme: {e}") 188 | 189 | def show_installation_dialog(self): 190 | dialog = QDialog(self) 191 | dialog.setWindowTitle("Installation Manager") 192 | dialog.setWindowIcon(QIcon(icon_path)) 193 | dialog.setFixedSize(380, 180) 194 | layout = QVBoxLayout(dialog) 195 | layout.setContentsMargins(20, 20, 20, 20) 196 | 197 | install_group = QGroupBox() 198 | install_group_layout = QHBoxLayout(install_group) 199 | install_group_layout.setContentsMargins(10, 10, 10, 10) 200 | 201 | ahk_vbox = QVBoxLayout() 202 | ahk_checkbox = QCheckBox("AutoHotkey", dialog) 203 | ahk_installed = os.path.exists(ahkv2_dir) 204 | ahk_checkbox.setChecked(ahk_installed) 205 | ahk_checkbox.setEnabled(False) 206 | ahk_button = QPushButton(dialog) 207 | ahk_button.setText( 208 | "Uninstall AutoHotkey" 209 | if ahk_installed 210 | else "Install AutoHotkey") 211 | ahk_vbox.addWidget( 212 | ahk_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter) 213 | ahk_vbox.addWidget(ahk_button) 214 | 215 | driver_vbox = QVBoxLayout() 216 | driver_checkbox = QCheckBox("Interception Driver", dialog) 217 | driver_installed = os.path.exists(driver_path) 218 | driver_checkbox.setChecked(driver_installed) 219 | driver_checkbox.setEnabled(False) 220 | driver_button = QPushButton(dialog) 221 | driver_button.setText( 222 | "Uninstall Interception Driver" 223 | if driver_installed 224 | else "Install Interception Driver") 225 | driver_vbox.addWidget( 226 | driver_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter) 227 | driver_vbox.addWidget(driver_button) 228 | 229 | install_group_layout.addLayout(ahk_vbox) 230 | install_group_layout.addLayout(driver_vbox) 231 | 232 | layout.addWidget(install_group) 233 | 234 | def ahk_action(): 235 | if ahk_installed: 236 | try: 237 | subprocess.Popen(ahk_uninstall_path, shell=True) 238 | except Exception as e: 239 | QMessageBox.critical(dialog, "Error", f"Failed to start uninstall: {e}") # noqa 240 | else: 241 | webbrowser.open("https://www.autohotkey.com") 242 | 243 | def driver_action(): 244 | try: 245 | if driver_installed: 246 | ctypes.windll.shell32.ShellExecuteW( 247 | None, "runas", 248 | interception_uninstall_path, None, None, 1 249 | ) 250 | else: 251 | ctypes.windll.shell32.ShellExecuteW( 252 | None, "runas", interception_install_path, None, None, 1 253 | ) 254 | except Exception as e: 255 | QMessageBox.critical(dialog, "Error", f"Failed to run driver installer/uninstaller: {e}") # noqa 256 | 257 | ahk_button.clicked.connect(ahk_action) 258 | driver_button.clicked.connect(driver_action) 259 | 260 | dialog.exec() 261 | -------------------------------------------------------------------------------- /src/ui/edit_script/select_program.py: -------------------------------------------------------------------------------- 1 | import os 2 | import win32gui 3 | import win32process 4 | import psutil 5 | from PySide6.QtWidgets import ( 6 | QDialog, QLabel, QLineEdit, QPushButton, QTreeWidget, 7 | QTreeWidgetItem, QVBoxLayout, QHBoxLayout, QHeaderView 8 | ) 9 | from PySide6.QtCore import Qt 10 | from PySide6.QtGui import QIcon 11 | from utility.constant import (icon_path) 12 | 13 | 14 | class SelectProgram: 15 | def multi_check(self, texts): 16 | item = QTreeWidgetItem(texts) 17 | 18 | for col in range(3): 19 | item.setFlags(item.flags() | Qt.ItemIsUserCheckable) 20 | item.setCheckState(col, Qt.Unchecked) 21 | return item 22 | 23 | def program_window(self, entry_widget): 24 | self.select_program_window = None 25 | 26 | if (self.select_program_window 27 | and self.select_program_window.isVisible()): 28 | self.select_program_window.raise_() 29 | return 30 | 31 | if (hasattr(self, 'edit_window') 32 | and self.edit_window 33 | and self.edit_window.isVisible()): 34 | parent_window = self.edit_window 35 | else: 36 | parent_window = self 37 | 38 | self.select_program_window = QDialog(parent_window) 39 | self.select_program_window.setWindowTitle("Select Programs") 40 | self.select_program_window.setWindowIcon(QIcon(icon_path)) 41 | self.select_program_window.setFixedSize(600, 300) 42 | self.select_program_window.setModal(True) 43 | self.select_program_window.setAttribute( 44 | Qt.WidgetAttribute.WA_DeleteOnClose) 45 | 46 | main_layout = QVBoxLayout(self.select_program_window) 47 | 48 | self.program_tree = QTreeWidget(self.select_program_window) 49 | self.program_tree.setHeaderLabels(["Window Title", "Class", "Process"]) 50 | self.program_tree.setSortingEnabled(True) 51 | main_layout.addWidget(self.program_tree) 52 | 53 | header = self.program_tree.header() 54 | 55 | for col in range(self.program_tree.columnCount()): 56 | header.setSectionResizeMode(col, QHeaderView.Interactive) 57 | self.program_tree.setColumnWidth(col, 120) 58 | 59 | def fit_sorted_column(): 60 | sort_col = header.sortIndicatorSection() 61 | 62 | header.setSectionResizeMode(sort_col, QHeaderView.ResizeToContents) 63 | self.program_tree.resizeColumnToContents(sort_col) 64 | 65 | sorted_col_width = self.program_tree.columnWidth(sort_col) 66 | 67 | header.setSectionResizeMode(sort_col, QHeaderView.Interactive) 68 | 69 | total_width = self.program_tree.viewport().width() 70 | other_cols = [i for i in range(self.program_tree.columnCount()) 71 | if i != sort_col] 72 | 73 | min_other_col_width = 80 74 | 75 | remaining_width = max(total_width - sorted_col_width, 76 | min_other_col_width * len(other_cols)) 77 | other_col_width = remaining_width // len(other_cols) 78 | for col in other_cols: 79 | self.program_tree.setColumnWidth(col, other_col_width) 80 | 81 | header.sectionClicked.connect(lambda _: fit_sorted_column()) 82 | 83 | button_layout = QHBoxLayout() 84 | main_layout.addLayout(button_layout) 85 | 86 | save_button = QPushButton("Select", self.select_program_window) 87 | save_button.clicked.connect( 88 | lambda: self.save_selected_programs(entry_widget)) 89 | button_layout.addWidget(save_button) 90 | 91 | search_layout = QHBoxLayout() 92 | button_layout.addLayout(search_layout) 93 | 94 | search_label = QLabel("Search:", self.select_program_window) 95 | search_layout.addWidget(search_label) 96 | 97 | search_entry = QLineEdit(self.select_program_window) 98 | search_layout.addWidget(search_entry) 99 | 100 | search_entry.textChanged.connect(self.search_programs) 101 | 102 | refresh_button = QPushButton("Refresh", self.select_program_window) 103 | refresh_button.clicked.connect(lambda: self.update_program_treeview( 104 | show_all_processes=self.show_all_button.text() == "Show App Only" 105 | )) 106 | search_layout.addWidget(refresh_button) 107 | 108 | self.show_all_button = QPushButton( 109 | "Show All Processes", self.select_program_window) 110 | self.show_all_button.clicked.connect(self.toggle_show_all_processes) 111 | search_layout.addWidget(self.show_all_button) 112 | 113 | self.update_program_treeview(show_all_processes=False) 114 | fit_sorted_column() 115 | 116 | self.select_program_window.exec() 117 | 118 | def get_running_processes(self, app_only=True): 119 | if app_only: 120 | pid_name_map = {} 121 | for proc in psutil.process_iter(['pid', 'name']): 122 | pid_name_map[proc.info['pid']] = proc.info['name'] 123 | 124 | results = [] 125 | 126 | def enum_window_callback(hwnd, _): 127 | if not win32gui.IsWindowVisible(hwnd): 128 | return 129 | title = win32gui.GetWindowText(hwnd) 130 | if not title: 131 | return 132 | class_name = win32gui.GetClassName(hwnd) 133 | _, pid = win32process.GetWindowThreadProcessId(hwnd) 134 | proc_name = pid_name_map.get(pid, "") 135 | results.append((title, class_name, proc_name, "Application")) 136 | 137 | win32gui.EnumWindows(enum_window_callback, None) 138 | return results 139 | else: 140 | 141 | processes = [] 142 | for proc in psutil.process_iter(['pid', 'name', 'exe', 'status']): 143 | try: 144 | if (proc.info['name'].lower() in 145 | ["system", "system idle process", 146 | "svchost.exe", "taskhostw.exe", 147 | "explorer.exe"]): 148 | continue 149 | pid = proc.info['pid'] 150 | exe_name = proc.info['exe'] if 'exe' in proc.info else None 151 | exe_name = (os.path.basename(exe_name) 152 | if exe_name 153 | else proc.info['name']) 154 | process_type = ("Application" 155 | if self.is_visible_application(pid) 156 | else "System") 157 | try: 158 | def window_callback(hwnd, windows): 159 | _, process_pid = ( 160 | win32process.GetWindowThreadProcessId(hwnd)) 161 | if (process_pid == pid 162 | and win32gui.IsWindowVisible(hwnd)): 163 | windows.append( 164 | (win32gui.GetClassName(hwnd), 165 | win32gui.GetWindowText(hwnd))) 166 | windows = [] 167 | win32gui.EnumWindows(window_callback, windows) 168 | if windows: 169 | class_name, window_title = windows[0] 170 | else: 171 | class_name, window_title = "N/A", "N/A" 172 | except Exception: 173 | class_name, window_title = "N/A", "N/A" 174 | processes.append( 175 | (window_title, class_name, 176 | exe_name, process_type)) 177 | except (psutil.NoSuchProcess, psutil.AccessDenied): 178 | continue 179 | return processes 180 | 181 | def update_program_treeview(self, show_all_processes=None): 182 | if show_all_processes is None: 183 | show_all_processes = ( 184 | self.show_all_button.text()) == "Show All Processes" 185 | self.program_tree.clear() 186 | 187 | processes = self.get_running_processes(app_only=not show_all_processes) 188 | for proc in processes: 189 | window_title, class_name, proc_name = proc[:3] 190 | p_type = proc[3] if len(proc) > 3 else "Application" 191 | if show_all_processes or p_type == "Application": 192 | item = self.multi_check([window_title, class_name, proc_name]) 193 | self.program_tree.addTopLevelItem(item) 194 | 195 | header = self.program_tree.header() 196 | sort_col = header.sortIndicatorSection() 197 | header.setSectionResizeMode(sort_col, QHeaderView.ResizeToContents) 198 | self.program_tree.resizeColumnToContents(sort_col) 199 | header.setSectionResizeMode(sort_col, QHeaderView.Interactive) 200 | sorted_col_width = self.program_tree.columnWidth(sort_col) 201 | total_width = self.program_tree.viewport().width() 202 | other_cols = [i for i in range(self.program_tree.columnCount()) 203 | if i != sort_col] 204 | min_other_col_width = 80 205 | remaining_width = max( 206 | total_width - sorted_col_width, 207 | min_other_col_width * len(other_cols)) 208 | other_col_width = remaining_width // len(other_cols) 209 | for col in other_cols: 210 | self.program_tree.setColumnWidth(col, other_col_width) 211 | 212 | def toggle_show_all_processes(self): 213 | current_text = self.show_all_button.text() 214 | if current_text == "Show All Processes": 215 | self.show_all_button.setText("Show App Only") 216 | self.update_program_treeview(show_all_processes=True) 217 | else: 218 | self.show_all_button.setText("Show All Processes") 219 | self.update_program_treeview(show_all_processes=False) 220 | 221 | def search_programs(self, query): 222 | for index in range(self.program_tree.topLevelItemCount()): 223 | item = self.program_tree.topLevelItem(index) 224 | item.setHidden(query.lower() not in item.text(0).lower()) 225 | 226 | def save_selected_programs(self, entry_widget): 227 | name_checked = [] 228 | class_checked = [] 229 | process_checked = [] 230 | for index in range(self.program_tree.topLevelItemCount()): 231 | item = self.program_tree.topLevelItem(index) 232 | if item.checkState(0) == Qt.Checked: 233 | name_checked.append(f"Name - {item.text(0).strip(' ✔')}") 234 | if item.checkState(1) == Qt.Checked: 235 | class_checked.append(f"Class - {item.text(1).strip(' ✔')}") 236 | if item.checkState(2) == Qt.Checked: 237 | process_checked.append(f"Process - {item.text(2).strip(' ✔')}") 238 | selected_programs = name_checked + class_checked + process_checked 239 | 240 | if selected_programs: 241 | entry_widget.setText(", ".join(selected_programs)) 242 | self.select_program_window.accept() 243 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | Third-Party Libraries and Their Licenses: 177 | 178 | This project uses the following third-party libraries: 179 | 180 | 1. **Python Standard Library** 181 | - License: Python Software Foundation License 182 | - License Text: https://docs.python.org/3/license.html 183 | - Acknowledgment: This project uses modules like `os`, `shutil`, `tkinter`, `subprocess`, `json`, `time`, and `sys`, licensed under the Python Software Foundation License. 184 | 185 | 2. **pynput** 186 | - License: MIT License 187 | - License Text: https://opensource.org/licenses/MIT 188 | - Acknowledgment: This project uses `pynput` under the MIT License. 189 | 190 | 3. **win32com.client** 191 | - License: Python Software Foundation License 192 | - License Text: https://docs.python.org/3/license.html 193 | - Acknowledgment: This project uses `win32com.client` under the Python Software Foundation License. 194 | 195 | 4. **PIL (Python Imaging Library)** 196 | - License: MIT License 197 | - License Text: https://opensource.org/licenses/MIT 198 | - Acknowledgment: This project uses `PIL` under the MIT License. 199 | 200 | 5. **keyboard** 201 | - License: MIT License 202 | - License Text: https://opensource.org/licenses/MIT 203 | - Acknowledgment: This project uses `keyboard` under the MIT License. 204 | 205 | 6. **Winshell** 206 | - License: MIT License 207 | - License Text: https://opensource.org/licenses/MIT 208 | - Acknowledgment: This project uses `Winshell` under the MIT License. 209 | 210 | 7. **AutoHotkey Interception** 211 | - License: MIT License 212 | - License Text: https://opensource.org/licenses/MIT 213 | - Acknowledgment: This project uses `AutoHotkey Interception` under the MIT License. 214 | 215 | 8. **Interception** 216 | - License: LGPL 3.0 License 217 | - License Text: https://www.gnu.org/licenses/lgpl-3.0.en.html#license-text 218 | - Acknowledgment: This project uses `Interception` under the LGPL 3.0 License. 219 | - Source Code: https://github.com/oblitum/Interception 220 | 221 | END OF TERMS AND CONDITIONS 222 | 223 | Copyright 2024 Fajar Rahmad Jaya 224 | 225 | Licensed under the Apache License, Version 2.0 (the "License"); 226 | you may not use this file except in compliance with the License. 227 | You may obtain a copy of the License at 228 | 229 | http://www.apache.org/licenses/LICENSE-2.0 230 | 231 | Unless required by applicable law or agreed to in writing, software 232 | distributed under the License is distributed on an "AS IS" BASIS, 233 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 234 | See the License for the specific language governing permissions and 235 | limitations under the License. 236 | -------------------------------------------------------------------------------- /src/utility/constant.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | # Directory 6 | script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 7 | data_dir = os.path.join(script_dir, '_internal', 'Data') 8 | appdata_dir = os.path.join(os.getenv('APPDATA'), 'KeyTik') 9 | 10 | # General 11 | current_version = "v2.3.2" 12 | condition_path = os.path.join(appdata_dir, "path.json") 13 | theme_path = os.path.join(appdata_dir, "theme.json") 14 | dont_show_path = os.path.join(appdata_dir, "dont_show.json") 15 | exit_keys_file = os.path.join(appdata_dir, "exit_keys.json") 16 | pinned_file = os.path.join(appdata_dir, "pinned_profiles.json") 17 | icon_path = os.path.join(data_dir, "icon.ico") 18 | keylist_path = os.path.join(data_dir, "key_list.json") 19 | interception_install_path = os.path.join(data_dir, "inter_install.bat") 20 | interception_uninstall_path = os.path.join(data_dir, "inter_uninstall.bat") 21 | driver_path = os.path.join(os.getenv('SystemRoot'), "System32", "drivers", "interception.sys") # noqa 22 | 23 | # Cache 24 | welcome_cache = {} 25 | 26 | # Unicode 27 | unicode_blocks = [ 28 | (0x0000, 0x007F, "Basic Latin"), 29 | (0x0080, 0x00FF, "Latin-1 Supplement"), 30 | (0x0100, 0x017F, "Latin Extended-A"), 31 | (0x0180, 0x024F, "Latin Extended-B"), 32 | (0x0250, 0x02AF, "IPA Extensions"), 33 | (0x02B0, 0x02FF, "Spacing Modifier Letters"), 34 | (0x0300, 0x036F, "Combining Diacritical Marks"), 35 | (0x0370, 0x03FF, "Greek and Coptic"), 36 | (0x0400, 0x04FF, "Cyrillic"), 37 | (0x0500, 0x052F, "Cyrillic Supplement"), 38 | (0x0530, 0x058F, "Armenian"), 39 | (0x0590, 0x05FF, "Hebrew"), 40 | (0x0600, 0x06FF, "Arabic"), 41 | (0x0700, 0x074F, "Syriac"), 42 | (0x0750, 0x077F, "Arabic Supplement"), 43 | (0x0780, 0x07BF, "Thaana"), 44 | (0x07C0, 0x07FF, "NKo"), 45 | (0x0800, 0x083F, "Samaritan"), 46 | (0x0840, 0x085F, "Mandaic"), 47 | (0x0860, 0x086F, "Syriac Supplement"), 48 | (0x08A0, 0x08FF, "Arabic Extended-A"), 49 | (0x0900, 0x097F, "Devanagari"), 50 | (0x0980, 0x09FF, "Bengali"), 51 | (0x0A00, 0x0A7F, "Gurmukhi"), 52 | (0x0A80, 0x0AFF, "Gujarati"), 53 | (0x0B00, 0x0B7F, "Oriya"), 54 | (0x0B80, 0x0BFF, "Tamil"), 55 | (0x0C00, 0x0C7F, "Telugu"), 56 | (0x0C80, 0x0CFF, "Kannada"), 57 | (0x0D00, 0x0D7F, "Malayalam"), 58 | (0x0D80, 0x0DFF, "Sinhala"), 59 | (0x0E00, 0x0E7F, "Thai"), 60 | (0x0E80, 0x0EFF, "Lao"), 61 | (0x0F00, 0x0FFF, "Tibetan"), 62 | (0x1000, 0x109F, "Myanmar"), 63 | (0x10A0, 0x10FF, "Georgian"), 64 | (0x1100, 0x11FF, "Hangul Jamo"), 65 | (0x1200, 0x137F, "Ethiopic"), 66 | (0x1380, 0x139F, "Ethiopic Supplement"), 67 | (0x13A0, 0x13FF, "Cherokee"), 68 | (0x1400, 0x167F, "Unified Canadian Aboriginal Syllabics"), 69 | (0x1680, 0x169F, "Ogham"), 70 | (0x16A0, 0x16FF, "Runic"), 71 | (0x1700, 0x171F, "Tagalog"), 72 | (0x1720, 0x173F, "Hanunoo"), 73 | (0x1740, 0x175F, "Buhid"), 74 | (0x1760, 0x177F, "Tagbanwa"), 75 | (0x1780, 0x17FF, "Khmer"), 76 | (0x1800, 0x18AF, "Mongolian"), 77 | (0x18B0, 0x18FF, "Unified Canadian Aboriginal Syllabics Extended"), 78 | (0x1900, 0x194F, "Limbu"), 79 | (0x1950, 0x197F, "Tai Le"), 80 | (0x1980, 0x19DF, "New Tai Lue"), 81 | (0x19E0, 0x19FF, "Khmer Symbols"), 82 | (0x1A00, 0x1A1F, "Buginese"), 83 | (0x1A20, 0x1AAF, "Tai Tham"), 84 | (0x1AB0, 0x1AFF, "Combining Diacritical Marks Extended"), 85 | (0x1B00, 0x1B7F, "Balinese"), 86 | (0x1B80, 0x1BBF, "Sundanese"), 87 | (0x1BC0, 0x1BFF, "Batak"), 88 | (0x1C00, 0x1C4F, "Lepcha"), 89 | (0x1C50, 0x1C7F, "Ol Chiki"), 90 | (0x1C80, 0x1C8F, "Cyrillic Extended-C"), 91 | (0x1C90, 0x1CBF, "Georgian Extended"), 92 | (0x1CC0, 0x1CCF, "Sundanese Supplement"), 93 | (0x1CD0, 0x1CFF, "Vedic Extensions"), 94 | (0x1D00, 0x1D7F, "Phonetic Extensions"), 95 | (0x1D80, 0x1DBF, "Phonetic Extensions Supplement"), 96 | (0x1DC0, 0x1DFF, "Combining Diacritical Marks Supplement"), 97 | (0x1E00, 0x1EFF, "Latin Extended Additional"), 98 | (0x1F00, 0x1FFF, "Greek Extended"), 99 | (0x2000, 0x206F, "General Punctuation"), 100 | (0x2070, 0x209F, "Superscripts and Subscripts"), 101 | (0x20A0, 0x20CF, "Currency Symbols"), 102 | (0x20D0, 0x20FF, "Combining Diacritical Marks for Symbols"), 103 | (0x2100, 0x214F, "Letterlike Symbols"), 104 | (0x2150, 0x218F, "Number Forms"), 105 | (0x2190, 0x21FF, "Arrows"), 106 | (0x2200, 0x22FF, "Mathematical Operators"), 107 | (0x2300, 0x23FF, "Miscellaneous Technical"), 108 | (0x2400, 0x243F, "Control Pictures"), 109 | (0x2440, 0x245F, "Optical Character Recognition"), 110 | (0x2460, 0x24FF, "Enclosed Alphanumerics"), 111 | (0x2500, 0x257F, "Box Drawing"), 112 | (0x2580, 0x259F, "Block Elements"), 113 | (0x25A0, 0x25FF, "Geometric Shapes"), 114 | (0x2600, 0x26FF, "Miscellaneous Symbols"), 115 | (0x2700, 0x27BF, "Dingbats"), 116 | (0x27C0, 0x27EF, "Miscellaneous Mathematical Symbols-A"), 117 | (0x27F0, 0x27FF, "Supplemental Arrows-A"), 118 | (0x2800, 0x28FF, "Braille Patterns"), 119 | (0x2900, 0x297F, "Supplemental Arrows-B"), 120 | (0x2980, 0x29FF, "Miscellaneous Mathematical Symbols-B"), 121 | (0x2A00, 0x2AFF, "Supplemental Mathematical Operators"), 122 | (0x2B00, 0x2BFF, "Miscellaneous Symbols and Arrows"), 123 | (0x2C00, 0x2C5F, "Glagolitic"), 124 | (0x2C60, 0x2C7F, "Latin Extended-C"), 125 | (0x2C80, 0x2CFF, "Coptic"), 126 | (0x2D00, 0x2D2F, "Georgian Supplement"), 127 | (0x2D30, 0x2D7F, "Tifinagh"), 128 | (0x2D80, 0x2DDF, "Ethiopic Extended"), 129 | (0x2DE0, 0x2DFF, "Cyrillic Extended-A"), 130 | (0x2E00, 0x2E7F, "Supplemental Punctuation"), 131 | (0x2E80, 0x2EFF, "CJK Radicals Supplement"), 132 | (0x2F00, 0x2FDF, "Kangxi Radicals"), 133 | (0x2FF0, 0x2FFF, "Ideographic Description Characters"), 134 | (0x3000, 0x303F, "CJK Symbols and Punctuation"), 135 | (0x3040, 0x309F, "Hiragana"), 136 | (0x30A0, 0x30FF, "Katakana"), 137 | (0x3100, 0x312F, "Bopomofo"), 138 | (0x3130, 0x318F, "Hangul Compatibility Jamo"), 139 | (0x3190, 0x319F, "Kanbun"), 140 | (0x31A0, 0x31BF, "Bopomofo Extended"), 141 | (0x31C0, 0x31EF, "CJK Strokes"), 142 | (0x31F0, 0x31FF, "Katakana Phonetic Extensions"), 143 | (0x3200, 0x32FF, "Enclosed CJK Letters and Months"), 144 | (0x3300, 0x33FF, "CJK Compatibility"), 145 | (0x3400, 0x4DBF, "CJK Unified Ideographs Extension A"), 146 | (0x4DC0, 0x4DFF, "Yijing Hexagram Symbols"), 147 | (0x4E00, 0x9FFF, "CJK Unified Ideographs"), 148 | (0xA000, 0xA48F, "Yi Syllables"), 149 | (0xA490, 0xA4CF, "Yi Radicals"), 150 | (0xA4D0, 0xA4FF, "Lisu"), 151 | (0xA500, 0xA63F, "Vai"), 152 | (0xA640, 0xA69F, "Cyrillic Extended-B"), 153 | (0xA6A0, 0xA6FF, "Bamum"), 154 | (0xA700, 0xA71F, "Modifier Tone Letters"), 155 | (0xA720, 0xA7FF, "Latin Extended-D"), 156 | (0xA800, 0xA82F, "Syloti Nagri"), 157 | (0xA830, 0xA83F, "Common Indic Number Forms"), 158 | (0xA840, 0xA87F, "Phags-pa"), 159 | (0xA880, 0xA8DF, "Saurashtra"), 160 | (0xA8E0, 0xA8FF, "Devanagari Extended"), 161 | (0xA900, 0xA92F, "Kayah Li"), 162 | (0xA930, 0xA95F, "Rejang"), 163 | (0xA960, 0xA97F, "Hangul Jamo Extended-A"), 164 | (0xA980, 0xA9DF, "Javanese"), 165 | (0xA9E0, 0xA9FF, "Myanmar Extended-B"), 166 | (0xAA00, 0xAA5F, "Cham"), 167 | (0xAA60, 0xAA7F, "Myanmar Extended-A"), 168 | (0xAA80, 0xAADF, "Tai Viet"), 169 | (0xAAE0, 0xAAFF, "Meetei Mayek Extensions"), 170 | (0xAB00, 0xAB2F, "Ethiopic Extended-A"), 171 | (0xAB30, 0xAB6F, "Latin Extended-E"), 172 | (0xAB70, 0xABBF, "Cherokee Supplement"), 173 | (0xABC0, 0xABFF, "Meetei Mayek"), 174 | (0xAC00, 0xD7AF, "Hangul Syllables"), 175 | (0xD7B0, 0xD7FF, "Hangul Jamo Extended-B"), 176 | (0xD800, 0xDB7F, "High Surrogates"), 177 | (0xDB80, 0xDBFF, "High Private Use Surrogates"), 178 | (0xDC00, 0xDFFF, "Low Surrogates"), 179 | (0xE000, 0xF8FF, "Private Use Area"), 180 | (0xF900, 0xFAFF, "CJK Compatibility Ideographs"), 181 | (0xFB00, 0xFB4F, "Alphabetic Presentation Forms"), 182 | (0xFB50, 0xFDFF, "Arabic Presentation Forms-A"), 183 | (0xFE00, 0xFE0F, "Variation Selectors"), 184 | (0xFE10, 0xFE1F, "Vertical Forms"), 185 | (0xFE20, 0xFE2F, "Combining Half Marks"), 186 | (0xFE30, 0xFE4F, "CJK Compatibility Forms"), 187 | (0xFE50, 0xFE6F, "Small Form Variants"), 188 | (0xFE70, 0xFEFF, "Arabic Presentation Forms-B"), 189 | (0xFF00, 0xFFEF, "Halfwidth and Fullwidth Forms"), 190 | (0xFFF0, 0xFFFF, "Specials"), 191 | (0x10000, 0x1007F, "Linear B Syllabary"), 192 | (0x10080, 0x100FF, "Linear B Ideograms"), 193 | (0x10100, 0x1013F, "Aegean Numbers"), 194 | (0x10140, 0x1018F, "Ancient Greek Numbers"), 195 | (0x10190, 0x101CF, "Ancient Symbols"), 196 | (0x101D0, 0x101FF, "Phaistos Disc"), 197 | (0x10280, 0x1029F, "Lycian"), 198 | (0x102A0, 0x102DF, "Carian"), 199 | (0x102E0, 0x102FF, "Coptic Epact Numbers"), 200 | (0x10300, 0x1032F, "Old Italic"), 201 | (0x10330, 0x1034F, "Gothic"), 202 | (0x10350, 0x1037F, "Old Permic"), 203 | (0x10380, 0x1039F, "Ugaritic"), 204 | (0x103A0, 0x103DF, "Old Persian"), 205 | (0x10400, 0x1044F, "Deseret"), 206 | (0x10450, 0x1047F, "Shavian"), 207 | (0x10480, 0x104AF, "Osmanya"), 208 | (0x104B0, 0x104FF, "Osage"), 209 | (0x10500, 0x1052F, "Elbasan"), 210 | (0x10530, 0x1056F, "Caucasian Albanian"), 211 | (0x10600, 0x1077F, "Linear A"), 212 | (0x10800, 0x1083F, "Cypriot Syllabary"), 213 | (0x10840, 0x1085F, "Imperial Aramaic"), 214 | (0x10860, 0x1087F, "Palmyrene"), 215 | (0x10880, 0x108AF, "Nabataean"), 216 | (0x108E0, 0x108FF, "Hatran"), 217 | (0x10900, 0x1091F, "Phoenician"), 218 | (0x10920, 0x1093F, "Lydian"), 219 | (0x10980, 0x1099F, "Meroitic Hieroglyphs"), 220 | (0x109A0, 0x109FF, "Meroitic Cursive"), 221 | (0x10A00, 0x10A5F, "Kharoshthi"), 222 | (0x10A60, 0x10A7F, "Old South Arabian"), 223 | (0x10A80, 0x10A9F, "Old North Arabian"), 224 | (0x10AC0, 0x10AFF, "Manichaean"), 225 | (0x10B00, 0x10B3F, "Avestan"), 226 | (0x10B40, 0x10B5F, "Inscriptional Parthian"), 227 | (0x10B60, 0x10B7F, "Inscriptional Pahlavi"), 228 | (0x10B80, 0x10BAF, "Psalter Pahlavi"), 229 | (0x10C00, 0x10C4F, "Old Turkic"), 230 | (0x10C80, 0x10CFF, "Old Hungarian"), 231 | (0x10D00, 0x10D3F, "Hanifi Rohingya"), 232 | (0x10E60, 0x10E7F, "Rumi Numeral Symbols"), 233 | (0x10E80, 0x10EBF, "Yezidi"), 234 | (0x10F00, 0x10F2F, "Old Sogdian"), 235 | (0x10F30, 0x10F6F, "Sogdian"), 236 | (0x10FB0, 0x10FDF, "Chorasmian"), 237 | (0x10FE0, 0x10FFF, "Elymaic"), 238 | (0x11000, 0x1107F, "Brahmi"), 239 | (0x11080, 0x110CF, "Kaithi"), 240 | (0x110D0, 0x110FF, "Sora Sompeng"), 241 | (0x11100, 0x1114F, "Chakma"), 242 | (0x11150, 0x1117F, "Mahajani"), 243 | (0x11180, 0x111DF, "Sharada"), 244 | (0x111E0, 0x111FF, "Sinhala Archaic Numbers"), 245 | (0x11200, 0x1124F, "Khojki"), 246 | (0x11280, 0x112AF, "Multani"), 247 | (0x112B0, 0x112FF, "Khudawadi"), 248 | (0x11300, 0x1137F, "Grantha"), 249 | (0x11400, 0x1147F, "Newa"), 250 | (0x11480, 0x114DF, "Tirhuta"), 251 | (0x11580, 0x115FF, "Siddham"), 252 | (0x11600, 0x1165F, "Modi"), 253 | (0x11660, 0x1167F, "Mongolian Supplement"), 254 | (0x11680, 0x116CF, "Takri"), 255 | (0x11700, 0x1173F, "Ahom"), 256 | (0x11800, 0x1184F, "Dogra"), 257 | (0x118A0, 0x118FF, "Warang Citi"), 258 | (0x11900, 0x1195F, "Dives Akuru"), 259 | (0x119A0, 0x119FF, "Nandinagari"), 260 | (0x11A00, 0x11A4F, "Zanabazar Square"), 261 | (0x11A50, 0x11AAF, "Soyombo"), 262 | (0x11AC0, 0x11AFF, "Pau Cin Hau"), 263 | (0x11C00, 0x11C6F, "Bhaiksuki"), 264 | (0x11C70, 0x11CBF, "Marchen"), 265 | (0x11D00, 0x11D5F, "Masaram Gondi"), 266 | (0x11D60, 0x11DAF, "Gunjala Gondi"), 267 | (0x11EE0, 0x11EFF, "Makasar"), 268 | (0x11FB0, 0x11FBF, "Lisu Supplement"), 269 | (0x11FC0, 0x11FFF, "Tamil Supplement"), 270 | (0x12000, 0x123FF, "Cuneiform"), 271 | (0x12400, 0x1247F, "Cuneiform Numbers and Punctuation"), 272 | (0x12480, 0x1254F, "Early Dynastic Cuneiform"), 273 | (0x13000, 0x1342F, "Egyptian Hieroglyphs"), 274 | (0x13430, 0x1343F, "Egyptian Hieroglyph Format Controls"), 275 | (0x14400, 0x1467F, "Anatolian Hieroglyphs"), 276 | (0x16800, 0x16A3F, "Bamum Supplement"), 277 | (0x16A40, 0x16A6F, "Mro"), 278 | (0x16AD0, 0x16AFF, "Bassa Vah"), 279 | (0x16B00, 0x16B8F, "Pahawh Hmong"), 280 | (0x16E40, 0x16E9F, "Medefaidrin"), 281 | (0x16F00, 0x16F9F, "Miao"), 282 | (0x16FE0, 0x16FFF, "Ideographic Symbols and Punctuation"), 283 | (0x17000, 0x187FF, "Tangut"), 284 | (0x18800, 0x18AFF, "Tangut Components"), 285 | (0x18B00, 0x18CFF, "Khitan Small Script"), 286 | (0x18D00, 0x18D8F, "Tangut Supplement"), 287 | (0x1B000, 0x1B0FF, "Kana Supplement"), 288 | (0x1B100, 0x1B12F, "Kana Extended-A"), 289 | (0x1B130, 0x1B16F, "Small Kana Extension"), 290 | (0x1B170, 0x1B2FF, "Nushu"), 291 | (0x1BC00, 0x1BC9F, "Duployan"), 292 | (0x1BCA0, 0x1BCAF, "Shorthand Format Controls"), 293 | (0x1D000, 0x1D0FF, "Byzantine Musical Symbols"), 294 | (0x1D100, 0x1D1FF, "Musical Symbols"), 295 | (0x1D200, 0x1D24F, "Ancient Greek Musical Notation"), 296 | (0x1D2E0, 0x1D2FF, "Mayan Numerals"), 297 | (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"), 298 | (0x1D360, 0x1D37F, "Counting Rod Numerals"), 299 | (0x1D400, 0x1D7FF, "Mathematical Alphanumeric Symbols"), 300 | (0x1D800, 0x1DAAF, "Sutton SignWriting"), 301 | (0x1E000, 0x1E02F, "Glagolitic Supplement"), 302 | (0x1E100, 0x1E14F, "Nyiakeng Puachue Hmong"), 303 | (0x1E2C0, 0x1E2FF, "Wancho"), 304 | (0x1E800, 0x1E8DF, "Mende Kikakui"), 305 | (0x1E900, 0x1E95F, "Adlam"), 306 | (0x1EC70, 0x1ECBF, "Indic Siyaq Numbers"), 307 | (0x1ED00, 0x1ED4F, "Ottoman Siyaq Numbers"), 308 | (0x1EE00, 0x1EEFF, "Arabic Mathematical Alphabetic Symbols"), 309 | (0x1F000, 0x1F02F, "Mahjong Tiles"), 310 | (0x1F030, 0x1F09F, "Domino Tiles"), 311 | (0x1F0A0, 0x1F0FF, "Playing Cards"), 312 | (0x1F100, 0x1F1FF, "Enclosed Alphanumeric Supplement"), 313 | (0x1F200, 0x1F2FF, "Enclosed Ideographic Supplement"), 314 | (0x1F300, 0x1F5FF, "Miscellaneous Symbols and Pictographs"), 315 | (0x1F600, 0x1F64F, "Emoticons"), 316 | (0x1F650, 0x1F67F, "Ornamental Dingbats"), 317 | (0x1F680, 0x1F6FF, "Transport and Map Symbols"), 318 | (0x1F700, 0x1F77F, "Alchemical Symbols"), 319 | (0x1F780, 0x1F7FF, "Geometric Shapes Extended"), 320 | (0x1F800, 0x1F8FF, "Supplemental Arrows-C"), 321 | (0x1F900, 0x1F9FF, "Supplemental Symbols and Pictographs"), 322 | (0x1FA00, 0x1FA6F, "Chess Symbols"), 323 | (0x1FA70, 0x1FAFF, "Symbols and Pictographs Extended-A"), 324 | (0x1FB00, 0x1FBFF, "Symbols for Legacy Computing"), 325 | (0x20000, 0x2A6DF, "CJK Unified Ideographs Extension B"), 326 | (0x2A700, 0x2B73F, "CJK Unified Ideographs Extension C"), 327 | (0x2B740, 0x2B81F, "CJK Unified Ideographs Extension D"), 328 | (0x2B820, 0x2CEAF, "CJK Unified Ideographs Extension E"), 329 | (0x2CEB0, 0x2EBEF, "CJK Unified Ideographs Extension F"), 330 | (0x2F800, 0x2FA1F, "CJK Compatibility Ideographs Supplement"), 331 | (0x30000, 0x3134F, "CJK Unified Ideographs Extension G"), 332 | (0xE0000, 0xE007F, "Tags"), 333 | (0xE0100, 0xE01EF, "Variation Selectors Supplement"), 334 | (0xF0000, 0xFFFFF, "Supplementary Private Use Area-A"), 335 | (0x100000, 0x10FFFF, "Supplementary Private Use Area-B"), 336 | ] 337 | -------------------------------------------------------------------------------- /src/ui/edit_script/edit_script_logic.py: -------------------------------------------------------------------------------- 1 | import os 2 | import keyboard 3 | import ctypes 4 | import time 5 | from pynput import mouse 6 | from PySide6.QtWidgets import (QMessageBox) 7 | from PySide6.QtCore import QTimer, Signal, QObject, QEvent 8 | from utility.constant import (interception_install_path) 9 | 10 | 11 | class InputBlocker(QObject): 12 | def eventFilter(self, obj, event): 13 | if event.type() in (QEvent.MouseButtonPress, QEvent.MouseButtonRelease, 14 | QEvent.KeyPress, QEvent.KeyRelease, 15 | QEvent.FocusIn, QEvent.FocusOut): 16 | return True 17 | if event.type() in (QEvent.Close, QEvent.WindowDeactivate, 18 | QEvent.Hide, QEvent.Leave): 19 | return True 20 | return False 21 | 22 | 23 | class EditScriptLogic(QObject): 24 | request_timer_start = Signal(object) 25 | 26 | def __init__(self): 27 | super().__init__() 28 | self.request_timer_start.connect(self.release_timer) 29 | 30 | def check_interception_driver(self): 31 | driver_path = r"C:\Windows\System32\drivers\keyboard.sys" 32 | 33 | if os.path.exists(driver_path): 34 | return True 35 | else: 36 | reply = QMessageBox.question( 37 | None, 38 | "Driver Not Found", 39 | "Interception driver is not installed. This driver is required to use assign on specific device feature.\n \n \nNote: Restart your device after installation.\n" # noqa 40 | "Would you like to install it now?", 41 | QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No 42 | ) 43 | 44 | if reply == QMessageBox.StandardButton.Yes: 45 | try: 46 | if os.path.exists(interception_install_path): 47 | 48 | install_dir = (os.path.dirname 49 | (interception_install_path)) 50 | 51 | ctypes.windll.shell32.ShellExecuteW( 52 | None, 53 | "runas", 54 | "cmd.exe", 55 | f"/k cd /d {install_dir} && {os.path.basename(interception_install_path)}", # noqa 56 | None, 57 | 1 58 | ) 59 | else: 60 | QMessageBox.critical( 61 | None, 62 | "Installation Failed", 63 | "Installation script not found. Please check your installation." # noqa 64 | ) 65 | except Exception as e: 66 | QMessageBox.critical(None, "Error", f"An error occurred during installation: {str(e)}") # noqa 67 | return False 68 | 69 | def update_entry(self): 70 | shortcut_combination = '+'.join(self.pressed_keys) 71 | if hasattr(self, "active_entry") and self.active_entry is not None: 72 | self.active_entry.setText(shortcut_combination) 73 | 74 | def get_script_name(self): 75 | script_name = self.script_name_entry.text().strip() 76 | if not script_name: 77 | QMessageBox.warning(None, "Input Error", "Please enter a Profile name.") # noqa 78 | return None 79 | 80 | if not script_name.endswith('.ahk'): 81 | script_name += '.ahk' 82 | 83 | return script_name 84 | 85 | def is_widget_valid(self, widget_tuple): 86 | try: 87 | entry_widget, button_widget = widget_tuple 88 | return entry_widget is not None and button_widget is not None 89 | except Exception: 90 | return False 91 | 92 | def key_listening(self, entry_widget, button): 93 | def toggle_other_buttons(state): 94 | if hasattr(self, 'key_rows'): 95 | for key_row in self.key_rows: 96 | (_, _, orig_button, remap_button, _, _, _) = key_row 97 | 98 | if orig_button != button and orig_button is not None: 99 | orig_button.setEnabled(state) 100 | if remap_button != button and remap_button is not None: 101 | remap_button.setEnabled(state) 102 | 103 | if hasattr(self, 'copas_rows'): 104 | for copas_row in self.copas_rows: 105 | (_, _, copy_button, paste_button, _, _) = copas_row 106 | 107 | if copy_button != button and copy_button is not None: 108 | copy_button.setEnabled(state) 109 | if paste_button != button and paste_button is not None: 110 | paste_button.setEnabled(state) 111 | 112 | for _, shortcut_button in self.shortcut_rows: 113 | if shortcut_button != button and shortcut_button is not None: 114 | shortcut_button.setEnabled(state) 115 | 116 | if not self.is_listening: 117 | 118 | self.is_listening = True 119 | self.active_entry = entry_widget 120 | self.previous_button_text = button.text() 121 | 122 | self.use_scan_code = False 123 | if hasattr(self, 'key_rows'): 124 | for key_row in self.key_rows: 125 | (orig_entry, remap_entry, orig_button, _, _, _, 126 | hold_interval_entry) = key_row 127 | 128 | if button == orig_button: 129 | parent_widget = button.parent() 130 | if parent_widget: 131 | sc_checkboxes = [child for child 132 | in parent_widget. 133 | findChildren(QObject) 134 | if child.objectName() == 135 | "sc_checkbox"] 136 | if sc_checkboxes: 137 | self.use_scan_code = ( 138 | sc_checkboxes[0].isChecked()) 139 | break 140 | 141 | entries_to_disable = [] 142 | if hasattr(self, 'script_name_entry'): 143 | entries_to_disable.append((self.script_name_entry, None)) 144 | if hasattr(self, 'keyboard_entry'): 145 | entries_to_disable.append((self.keyboard_entry, None)) 146 | if hasattr(self, 'program_entry'): 147 | entries_to_disable.append((self.program_entry, None)) 148 | 149 | if hasattr(self, 'key_rows'): 150 | for key_row in self.key_rows: 151 | ( 152 | orig_entry, remap_entry, _, _, _, _, 153 | hold_interval_entry 154 | ) = key_row 155 | entries_to_disable.append((orig_entry, None)) 156 | entries_to_disable.append((remap_entry, None)) 157 | entries_to_disable.append((hold_interval_entry, None)) 158 | 159 | if hasattr(self, 'shortcut_rows'): 160 | for shortcut_entry, _ in self.shortcut_rows: 161 | entries_to_disable.append((shortcut_entry, None)) 162 | 163 | if hasattr(self, 'copas_rows'): 164 | for copas_row in self.copas_rows: 165 | copy_entry, paste_entry, _, _, _, _ = copas_row 166 | entries_to_disable.append((copy_entry, None)) 167 | entries_to_disable.append((paste_entry, None)) 168 | 169 | self.disable_input(entries_to_disable) 170 | 171 | if hasattr(self, "edit_window"): 172 | if not hasattr(self, "_window_blocker"): 173 | self._window_blocker = InputBlocker() 174 | self.edit_window.installEventFilter(self._window_blocker) 175 | 176 | self.ignore_next_click = True 177 | toggle_other_buttons(False) 178 | 179 | button.clicked.disconnect() 180 | button.clicked.connect(lambda: self.key_listening 181 | (entry_widget, button)) 182 | 183 | self.currently_pressed_keys = [] 184 | self.last_combination = "" 185 | self.release_timer = QTimer() 186 | self.release_timer.setSingleShot(True) 187 | self.release_timer.timeout.connect(lambda: 188 | self.finalize_combination 189 | (entry_widget)) 190 | keyboard.hook(lambda event: self.multi_key_event 191 | (event, entry_widget, button)) 192 | 193 | else: 194 | self.is_listening = False 195 | self.active_entry = None 196 | 197 | entries_to_enable = [] 198 | if hasattr(self, 'script_name_entry'): 199 | entries_to_enable.append((self.script_name_entry, None)) 200 | 201 | if hasattr(self, 'keyboard_entry'): 202 | entries_to_enable.append((self.keyboard_entry, None)) 203 | 204 | if hasattr(self, 'program_entry'): 205 | entries_to_enable.append((self.program_entry, None)) 206 | 207 | if hasattr(self, 'key_rows'): 208 | for key_row in self.key_rows: 209 | ( 210 | orig_entry, remap_entry, _, _, _, _, 211 | hold_interval_entry 212 | ) = key_row 213 | entries_to_enable.append((orig_entry, None)) 214 | entries_to_enable.append((remap_entry, None)) 215 | entries_to_enable.append((hold_interval_entry, None)) 216 | 217 | if hasattr(self, 'shortcut_rows'): 218 | for shortcut_entry, _ in self.shortcut_rows: 219 | entries_to_enable.append((shortcut_entry, None)) 220 | 221 | if hasattr(self, 'copas_rows'): 222 | for copas_row in self.copas_rows: 223 | copy_entry, paste_entry, _, _, _, _ = copas_row 224 | entries_to_enable.append((copy_entry, None)) 225 | entries_to_enable.append((paste_entry, None)) 226 | 227 | self.enable_input(entries_to_enable) 228 | toggle_other_buttons(True) 229 | 230 | if hasattr(self, "edit_window") and hasattr( 231 | self, "_window_blocker"): 232 | self.edit_window.removeEventFilter(self._window_blocker) 233 | 234 | if button is not None: 235 | button.clicked.disconnect() 236 | button.clicked.connect(lambda: self.key_listening 237 | (entry_widget, button)) 238 | 239 | def multi_key_event(self, event, entry_widget, button): 240 | if not self.is_listening or self.active_entry != entry_widget: 241 | return 242 | 243 | if hasattr(self, 'use_scan_code') and self.use_scan_code: 244 | key = f"SC{event.scan_code:02X}" 245 | else: 246 | key = event.name 247 | 248 | if (len(key) == 1 and key.isupper() and key.isalpha()): 249 | key = key.lower() 250 | 251 | if event.event_type == "down": 252 | if key not in self.currently_pressed_keys: 253 | self.currently_pressed_keys.append(key) 254 | self.update_widget(entry_widget) 255 | if (hasattr(self, "release_timer") 256 | and self.release_timer.isActive()): 257 | self.release_timer.stop() 258 | 259 | elif event.event_type == "up": 260 | if key in self.currently_pressed_keys: 261 | self.currently_pressed_keys.remove(key) 262 | if not self.currently_pressed_keys: 263 | self.key_listening(entry_widget, button) 264 | self.request_timer_start.emit(entry_widget) 265 | 266 | else: 267 | if hasattr(self, "release_timer"): 268 | self.request_timer_start.emit(entry_widget) 269 | 270 | def mouse_listening(self, x, y, button, pressed): 271 | if self.is_listening and self.active_entry: 272 | if pressed: 273 | if self.ignore_next_click and button == mouse.Button.left: 274 | self.ignore_next_click = False 275 | return 276 | 277 | if button == mouse.Button.left: 278 | mouse_button = "Left Button" 279 | elif button == mouse.Button.right: 280 | mouse_button = "Right Button" 281 | elif button == mouse.Button.middle: 282 | mouse_button = "Middle Button" 283 | else: 284 | mouse_button = button.name 285 | 286 | current_time = time.time() 287 | 288 | if current_time - self.last_key_time > self.timeout: 289 | self.pressed_keys = [] 290 | 291 | if mouse_button not in self.pressed_keys: 292 | self.pressed_keys.append(mouse_button) 293 | self.update_entry() 294 | 295 | self.last_key_time = current_time 296 | 297 | else: 298 | if button == mouse.Button.left: 299 | mouse_button = "Left Button" 300 | elif button == mouse.Button.right: 301 | mouse_button = "Right Button" 302 | elif button == mouse.Button.middle: 303 | mouse_button = "Middle Button" 304 | else: 305 | mouse_button = button.name 306 | 307 | if mouse_button in self.pressed_keys: 308 | self.pressed_keys.remove(mouse_button) 309 | if not self.pressed_keys: 310 | self.key_listening(self.active_entry, None) 311 | self.request_timer_start.emit(self.active_entry) 312 | else: 313 | if hasattr(self, "release_timer"): 314 | self.request_timer_start.emit(self.active_entry) 315 | 316 | def release_timer(self, entry_widget): 317 | if hasattr(self, "release_timer"): 318 | self.release_timer.start(400) 319 | 320 | def format_key_combo(self, keys): 321 | def format_key(k): 322 | if len(k) == 1 and k.islower(): 323 | return k 324 | return k[:1].upper() + k[1:] if k else k 325 | 326 | if isinstance(keys, (list, set)): 327 | keys = list(keys) 328 | if len(keys) == 1: 329 | return format_key(keys[0]) 330 | return ' + '.join(format_key(k) for k in keys) 331 | 332 | def update_widget(self, entry_widget): 333 | combo = self.format_key_combo(self.currently_pressed_keys) 334 | entry_widget.setText(combo) 335 | self.last_combination = combo 336 | 337 | def finalize_combination(self, entry_widget): 338 | entry_widget.setText(self.last_combination) 339 | self.currently_pressed_keys = set() 340 | 341 | def disable_input(self, entry_rows): 342 | if not hasattr(self, "_input_blocker"): 343 | self._input_blocker = InputBlocker() 344 | for entry_tuple in entry_rows: 345 | entry = entry_tuple[0] 346 | if entry is not None: 347 | entry.installEventFilter(self._input_blocker) 348 | 349 | def enable_input(self, entry_rows): 350 | if hasattr(self, "_input_blocker"): 351 | for entry_tuple in entry_rows: 352 | entry = entry_tuple[0] 353 | if entry is not None: 354 | entry.removeEventFilter(self._input_blocker) 355 | 356 | def replace_raw_keys(self, key, key_map): 357 | return key_map.get(key, key) 358 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
51 |
75 |
76 |
89 |
90 |
99 |
100 |
110 |
111 |
120 |
121 |
195 |
198 |
201 |
204 |
207 |
210 |
213 |
216 |
219 |