Compare commits

...

10 Commits

Author SHA1 Message Date
aea9c334e0 Bump liberty, replace help2man with help2adoc
All checks were successful
Alpine 3.20 Success
2025-01-01 06:09:49 +01:00
e53cddb030 Fix up code style adjustments 2024-12-26 12:26:19 +01:00
8832ba2227 Improve Windows packaging
All checks were successful
macOS Success
Alpine 3.20 Success
2024-12-23 17:14:59 +01:00
7bd6993b59 Bump liberty
All checks were successful
Alpine 3.20 Success
2024-12-17 06:38:47 +01:00
8717f425f4 eizo-pcap-decode.go: update for newer models
All checks were successful
Alpine 3.20 Success
2024-11-28 13:29:55 +01:00
7e008c154b Bump release
All checks were successful
Alpine 3.20 Success
2024-11-28 11:22:43 +01:00
9d619115be 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.
2024-11-28 11:22:32 +01:00
02da76e958 eizo-pcap-decode.go: figure out USBHID
All checks were successful
Alpine 3.20 Success
2024-11-27 10:00:08 +01:00
be50fd4b8f Add an EIZO packet decoding tool
All checks were successful
Alpine 3.20 Success
It is fairly dumb, but it allows to make more sense of what is
going on over the wires while using EIZO software.
2024-11-26 11:10:15 +01:00
781fd392fe README.adoc: fix a typo
All checks were successful
Alpine 3.20 Success
2024-11-26 03:27:09 +01:00
11 changed files with 654 additions and 65 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required (VERSION 3.10) cmake_minimum_required (VERSION 3.12)
project (usb-drivers VERSION 1.0.0 project (usb-drivers VERSION 1.1.0
DESCRIPTION "User space USB drivers" LANGUAGES C) DESCRIPTION "User space USB drivers" LANGUAGES C)
# Moar warnings # Moar warnings
@@ -34,10 +34,21 @@ endif ()
# Dependencies # Dependencies
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake) set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
include (IconUtils)
find_package (PkgConfig REQUIRED) find_package (PkgConfig REQUIRED)
pkg_check_modules (libusb libusb-1.0) 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)
pkg_search_module (hidapi hidapi hidapi-hidraw hidapi-libusb) 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_LIBUSB "Compile with libusb-based utilities" ${libusb_FOUND})
option (WITH_HIDAPI "Compile with hidapi-based utilities" ${hidapi_FOUND}) option (WITH_HIDAPI "Compile with hidapi-based utilities" ${hidapi_FOUND})
@@ -85,7 +96,6 @@ endif ()
if (WITH_HIDAPI AND WIN32) if (WITH_HIDAPI AND WIN32)
list (APPEND targets_gui eizoctltray) list (APPEND targets_gui eizoctltray)
include (IconUtils)
set (icon_png_list) set (icon_png_list)
foreach (icon_size 16 32 48) foreach (icon_size 16 32 48)
icon_to_png (eizoctltray ${PROJECT_SOURCE_DIR}/eizoctltray.svg icon_to_png (eizoctltray ${PROJECT_SOURCE_DIR}/eizoctltray.svg
@@ -97,7 +107,6 @@ if (WITH_HIDAPI AND WIN32)
set (icon_ico ${PROJECT_BINARY_DIR}/eizoctltray.ico) set (icon_ico ${PROJECT_BINARY_DIR}/eizoctltray.ico)
icon_for_win32 (${icon_ico} "${icon_png_list}" "${icon_png}") icon_for_win32 (${icon_ico} "${icon_png_list}" "${icon_png}")
list (APPEND icon_ico_list )
set_property (SOURCE eizoctltray.rc set_property (SOURCE eizoctltray.rc
APPEND PROPERTY OBJECT_DEPENDS ${icon_ico}) APPEND PROPERTY OBJECT_DEPENDS ${icon_ico})
@@ -108,28 +117,55 @@ if (WITH_HIDAPI AND WIN32)
target_link_directories (eizoctltray PUBLIC ${hidapi_LIBRARY_DIRS}) target_link_directories (eizoctltray PUBLIC ${hidapi_LIBRARY_DIRS})
target_link_libraries (eizoctltray ${hidapi_LIBRARIES} powrprof) target_link_libraries (eizoctltray ${hidapi_LIBRARIES} powrprof)
endif () endif ()
if (WITH_HIDAPI AND APPLE)
list (APPEND targets_gui eizoctltray)
# Generate documentation from help output # We override the language for the command line target as well,
if (NOT CMAKE_CROSSCOMPILING) # but that doesn't and must not pose any problems.
find_program (HELP2MAN_EXECUTABLE help2man) enable_language (OBJC)
if (NOT HELP2MAN_EXECUTABLE) set_source_files_properties (eizoctl.c PROPERTIES LANGUAGE OBJC)
message (FATAL_ERROR "help2man not found")
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 () endif ()
# Generate documentation from help output
if (NOT WIN32 AND NOT CMAKE_CROSSCOMPILING)
set (HELP2ADOC "${PROJECT_SOURCE_DIR}/liberty/tools/help2adoc.awk")
set (ASCIIMAN "${PROJECT_SOURCE_DIR}/liberty/tools/asciiman.awk")
foreach (target ${targets}) foreach (target ${targets})
set (page_output "${PROJECT_BINARY_DIR}/${target}.1") set (page_adoc "${PROJECT_BINARY_DIR}/${target}.1.adoc")
list (APPEND project_MAN_PAGES "${page_output}") set (page_roff "${PROJECT_BINARY_DIR}/${target}.1")
add_custom_command (OUTPUT ${page_output} list (APPEND project_MAN_PAGES "${page_roff}")
COMMAND ${HELP2MAN_EXECUTABLE} -N
"${PROJECT_BINARY_DIR}/${target}" -o ${page_output} # $<TARGET_FILE:tgt> could be used, if we didn't have to escape it.
DEPENDS ${target} string (REPLACE "\\" "\\\\"
COMMENT "Generating man page for ${target}" VERBATIM) target_path "${PROJECT_BINARY_DIR}/${target}")
add_custom_command (OUTPUT "${page_adoc}"
COMMAND env LC_ALL=C awk -f "${HELP2ADOC}"
-v "Target=${target_path}" > "${page_adoc}"
DEPENDS "${target}" "${HELP2ADOC}"
COMMENT "Generating AsciiDoc man page for ${target}" VERBATIM)
add_custom_command (OUTPUT "${page_roff}"
COMMAND env LC_ALL=C awk -f "${ASCIIMAN}"
"${page_adoc}" > "${page_roff}"
DEPENDS "${page_adoc}" "${ASCIIMAN}"
COMMENT "Generating roff man page for ${target}" VERBATIM)
endforeach () endforeach ()
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES}) add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
endif () endif ()
# The files to be installed # The files to be installed
if (NOT WIN32)
include (GNUInstallDirs) include (GNUInstallDirs)
# These should be accessible by users, but need to touch system devices. # These should be accessible by users, but need to touch system devices.
@@ -143,13 +179,17 @@ install (TARGETS ${targets} DESTINATION ${CMAKE_INSTALL_BINDIR}
${SETUID}) ${SETUID})
install (TARGETS ${targets_gui} DESTINATION ${CMAKE_INSTALL_BINDIR}) install (TARGETS ${targets_gui} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
foreach (page ${project_MAN_PAGES}) foreach (page ${project_MAN_PAGES})
string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}") string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
install (FILES "${page}" install (FILES "${page}"
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}") DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
endforeach () endforeach ()
set (CPACK_SET_DESTDIR TRUE)
else ()
install (TARGETS ${targets} ${targets_gui} DESTINATION .)
endif ()
# CPack # CPack
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch") set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>") set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
@@ -162,5 +202,4 @@ set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user") set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_SET_DESTDIR TRUE)
include (CPack) include (CPack)

