├── 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 |
2 | KeyTik Icon 3 |

4 | 5 | [![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Fajar-RahmadJaya/KeyTik/total?style=plastic&logo=Github&logoColor=white&label=Total%20Downloads&color=white)](https://tooomm.github.io/github-release-stats/?username=Fajar-RahmadJaya&repository=KeyTik) 6 | [![SourceForge Downloads](https://img.shields.io/sourceforge/dt/KeyTik?style=plastic&logo=sourceforge&label=Total%20Downloads&color=orange)](https://sourceforge.net/projects/keytik/files/stats/timeline) 7 | 8 | 9 | 10 | # KeyTik: The All-in-One Automation Tool 11 | 12 | ### A Powerful Multi-Profile Key Mapper, Clicker, Macro, and More. 13 | 14 |
15 | 16 |
17 | 18 | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 19 |
20 | 21 |
22 | 23 | ## Overview 24 | 25 | KeyTik is a Python program that uses AutoHotkey to handle many things, including a powerful key mapper and various macros such as clickers and more. It comes with comprehensive [key support](https://keytik.com/docs/getting-started/key-list/keyboard), such as ASCII, ANSI, Unicode, Scan Code, Virtual Keyboard Code, and more. 26 | 27 | KeyTik is also packed with [features](#features) like Bind to Programs and Devices, Assign Shortcuts, Text Format, Hold Format, and more. 28 | 29 |
30 | 31 | > [!NOTE] 32 | > If you like KeyTik, don't forget to share and give it a star! 33 | 34 |
35 | 36 |
37 | 38 | ## Pro Version 39 | 40 |
41 | 42 | KeyTik Pro version is available at Gumroad at $20 for lifetime purchase. Check out KeyTik Pro at [https://fajarrahmadjaya.gumroad.com/l/keytik-pro](https://fajarrahmadjaya.gumroad.com/l/keytik-pro). 43 | 44 | Pro version has additional features compared to the normal version while not overwhelming it. Think of Pro version as a way to support the developer or as a more user-friendly version of the normal version. Pro version consists of additional UI for the auto clicker, file opener, screen clicker, and additional automation tools such as window manager, easy always-on-top, and multi copy-paste. 45 | 46 | > [!Tip] 47 | > Get it for $15 with 25% off — limited to the first 10 people. (4 people left) Check out [here](https://fajarrahmadjaya.gumroad.com/l/keytik-pro/KeyTikPro25) to claim your discount. 48 | 49 |
50 | Pro Version Preview gif 51 |
52 | 53 |
54 |
55 | Click to see more 56 | 57 | ## KeyTik Pro v4.3.0 Update 58 | **Changelog**: 59 | - Overall UI improvement. 60 | - Improve key list. 61 | - Keyboard and mouse key addition. 62 | - Add virtual keyboard code on key list. 63 | - Ability to detect and use scan code on default key. 64 | - Add ASCII character on key list. 65 | - Add ANSI character on key list. 66 | - Add Unicode character on key list. 67 | - Bug fixes. 68 | 69 | ## What You Get With the Pro Version 70 | On KeyTik Pro, you will get every feature on the normal version (see [Normal Version Features](/docs/introduction/features) for more) plus additional features. Below are the additional features on KeyTik Pro: 71 | 72 | ### Better Auto Clicker 73 | 74 |

75 | Pro Version Auto Clicker 76 |

77 |
78 | 79 | - Shortcuts: What key to press to activate auto clicker. 80 | - Click Interval: Interval between each click. 81 | - Key to press (Mouse Button): What key to press for auto clicker (not just left click or right click but more keys on the keyboard such as all alphabet, shift, ctrl and more) 82 | - Click type: How will the key press behave. Single click, Double Click, Hold. 83 | - Click Location: Where click will pressed. Follow cursor, clicking on cursor. Fixed position, position on screen. 84 | - Click Repeat: How long does the click will pressed. Infinite clicks, stop until it deactivates by shortcuts. Fixed count, click as much as the count specified. 85 | 86 | ### Better Files Opener 87 | 88 |

89 | Pro Version Files Opener 90 |

91 |
92 | 93 | - Using shortcuts to open multiple files. 94 | - You can specify what files to open yourself without limit. 95 | 96 | ### Multi Copy Paste 97 | 98 |

99 | Pro Version Multi Copy Paste 100 |

101 |
102 | 103 | - Make multiple shortcuts for copy and paste. 104 | - Each shortcuts have different room to save copied text. 105 | - For example: First copy shortcuts copied "text1" and second copy shortcuts copied "text2". Pressing second paste shortcuts will paste "text2" and pressing first shortcuts will paste "text1". 106 | 107 | ### Always on Top Manager 108 | 109 |

110 | Pro Version Always on Top Manager 111 |

112 |
113 | 114 | - Make the window where cursor is located always on top with a shortcut or click. 115 | - Shortcuts or clicks can be changed by user. 116 | 117 | ### Window Size Changer 118 | 119 |

120 | Pro Version Window Mnaager 121 |

122 |
123 | 124 | - Change window size with shortcut or click. 125 | - How window will be changed: Full, horizontal half top, horizontal half bottom, vertical half left, vertical half right, quarter top left, quarter top right, quarter bottom left, quarter bottom right, original position. 126 | - The window size will be changed alternately with each click. 127 | - Shortcuts or clicks can be changed by user. 128 | 129 | ## Future Plan for the Pro Version 130 | - Additional UI for file openers. (Completed) 131 | - Additional UI for auto clicker. (Completed) 132 | - Additional UI for screen clicker. (On Progress) 133 | - Full macro. Combining auto clicker, screen clicker, keyboard remap, and file opener in a single profile. 134 | - Possibly AI integration. 135 | - New automation tool, make specific program window always on top. (Completed) 136 | - New automation tool, OCR translator. Much like snipping tool but it will translate the chosen screen. 137 | - New automation tool, easy window always on top. Make window in the cursor to always on top with one click or shortcut. (Completed) 138 | - New automation tool, window size changer. Change window size to full size, half screen vertical, half screen horizontal, quarter screen, with a single click or shortcut. (Completed) 139 | - Macro recording. Record any input and simulate it. 140 | - Upcoming KeyTik update will be implemented on KeyTik Pro first. 141 | 142 | 143 | Note: 144 | - On Progress: Worked on. 145 | - Completed: Implemented. 146 | - No Description: Not implemented and yet worked on. 147 |
148 |
149 | 150 |
151 | 152 | ## Platform 153 | 154 |
155 | 156 | KeyTik is available at the following platforms: 157 | - [KeyTik Website](https://keytik.com) 158 | - [Source Forge](https://sourceforge.net/projects/keytik/) 159 | 160 |
161 | 162 |
163 | 164 | ## Table Of Content 165 |
166 | 167 | 1. [Screenshots Preview](#screenshots-preview) 168 | 2. [Features](#features) 169 | - [Key Features](#key-features) 170 | - [Additional Feature](#additional-feature) 171 | 3. [Have a Suggestion or Question?](#have-a-suggestion-or-question) 172 | 4. [License](#license) 173 | 5. [Contributing](#contributing) 174 | 6. [Star History](#star-history) 175 | 176 | ### Other Resources 177 | 1. [Video Guide](https://www.youtube.com/@Fajar-RahmadJaya) 178 | 2. [Installation](https://keytik.com/download/#installation) 179 | 3. [Use Case Example](https://keytik.com/docs/introduction/use-case) 180 | 4. [Automation Tool](https://keytik.com/docs/getting-started/automation-tool) 181 | 5. [KeyTik Mechanism](https://keytik.com/docs/getting-started/mechanism) 182 | 6. [List of Supported Key](https://keytik.com/docs/getting-started/key-list/keyboard) 183 | 7. [Safety](https://keytik.com/docs/introduction/safety) 184 | 8. [Full Documentation](https://keytik.com/docs/introduction/overview) 185 |
186 | 187 |
188 | 189 | ## Screenshots Preview 190 | 191 |
192 | 193 |
194 | Dark Mode 195 |
196 |
197 | Light Mode 198 |
199 |
200 | Default Mode 201 |
202 |
203 | Choosing Key 1 204 |
205 |
206 | Choosing Key 2 207 |
208 |
209 | Key Format Example 210 |
211 |
212 | Text Mode 213 |
214 |
215 | Example 3 216 |
217 |
218 | Select Programs.png 219 |
220 | 221 |
222 | 223 |
224 | 225 | ## Features 226 | 227 |
228 | 229 | ### Key Features 230 | | **No** | **Feature** | **Description** | 231 | |--------|-----------------------------------------------------|-----------------| 232 | | 1 | **Multiple Remap/Macro Profile** | Not like most of keyboard remapper, KeyTik can handle multiple keyboard remap. You don't have to set remap again when you need to use another remap then set it back again after done. Just create multiple remap and activate or deactivate it whenever you want. | 233 | | 2 | **Double Click Format** | Remap double click into other keys. Example: Double pressing left click will send middle click. | 234 | | 3 | **Text Format** | Remap key into raw text. Example: Pressing Shift + 1 will send "Worcestershire Sauce". | 235 | | 4 | **Hold Format** | Remap key into a hold action. Example: Triggering mouse wheel up will hold left click for 10 seconds. | 236 | | 5 | **Multi Key Format** | Not just single keys, KeyTik supports remapping multiple keys too. This can be used for remapping or sending key. Example: Pressing Left Alt + v will send Shift + v.| 237 | | 6 | **Vast Keyboard and Mouse Key Support** | Supports a wide range of keyboard and mouse keys, even unusual ones. See [List of Available Key](https://keytik.com/docs/getting-started/key-list/keyboard) for more. There are around 115 keyboard and mouse specific keys (like Tab, Shift, etc). | 238 | | 7 | **ASCII Character Support** | Supports remapping and sending ASCII characters. There are around 94 ASCII characters are supported. | 239 | | 8 | **ANSI Character Support** | Supports sending ANSI characters. There are around 122 ANSI characters are supported. | 240 | | 9 | **Unicode Character Support** | Supports sending Unicode characters. Unicode contains a vast number of characters. KeyTik groups them using Unicode blocks, and each block consists of different characters. There are around 302 supported blocks, with approximately 159,000+ Unicode characters. | 241 | | 9 | **Virtual Keyboard Code Support** | Supports remapping and sending VK codes. Virtual keyboard codes (VK codes) are keys defined by Windows. There are around 258 VK codes are supported. | 242 | | 10 | **Scan Code Support** | Supports remapping keys via SC. Scan codes (SC) are hardware-specific codes that indicate key location. This is useful if you can't find your key. SC will remaps the key at a specific location instead of a specific key. The number of supported scan codes depends on your keyboard. | 243 | | 11 | **Assign Shortcut on Profile** | Assign shortcuts to start or stop profiles. Supports Caps Lock On and Caps Lock Off states. Currently, shortcuts only start or stop the profile. We plan to add shortcut switching in the future, so shortcuts can change the remap when pressed. This is similar to how Caps Lock or Num Lock works. | 244 | | 12 | **Bind Profile to Specific Keyboard and Mouse** | Make script or remap profile to only work for specific physical keyboard or mouse using device VID & PID or device handle as identifier.| 245 | | 13 | **Bind Profile to Specific Program** | Make script or remap profile to only work for specific programs class, like specific Chrome tab or entire program.| 246 | | 14 | **Auto Clicker** | KeyTik comes with Auto Clicker in the download. On default, it simulate 'left click' when 'e' is held. You can change the 'left click', 'e', interval part to your preference.| 247 | | 15 | **Screen Clicker** | KeyTik also comes with Screen Clicker in the download. It work with simulate 'left click' on specific screen coordinate. You can change coordinate and interval to your preference. Don't worry because KeyTik also comes with tool to find screen coordinate then it will automatically copy coordinate and you can paste it to screen clicker in text mode.| 248 | | 16 | **Screen Coordinate Auto Detect And Copy** | To make screen clicker editing easier, KeyTik also comes with coordinate finder. On default, you just need to press 'space' then it will show coordinate and automatically copy it. You can also change 'space' part to your preference.| 249 | | 17 | **Multiple Files Opener** | Multiple files opener also comes with KeyTik download. It work with, if you click key or key combination, then it will open the files. You can change the files with your files or programs path to your preference.| 250 | 251 | ### Additional Feature 252 | | **No** | **Feature** | **Description** | 253 | |--------|-----------------------------------------------------|-----------------| 254 | | 1 | **Manage Profiles** | Run, Exit, Delete, Store, Edit, Pin each profiles for better control over profiles.| 255 | | 2 | **Run Profile on Startup** | Run profiles on startup, so it will automatically activate when you open your device—no need to manually activate it each time. | 256 | | 6 | **Make Window Always on Top** | "Always on top" feature lets you easily remap keys while other windows are open, without minimizing KeyTik window. This is especially useful during gaming. | 257 | | 7| **Show Stored Profile** | Display your stored profile or restore it to main window. | 258 | | 8 | **Import Profile** | Use AutoHotkey script from external source like download and make it as profile. | 259 | | 9 | **Automatically Take Key Input** | A button that can make you click your desired key and it will automatically fill key entry | 260 | 261 | 262 |
263 | 264 |
265 | 266 | ## Have a Suggestion or Question? 267 | 268 |
269 | 270 | If you have any suggestions or question, don't hesitate to submit it on the issues page. 271 | - **[Automation Tool Suggestion](https://github.com/Fajar-RahmadJaya/KeyTik/issues/new?assignees=&labels=Automation+Tool+Suggestion%2C+Supported+Key+Suggestion&projects=&template=automation-tool-suggestion.md&title=Automation+Tool+Suggestion)**: Suggest additions to KeyTik’s built-in automation tools. 272 | - **[Bug Report](https://github.com/Fajar-RahmadJaya/KeyTik/issues/new?assignees=&labels=bug&projects=&template=bug-report.md&title=Bug+Report)**: Report any bugs or issue. 273 | - **[Feature Suggestion](https://github.com/Fajar-RahmadJaya/KeyTik/issues/new?assignees=&labels=Feature+Suggestion&projects=&template=feature_suggestion.md&title=Feature+Suggestion)**: Suggest an idea for a new feature. 274 | - **[Question](https://github.com/Fajar-RahmadJaya/KeyTik/issues/new?assignees=&labels=Question&projects=&template=question.md&title=Question)**: Ask any question. 275 | - **[Supported Key Suggestion](https://github.com/Fajar-RahmadJaya/KeyTik/issues/new?assignees=&labels=Supported+Key+Suggestion&projects=&template=supported-key-suggestion.md&title=Supported+Key+Suggestion)**: Suggest a key to include in the list. 276 | - **[Windows Warning Report](https://github.com/Fajar-RahmadJaya/KeyTik/issues/new?assignees=&labels=Windows+Warning+Report&projects=&template=windows-warning-report.md&title=Windows+Warning+Report)**: Report any Windows warnings, such as untrusted author notifications or false positives. 277 | 278 |
279 | 280 |
281 | 282 | ## License 283 | This project is licensed under the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0). You can freely use, modify, and distribute this code under the terms of the license. 284 | 285 |
286 | 287 |
288 | 289 |
290 | 291 | ## Contributing 292 | Pull requests are welcome! We welcome contributions of all kinds, including bug fixes, features, improvements, documentation improvement and more. Check out the [Contribution Guidelines](https://github.com/Fajar-RahmadJaya/KeyTik/blob/main/CONTRIBUTING.md) for more info. 293 | 294 |
295 | 296 |
297 | 298 |
299 | 300 | ## Star History 301 | 302 |
303 | 304 | 305 | 306 | 307 | 308 | Star History Chart 309 | 310 | 311 | -------------------------------------------------------------------------------- /src/ui/edit_script/choose_key.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unicodedata 3 | from PySide6.QtWidgets import ( 4 | QDialog, QTreeWidget, QVBoxLayout, QHBoxLayout, 5 | QLabel, QLineEdit, QPushButton, QTreeWidgetItem, QHeaderView, 6 | QListWidget, QListWidgetItem, QCheckBox 7 | ) 8 | from PySide6.QtGui import QIcon 9 | from PySide6.QtCore import Qt, QPoint, QEvent 10 | from utility.constant import (icon_path, keylist_path, unicode_blocks) 11 | from utility.icon import (get_icon, icon_question, icon_filter) 12 | 13 | 14 | class ChooseKey: 15 | def get_unicode_block_range(self, block_name): 16 | for start, end, name in unicode_blocks: 17 | if name == block_name: 18 | return start, end 19 | return None, None 20 | 21 | def get_unicode_block_data(self, block_name): 22 | start, end = self.get_unicode_block_range(block_name) 23 | if start is None: 24 | return {} 25 | block_dict = {} 26 | for codepoint in range(start, end + 1): 27 | try: 28 | char = chr(codepoint) 29 | name = unicodedata.name(char) 30 | if not char.strip(): 31 | continue 32 | block_dict[char] = { 33 | "translate": str(codepoint), 34 | "description": name 35 | } 36 | except ValueError: 37 | continue 38 | return block_dict 39 | 40 | def load_keylist(self): 41 | try: 42 | with open(keylist_path, "r", encoding="utf-8") as f: 43 | key_data = json.loads(f.read()) 44 | if key_data and isinstance(key_data, list): 45 | key_data = key_data[0] 46 | return key_data 47 | except Exception: 48 | return {} 49 | 50 | def choose_key(self, target_entry=None, context=None): 51 | self.choose_key_entry = None 52 | self.choose_key_target_entry = target_entry 53 | choose_key_window = None 54 | self.checked_keys_list = [] 55 | self.expanded_unicode_blocks = [] 56 | 57 | context_hide = { 58 | "shortcut": {"ANSI Keys"} | set([b[2] for b in unicode_blocks]), 59 | "default": {"Shortcut Special", "ANSI Keys"} 60 | | set([b[2] for b in unicode_blocks]), 61 | "remap": {"Shortcut Special"} 62 | } 63 | hide_parents = set() 64 | if context in context_hide: 65 | hide_parents = context_hide[context] 66 | 67 | if (choose_key_window 68 | and choose_key_window.isVisible()): 69 | choose_key_window.raise_() 70 | return 71 | 72 | if (hasattr(self, 'edit_window') 73 | and self.edit_window 74 | and self.edit_window.isVisible()): 75 | parent_window = self.edit_window 76 | else: 77 | parent_window = self 78 | 79 | choose_key_window = QDialog(parent_window) 80 | choose_key_window.setWindowTitle("Choose Key") 81 | choose_key_window.setWindowIcon(QIcon(icon_path)) 82 | choose_key_window.setFixedSize(400, 425) 83 | 84 | main_layout = QVBoxLayout(choose_key_window) 85 | 86 | choose_search_layout = QHBoxLayout() 87 | choose_search_layout.setContentsMargins(30, 0, 30, 5) 88 | main_layout.addLayout(choose_search_layout) 89 | 90 | filter_button = QPushButton() 91 | filter_button.setIcon(get_icon(icon_filter)) 92 | choose_search_layout.addWidget(filter_button) 93 | 94 | search_entry = QLineEdit() 95 | search_entry.setPlaceholderText(" Search Key") 96 | search_entry.setFixedWidth(170) 97 | choose_search_layout.addWidget(search_entry) 98 | 99 | search_unicode_checkbox = QCheckBox("Search Unicode") 100 | search_unicode_checkbox.setChecked(False) 101 | search_unicode_checkbox.setToolTip( 102 | "Search key by name and description.\n" 103 | "Unicode search may be slow. Enable only if needed.\n" 104 | "Letter search needs at least 3 letters." 105 | ) 106 | choose_search_layout.addWidget(search_unicode_checkbox) 107 | self.search_unicode_checkbox = search_unicode_checkbox 108 | 109 | self.filter_popup = QDialog(choose_key_window, Qt.Popup) 110 | self.filter_popup.setWindowFlags(Qt.Popup) 111 | self.filter_popup.setModal(False) 112 | self.filter_popup.setFixedHeight(120) 113 | self.filter_popup.setFocusPolicy(Qt.StrongFocus) 114 | filter_popup_layout = QVBoxLayout(self.filter_popup) 115 | filter_popup_layout.setContentsMargins(0, 0, 0, 0) 116 | self.filter_dropdown = QListWidget() 117 | self.filter_dropdown.setSelectionMode(QListWidget.NoSelection) 118 | filter_popup_layout.addWidget(self.filter_dropdown) 119 | 120 | choose_key_tree = QTreeWidget() 121 | choose_key_tree.setColumnCount(2) 122 | choose_key_tree.setHeaderLabels(["List of Key", ""]) 123 | choose_key_tree.setColumnWidth(0, 330) 124 | choose_key_tree.setColumnWidth(1, 20) 125 | choose_key_tree.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 126 | choose_key_tree.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 127 | header = choose_key_tree.header() 128 | header.setDefaultAlignment(Qt.AlignCenter) 129 | header.setSectionResizeMode(0, QHeaderView.Fixed) 130 | header.setSectionResizeMode(1, QHeaderView.Fixed) 131 | header.setStretchLastSection(False) 132 | main_layout.addWidget(choose_key_tree) 133 | 134 | try: 135 | key_data = self.load_keylist() 136 | self.key_data = key_data 137 | self.parent_names = list(key_data.keys()) 138 | 139 | self.filter_parents = set(self.parent_names) - hide_parents 140 | for parent in self.parent_names: 141 | if parent in hide_parents: 142 | continue 143 | item = QListWidgetItem(parent) 144 | item.setFlags(item.flags() | Qt.ItemIsUserCheckable) 145 | item.setCheckState(Qt.Checked) 146 | self.filter_dropdown.addItem(item) 147 | 148 | self.populate_tree(choose_key_tree, key_data, 149 | filter_parents=self.filter_parents, 150 | hide_parents=hide_parents) 151 | except Exception as e: 152 | print(f"Failed to load key list: {e}") 153 | 154 | choose_key_tree.itemClicked.connect(self.click_checkbox) 155 | 156 | search_entry.textChanged.connect( 157 | lambda text: self.populate_tree( 158 | choose_key_tree, 159 | self.key_data, 160 | text, 161 | self.get_checked_filter(self.filter_dropdown), 162 | self.search_unicode_checkbox.isChecked(), 163 | hide_parents=hide_parents 164 | ) 165 | ) 166 | self.search_unicode_checkbox.toggled.connect( 167 | lambda checked: self.populate_tree( 168 | choose_key_tree, 169 | self.key_data, 170 | search_entry.text(), 171 | self.get_checked_filter(self.filter_dropdown), 172 | checked, 173 | hide_parents=hide_parents 174 | ) 175 | ) 176 | 177 | def show_filter_popup(): 178 | if self.filter_popup.isVisible(): 179 | self.filter_popup.hide() 180 | else: 181 | global_pos = search_entry.mapToGlobal( 182 | QPoint(0, search_entry.height())) 183 | self.filter_popup.setFixedWidth(search_entry.width()) 184 | self.filter_popup.move(global_pos) 185 | self.filter_popup.show() 186 | self.filter_popup.raise_() 187 | self.filter_popup.activateWindow() 188 | self.filter_dropdown.setFocus() 189 | filter_button.clicked.connect(show_filter_popup) 190 | 191 | def eventFilter(obj, event): 192 | if event.type() == QEvent.Type.MouseButtonPress: 193 | if not (self.filter_popup.geometry(). 194 | contains(event.globalPos())): 195 | self.filter_popup.hide() 196 | return False 197 | choose_key_window.installEventFilter(self) 198 | self.eventFilter = eventFilter 199 | 200 | self.filter_popup.focusOutEvent = (lambda event: 201 | self.filter_popup.hide()) 202 | 203 | def apply_filter(item): 204 | checked_parents = self.get_checked_filter(self.filter_dropdown) 205 | self.populate_tree( 206 | choose_key_tree, 207 | self.key_data, 208 | search_entry.text(), 209 | checked_parents 210 | ) 211 | self.filter_dropdown.itemChanged.connect(apply_filter) 212 | 213 | choose_key_layout = QHBoxLayout() 214 | choose_key_layout.setContentsMargins(25, 10, 25, 10) 215 | main_layout.addLayout(choose_key_layout) 216 | 217 | choose_key_label = QLabel("Selected Key:") 218 | choose_key_layout.addWidget(choose_key_label) 219 | 220 | choose_key_entry = QLineEdit() 221 | choose_key_entry.setReadOnly(True) 222 | self.choose_key_entry = choose_key_entry 223 | choose_key_layout.addWidget(choose_key_entry) 224 | 225 | select_key_button = QPushButton("Save Keys") 226 | choose_key_layout.addWidget(select_key_button) 227 | 228 | def on_save_keys(): 229 | if self.choose_key_target_entry is not None: 230 | self.choose_key_target_entry.setText( 231 | self.choose_key_entry.text()) 232 | choose_key_window.accept() 233 | select_key_button.clicked.connect(on_save_keys) 234 | 235 | choose_key_window.exec() 236 | 237 | def get_checked_filter(self, filter_dropdown): 238 | return [ 239 | filter_dropdown.item(i).text() 240 | for i in range(filter_dropdown.count()) 241 | if filter_dropdown.item(i).checkState() == Qt.Checked 242 | ] 243 | 244 | def populate_tree(self, tree_widget, key_data, 245 | filter_text="", filter_parents=None, 246 | search_unicode=True, hide_parents=None): 247 | if hide_parents is None: 248 | hide_parents = set() 249 | if not hasattr(self, 'expanded_unicode_blocks'): 250 | self.expanded_unicode_blocks = [] 251 | 252 | prev_expanded_unicode_blocks = [] 253 | for i in range(tree_widget.topLevelItemCount()): 254 | item = tree_widget.topLevelItem(i) 255 | if (item.data(0, Qt.UserRole) == 256 | "unicode_block" and item.isExpanded()): 257 | prev_expanded_unicode_blocks.append(item.text(0)) 258 | has_items = tree_widget.topLevelItemCount() > 0 259 | if has_items: 260 | self.expanded_unicode_blocks = prev_expanded_unicode_blocks 261 | 262 | tree_widget.clear() 263 | filter_text = filter_text.strip().lower() 264 | if filter_parents is None: 265 | filter_parents = getattr(self, 'parent_names', []) 266 | for parent_name, children in key_data.items(): 267 | if parent_name in hide_parents: 268 | continue 269 | parent_match = filter_text and filter_text in parent_name.lower() 270 | matching_children = [] 271 | if parent_name in [b[2] for b in unicode_blocks] and not children: 272 | if not filter_text or parent_match: 273 | parent_item = QTreeWidgetItem([parent_name, ""]) 274 | tree_widget.addTopLevelItem(parent_item) 275 | parent_item.setExpanded(False) 276 | parent_item.setData(0, Qt.UserRole, "unicode_block") 277 | dummy_child = QTreeWidgetItem(["", ""]) 278 | parent_item.addChild(dummy_child) 279 | else: 280 | for child_name, child_info in children.items(): 281 | child_name_match = ( 282 | filter_text and filter_text in child_name.lower()) 283 | child_desc_match = ( 284 | filter_text and filter_text in 285 | child_info.get("description", "").lower()) 286 | if (not filter_text or 287 | parent_match or 288 | child_name_match or 289 | child_desc_match): 290 | matching_children.append((child_name, child_info)) 291 | if parent_match or matching_children: 292 | parent_item = QTreeWidgetItem([parent_name, ""]) 293 | tree_widget.addTopLevelItem(parent_item) 294 | for child_name, child_info in matching_children: 295 | child_item = QTreeWidgetItem([" " + child_name, ""]) 296 | child_item.setFlags( 297 | child_item.flags() | Qt.ItemIsUserCheckable) 298 | key_tuple = (parent_name, child_name) 299 | if key_tuple in getattr(self, 'checked_keys_list', []): 300 | child_item.setCheckState(0, Qt.Checked) 301 | else: 302 | child_item.setCheckState(0, Qt.Unchecked) 303 | description = child_info.get("description", "") 304 | if description: 305 | child_item.setIcon(1, QIcon(icon_question)) 306 | child_item.setToolTip(1, description) 307 | parent_item.addChild(child_item) 308 | parent_item.setExpanded(True) 309 | 310 | def get_letter(s): 311 | return s.isalpha() and all('a' <= c <= 'z' for c in s) 312 | 313 | unicode_search_enabled = False 314 | if search_unicode and filter_text: 315 | if get_letter(filter_text): 316 | if len(filter_text) >= 3: 317 | unicode_search_enabled = True 318 | else: 319 | if len(filter_text) >= 1: 320 | unicode_search_enabled = True 321 | 322 | if unicode_search_enabled: 323 | unicode_matches = {} 324 | for start, end, block_name in unicode_blocks: 325 | if block_name in hide_parents: 326 | continue 327 | for codepoint in range(start, end + 1): 328 | try: 329 | char = chr(codepoint) 330 | if not char.strip(): 331 | continue 332 | try: 333 | char_name = unicodedata.name(char) 334 | except ValueError: 335 | char_name = "" 336 | if get_letter(filter_text): 337 | match = filter_text in char_name.lower() 338 | else: 339 | match = (filter_text in char_name.lower() or 340 | filter_text in char.lower()) 341 | if match: 342 | if block_name not in unicode_matches: 343 | unicode_matches[block_name] = [] 344 | unicode_matches[block_name].append( 345 | (char, { 346 | "translate": f"{codepoint:04X}", 347 | "description": char_name 348 | }) 349 | ) 350 | except Exception: 351 | continue 352 | for block_name, chars in unicode_matches.items(): 353 | parent_item = QTreeWidgetItem([block_name, ""]) 354 | parent_item.setData(0, Qt.UserRole, "unicode_block") 355 | tree_widget.addTopLevelItem(parent_item) 356 | for char, info in chars: 357 | child_item = QTreeWidgetItem([" " + char, ""]) 358 | child_item.setFlags( 359 | child_item.flags() | Qt.ItemIsUserCheckable) 360 | key_tuple = (block_name, char) 361 | if key_tuple in getattr(self, 'checked_keys_list', []): 362 | child_item.setCheckState(0, Qt.Checked) 363 | else: 364 | child_item.setCheckState(0, Qt.Unchecked) 365 | description = info.get("description", "") 366 | if description: 367 | child_item.setIcon(1, QIcon(icon_question)) 368 | child_item.setToolTip(1, description) 369 | parent_item.addChild(child_item) 370 | parent_item.setExpanded(True) 371 | 372 | self.insert_choose_entry(tree_widget) 373 | 374 | tree_widget.itemExpanded.connect(self.load_unicode) 375 | 376 | for i in range(tree_widget.topLevelItemCount()): 377 | item = tree_widget.topLevelItem(i) 378 | if item.data(0, Qt.UserRole) == "unicode_block": 379 | if item.text(0) in self.expanded_unicode_blocks: 380 | if item.childCount() == 1 and item.child(0).text(0) == "": 381 | self.load_unicode(item) 382 | item.setExpanded(True) 383 | 384 | def load_unicode(self, item): 385 | if item.data(0, Qt.UserRole) == "unicode_block": 386 | if item.childCount() == 1 and item.child(0).text(0) == "": 387 | item.removeChild(item.child(0)) 388 | if item.childCount() == 0: 389 | block_name = item.text(0) 390 | block_data = self.get_unicode_block_data(block_name) 391 | for char, info in block_data.items(): 392 | child_item = QTreeWidgetItem([" " + char, ""]) 393 | child_item.setFlags( 394 | child_item.flags() | Qt.ItemIsUserCheckable) 395 | key_tuple = (block_name, char) 396 | if key_tuple in getattr(self, 'checked_keys_list', []): 397 | child_item.setCheckState(0, Qt.Checked) 398 | else: 399 | child_item.setCheckState(0, Qt.Unchecked) 400 | description = info.get("description", "") 401 | if description: 402 | child_item.setIcon(1, QIcon(icon_question)) 403 | child_item.setToolTip(1, description) 404 | item.addChild(child_item) 405 | item.setExpanded(True) 406 | 407 | def click_checkbox(self, item, _): 408 | if item.parent() is not None: 409 | parent_name = item.parent().text(0) 410 | child_name = item.text(0).strip() 411 | key_tuple = (parent_name, child_name) 412 | if item.checkState(0) == Qt.Checked: 413 | if key_tuple not in self.checked_keys_list: 414 | self.checked_keys_list.append(key_tuple) 415 | else: 416 | if key_tuple in self.checked_keys_list: 417 | self.checked_keys_list.remove(key_tuple) 418 | tree_widget = item.treeWidget() 419 | self.insert_choose_entry(tree_widget) 420 | 421 | if item.parent() is None: 422 | item.setExpanded(not item.isExpanded()) 423 | 424 | def insert_choose_entry(self, _): 425 | if getattr(self, 'choose_key_entry', None) is not None: 426 | self.choose_key_entry.setText( 427 | ' + '.join([child for _, child in self.checked_keys_list])) 428 | 429 | def is_unicode_key(self, key): 430 | key_data = self.load_keylist() 431 | for parent, children in key_data.items(): 432 | if key in children: 433 | return False 434 | return True 435 | --------------------------------------------------------------------------------