Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
b4c1817c10 | |||
32ea934947 | |||
55984bc7ef | |||
dab190e857 | |||
2594f8467d | |||
9039db44f6 | |||
89cab6fa39 | |||
aea9c334e0 | |||
e53cddb030 | |||
8832ba2227 | |||
7bd6993b59 | |||
8717f425f4 | |||
7e008c154b | |||
9d619115be | |||
02da76e958 | |||
be50fd4b8f | |||
781fd392fe |
@ -1,5 +1,5 @@
|
||||
cmake_minimum_required (VERSION 3.10)
|
||||
project (usb-drivers VERSION 1.0.0
|
||||
cmake_minimum_required (VERSION 3.12)
|
||||
project (usb-drivers VERSION 1.1.0
|
||||
DESCRIPTION "User space USB drivers" LANGUAGES C)
|
||||
|
||||
# Moar warnings
|
||||
@ -34,10 +34,21 @@ endif ()
|
||||
|
||||
# Dependencies
|
||||
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
|
||||
include (IconUtils)
|
||||
|
||||
find_package (PkgConfig REQUIRED)
|
||||
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)
|
||||
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})
|
||||
@ -85,7 +96,6 @@ endif ()
|
||||
if (WITH_HIDAPI AND WIN32)
|
||||
list (APPEND targets_gui eizoctltray)
|
||||
|
||||
include (IconUtils)
|
||||
set (icon_png_list)
|
||||
foreach (icon_size 16 32 48)
|
||||
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)
|
||||
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,28 +117,55 @@ 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)
|
||||
|
||||
# Generate documentation from help output
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
find_program (HELP2MAN_EXECUTABLE help2man)
|
||||
if (NOT HELP2MAN_EXECUTABLE)
|
||||
message (FATAL_ERROR "help2man not found")
|
||||
# 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 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})
|
||||
set (page_output "${PROJECT_BINARY_DIR}/${target}.1")
|
||||
list (APPEND project_MAN_PAGES "${page_output}")
|
||||
add_custom_command (OUTPUT ${page_output}
|
||||
COMMAND ${HELP2MAN_EXECUTABLE} -N
|
||||
"${PROJECT_BINARY_DIR}/${target}" -o ${page_output}
|
||||
DEPENDS ${target}
|
||||
COMMENT "Generating man page for ${target}" VERBATIM)
|
||||
set (page_adoc "${PROJECT_BINARY_DIR}/${target}.1.adoc")
|
||||
set (page_roff "${PROJECT_BINARY_DIR}/${target}.1")
|
||||
list (APPEND project_MAN_PAGES "${page_roff}")
|
||||
|
||||
# $<TARGET_FILE:tgt> could be used, if we didn't have to escape it.
|
||||
string (REPLACE "\\" "\\\\"
|
||||
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 ()
|
||||
|
||||
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
|
||||
endif ()
|
||||
|
||||
# The files to be installed
|
||||
if (NOT WIN32)
|
||||
include (GNUInstallDirs)
|
||||
|
||||
# These should be accessible by users, but need to touch system devices.
|
||||
@ -143,13 +179,17 @@ install (TARGETS ${targets} DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
${SETUID})
|
||||
install (TARGETS ${targets_gui} DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
|
||||
|
||||
foreach (page ${project_MAN_PAGES})
|
||||
string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
|
||||
install (FILES "${page}"
|
||||
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
|
||||
endforeach ()
|
||||
|
||||
set (CPACK_SET_DESTDIR TRUE)
|
||||
else ()
|
||||
install (TARGETS ${targets} ${targets_gui} DESTINATION .)
|
||||
endif ()
|
||||
|
||||
# CPack
|
||||
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
|
||||
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_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
|
||||
|
||||
set (CPACK_SET_DESTDIR TRUE)
|
||||
include (CPack)
|
||||
|
2
LICENSE
2
LICENSE
@ -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.
|
||||
|
16
NEWS
Normal file
16
NEWS
Normal file
@ -0,0 +1,16 @@
|
||||
Unreleased
|
||||
|
||||
* eizoctl: added a --quiet option to suppress information and/or errors
|
||||
|
||||
* eizoctl: fixed input port reporting
|
||||
|
||||
|
||||
1.1.0 (2024-11-28)
|
||||
|
||||
* Ported eizoctltray to macOS as well
|
||||
|
||||
|
||||
1.0.0 (2024-11-26)
|
||||
|
||||
* Initial release
|
||||
|
78
README.adoc
78
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.
|
||||
|
||||
@ -19,11 +20,55 @@ 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.
|
||||
When holding the Shift or Control keys while switching singnal inputs,
|
||||
_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]
|
||||
|
||||
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"
|
||||
}
|
||||
```
|
||||
|
||||
elksmart-comm
|
||||
~~~~~~~~~~~~~
|
||||
@ -52,13 +97,13 @@ 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
|
||||
--------
|
||||
Build dependencies:
|
||||
CMake, pkg-config, liberty (included), help2man +
|
||||
CMake, pkg-config, liberty (included) +
|
||||
Runtime dependencies:
|
||||
libusb-1.0 (elksmart-comm, razer-bw-te-ctl), hidapi >= 0.14 (eizoctl)
|
||||
|
||||
@ -77,6 +122,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,
|
||||
|
240
eizo-pcap-decode.go
Normal file
240
eizo-pcap-decode.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
455
eizoctl.c
455
eizoctl.c
@ -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);
|
||||
return buffer;
|
||||
snprintf(buf, sizeof buf, "%s %d", stem, (number + 1));
|
||||
return buf;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
@ -880,8 +890,22 @@ 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)
|
||||
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)) {
|
||||
@ -893,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)
|
||||
{
|
||||
@ -920,8 +964,40 @@ eizo_restart(struct eizo_monitor *m)
|
||||
|
||||
// --- Main --------------------------------------------------------------------
|
||||
|
||||
struct catbuf {
|
||||
char buf[4096];
|
||||
size_t len;
|
||||
};
|
||||
|
||||
static const char *
|
||||
catf(struct catbuf *b, const char *format, ...) ATTRIBUTE_PRINTF(2, 3);
|
||||
|
||||
static const char *
|
||||
catf(struct catbuf *b, const char *format, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, format);
|
||||
int result = vsnprintf(b->buf + b->len, sizeof b->buf - b->len, format, ap);
|
||||
va_end(ap);
|
||||
if (result >= 0) {
|
||||
b->len += result;
|
||||
if (b->len >= sizeof b->buf)
|
||||
b->len = sizeof b->buf - 1;
|
||||
}
|
||||
return b->buf;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
typedef void (*print_fn)(const char *format, ...) ATTRIBUTE_PRINTF(1, 2);
|
||||
|
||||
static void print_dummy(const char *format, ...)
|
||||
{
|
||||
(void) format;
|
||||
}
|
||||
|
||||
static bool
|
||||
eizo_watch(struct eizo_monitor *m)
|
||||
eizo_watch(struct eizo_monitor *m, print_fn output, print_fn error)
|
||||
{
|
||||
uint8_t buf[1024] = {};
|
||||
int res = 0;
|
||||
@ -931,57 +1007,72 @@ eizo_watch(struct eizo_monitor *m)
|
||||
|
||||
if (buf[0] != EIZO_REPORT_ID_GET &&
|
||||
buf[0] != EIZO_REPORT_ID_GET_LONG) {
|
||||
printf("Unknown report ID\n");
|
||||
error("Unknown report ID: %02x\n", buf[0]);
|
||||
continue;
|
||||
}
|
||||
|
||||
struct catbuf message = {{0}, 0};
|
||||
uint16_t page = peek_u16le(&buf[1]), id = peek_u16le(&buf[3]);
|
||||
uint32_t usage = page << 16 | id;
|
||||
printf("%08x", usage);
|
||||
catf(&message, "%08x", usage);
|
||||
|
||||
const struct parser_report *r = eizo_monitor_subreport(m, usage);
|
||||
if (!r) {
|
||||
printf(" unknown usage\n");
|
||||
output(catf(&message, " unknown usage\n"));
|
||||
continue;
|
||||
}
|
||||
size_t rlen = r->report_size / 8 * r->report_count;
|
||||
if ((size_t) res < 7 + rlen) {
|
||||
printf(" received data too short\n");
|
||||
output(catf(&message, " received data too short\n"));
|
||||
continue;
|
||||
}
|
||||
if (r->report_size == 16)
|
||||
for (size_t i = 0; i + 1 < rlen; i += 2)
|
||||
printf(" %04x", peek_u16le(&buf[7 + i]));
|
||||
catf(&message, " %04x", peek_u16le(&buf[7 + i]));
|
||||
else
|
||||
for (size_t i = 0; i < rlen; i++)
|
||||
printf(" %02x", buf[7 + i]);
|
||||
printf("\n");
|
||||
catf(&message, " %02x", buf[7 + i]);
|
||||
output(catf(&message, "\n"));
|
||||
}
|
||||
}
|
||||
|
||||
typedef void (*print_fn)(const char *format, ...) ATTRIBUTE_PRINTF(1, 2);
|
||||
static const char *usage = "Usage: %s OPTION...\n\n"
|
||||
" -b, --brightness [+-]BRIGHTNESS\n"
|
||||
" Change monitor brightness; values go from 0 to 1 and may be relative.\n"
|
||||
" -i, --input NAME\n"
|
||||
" Change monitor input ports; use '?' to retrieve current values.\n"
|
||||
" -r, --restart\n"
|
||||
" Reboot monitors.\n"
|
||||
" -e, --events\n"
|
||||
" Watch for events reported by monitors.\n"
|
||||
" -q, --quiet\n"
|
||||
" Use once to suppress informative messages, twice to suppress errors.\n"
|
||||
" -h, --help\n"
|
||||
" Display this help and exit.\n"
|
||||
" -V, --version\n"
|
||||
" Output version information and exit.\n";
|
||||
|
||||
static int
|
||||
run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
|
||||
run(int argc, char *argv[], print_fn output, print_fn error)
|
||||
{
|
||||
const char *name = argv[0];
|
||||
const char *usage = "Usage: %s [--brightness [+-]BRIGHTNESS] [--input NAME]"
|
||||
" [--restart] [--events]\n";
|
||||
static struct option opts[] = {
|
||||
{"input", required_argument, NULL, 'i'},
|
||||
{"brightness", required_argument, NULL, 'b'},
|
||||
{"input", required_argument, NULL, 'i'},
|
||||
{"restart", no_argument, NULL, 'r'},
|
||||
{"events", no_argument, NULL, 'e'},
|
||||
{"quiet", no_argument, NULL, 'q'},
|
||||
{"help", no_argument, NULL, 'h'},
|
||||
{"version", no_argument, NULL, 'V'},
|
||||
{}
|
||||
};
|
||||
|
||||
int quiet = 0;
|
||||
double brightness = NAN;
|
||||
bool relative = false, restart = false, events = false;
|
||||
const char *port = NULL;
|
||||
int c = 0;
|
||||
while ((c = getopt_long(argc, argv, "b:i:h", opts, NULL)) != -1)
|
||||
while ((c = getopt_long(argc, argv, "b:i:reqhV", opts, NULL)) != -1)
|
||||
switch (c) {
|
||||
case 'b':
|
||||
relative = *optarg == '+' || *optarg == '-';
|
||||
@ -999,6 +1090,9 @@ run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
|
||||
case 'e':
|
||||
events = true;
|
||||
break;
|
||||
case 'q':
|
||||
quiet++;
|
||||
break;
|
||||
case 'h':
|
||||
output(usage, name);
|
||||
return 0;
|
||||
@ -1024,6 +1118,10 @@ run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
|
||||
error("%ls\n", hid_error(NULL));
|
||||
return 1;
|
||||
}
|
||||
if (quiet > 0)
|
||||
output = print_dummy;
|
||||
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.
|
||||
@ -1043,25 +1141,25 @@ run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
|
||||
double next = relative ? brightness + prev : brightness;
|
||||
if (!eizo_set_brightness(&m, next))
|
||||
error("Failed to set brightness: %s\n", m.error);
|
||||
else if (verbose)
|
||||
else
|
||||
output("%s %s: brightness: %.2f -> %.2f\n",
|
||||
m.product, m.serial, prev, next);
|
||||
}
|
||||
}
|
||||
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, "?")) {
|
||||
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 {
|
||||
if (!eizo_set_input_port(&m, next))
|
||||
error("Failed to set input port: %s\n", m.error);
|
||||
else if (verbose)
|
||||
else
|
||||
output("%s %s: input: %s -> %s\n",
|
||||
m.product, m.serial, eizo_port_to_name(prev), port);
|
||||
}
|
||||
@ -1069,13 +1167,13 @@ run(int argc, char *argv[], print_fn output, print_fn error, bool verbose)
|
||||
if (restart) {
|
||||
if (!eizo_restart(&m))
|
||||
error("Failed to restart: %s\n", m.error);
|
||||
else if (verbose)
|
||||
else
|
||||
output("%s %s: restart\n", m.product, m.serial);
|
||||
}
|
||||
if (events) {
|
||||
if (!verbose)
|
||||
if (quiet)
|
||||
error("Watching events is not possible in this mode\n");
|
||||
else if (!eizo_watch(&m))
|
||||
else if (!eizo_watch(&m, output, error))
|
||||
error("%s\n", m.error);
|
||||
}
|
||||
|
||||
@ -1110,11 +1208,11 @@ stdio_error(const char *format, ...)
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
return run(argc, argv, stdio_output, stdio_error, true);
|
||||
return run(argc, argv, stdio_output, stdio_error);
|
||||
}
|
||||
|
||||
// --- Windows -----------------------------------------------------------------
|
||||
#else
|
||||
#elif defined _WIN32
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
@ -1153,8 +1251,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);
|
||||
}
|
||||
}
|
||||
@ -1208,36 +1306,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];
|
||||
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)
|
||||
@ -1390,7 +1467,7 @@ wWinMain(
|
||||
char *mb = mbargv[i + 1] = calloc(len, sizeof *mb);
|
||||
wcstombs(mb, argv[i], len);
|
||||
}
|
||||
return run(argc + 1, mbargv, message_output, message_error, false);
|
||||
return run(argc + 1, mbargv, message_output, message_error);
|
||||
}
|
||||
LocalFree(argv);
|
||||
|
||||
@ -1442,4 +1519,262 @@ 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;
|
||||
|
||||
for (size_t i = 0; ports[i]; i++) {
|
||||
NSString *title = [NSString stringWithUTF8String:
|
||||
eizo_resolve_port_to_name(m.monitor, ports[i])];
|
||||
|
||||
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);
|
||||
|
||||
NSApplication *app = [NSApplication sharedApplication];
|
||||
ApplicationDelegate *delegate = [ApplicationDelegate new];
|
||||
app.delegate = delegate;
|
||||
[app setActivationPolicy:NSApplicationActivationPolicyAccessory];
|
||||
[app run];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
BIN
eizoctltray-mac.png
Normal file
BIN
eizoctltray-mac.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
eizoctltray-win.png
Normal file
BIN
eizoctltray-win.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.7 KiB |
BIN
eizoctltray.png
BIN
eizoctltray.png
Binary file not shown.
Before Width: | Height: | Size: 7.7 KiB |
2
liberty
2
liberty
@ -1 +1 @@
|
||||
Subproject commit 492815c8fc38ad6e333b2f1c5094a329e3076155
|
||||
Subproject commit 9268fb8eba4a60499809965b3b69c2eb7e3798e7
|
Loading…
x
Reference in New Issue
Block a user