9
NEWS Normal file
View File

@@ -0,0 +1,9 @@
1.1.0 (2024-11-28)
* Ported eizoctltray to macOS as well
1.0.0 (2024-11-26)
* Initial release

View File

@@ -19,11 +19,12 @@ and may not run at the same time, as it would contend for device access.
eizoctltray 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 singnal inputs, When holding the Shift or Control keys while switching signal inputs,
it will also suspend or power off the system, respectively. 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 elksmart-comm
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
@@ -58,7 +59,7 @@ https://git.janouch.name/p/usb-drivers/releases[the Releases page on Gitea].
Building Building
-------- --------
Build dependencies: Build dependencies:
CMake, pkg-config, liberty (included), help2man + CMake, pkg-config, liberty (included) +
Runtime dependencies: Runtime dependencies:
libusb-1.0 (elksmart-comm, razer-bw-te-ctl), hidapi >= 0.14 (eizoctl) libusb-1.0 (elksmart-comm, razer-bw-te-ctl), hidapi >= 0.14 (eizoctl)
@@ -77,6 +78,29 @@ Or you can try telling CMake to make a package for you. For Debian it is:
$ cpack -G DEB $ cpack -G DEB
# dpkg -i usb-drivers-*.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 Contributing and Support
------------------------ ------------------------
Use https://git.janouch.name/p/usb-drivers to report bugs, request features, Use https://git.janouch.name/p/usb-drivers to report bugs, request features,

