/* * This file is part of espanso. * * Copyright (C) 2019 Federico Terzi * * espanso is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * espanso is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with espanso. If not, see . */ #include "bridge.h" #include #include #include #include #include #include #define UNICODE #include #include #include #pragma comment( lib, "gdiplus.lib" ) #include // How many milliseconds must pass between keystrokes to refresh the keyboard layout const long refreshKeyboardLayoutInterval = 2000; void * manager_instance; // Keyboard listening DWORD lastKeyboardPressTick = 0; HKL currentKeyboardLayout; HWND window; const wchar_t* const winclass = L"Espanso"; // UI #define APPWM_ICON_CLICK (WM_APP + 1) #define APPWM_NOTIFICATION_POPUP (WM_APP + 2) #define APPWM_NOTIFICATION_CLOSE (WM_APP + 3) #define APPWM_SHOW_CONTEXT_MENU (WM_APP + 4) const wchar_t* const notification_winclass = L"EspansoNotification"; HWND nw = NULL; HWND hwnd_st_u = NULL; HBITMAP g_espanso_bmp = NULL; HICON g_espanso_ico = NULL; NOTIFYICONDATA nid = {}; UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); // Callbacks KeypressCallback keypress_callback = NULL; IconClickCallback icon_click_callback = NULL; ContextMenuClickCallback context_menu_click_callback = NULL; void register_keypress_callback(KeypressCallback callback) { keypress_callback = callback; } void register_icon_click_callback(IconClickCallback callback) { icon_click_callback = callback; } void register_context_menu_click_callback(ContextMenuClickCallback callback) { context_menu_click_callback = callback; } /* * Message handler procedure for the windows */ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp) { HDC hdcStatic = NULL; switch (msg) { case WM_DESTROY: std::cout << "\ndestroying window\n"; PostQuitMessage(0); DeleteObject(g_espanso_bmp); DeleteObject(g_espanso_ico); return 0L; case WM_COMMAND: // Click on the tray icon context menu { UINT idItem = (UINT)LOWORD(wp); UINT flags = (UINT)HIWORD(wp); if (flags == 0) { context_menu_click_callback(manager_instance, (int32_t)idItem); } break; } case APPWM_NOTIFICATION_POPUP: // Request to show a notification { std::unique_ptr ptr(reinterpret_cast(wp)); SetWindowText(hwnd_st_u, L" "); // Clear the previous text SetWindowText(hwnd_st_u, ptr.get()); // Show the window ShowWindow(nw, SW_SHOWNOACTIVATE); break; } case APPWM_NOTIFICATION_CLOSE: // Request to close a notification { // Hide the window ShowWindow(nw, SW_HIDE); break; } case APPWM_SHOW_CONTEXT_MENU: // Request to show context menu { HMENU hPopupMenu = CreatePopupMenu(); // Create the menu int32_t count = static_cast(lp); std::unique_ptr items(reinterpret_cast(wp)); for (int i = 0; i lpb(dwSize); // Request the Raw input data if (GetRawInputData((HRAWINPUT)lp, RID_INPUT, lpb.data(), &dwSize, sizeof(RAWINPUTHEADER)) != dwSize) { return 0; } // Convert the input data RAWINPUT* raw = reinterpret_cast(lpb.data()); // Make sure it's a keyboard type event, relative to a key press. if (raw->header.dwType == RIM_TYPEKEYBOARD) { // We only want KEY UP AND KEY DOWN events if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP) { return 0; } int is_key_down = raw->data.keyboard.Message == WM_KEYDOWN; DWORD currentTick = GetTickCount(); // If enough time has passed between the last keypress and now, refresh the keyboard layout if ((currentTick - lastKeyboardPressTick) > refreshKeyboardLayoutInterval) { // Because keyboard layouts on windows are Window-specific, to get the current // layout we need to get the foreground window and get its layout. HWND hwnd = GetForegroundWindow(); if (hwnd) { DWORD threadID = GetWindowThreadProcessId(hwnd, NULL); HKL newKeyboardLayout = GetKeyboardLayout(threadID); // It's not always valid, so update the current value only if available. if (newKeyboardLayout != 0) { currentKeyboardLayout = newKeyboardLayout; } } lastKeyboardPressTick = currentTick; } // Get keyboard state ( necessary to decode the associated Unicode char ) std::vector lpKeyState(256); if (GetKeyboardState(lpKeyState.data())) { // Convert the virtual key to an unicode char std::array buffer; // This flag is needed to avoid chaning the keyboard state for some layouts. // Refer to issue: https://github.com/federico-terzi/espanso/issues/86 UINT flags = 1 << 2; int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), buffer.data(), buffer.size(), flags, currentKeyboardLayout); //std::cout << result << " " << buffer[0] << " " << raw->data.keyboard.VKey << std::endl; // We need to call the callback in two different ways based on the type of key // The only modifier we use that has a result > 0 is the BACKSPACE, so we have to consider it. if (result >= 1 && raw->data.keyboard.VKey != VK_BACK) { keypress_callback(manager_instance, reinterpret_cast(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, is_key_down); }else{ keypress_callback(manager_instance, nullptr, 0, 1, raw->data.keyboard.VKey, is_key_down); } } }else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse input, registered as "other" events. Needed to improve the reliability of word matches { if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN)) != 0) { //std::cout << "mouse down" << std::endl; keypress_callback(manager_instance, nullptr, 0, 2, raw->data.mouse.usButtonFlags, 0); } } return 0; } default: if (msg == WM_TASKBARCREATED) { // Explorer crashed, recreate the icon Shell_NotifyIcon(NIM_ADD, &nid); } return DefWindowProc(window, msg, wp, lp); } } int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path) { manager_instance = self; // Load the images g_espanso_bmp = (HBITMAP)LoadImage(NULL, bmp_path, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); g_espanso_ico = (HICON)LoadImage(NULL, ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE); // Make the notification capable of handling different screen definitions SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); // Initialize the default keyboard layout currentKeyboardLayout = GetKeyboardLayout(0); // Initialize the Worker window // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa WNDCLASSEX wndclass = { sizeof(WNDCLASSEX), // cbSize: Size of this structure 0, // style: Class styles window_procedure, // lpfnWndProc: Pointer to the window procedure 0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure 0, // cbWndExtra: The number of extra bytes to allocate following the window instance. GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class. NULL, // hIcon: A handle to the class icon. LoadCursor(0,IDC_ARROW), // hCursor: A handle to the class cursor. NULL, // hbrBackground: A handle to the class background brush. NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu winclass, // lpszClassName: A pointer to a null-terminated string or is an atom. NULL // hIconSm: A handle to a small icon that is associated with the window class. }; // Notification Window // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa WNDCLASSEX notificationwndclass = { sizeof(WNDCLASSEX), // cbSize: Size of this structure 0, // style: Class styles window_procedure, // lpfnWndProc: Pointer to the window procedure 0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure 0, // cbWndExtra: The number of extra bytes to allocate following the window instance. GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class. NULL, // hIcon: A handle to the class icon. LoadCursor(0,IDC_ARROW), // hCursor: A handle to the class cursor. NULL, // hbrBackground: A handle to the class background brush. NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu notification_winclass, // lpszClassName: A pointer to a null-terminated string or is an atom. NULL // hIconSm: A handle to a small icon that is associated with the window class. }; if (RegisterClassEx(&wndclass) && RegisterClassEx(¬ificationwndclass)) { // Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw window = CreateWindowEx( 0, // dwExStyle: The extended window style of the window being created. winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass L"Espanso Worker Window", // lpWindowName: The window name. WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created. CW_USEDEFAULT, // X: The initial horizontal position of the window. CW_USEDEFAULT, // Y: The initial vertical position of the window. 100, // nWidth: The width, in device units, of the window. 100, // nHeight: The height, in device units, of the window. NULL, // hWndParent: handle to the parent or owner window of the window being created. NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style. GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window. NULL // lpParam: Pointer to a value to be passed to the window ); // Register raw inputs RAWINPUTDEVICE Rid[2]; Rid[0].usUsagePage = 0x01; Rid[0].usUsage = 0x06; Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; // adds HID keyboard and also ignores legacy keyboard messages Rid[0].hwndTarget = window; Rid[1].usUsagePage = 0x01; Rid[1].usUsage = 0x02; Rid[1].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; // adds HID mouse and also ignores legacy mouse messages Rid[1].hwndTarget = window; if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE) { // Something went wrong, error. return -1; } // Initialize the notification window nw = CreateWindowEx( WS_EX_TOOLWINDOW | WS_EX_TOPMOST, // dwExStyle: The extended window style of the window being created. notification_winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass L"Espanso Notification", // lpWindowName: The window name. WS_POPUPWINDOW, // dwStyle: The style of the window being created. CW_USEDEFAULT, // X: The initial horizontal position of the window. CW_USEDEFAULT, // Y: The initial vertical position of the window. 300, // nWidth: The width, in device units, of the window. 100, // nHeight: The height, in device units, of the window. NULL, // hWndParent: handle to the parent or owner window of the window being created. NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style. GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window. NULL // lpParam: Pointer to a value to be passed to the window ); if (nw) { int x, w, y, h; y = 40; h = 30; x = 100; w = 180; hwnd_st_u = CreateWindowEx(0, L"static", L"ST_U", WS_CHILD | WS_VISIBLE | WS_TABSTOP | SS_CENTER, x, y, w, h, nw, (HMENU)(501), (HINSTANCE)GetWindowLong(nw, GWLP_HINSTANCE), NULL); SetWindowText(hwnd_st_u, L"Loading..."); int posX = GetSystemMetrics(SM_CXSCREEN) - 350; int posY = GetSystemMetrics(SM_CYSCREEN) - 200; SetWindowPos(nw, HWND_TOP, posX, posY, 0, 0, SWP_NOSIZE); // Hide the window ShowWindow(nw, SW_HIDE); // Setup the icon in the notification space SendMessage(nw, WM_SETICON, ICON_BIG, (LPARAM)g_espanso_ico); SendMessage(nw, WM_SETICON, ICON_SMALL, (LPARAM)g_espanso_ico); //Notification nid.cbSize = sizeof(nid); nid.hWnd = nw; nid.uID = 1; nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE; nid.uCallbackMessage = APPWM_ICON_CLICK; nid.hIcon = g_espanso_ico; StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), L"espanso"); // Show the notification. Shell_NotifyIcon(NIM_ADD, &nid); } }else{ // Something went wrong, error. return -1; } return 1; } void eventloop() { if (window) { // Hide the window ShowWindow(window, SW_HIDE); // Enter the Event loop MSG msg; while (GetMessage(&msg, 0, 0, 0)) DispatchMessage(&msg); } // Something went wrong, this should have been an infinite loop. } /* * Type the given string simulating keyboard presses. */ void send_string(const wchar_t * string) { std::wstring msg = string; std::vector vec; for (auto ch : msg) { INPUT input = { 0 }; input.type = INPUT_KEYBOARD; input.ki.dwFlags = KEYEVENTF_UNICODE; input.ki.wScan = ch; vec.push_back(input); input.ki.dwFlags |= KEYEVENTF_KEYUP; vec.push_back(input); } SendInput(vec.size(), vec.data(), sizeof(INPUT)); } /* * Send the backspace keypress, *count* times. */ void delete_string(int32_t count) { send_multi_vkey(VK_BACK, count); } void send_vkey(int32_t vk) { std::vector vec; INPUT input = { 0 }; input.type = INPUT_KEYBOARD; input.ki.wScan = 0; input.ki.time = 0; input.ki.dwExtraInfo = 0; input.ki.wVk = vk; input.ki.dwFlags = 0; // 0 for key press vec.push_back(input); input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release vec.push_back(input); SendInput(vec.size(), vec.data(), sizeof(INPUT)); } void send_multi_vkey(int32_t vk, int32_t count) { std::vector vec; for (int i = 0; i < count; i++) { INPUT input = { 0 }; input.type = INPUT_KEYBOARD; input.ki.wScan = 0; input.ki.time = 0; input.ki.dwExtraInfo = 0; input.ki.wVk = vk; input.ki.dwFlags = 0; // 0 for key press vec.push_back(input); input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release vec.push_back(input); } SendInput(vec.size(), vec.data(), sizeof(INPUT)); } void trigger_paste() { std::vector vec; INPUT input = { 0 }; input.type = INPUT_KEYBOARD; input.ki.wScan = 0; input.ki.time = 0; input.ki.dwExtraInfo = 0; input.ki.wVk = VK_CONTROL; input.ki.dwFlags = 0; // 0 for key press vec.push_back(input); input.ki.wVk = 0x56; // V KEY vec.push_back(input); input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release vec.push_back(input); input.ki.wVk = VK_CONTROL; vec.push_back(input); SendInput(vec.size(), vec.data(), sizeof(INPUT)); } void trigger_copy() { std::vector vec; INPUT input = { 0 }; input.type = INPUT_KEYBOARD; input.ki.wScan = 0; input.ki.time = 0; input.ki.dwExtraInfo = 0; input.ki.wVk = VK_CONTROL; input.ki.dwFlags = 0; // 0 for key press vec.push_back(input); input.ki.wVk = 0x43; // C KEY vec.push_back(input); input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release vec.push_back(input); input.ki.wVk = VK_CONTROL; vec.push_back(input); SendInput(vec.size(), vec.data(), sizeof(INPUT)); } // SYSTEM int32_t get_active_window_name(wchar_t * buffer, int32_t size) { HWND hwnd = GetForegroundWindow(); return GetWindowText(hwnd, buffer, size); } int32_t get_active_window_executable(wchar_t * buffer, int32_t size) { HWND hwnd = GetForegroundWindow(); // Extract the window PID DWORD windowPid; GetWindowThreadProcessId(hwnd, &windowPid); DWORD dsize = (DWORD) size; // Extract the process executable file path HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, windowPid); int res = QueryFullProcessImageNameW(process, 0, buffer, &dsize); CloseHandle(process); return res; } // Notifications int32_t show_notification(wchar_t * message) { if (nw != NULL) { wchar_t * buffer = new wchar_t[100]; swprintf(buffer, 100, L"%ls", message); PostMessage(nw, APPWM_NOTIFICATION_POPUP, reinterpret_cast(buffer), 0); return 1; } return -1; } void close_notification() { if (nw != NULL) { PostMessage(nw, APPWM_NOTIFICATION_CLOSE, 0, 0); } } int32_t show_context_menu(MenuItem * items, int32_t count) { if (nw != NULL) { MenuItem * items_buffer = new MenuItem[count]; memcpy(items_buffer, items, sizeof(MenuItem)*count); PostMessage(nw, APPWM_SHOW_CONTEXT_MENU, reinterpret_cast(items_buffer), static_cast(count)); return 1; } return -1; } void cleanup_ui() { Shell_NotifyIcon(NIM_DELETE, &nid); } // SYSTEM int32_t start_daemon_process() { wchar_t cmd[MAX_PATH]; swprintf(cmd, MAX_PATH, L"espanso.exe daemon"); // Get current espanso directory TCHAR espansoFilePath[MAX_PATH]; GetModuleFileName(NULL, espansoFilePath, MAX_PATH); STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; // Documentation: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw BOOL res = CreateProcess( espansoFilePath, cmd, NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &si, &pi ); if (!res) { return -1; } return 1; } // CLIPBOARD int32_t set_clipboard(wchar_t *text) { const size_t len = wcslen(text) + 1; HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len * sizeof(wchar_t)); memcpy(GlobalLock(hMem), text, len * sizeof(wchar_t)); GlobalUnlock(hMem); if (!OpenClipboard(NULL)) { return -1; } EmptyClipboard(); if (!SetClipboardData(CF_UNICODETEXT, hMem)) { return -2; } CloseClipboard(); } int32_t get_clipboard(wchar_t *buffer, int32_t size) { if (!OpenClipboard(NULL)) { return -1; } // Get handle of clipboard object for ANSI text HANDLE hData = GetClipboardData(CF_UNICODETEXT); if (!hData) { return -2; } HGLOBAL hMem = GlobalLock(hData); if (!hMem) { return -3; } GlobalUnlock(hMem); swprintf(buffer, size, L"%s", hMem); CloseClipboard(); } int32_t set_clipboard_image(wchar_t *path) { bool result = false; Gdiplus::GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); Gdiplus::Bitmap *gdibmp = Gdiplus::Bitmap::FromFile(path); if (gdibmp) { HBITMAP hbitmap; gdibmp->GetHBITMAP(0, &hbitmap); if (OpenClipboard(NULL)) { EmptyClipboard(); DIBSECTION ds; if (GetObject(hbitmap, sizeof(DIBSECTION), &ds)) { HDC hdc = GetDC(HWND_DESKTOP); //create compatible bitmap (get DDB from DIB) HBITMAP hbitmap_ddb = CreateDIBitmap(hdc, &ds.dsBmih, CBM_INIT, ds.dsBm.bmBits, (BITMAPINFO*)&ds.dsBmih, DIB_RGB_COLORS); ReleaseDC(HWND_DESKTOP, hdc); SetClipboardData(CF_BITMAP, hbitmap_ddb); DeleteObject(hbitmap_ddb); result = true; } CloseClipboard(); } //cleanup: DeleteObject(hbitmap); delete gdibmp; } Gdiplus::GdiplusShutdown(gdiplusToken); return result; }