diff options
author | Přemysl Eric Janouch <p@janouch.name> | 2024-11-28 09:31:11 +0100 |
---|---|---|
committer | Přemysl Eric Janouch <p@janouch.name> | 2024-11-28 11:22:32 +0100 |
commit | 9d619115beea442d05ccb243cd95cbb613cebc87 (patch) | |
tree | 33c93e3cf8cc0cedfe39fa52e78402be2edccf95 | |
parent | 02da76e9583030d0b5742687b16a77641b232cbe (diff) | |
download | usb-drivers-9d619115beea442d05ccb243cd95cbb613cebc87.tar.gz usb-drivers-9d619115beea442d05ccb243cd95cbb613cebc87.tar.xz usb-drivers-9d619115beea442d05ccb243cd95cbb613cebc87.zip |
Port eizoctltray to macOS
Also bump minimum CMake version for hidapi_ROOT,
and don't try to run the help2man Perl script in MSYS2.
AppKit is a very miserable thing.
-rw-r--r-- | CMakeLists.txt | 78 | ||||
-rw-r--r-- | README.adoc | 28 | ||||
-rw-r--r-- | eizoctl.c | 305 | ||||
-rw-r--r-- | eizoctltray-mac.png | bin | 0 -> 14068 bytes | |||
-rw-r--r-- | eizoctltray-win.png | bin | 0 -> 7858 bytes | |||
-rw-r--r-- | eizoctltray.png | bin | 7854 -> 0 bytes |
6 files changed, 391 insertions, 20 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 5651be1..6ea3e5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.12) project (usb-drivers VERSION 1.0.0 DESCRIPTION "User space USB drivers" LANGUAGES C) @@ -35,9 +35,62 @@ endif () # Dependencies set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake) +# TODO(p): Shove this into IconUtils.cmake. +function (icon_to_iconset_size name svg size iconset outputs) + math (EXPR _size2x "${size} * 2") + set (_dimensions "${size}x${size}") + set (_png1x "${iconset}/icon_${_dimensions}.png") + set (_png2x "${iconset}/icon_${_dimensions}@2x.png") + set (${outputs} "${_png1x};${_png2x}" PARENT_SCOPE) + + set (_find_program_REQUIRE) + if (NOT ${CMAKE_VERSION} VERSION_LESS 3.18.0) + set (_find_program_REQUIRE REQUIRED) + endif () + + find_program (rsvg_convert_EXECUTABLE rsvg-convert ${_find_program_REQUIRE}) + add_custom_command (OUTPUT "${_png1x}" "${_png2x}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${iconset}" + COMMAND ${rsvg_convert_EXECUTABLE} "--output=${_png1x}" + "--width=${size}" "--height=${size}" -- "${svg}" + COMMAND ${rsvg_convert_EXECUTABLE} "--output=${_png2x}" + "--width=${_size2x}" "--height=${_size2x}" -- "${svg}" + DEPENDS "${svg}" + COMMENT "Generating ${name} ${_dimensions} icons" VERBATIM) +endfunction () +function (icon_to_icns svg output_basename output) + get_filename_component (_name "${output_basename}" NAME_WE) + set (_iconset "${PROJECT_BINARY_DIR}/${_name}.iconset") + set (_icon "${PROJECT_BINARY_DIR}/${output_basename}") + set (${output} "${_icon}" PARENT_SCOPE) + + set (_icon_png_list) + foreach (_icon_size 16 32 128 256 512) + icon_to_iconset_size ("${_name}" "${svg}" + "${_icon_size}" "${_iconset}" _icon_pngs) + list (APPEND _icon_png_list ${_icon_pngs}) + endforeach () + add_custom_command (OUTPUT "${_icon}" + COMMAND iconutil -c icns -o "${_icon}" "${_iconset}" + DEPENDS ${_icon_png_list} + COMMENT "Generating ${_name} icon" VERBATIM) + set_source_files_properties ("${_icon}" PROPERTIES + MACOSX_PACKAGE_LOCATION Resources) +endfunction () + find_package (PkgConfig REQUIRED) pkg_check_modules (libusb libusb-1.0) -pkg_search_module (hidapi hidapi hidapi-hidraw hidapi-libusb) + +# On MSYS2, the CMake package cannot link statically, but pkg-config can. +# On macOS, we explicitly want to use the CMake package. +if (WIN32) + 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}) option (WITH_HIDAPI "Compile with hidapi-based utilities" ${hidapi_FOUND}) @@ -97,7 +150,6 @@ if (WITH_HIDAPI AND WIN32) set (icon_ico ${PROJECT_BINARY_DIR}/eizoctltray.ico) icon_for_win32 (${icon_ico} "${icon_png_list}" "${icon_png}") - list (APPEND icon_ico_list ) set_property (SOURCE eizoctltray.rc APPEND PROPERTY OBJECT_DEPENDS ${icon_ico}) @@ -108,9 +160,27 @@ if (WITH_HIDAPI AND WIN32) target_link_directories (eizoctltray PUBLIC ${hidapi_LIBRARY_DIRS}) target_link_libraries (eizoctltray ${hidapi_LIBRARIES} powrprof) endif () +if (WITH_HIDAPI AND APPLE) + list (APPEND targets_gui eizoctltray) + + # We override the language for the command line target as well, + # but that doesn't and must not pose any problems. + enable_language (OBJC) + set_source_files_properties (eizoctl.c PROPERTIES LANGUAGE OBJC) + + set (MACOSX_BUNDLE_GUI_IDENTIFIER name.janouch.eizoctltray) + set (MACOSX_BUNDLE_ICON_FILE eizoctltray.icns) + icon_to_icns (${PROJECT_SOURCE_DIR}/eizoctltray.svg + "${MACOSX_BUNDLE_ICON_FILE}" icon) + + add_executable (eizoctltray MACOSX_BUNDLE eizoctl.c "${icon}") + target_compile_definitions (eizoctltray PUBLIC -DTRAY) + target_compile_options (eizoctltray PUBLIC -fobjc-arc) + target_link_libraries (eizoctltray ${hidapi_LIBRARIES} "-framework Cocoa") +endif () # Generate documentation from help output -if (NOT CMAKE_CROSSCOMPILING) +if (NOT WIN32 AND NOT CMAKE_CROSSCOMPILING) find_program (HELP2MAN_EXECUTABLE help2man) if (NOT HELP2MAN_EXECUTABLE) message (FATAL_ERROR "help2man not found") diff --git a/README.adoc b/README.adoc index 80e7de6..83d859e 100644 --- a/README.adoc +++ b/README.adoc @@ -19,11 +19,12 @@ and may not run at the same time, as it would contend for device access. eizoctltray ~~~~~~~~~~~ -_eizoctltray_ is a derived Windows utility that can stay in the systray. +_eizoctltray_ is a derived Windows/macOS utility that can stay in the systray. When holding the Shift or Control keys while switching signal inputs, it will also suspend or power off the system, respectively. -image::eizoctltray.png["eizoctltray with expanded context menu", 343, 229] +image:eizoctltray-win.png["eizoctltray on Windows with expanded menu", 343, 278] +image:eizoctltray-mac.png["eizoctltray on macOS with expanded menu", 343, 278] elksmart-comm ~~~~~~~~~~~~~ @@ -77,6 +78,29 @@ Or you can try telling CMake to make a package for you. For Debian it is: $ cpack -G DEB # dpkg -i usb-drivers-*.deb +Windows +~~~~~~~ +You can either build within an MSYS2 environment, +or cross-compile using Mingw-w64: + + $ sh -e cmake/Win64Depends.sh + $ cmake -DCMAKE_TOOLCHAIN_FILE=liberty/cmake/toolchains/MinGW-w64-x64.cmake \ + -DCMAKE_BUILD_TYPE=Release -B build + $ cmake --build build + +macOS +~~~~~ +You can either build _eizoctltray_ against Homebrew, +or link hidapi statically for a standalone portable app: + + $ git clone https://github.com/libusb/hidapi.git + $ cmake -S hidapi -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX=$PWD/hidapi-build \ + -DCMAKE_BUILD_TYPE=Release -B hidapi-build + $ cmake --build hidapi-build -- install + $ cmake -Dhidapi_ROOT=$PWD/hidapi-build -DCMAKE_BUILD_TYPE=Release -B build + $ cmake --build build + Contributing and Support ------------------------ Use https://git.janouch.name/p/usb-drivers to report bugs, request features, @@ -880,6 +880,20 @@ eizo_get_input_port(struct eizo_monitor *m, uint16_t *port) return true; } +static void +eizo_get_input_ports(struct eizo_monitor *m, uint16_t *ports, size_t size) +{ + struct eizo_profile_item *item = &m->profile[EIZO_PROFILE_KEY_INPUT_PORTS]; + if (item->len) { + for (size_t i = 0; i < size && i < item->len / 4; i++) + ports[i] = peek_u16le(item->data + i * 4); + } else { + const uint16_t *db = eizo_ports_by_product_name(m->product); + for (size_t i = 0; i < size && db && db[i]; i++) + ports[i] = db[i]; + } +} + static uint16_t eizo_resolve_port(struct eizo_monitor *m, const char *port) { @@ -1114,7 +1128,7 @@ main(int argc, char *argv[]) } // --- Windows ----------------------------------------------------------------- -#else +#elif defined _WIN32 #define WIN32_LEAN_AND_MEAN #include <windows.h> @@ -1208,24 +1222,15 @@ append_monitor(struct eizo_monitor *m, HMENU menu, UINT_PTR base) AppendMenu(menu, flags_darker, base + IDM_DARKER, L"Darker"); AppendMenu(menu, MF_SEPARATOR, 0, NULL); - uint16_t ports[16] = {0}; - struct eizo_profile_item *item = &m->profile[EIZO_PROFILE_KEY_INPUT_PORTS]; - if (item->len) { - for (size_t i = 0; i < 15 && i < item->len / 4; i++) - ports[i] = peek_u16le(item->data + i * 4); - } else { - const uint16_t *db = eizo_ports_by_product_name(m->product); - for (size_t i = 0; i < 15 && db && db[i]; i++) - ports[i] = db[i]; - } - - uint16_t current = 0; + uint16_t ports[16] = {0}, current = 0; + eizo_get_input_ports(m, ports, sizeof ports / sizeof ports[0] - 1); (void) eizo_get_input_port(m, ¤t); if (!ports[0]) ports[0] = current; // USB-C ports are a bit tricky, they only need to be /displayed/ as such. - item = &m->profile[EIZO_PROFILE_KEY_USB_C_INPUT_PORTS]; + 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++) @@ -1442,4 +1447,276 @@ wWinMain( return msg.wParam; } +// --- macOS ------------------------------------------------------------------- +#elif defined __APPLE__ + +#include <AppKit/AppKit.h> +#include <AppKit/NSStatusBar.h> +#include <Foundation/Foundation.h> + +static void message_output(const char *format, ...) ATTRIBUTE_PRINTF(1, 2); +static void message_error(const char *format, ...) ATTRIBUTE_PRINTF(1, 2); + +static void +message_output(const char *format, ...) +{ + va_list ap; + va_start(ap, format); + NSString *message = [[NSString alloc] + initWithFormat:[NSString stringWithUTF8String: format] arguments:ap]; + va_end(ap); + + NSAlert *alert = [NSAlert new]; + [alert setMessageText:message]; + [alert setAlertStyle:NSAlertStyleInformational]; + // XXX: How to make the OK button the first responder? + [alert addButtonWithTitle:@"OK"]; + [NSApp activate]; + [alert.window makeKeyAndOrderFront:nil]; + [alert runModal]; +} + +static void +message_error(const char *format, ...) +{ + va_list ap; + va_start(ap, format); + NSString *message = [[NSString alloc] + initWithFormat:[NSString stringWithUTF8String: format] arguments:ap]; + va_end(ap); + + NSAlert *alert = [NSAlert new]; + [alert setMessageText:message]; + [alert setAlertStyle:NSAlertStyleCritical]; + [alert addButtonWithTitle:@"OK"]; + [NSApp activate]; + [alert.window makeKeyAndOrderFront:nil]; + [alert runModal]; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +/// Monitor provides reference counting, and enables use of NSArray. +@interface Monitor : NSObject +@property (assign, nonatomic) struct eizo_monitor *monitor; +- (instancetype)initWithMonitor:(struct eizo_monitor *)monitor; +@end + +@implementation Monitor + +- (instancetype)initWithMonitor:(struct eizo_monitor *)monitor { + if (self = [super init]) { + _monitor = monitor; + } + return self; +} + +- (void)dealloc { + if (_monitor) { + eizo_monitor_close(_monitor); + free(_monitor); + _monitor = NULL; + } +} + +@end + +@interface ApplicationDelegate + : NSObject <NSApplicationDelegate, NSMenuDelegate> +@property (strong, nonatomic) NSStatusItem *statusItem; +@property (strong, nonatomic) NSMutableArray<Monitor *> *monitors; +@end + +@implementation ApplicationDelegate + +- (Monitor *)getMonitorFrom:(NSControl *)control { + NSInteger index = control.tag / 0x1000; + if (!self.monitors || index < 0 || index >= self.monitors.count) + return nil; + return self.monitors[index]; +} + +- (void)setBrightness:(NSControl *)sender { + Monitor *m = [self getMonitorFrom:sender]; + if (!m) + return; + eizo_set_brightness(m.monitor, sender.doubleValue); +} + +- (void)setInputPort:(NSControl *)sender { + Monitor *m = [self getMonitorFrom:sender]; + NSUInteger input = sender.tag % 0x1000; + if (!m) + return; + eizo_set_input_port(m.monitor, input); + + NSEventModifierFlags mods = [NSEvent modifierFlags]; + if (mods & NSEventModifierFlagShift) { + NSTask *task = [[NSTask alloc] init]; + task.launchPath = @"/usr/bin/pmset"; + task.arguments = @[@"sleepnow"]; + [task launch]; + } +} + +- (void)appendMonitor:(Monitor *)m toMenu:(NSMenu *)menu base:(NSInteger)base { + NSMenuItem *titleItem = [NSMenuItem new]; + titleItem.attributedTitle = [[NSAttributedString alloc] + initWithString:[NSString stringWithFormat:@"%s %s", + m.monitor->product, m.monitor->serial] + attributes:@{ NSFontAttributeName: [NSFont boldSystemFontOfSize:0] }]; + [menu addItem:titleItem]; + [menu addItem:[NSMenuItem separatorItem]]; + [menu addItem:[NSMenuItem sectionHeaderWithTitle:@"Brightness"]]; + + double brightness = 0; + (void) eizo_get_brightness(m.monitor, &brightness); + + // XXX: So, while having a slider is strictly more useful, + // this is not something you're supposed to do in AppKit, if only because: + // - It does not respond to keyboard. + // - Positioning it properly is dark magic. + NSSlider *slider = [NSSlider + sliderWithValue:brightness minValue:0. maxValue:1. + target:self action:@selector(setBrightness:)]; + slider.tag = base; + slider.continuous = true; + + NSView *sliderView = [[NSView alloc] + initWithFrame:NSMakeRect(0, 0, 200., slider.knobThickness + 2.)]; + [sliderView addSubview:slider]; + slider.translatesAutoresizingMaskIntoConstraints = false; + [NSLayoutConstraint activateConstraints:@[ + [slider.leftAnchor + constraintEqualToAnchor:sliderView.leftAnchor constant:+23.], + [slider.rightAnchor + constraintEqualToAnchor:sliderView.rightAnchor constant:-6.], + [slider.centerYAnchor + constraintEqualToAnchor:sliderView.centerYAnchor] + ]]; + + NSMenuItem *brightnessItem = [[NSMenuItem alloc] + initWithTitle:@"" action:nil keyEquivalent:@""]; + brightnessItem.view = sliderView; + + [menu addItem:brightnessItem]; + [menu addItem:[NSMenuItem separatorItem]]; + [menu addItem:[NSMenuItem sectionHeaderWithTitle:@"Input ports"]]; + + uint16_t ports[16] = {0}, current = 0; + eizo_get_input_ports(m.monitor, ports, sizeof ports / sizeof ports[0] - 1); + (void) eizo_get_input_port(m.monitor, ¤t); + 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]; + + NSMenuItem *inputPortItem = [[NSMenuItem alloc] + initWithTitle:title action:@selector(setInputPort:) + keyEquivalent:@""]; + inputPortItem.tag = base + ports[i]; + if (ports[i] == current) + inputPortItem.state = NSControlStateValueOn; + [menu addItem:inputPortItem]; + } +} + +- (void)showMenu { + struct hid_device_info *devs = hid_enumerate(USB_VID_EIZO, 0); + NSMutableArray<Monitor *> *monitors = [NSMutableArray array]; + NSMenu *menu = [NSMenu new]; + [menu setDelegate:self]; + for (struct hid_device_info *p = devs; p; p = p->next) { + struct eizo_monitor *m = calloc(1, sizeof *m); + if (!m) + continue; + + if (!eizo_monitor_open(m, p)) { + message_error("%s", m->error); + free(m); + continue; + } + + Monitor *monitor = [[Monitor alloc] initWithMonitor:m]; + [self appendMonitor:monitor toMenu:menu base:0x1000 * monitors.count]; + [menu addItem:[NSMenuItem separatorItem]]; + [monitors addObject:monitor]; + } + if (!monitors.count) { + NSMenuItem *item = [[NSMenuItem alloc] + initWithTitle:@"No monitors found" action:nil keyEquivalent:@""]; + item.enabled = false; + [menu addItem:item]; + } + + [menu addItem:[NSMenuItem separatorItem]]; + [menu addItem:[[NSMenuItem alloc] + initWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"]]; + + self.monitors = monitors; + + // XXX: Unfortunately, this is not how menus should behave, + // but we really want to generate the menu on demand. + self.statusItem.menu = menu; + [self.statusItem.button performClick:nil]; + self.statusItem.menu = nil; +} + +- (void)menuDidClose:(NSMenu *)menu { + // Close and free up the devices as soon as possible, but no sooner. + dispatch_async(dispatch_get_main_queue(), ^{ + self.monitors = nil; + }); +} + +- (void)applicationDidFinishLaunching:(NSNotification *)notification { + NSStatusBar *systemBar = [NSStatusBar systemStatusBar]; + self.statusItem = [systemBar statusItemWithLength:NSSquareStatusItemLength]; + if (!self.statusItem.button) + return; + + // Not bothering with templates, + // the icon would need to have a hole through it to look better. + NSImage *image = [NSApp applicationIconImage]; + // One would expect the status bar to pick a reasonable size + // automatically, but that is not what happens. + image.size = NSMakeSize(systemBar.thickness, systemBar.thickness); + self.statusItem.button.image = image; + self.statusItem.button.action = @selector(showMenu); +} + +@end + +int +main(int argc, char *argv[]) +{ + @autoreleasepool { + if (argc > 1) + return run(argc, argv, message_output, message_error, true); + + NSApplication *app = [NSApplication sharedApplication]; + ApplicationDelegate *delegate = [ApplicationDelegate new]; + app.delegate = delegate; + [app setActivationPolicy:NSApplicationActivationPolicyAccessory]; + [app run]; + } + return 0; +} + #endif diff --git a/eizoctltray-mac.png b/eizoctltray-mac.png Binary files differnew file mode 100644 index 0000000..09c13c2 --- /dev/null +++ b/eizoctltray-mac.png diff --git a/eizoctltray-win.png b/eizoctltray-win.png Binary files differnew file mode 100644 index 0000000..5daa09a --- /dev/null +++ b/eizoctltray-win.png diff --git a/eizoctltray.png b/eizoctltray.png Binary files differdeleted file mode 100644 index 331259d..0000000 --- a/eizoctltray.png +++ /dev/null |