diff options
-rw-r--r-- | CMakeLists.txt | 15 | ||||
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | README.adoc | 57 | ||||
-rw-r--r-- | eizoctl.c | 132 |
4 files changed, 145 insertions, 61 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e91b48..c240a16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,13 +41,16 @@ pkg_check_modules (libusb libusb-1.0) # On MSYS2, the CMake package cannot link statically, but pkg-config can. # On macOS, we explicitly want to use the CMake package. -if (WIN32) +if (NOT WIN32) + find_package (hidapi QUIET) + if (hidapi_FOUND) + set (hidapi_INCLUDE_DIRS) + set (hidapi_LIBRARY_DIRS) + set (hidapi_LIBRARIES hidapi::hidapi) + endif () +endif () +if (NOT hidapi_FOUND) pkg_search_module (hidapi hidapi hidapi-hidraw hidapi-libusb) -else () - find_package (hidapi) - set (hidapi_INCLUDE_DIRS) - set (hidapi_LIBRARY_DIRS) - set (hidapi_LIBRARIES hidapi::hidapi) endif () option (WITH_LIBUSB "Compile with libusb-based utilities" ${libusb_FOUND}) @@ -1,4 +1,4 @@ -Copyright (c) 2013, 2024, Přemysl Eric Janouch <p@janouch.name> +Copyright (c) 2013, 2024 - 2025, Přemysl Eric Janouch <p@janouch.name> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/README.adoc b/README.adoc index 99f8c74..0efca70 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,7 @@ USB drivers =========== :compact-option: +:source-highlighter: chroma _usb-drivers_ is a collection of utilities to control various hardware over USB. @@ -26,6 +27,60 @@ it will also suspend or power off the system, respectively. image:eizoctltray-win.png["eizoctltray on Windows with expanded menu", 343, 278] image:eizoctltray-mac.png["eizoctltray on macOS with expanded menu", 343, 278] +Installation +^^^^^^^^^^^^ +On Windows, copy it to +__Users\*\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup__. + +On macOS, copy it to the _Applications_ folder, +then add it in _System Settings → General → Login Items → Open at Login_. + +Automation +^^^^^^^^^^ +_eizoctltray_ can also be used the same way as _eizoctl_, just with any output +redirected to message windows, rather than a console window or a terminal. +This is useful for automation, such as with AutoHotkey. + +Beware that Windows is not a fan of how rapidly EIZO monitors switch upstream +USB ports. Thus, if you invoke port switching with keyboard shortcuts, +remember to add a small delay, so that pressed modifier keys are not remembered. +You will also want to silence any error messages. + +.AutoHotkey example +```autohotkey +#Requires AutoHotkey v2.0 +exe := A_Startup . "\eizoctltray.exe" +^#F1:: { ; Windows + Control + F1 + Sleep 500 + Run exe " -qq --input HDMI" +} +^#F2:: { ; Windows + Control + F2 + Sleep 500 + Run exe " -qq --input DP" +} +^#F3:: { ; Windows + Control + F3 + Sleep 500 + Run exe " -qq --input USB-C" +} +#Home:: { ; Windows + Home + Run exe " -q --brightness +0.1" +} +#End:: { ; Windows + End + Run exe " -q --brightness -0.1" +} +``` + +On macOS, the simplest way to bind keyboard shortcuts is the Shortcuts app, +with _Run Shell Scripts_ actions: + +``` +/Applications/eizoctltray.app/Contents/MacOS/eizoctltray -q --input HDMI +``` + +If you have issues with entering a specific key combination, like I did +with ^⌘F1 etc., try changing it later within _System Settings_ → _Keyboard_ → +_Keyboard Shortcuts..._ → _Services_ → _Shortcuts_. + elksmart-comm ~~~~~~~~~~~~~ _elksmart-comm_ interfaces with ELK Smart infrared dongles EKX4S and EKX5S-T, @@ -53,7 +108,7 @@ Regular releases are sporadic. git master should be stable enough. You can get a package with the latest development version as a https://git.janouch.name/p/nixexprs[Nix derivation]. -Windows binaries can be downloaded from +Windows/macOS binaries can be downloaded from https://git.janouch.name/p/usb-drivers/releases[the Releases page on Gitea]. Building @@ -4,7 +4,7 @@ * This program stays independent of the liberty library * in order to build on Windows. * - * Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name> + * Copyright (c) 2025, Přemysl Eric Janouch <p@janouch.name> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. @@ -18,6 +18,14 @@ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ + +// On Windows, vswprintf() interprets %s in the width of the format string, +// and %hs is not really compliant with any standard: +// https://devblogs.microsoft.com/oldnewthing/20190830-00/?p=102823 +#ifdef _WIN32 +#define __USE_MINGW_ANSI_STDIO +#endif + #include <stdarg.h> #include <stdbool.h> #include <stdint.h> @@ -41,7 +49,9 @@ #define hid_init hidapi_hid_init #endif -#if defined __GNUC__ +#if defined __MINGW_GNU_PRINTF +#define ATTRIBUTE_PRINTF(x, y) __MINGW_GNU_PRINTF((x), (y)) +#elif defined __GNUC__ #define ATTRIBUTE_PRINTF(x, y) __attribute__((format(printf, x, y))) #else #define ATTRIBUTE_PRINTF(x, y) @@ -803,7 +813,7 @@ eizo_port_by_name(const char *name) return index; } -static char * +static const char * eizo_port_to_name(uint16_t port) { const char *stem = NULL; @@ -811,14 +821,14 @@ eizo_port_to_name(uint16_t port) if (group && group < sizeof g_port_names / sizeof g_port_names[0]) stem = g_port_names[group][0]; - static char buffer[32] = ""; + static char buf[32] = ""; if (!stem) - snprintf(buffer, sizeof buffer, "%x", port); + snprintf(buf, sizeof buf, "%x", port); else if (!number) - snprintf(buffer, sizeof buffer, "%s", stem); + snprintf(buf, sizeof buf, "%s", stem); else - snprintf(buffer, sizeof buffer, "%s %d", stem, (number + 1)); - return buffer; + snprintf(buf, sizeof buf, "%s %d", stem, (number + 1)); + return buf; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -895,7 +905,7 @@ eizo_get_input_ports(struct eizo_monitor *m, uint16_t *ports, size_t size) } static uint16_t -eizo_resolve_port(struct eizo_monitor *m, const char *port) +eizo_resolve_port_by_name(struct eizo_monitor *m, const char *port) { uint8_t usb_c_index = 0; if (eizo_port_by_name_in_group(port, g_port_names_usb_c, &usb_c_index)) { @@ -907,6 +917,26 @@ eizo_resolve_port(struct eizo_monitor *m, const char *port) return eizo_port_by_name(port); } +static const char * +eizo_resolve_port_to_name(struct eizo_monitor *m, uint16_t port) +{ + // USB-C ports are a bit tricky, they only need to be /displayed/ as such. + struct eizo_profile_item *item = + &m->profile[EIZO_PROFILE_KEY_USB_C_INPUT_PORTS]; + for (uint8_t i = 0; i < item->len / 2; i++) { + if (port != peek_u16le(item->data + i * 2)) + continue; + + static char buf[32] = ""; + if (!i) + snprintf(buf, sizeof buf, "%s", g_port_names_usb_c[0]); + else + snprintf(buf, sizeof buf, "%s %u", g_port_names_usb_c[0], (i + 1)); + return buf; + } + return eizo_port_to_name(port); +} + static bool eizo_set_input_port(struct eizo_monitor *m, uint16_t port) { @@ -951,8 +981,8 @@ catf(struct catbuf *b, const char *format, ...) va_end(ap); if (result >= 0) { b->len += result; - if (b->len > sizeof b->buf) - b->len = sizeof b->buf; + if (b->len >= sizeof b->buf) + b->len = sizeof b->buf - 1; } return b->buf; } @@ -1007,6 +1037,10 @@ eizo_watch(struct eizo_monitor *m, print_fn output, print_fn error) } static const char *usage = "Usage: %s OPTION...\n\n" + " -l, --list\n" + " List all connected EIZO monitors, with their serial number.\n" + " -s, --serial SERIAL\n" + " Only act on the monitor matching the specified serial number.\n" " -b, --brightness [+-]BRIGHTNESS\n" " Change monitor brightness; values go from 0 to 1 and may be relative.\n" " -i, --input NAME\n" @@ -1027,6 +1061,8 @@ run(int argc, char *argv[], print_fn output, print_fn error) { const char *name = argv[0]; static struct option opts[] = { + {"list", no_argument, NULL, 'l'}, + {"serial", required_argument, NULL, 's'}, {"brightness", required_argument, NULL, 'b'}, {"input", required_argument, NULL, 'i'}, {"restart", no_argument, NULL, 'r'}, @@ -1039,11 +1075,17 @@ run(int argc, char *argv[], print_fn output, print_fn error) int quiet = 0; double brightness = NAN; - bool relative = false, restart = false, events = false; - const char *port = NULL; + bool list = false, relative = false, restart = false, events = false; + const char *serial = NULL, *port = NULL; int c = 0; - while ((c = getopt_long(argc, argv, "b:i:h", opts, NULL)) != -1) + while ((c = getopt_long(argc, argv, "ls:b:i:reqhV", opts, NULL)) != -1) switch (c) { + case 'l': + list = true; + break; + case 's': + serial = optarg; + break; case 'b': relative = *optarg == '+' || *optarg == '-'; if (sscanf(optarg, "%lf", &brightness) && isfinite(brightness)) @@ -1093,8 +1135,6 @@ run(int argc, char *argv[], print_fn output, print_fn error) if (quiet > 1) error = print_dummy; - // It should be possible to choose a particular monitor, - // but it is generally more useful to operate on all of them. struct hid_device_info *devs = hid_enumerate(USB_VID_EIZO, 0), *p = devs; for (; p; p = p->next) { struct eizo_monitor m = {}; @@ -1103,6 +1143,15 @@ run(int argc, char *argv[], print_fn output, print_fn error) continue; } + if (list) + output("%s %s\n", m.product, m.serial); + + // Generously assuming that different products/models + // don't share serial numbers, + // which would otherwise deserve another filtering option. + if (serial && strcmp(serial, m.serial)) + goto next; + if (isfinite(brightness)) { double prev = 0.; if (!eizo_get_brightness(&m, &prev)) { @@ -1118,13 +1167,12 @@ run(int argc, char *argv[], print_fn output, print_fn error) } if (port) { uint16_t prev = 0; - uint16_t next = eizo_resolve_port(&m, port); + uint16_t next = eizo_resolve_port_by_name(&m, port); if (!eizo_get_input_port(&m, &prev)) { error("Failed to get input port: %s\n", m.error); } else if (!strcmp(port, "?")) { - // XXX: This does not report USB-C. output("%s %s: input: %s\n", - m.product, m.serial, eizo_port_to_name(prev)); + m.product, m.serial, eizo_resolve_port_to_name(&m, prev)); } else if (!next) { error("Failed to resolve port name: %s\n", port); } else { @@ -1148,6 +1196,7 @@ run(int argc, char *argv[], print_fn output, print_fn error) error("%s\n", m.error); } + next: eizo_monitor_close(&m); } hid_free_enumeration(devs); @@ -1202,10 +1251,13 @@ message_printf(const char *format, va_list ap) return NULL; mbstowcs(format_wide, format, format_wide_len); - int message_len = vswprintf(NULL, 0, format_wide, ap) + 1; + // Note that just vswprintf() cannot be used like this + // (at least since mingw-w64 commit c85d64), + // and vsnwprintf() is a MinGW extension, acting like C11 vsnwprintf_s. + int message_len = vsnwprintf(NULL, 0, format_wide, ap) + 1; wchar_t *message = calloc(message_len, sizeof *message); if (message_len > 0 && message) - vswprintf(message, message_len, format_wide, ap); + vsnwprintf(message, message_len, format_wide, ap); free(format_wide); return message; @@ -1222,8 +1274,8 @@ message_output(const char *format, ...) wchar_t *message = message_printf(format, ap); va_end(ap); if (message) { - MessageBox( - NULL, message, NULL, MB_ICONINFORMATION | MB_OK | MB_APPLMODAL); + MessageBox(NULL, message, + L"Message", MB_ICONINFORMATION | MB_OK | MB_APPLMODAL); free(message); } } @@ -1283,21 +1335,9 @@ append_monitor(struct eizo_monitor *m, HMENU menu, UINT_PTR base) if (!ports[0]) ports[0] = current; - // USB-C ports are a bit tricky, they only need to be /displayed/ as such. - struct eizo_profile_item *item = - &m->profile[EIZO_PROFILE_KEY_USB_C_INPUT_PORTS]; for (size_t i = 0; ports[i]; i++) { - uint8_t usb_c = 0; - for (size_t u = 0; u < item->len / 2; u++) - if (ports[i] == peek_u16le(item->data + u * 2)) - usb_c = u + 1; - - if (!usb_c) - snwprintf(buf, sizeof buf, L"%s", eizo_port_to_name(ports[i])); - else if (usb_c == 1) - snwprintf(buf, sizeof buf, L"%s", g_port_names_usb_c[0]); - else - snwprintf(buf, sizeof buf, L"%s %u", g_port_names_usb_c[0], usb_c); + snwprintf(buf, sizeof buf, L"%s", + eizo_resolve_port_to_name(m, ports[i])); UINT flags = MF_STRING; if (ports[i] == current) @@ -1664,23 +1704,9 @@ message_error(const char *format, ...) if (!ports[0]) ports[0] = current; - // USB-C ports are a bit tricky, they only need to be /displayed/ as such. - struct eizo_profile_item *item = - &m.monitor->profile[EIZO_PROFILE_KEY_USB_C_INPUT_PORTS]; for (size_t i = 0; ports[i]; i++) { - uint8_t usb_c = 0; - for (size_t u = 0; u < item->len / 2; u++) - if (ports[i] == peek_u16le(item->data + u * 2)) - usb_c = u + 1; - - NSString *title = nil; - if (!usb_c) - title = [NSString stringWithUTF8String:eizo_port_to_name(ports[i])]; - else if (usb_c == 1) - title = [NSString stringWithUTF8String:g_port_names_usb_c[0]]; - else - title = [NSString stringWithFormat:@"%s %u", - g_port_names_usb_c[0], usb_c]; + NSString *title = [NSString stringWithUTF8String: + eizo_resolve_port_to_name(m.monitor, ports[i])]; NSMenuItem *inputPortItem = [[NSMenuItem alloc] initWithTitle:title action:@selector(setInputPort:) |