240
eizo-pcap-decode.go Normal file
View File

@@ -0,0 +1,240 @@
// Usage: tshark { -r FILE | -i INTERFACE } -l -T ek --disable-protocol usbhid \
// | go run eizo-pcap-decode.go [ | less -R]
//
// This cannot be done through -T json, because tshark doesn't immediately
// flush the current object's trailing newline, but rather waits to decide
// if it should follow it with a comma. Even with -l, it will flush it late.
// It would be good if we could convince it not to wrap packets in a big array.
package main
import (
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
)
type Packet struct {
Layers struct {
USB struct {
Source string `json:"usb_usb_src"`
Destination string `json:"usb_usb_dst"`
Direction string `json:"usb_usb_endpoint_address_direction"`
MacEndpointType string `json:"usb_usb_darwin_endpoint_type"`
TransferType string `json:"usb_usb_transfer_type"`
} `json:"usb"`
CapData string `json:"usb_usb_capdata"`
ControlResponse string `json:"usb_usb_control_Response"`
DataFragment string `json:"usb_usb_data_fragment"`
} `json:"layers"`
}
func (p *Packet) addr() string {
if p.Layers.USB.Source == "host" {
return p.Layers.USB.Destination
} else {
return p.Layers.USB.Source
}
}
func (p *Packet) isInterrupt() bool {
return p.Layers.USB.MacEndpointType == "3" ||
p.Layers.USB.TransferType == "0x01"
}
func (p *Packet) isControl() bool {
return p.Layers.USB.MacEndpointType == "0" ||
p.Layers.USB.TransferType == "0x02"
}
func (p *Packet) isIncoming() bool {
return p.Layers.USB.Direction == "1"
}
func hexDecode(encoded string) []byte {
decoded, err := hex.DecodeString(strings.ReplaceAll(encoded, ":", ""))
if err != nil {
panic(err)
}
return decoded
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var (
raw *bool
le = binary.LittleEndian
fmtIn, fmtOut, fmtReset string
)
func decodeSubreport(id byte, data []byte) string {
critical := isCriticalSubreport(id)
if len(data) < 6 || critical && len(data) < 8 {
return fmt.Sprintf("%x", data)
}
var cs uint16
if critical {
data, cs = data[2:], le.Uint16(data[0:2])
}
usage := uint32(le.Uint16(data[:2]))<<16 | uint32(le.Uint16(data[2:4]))
filtered := make([]byte, len(data)-6)
for i, b := range data[6:] {
if b < 32 || b > 126 {
filtered[i] = '.'
} else {
filtered[i] = b
}
}
if critical {
return fmt.Sprintf("<> %08x %04x=%04x %x %s",
usage, cs, le.Uint16(data[4:6]), data[6:], string(filtered))
} else if usage == 0xff0000f1 {
// No idea what this is, but it follows the format.
return fmt.Sprintf("<> %08x %04x %s",
usage, le.Uint16(data[4:6]), decodeMP(data[6:]))
} else {
return fmt.Sprintf("<> %08x %04x %x %s",
usage, le.Uint16(data[4:6]), data[6:], string(filtered))
}
}
func decodeResult(data []byte) string {
if len(data) < 7 {
return fmt.Sprintf("%x", data)
}
usage := uint32(le.Uint16(data[:2]))<<16 | uint32(le.Uint16(data[2:4]))
return fmt.Sprintf(">< %08x %04x %02x", usage, le.Uint16(data[4:6]),
data[6])
}
func decodeMP(data []byte) string {
var out string
for i := 0; i+1 < len(data); {
sz := int(data[i+1])
if data[i] == 0xff || i+sz > len(data) {
break
}
if out != "" {
out += " "
}
out += fmt.Sprintf("[%02x] %x", data[i], data[i+2:i+2+sz])
i += 2 + sz
}
return out
}
func isSetSubreport(id byte) bool {
switch id {
case 2, 4, 11, 13:
return true
}
return false
}
func isGetSubreport(id byte) bool {
switch id {
case 3, 5, 12, 14:
return true
}
return false
}
func isCriticalSubreport(id byte) bool {
switch id {
case 11, 12, 13, 14:
return true
}
return false
}
func isSubreport(id byte) bool {
return isSetSubreport(id) || isGetSubreport(id)
}
func processInterrupt(p *Packet) {
data := hexDecode(p.Layers.CapData)
if len(data) < 1 {
return
}
if *raw {
fmt.Printf("%s INT %02x %x\n", p.addr(), data[0], data[1:])
} else if isSubreport(data[0]) {
fmt.Printf("%s INT %s\n", p.addr(), decodeSubreport(data[0], data[1:]))
}
}
func processControl(p *Packet) {
// macOS (Darwin) and Linux report Set_Feature differently.
data := hexDecode(p.Layers.ControlResponse)
if len(data) == 0 {
data = hexDecode(p.Layers.DataFragment)
}
if len(data) < 1 {
return
}
if p.isIncoming() {
if *raw {
fmt.Printf("%s IN %02x %x\n", p.addr(), data[0], data[1:])
} else if data[0] == 1 {
fmt.Printf("%s IN SR %x\n", p.addr(), data[5:])
} else if isGetSubreport(data[0]) {
fmt.Printf("%s IN %s%s%s\n", p.addr(),
fmtIn, decodeSubreport(data[0], data[1:]), fmtReset)
} else if data[0] == 6 {
fmt.Printf("%s IN PC %04x\n", p.addr(), le.Uint16(data[1:]))
} else if data[0] == 7 {
fmt.Printf("%s IN %s\n", p.addr(), decodeResult(data[1:]))
} else if data[0] == 8 {
fmt.Printf("%s IN ID %s %s\n", p.addr(), data[1:9], data[9:])
} else if data[0] == 9 {
fmt.Printf("%s IN MP %s\n", p.addr(), decodeMP(data[1:]))
} else if data[0] == 10 {
fmt.Printf("%s IN CS %04x\n", p.addr(), le.Uint16(data[1:]))
} else {
fmt.Printf("%s IN %02x %x\n", p.addr(), data[0], data[1:])
}
} else {
if *raw {
fmt.Printf("%s OUT %02x %x\n", p.addr(), data[0], data[1:])
} else if isSetSubreport(data[0]) {
fmt.Printf("%s OUT %s%s%s\n", p.addr(),
fmtOut, decodeSubreport(data[0], data[1:]), fmtReset)
} else if data[0] == 10 {
fmt.Printf("%s OUT CS %04x\n", p.addr(), le.Uint16(data[1:]))
} else if data[0] != 1 && !isGetSubreport(data[0]) {
fmt.Printf("%s OUT %02x %x\n", p.addr(), data[0], data[1:])
}
}
}
func main() {
raw = flag.Bool("raw", false, "Do not decode EIZO packets")
flag.Parse()
if _, ok := os.LookupEnv("NO_COLOR"); !ok {
fmtIn, fmtOut, fmtReset = "\x1b[34m", "\x1b[31m", "\x1b[m"
}
decoder := json.NewDecoder(os.Stdin)
for {
var p Packet
if err := decoder.Decode(&p); err != nil {
if errors.Is(err, io.EOF) {
break
}
fmt.Fprintf(os.Stderr, "%v\n", err)
} else if p.isInterrupt() {
processInterrupt(&p)
} else if p.isControl() {
processControl(&p)
}
}
}

305
eizoctl.c
View File

@@ -880,6 +880,20 @@ eizo_get_input_port(struct eizo_monitor *m, uint16_t *port)
return true; 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 static uint16_t
eizo_resolve_port(struct eizo_monitor *m, const char *port) eizo_resolve_port(struct eizo_monitor *m, const char *port)
{ {
@@ -1114,7 +1128,7 @@ main(int argc, char *argv[])
} }
// --- Windows ----------------------------------------------------------------- // --- Windows -----------------------------------------------------------------
#else #elif defined _WIN32
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#include <windows.h> #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, flags_darker, base + IDM_DARKER, L"Darker");
AppendMenu(menu, MF_SEPARATOR, 0, NULL); AppendMenu(menu, MF_SEPARATOR, 0, NULL);
uint16_t ports[16] = {0}; uint16_t ports[16] = {0}, current = 0;
struct eizo_profile_item *item = &m->profile[EIZO_PROFILE_KEY_INPUT_PORTS]; eizo_get_input_ports(m, ports, sizeof ports / sizeof ports[0] - 1);
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;
(void) eizo_get_input_port(m, &current); (void) eizo_get_input_port(m, &current);
if (!ports[0]) if (!ports[0])
ports[0] = current; ports[0] = current;
// USB-C ports are a bit tricky, they only need to be /displayed/ as such. // 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++) { for (size_t i = 0; ports[i]; i++) {
uint8_t usb_c = 0; uint8_t usb_c = 0;
for (size_t u = 0; u < item->len / 2; u++) for (size_t u = 0; u < item->len / 2; u++)
@@ -1442,4 +1447,276 @@ wWinMain(
return msg.wParam; 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 #endif

BIN
eizoctltray-mac.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
eizoctltray-win.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Submodule liberty updated: 492815c8fc...9268fb8eba