aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2024-11-28 09:31:11 +0100
committerPřemysl Eric Janouch <p@janouch.name>2024-11-28 11:22:32 +0100
commit9d619115beea442d05ccb243cd95cbb613cebc87 (patch)
tree33c93e3cf8cc0cedfe39fa52e78402be2edccf95
parent02da76e9583030d0b5742687b16a77641b232cbe (diff)
downloadusb-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.txt78
-rw-r--r--README.adoc28
-rw-r--r--eizoctl.c305
-rw-r--r--eizoctltray-mac.pngbin0 -> 14068 bytes
-rw-r--r--eizoctltray-win.pngbin0 -> 7858 bytes
-rw-r--r--eizoctltray.pngbin7854 -> 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,
diff --git a/eizoctl.c b/eizoctl.c
index 727171c..4207f99 100644
--- a/eizoctl.c
+++ b/eizoctl.c
@@ -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, &current);
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, &current);
+ 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
new file mode 100644
index 0000000..09c13c2
--- /dev/null
+++ b/eizoctltray-mac.png
Binary files differ
diff --git a/eizoctltray-win.png b/eizoctltray-win.png
new file mode 100644
index 0000000..5daa09a
--- /dev/null
+++ b/eizoctltray-win.png
Binary files differ
diff --git a/eizoctltray.png b/eizoctltray.png
deleted file mode 100644
index 331259d..0000000
--- a/eizoctltray.png
+++ /dev/null
Binary files differ