diff --git a/build.zig b/build.zig index 958591c..503db85 100644 --- a/build.zig +++ b/build.zig @@ -46,6 +46,7 @@ pub fn build(b: *std.build.Builder) void { // woah...we don't actually need libc! exe.linkSystemLibrary("user32"); exe.linkSystemLibrary("kernel32"); + exe.linkSystemLibrary("shell32"); } exe.install(); diff --git a/src/main-windows.zig b/src/main-windows.zig index b0a7dba..6a51c49 100644 --- a/src/main-windows.zig +++ b/src/main-windows.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const w = std.os.windows; extern "user32" fn MessageBoxA(hWnd: ?w.HANDLE, lpText: ?w.LPCSTR, lpCaption: ?w.LPCSTR, uType: w.UINT) callconv(w.WINAPI) c_int; @@ -341,6 +342,10 @@ const WM_DRAWCLIPBOARD = @as(u32, 776); const WM_SIZECLIPBOARD = @as(u32, 779); const WM_CHANGECBCHAIN = @as(u32, 781); +const WM_USER = @as(u32, 1024); +// USER constants here - not part of Windows headers +const WM_TRAY = @as(u32, WM_USER + 1); + const RECT = extern struct { left: i32, top: i32, @@ -616,6 +621,199 @@ extern "KERNEL32" fn GlobalFree( hMem: isize, ) callconv(w.WINAPI) isize; +// Shell +const Arch = enum { X86, X64, Arm64 }; +const arch: Arch = switch (builtin.target.cpu.arch) { + .i386 => .X86, + .x86_64 => .X64, + .arm, .armeb => .Arm64, + else => @compileError("unable to determine win32 arch"), +}; +const NOTIFY_ICON_DATA_FLAGS = enum(u32) { + MESSAGE = 1, + ICON = 2, + TIP = 4, + STATE = 8, + INFO = 16, + GUID = 32, + REALTIME = 64, + SHOWTIP = 128, + _, + pub fn initFlags(o: struct { + MESSAGE: u1 = 0, + ICON: u1 = 0, + TIP: u1 = 0, + STATE: u1 = 0, + INFO: u1 = 0, + GUID: u1 = 0, + REALTIME: u1 = 0, + SHOWTIP: u1 = 0, + }) NOTIFY_ICON_DATA_FLAGS { + return @intToEnum(NOTIFY_ICON_DATA_FLAGS, (if (o.MESSAGE == 1) @enumToInt(NOTIFY_ICON_DATA_FLAGS.MESSAGE) else 0) | (if (o.ICON == 1) @enumToInt(NOTIFY_ICON_DATA_FLAGS.ICON) else 0) | (if (o.TIP == 1) @enumToInt(NOTIFY_ICON_DATA_FLAGS.TIP) else 0) | (if (o.STATE == 1) @enumToInt(NOTIFY_ICON_DATA_FLAGS.STATE) else 0) | (if (o.INFO == 1) @enumToInt(NOTIFY_ICON_DATA_FLAGS.INFO) else 0) | (if (o.GUID == 1) @enumToInt(NOTIFY_ICON_DATA_FLAGS.GUID) else 0) | (if (o.REALTIME == 1) @enumToInt(NOTIFY_ICON_DATA_FLAGS.REALTIME) else 0) | (if (o.SHOWTIP == 1) @enumToInt(NOTIFY_ICON_DATA_FLAGS.SHOWTIP) else 0)); + } +}; +const NIF_MESSAGE = NOTIFY_ICON_DATA_FLAGS.MESSAGE; +const NIF_ICON = NOTIFY_ICON_DATA_FLAGS.ICON; +const NIF_TIP = NOTIFY_ICON_DATA_FLAGS.TIP; +const NIF_STATE = NOTIFY_ICON_DATA_FLAGS.STATE; +const NIF_INFO = NOTIFY_ICON_DATA_FLAGS.INFO; +const NIF_GUID = NOTIFY_ICON_DATA_FLAGS.GUID; +const NIF_REALTIME = NOTIFY_ICON_DATA_FLAGS.REALTIME; +const NIF_SHOWTIP = NOTIFY_ICON_DATA_FLAGS.SHOWTIP; +const NOTIFYICONDATAA = switch (arch) { + .X64, .Arm64 => extern struct { + cbSize: u32, + hWnd: ?w.HWND, + uID: u32, + uFlags: NOTIFY_ICON_DATA_FLAGS, + uCallbackMessage: u32, + hIcon: ?w.HICON, + szTip: [128]w.CHAR, + dwState: u32, + dwStateMask: u32, + szInfo: [256]w.CHAR, + Anonymous: extern union { + uTimeout: u32, + uVersion: u32, + }, + szInfoTitle: [64]w.CHAR, + dwInfoFlags: u32, + guidItem: Guid, + hBalloonIcon: ?w.HICON, + }, + .X86 => packed struct { + cbSize: u32, + hWnd: ?w.HWND, + uID: u32, + uFlags: NOTIFY_ICON_DATA_FLAGS, + uCallbackMessage: u32, + hIcon: ?w.HICON, + szTip: [128]w.CHAR, + dwState: u32, + dwStateMask: u32, + szInfo: [256]w.CHAR, + Anonymous: packed union { + uTimeout: u32, + uVersion: u32, + }, + szInfoTitle: [64]w.CHAR, + dwInfoFlags: u32, + guidItem: Guid, + hBalloonIcon: ?w.HICON, + }, +}; +const NOTIFY_ICON_MESSAGE = enum(u32) { + ADD = 0, + MODIFY = 1, + DELETE = 2, + SETFOCUS = 3, + SETVERSION = 4, +}; +const NIM_ADD = NOTIFY_ICON_MESSAGE.ADD; +const NIM_MODIFY = NOTIFY_ICON_MESSAGE.MODIFY; +const NIM_DELETE = NOTIFY_ICON_MESSAGE.DELETE; +const NIM_SETFOCUS = NOTIFY_ICON_MESSAGE.SETFOCUS; +const NIM_SETVERSION = NOTIFY_ICON_MESSAGE.SETVERSION; + +const WM_LBUTTONDOWN = @as(u32, 513); +const WM_LBUTTONUP = @as(u32, 514); +const WM_LBUTTONDBLCLK = @as(u32, 515); + +extern "SHELL32" fn Shell_NotifyIconA( + dwMessage: NOTIFY_ICON_MESSAGE, + lpData: ?*NOTIFYICONDATAA, +) callconv(w.WINAPI) BOOL; + +pub fn typedConst(comptime T: type, comptime value: anytype) T { + return typedConst2(T, T, value); +} + +pub fn typedConst2(comptime ReturnType: type, comptime SwitchType: type, comptime value: anytype) ReturnType { + const target_type_error = @as([]const u8, "typedConst cannot convert to " ++ @typeName(ReturnType)); + const value_type_error = @as([]const u8, "typedConst cannot convert " ++ @typeName(@TypeOf(value)) ++ " to " ++ @typeName(ReturnType)); + + switch (@typeInfo(SwitchType)) { + .Int => |target_type_info| { + if (value >= std.math.maxInt(SwitchType)) { + if (target_type_info.signedness == .signed) { + const UnsignedT = @Type(std.builtin.TypeInfo{ .Int = .{ .signedness = .unsigned, .bits = target_type_info.bits } }); + return @bitCast(SwitchType, @as(UnsignedT, value)); + } + } + return value; + }, + .Pointer => |target_type_info| switch (target_type_info.size) { + .One, .Many, .C => { + switch (@typeInfo(@TypeOf(value))) { + .ComptimeInt, .Int => { + const usize_value = if (value >= 0) value else @bitCast(usize, @as(isize, value)); + return @intToPtr(ReturnType, usize_value); + }, + else => @compileError(value_type_error), + } + }, + else => target_type_error, + }, + .Optional => |target_type_info| switch (@typeInfo(target_type_info.child)) { + .Pointer => return typedConst2(ReturnType, target_type_info.child, value), + else => target_type_error, + }, + .Enum => |_| switch (@typeInfo(@TypeOf(value))) { + .Int => return @intToEnum(ReturnType, value), + else => target_type_error, + }, + else => @compileError(target_type_error), + } +} + +const Guid = extern union { + Ints: extern struct { + a: u32, + b: u16, + c: u16, + d: [8]u8, + }, + Bytes: [16]u8, + + const big_endian_hex_offsets = [16]u6{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 }; + const little_endian_hex_offsets = [16]u6{ 6, 4, 2, 0, 11, 9, 16, 14, 19, 21, 24, 26, 28, 30, 32, 34 }; + const hex_offsets = switch (builtin.target.cpu.arch.endian()) { + .Big => big_endian_hex_offsets, + .Little => little_endian_hex_offsets, + }; + + pub fn initString(s: []const u8) Guid { + var guid = Guid{ .Bytes = undefined }; + for (hex_offsets) |hex_offset, i| { + //guid.Bytes[i] = decodeHexByte(s[offset..offset+2]); + guid.Bytes[i] = decodeHexByte([2]u8{ s[hex_offset], s[hex_offset + 1] }); + } + return guid; + } +}; +comptime { + std.debug.assert(@sizeOf(Guid) == 16); +} + +// TODO: is this in the standard lib somewhere? +fn hexVal(c: u8) u4 { + if (c <= '9') return @intCast(u4, c - '0'); + if (c >= 'a') return @intCast(u4, c + 10 - 'a'); + return @intCast(u4, c + 10 - 'A'); +} + +// TODO: is this in the standard lib somewhere? +fn decodeHexByte(hex: [2]u8) u8 { + return @intCast(u8, hexVal(hex[0])) << 4 | hexVal(hex[1]); +} + +pub const IDI_APPLICATION = typedConst([*:0]const u16, @as(u32, 32512)); + +pub extern "USER32" fn LoadIconW( + hInstance: ?w.HINSTANCE, + lpIconName: ?[*:0]const u16, +) callconv(w.WINAPI) ?w.HICON; + // resource.h // #define IDD_MFPLAYBACK_DIALOG 102 // #define IDM_EXIT 105 @@ -627,10 +825,22 @@ extern "KERNEL32" fn GlobalFree( // #define IDC_STATIC -1 const IDM_EXIT = @as(u32, 105); +var h_instance: w.HINSTANCE = undefined; + +fn getWinStyleString(comptime buflen: u32, str: []const u8) [buflen]u8 { + var buf: [buflen]u8 = .{0} ** buflen; + for (str) |c, i| buf[i] = c; + return buf; +} + pub export fn wWinMain(hInstance: w.HINSTANCE, hPrevInstance: ?w.HINSTANCE, lpCmdLine: w.PWSTR, nCmdShow: w.INT) w.INT { _ = hPrevInstance; _ = lpCmdLine; + _ = nCmdShow; + h_instance = hInstance; + + dbg_msg = getWinStyleString(17, "TODO: Send now: x"); // Register the window class. var wc: WNDCLASSA = .{ .lpszClassName = "Clipboard watcher", @@ -662,7 +872,7 @@ pub export fn wWinMain(hInstance: w.HINSTANCE, hPrevInstance: ?w.HINSTANCE, lpCm CW_USEDEFAULT, CW_USEDEFAULT, - null, // Parent window + null, // Parent window: use HWND_MESSAGE to make this completely invisible null, // Menu hInstance, // Instance handle null, // Additional application data @@ -671,8 +881,6 @@ pub export fn wWinMain(hInstance: w.HINSTANCE, hPrevInstance: ?w.HINSTANCE, lpCm if (hwnd == null) return 0; - _ = ShowWindow(hwnd, @intToEnum(SHOW_WINDOW_CMD, nCmdShow)); - // Run the message loop. var msg: MSG = .{ @@ -699,6 +907,14 @@ pub export fn wWinMain(hInstance: w.HINSTANCE, hPrevInstance: ?w.HINSTANCE, lpCm var uFormat: w.UINT = @bitReverse(c_uint, 0); var fAuto: w.BOOL = w.TRUE; var hwndNextViewer: ?w.HWND = null; +var icon_data: NOTIFYICONDATAA = undefined; +var dbg_msg: [17]u8 = undefined; +var cnt: u8 = 0; +var window_state: union(enum(u8)) { + INITIAL = 0, + NORMAL = 1, + MINIMIZED = 2, +} = .INITIAL; fn MainWndProc(hwnd: w.HWND, uMsg: u32, wParam: w.WPARAM, lParam: w.LPARAM) callconv(w.WINAPI) w.LRESULT { //APIENTRY { // static HWND hwndNextViewer; @@ -779,6 +995,10 @@ fn MainWndProc(hwnd: w.HWND, uMsg: u32, wParam: w.WPARAM, lParam: w.LPARAM) call var lpstr = GlobalLock(hnd).?; _ = GetClientRect(hwnd, &rc); + // TODO: copy our lpstr and ship it to our common handler + cnt = (cnt + 1) % 10; + dbg_msg[dbg_msg.len - 1] = '0' + cnt; + _ = MessageBoxA(hwnd, @ptrCast([*:0]const u8, &dbg_msg), "Debug", 0); _ = DrawTextA(hdc, @ptrCast([*:0]const u8, lpstr), -1, &rc, DT_LEFT); _ = GlobalUnlock(hnd); @@ -839,8 +1059,57 @@ fn MainWndProc(hwnd: w.HWND, uMsg: u32, wParam: w.WPARAM, lParam: w.LPARAM) call // Add the window to the clipboard viewer chain. hwndNextViewer = SetClipboardViewer(hwnd); + var tip = getWinStyleString(128, "Clipboard processor"); + icon_data = .{ + .cbSize = @sizeOf(@TypeOf(icon_data)), + .hWnd = hwnd, + .uFlags = NOTIFY_ICON_DATA_FLAGS.initFlags(.{ + .MESSAGE = 1, + .ICON = 1, + .TIP = 1, + }), + .uCallbackMessage = WM_TRAY, + .uID = 4242, + .hIcon = LoadIconW(null, IDI_APPLICATION), // TODO: Custom icon + .szTip = tip, + .dwState = 0, + .dwStateMask = 0, + .szInfo = getWinStyleString(256, "Info"), + .Anonymous = .{ .uTimeout = 0 }, + .szInfoTitle = getWinStyleString(64, "Info Title"), + .dwInfoFlags = 0, + .guidItem = Guid.initString("3e781b84-3ffd-44a1-b3ab-11d0f90136f9"), // generated from duckduckgo + .hBalloonIcon = null, + }; + // stData.hIcon = g_hIcon = LoadIcon(g_hInstance, MAKEINTRESOURCE(IDI_TRAYICON)); + // LoadStringSafe(IDS_TIP, stData.szTip, _countof(stData.szTip)); + if (Shell_NotifyIconA(NIM_ADD, &icon_data) != w.TRUE) { + _ = MessageBoxA(hwnd, "Notification Icon failed to create", "Error", 0); + return -1; // oops + } }, + WM_TRAY => { + switch (lParam) { + WM_LBUTTONUP => { + switch (window_state) { + .INITIAL => { + _ = ShowWindow(hwnd, SHOW_WINDOW_CMD.SHOWNORMAL); + window_state = .NORMAL; + }, + .NORMAL => { + _ = ShowWindow(hwnd, SHOW_WINDOW_CMD.HIDE); + window_state = .MINIMIZED; + }, + .MINIMIZED => { + _ = ShowWindow(hwnd, SHOW_WINDOW_CMD.SHOW); + window_state = .NORMAL; + }, + } + }, + else => {}, + } + }, WM_CHANGECBCHAIN => { // If the next window is closing, repair the chain. @@ -858,6 +1127,7 @@ fn MainWndProc(hwnd: w.HWND, uMsg: u32, wParam: w.WPARAM, lParam: w.LPARAM) call WM_DESTROY => { _ = ChangeClipboardChain(hwnd, hwndNextViewer); + _ = Shell_NotifyIconA(NIM_DELETE, &icon_data); PostQuitMessage(0); },