From fee703567f1b5fce7d7ff2118024b39577bf8711 Mon Sep 17 00:00:00 2001
From: Přemysl Janouch 
Date: Thu, 9 Feb 2017 20:12:09 +0100
Subject: Add input-switch
Moving common stuff for DDC/CI communicating programs to ddc-ci.c.
---
 CMakeLists.txt |   5 +-
 README.adoc    |   1 +
 brightness.c   | 218 ++------------------------------------------------
 ddc-ci.c       | 246 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 input-switch.c | 153 +++++++++++++++++++++++++++++++++++
 5 files changed, 411 insertions(+), 212 deletions(-)
 create mode 100644 ddc-ci.c
 create mode 100644 input-switch.c
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 31cbfa1..15e117e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -42,6 +42,9 @@ add_threads (wmstatus)
 add_executable (brightness brightness.c)
 target_link_libraries (brightness ${project_libraries})
 
+add_executable (input-switch input-switch.c)
+target_link_libraries (input-switch ${project_libraries})
+
 add_executable (fancontrol-ng fancontrol-ng.c)
 target_link_libraries (fancontrol-ng ${project_libraries})
 
@@ -76,7 +79,7 @@ if (WITH_GDM)
 	install (TARGETS gdm-switch-user DESTINATION ${CMAKE_INSTALL_BINDIR})
 endif (WITH_GDM)
 
-install (TARGETS wmstatus brightness fancontrol-ng siprandom
+install (TARGETS wmstatus brightness input-switch fancontrol-ng siprandom
 	DESTINATION ${CMAKE_INSTALL_BINDIR})
 install (PROGRAMS shellify DESTINATION ${CMAKE_INSTALL_BINDIR})
 install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
diff --git a/README.adoc b/README.adoc
index 0d3ab97..d1bdf0c 100644
--- a/README.adoc
+++ b/README.adoc
@@ -9,6 +9,7 @@ to other people as well:
    includes PulseAudio volume management and hand-written NUT and MPD clients,
    all in the name of liberation from GPL-licensed software of course.
  - 'brightness' allows me to change the brightness of w/e display device I have.
+ - 'input-switch' likewise switches the input source of external displays.
  - 'fancontrol-ng' is a clone of fancontrol that can handle errors on resume
    from suspend instead of setting fans to maximum speed and quitting;
    in general it doesn't handle everything the original does
diff --git a/brightness.c b/brightness.c
index 3b52fc5..4bf63a7 100644
--- a/brightness.c
+++ b/brightness.c
@@ -17,51 +17,20 @@
  *
  */
 
-// Sources: ddcciv1r1.pdf, i2c-dev.c in Linux, ddccontrol source code,
-//   http://www.boichat.ch/nicolas/ddcci/specs.html was also helpful
-
 // This makes openat() available even though I set _POSIX_C_SOURCE and
 // _XOPEN_SOURCE to a version of POSIX older than 2008
 #define _GNU_SOURCE
 
-// Undo some dwm Makefile damage and import my everything-library
 #include "config.h"
 #undef PROGRAM_NAME
 #define PROGRAM_NAME "brightness"
 #include "liberty/liberty.c"
 
-#include 
+#include "ddc-ci.c"
 #include 
 
-#include 
-#ifndef I2C_FUNC_I2C
-// Fuck you, openSUSE, for fucking up the previous file, see e.g.
-// https://github.com/solettaproject/soletta/commit/427f47f
-#include 
-#endif
-
 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
-static void
-log_message_custom (void *user_data, const char *quote, const char *fmt,
-	va_list ap)
-{
-	(void) user_data;
-	FILE *stream = stdout;
-
-	fprintf (stream, PROGRAM_NAME ": ");
-	fputs (quote, stream);
-	vfprintf (stream, fmt, ap);
-	fputs ("\n", stream);
-}
-
-static void
-wait_ms (long ms)
-{
-	struct timespec ts = { 0, ms * 1000 * 1000 };
-	nanosleep (&ts, NULL);
-}
-
 static bool
 xstrtol (const char *s, long *out)
 {
@@ -73,189 +42,16 @@ xstrtol (const char *s, long *out)
 
 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
-#define DDC_LENGTH_XOR 0x80
-
-enum
-{
-	DDC_ADDRESS_HOST          = 0x50,   ///< Bus master's base address
-	DDC_ADDRESS_DISPLAY       = 0x6E    ///< The display's base address
-};
-
-enum { I2C_WRITE, I2C_READ };
-
-enum
-{
-	DDC_GET_VCP_FEATURE       = 0x01,   ///< Request info about a feature
-	DDC_GET_VCP_FEATURE_REPLY = 0x02,   ///< Feature info response
-	DDC_SET_VCP_FEATURE       = 0x03    ///< Set or activate a feature
-};
-
-enum
-{
-	VCP_BRIGHTNESS            = 0x10,   ///< Standard VCP opcode for brightness
-	VCP_CONTRAST              = 0x12    ///< Standard VCP opcode for contrast
-};
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static bool
-check_edid (int fd, struct error **e)
-{
-	uint8_t edid_req[] = { 0x00 };
-	uint8_t buf[8] = "";
-	struct i2c_msg bufs[] =
-	{
-		{ .addr = 0x50, .flags = 0,
-		  .len = 1, .buf = edid_req },
-		{ .addr = 0x50, .flags = I2C_M_RD,
-		  .len = sizeof buf, .buf = buf },
-	};
-
-	struct i2c_rdwr_ioctl_data data;
-	data.msgs = bufs;
-	data.nmsgs = 2;
-
-	if (ioctl (fd, I2C_RDWR, &data) < 0)
-		return error_set (e, "%s: %s", "ioctl", strerror (errno));
-	if (memcmp ("\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00", buf, sizeof buf))
-		return error_set (e, "invalid EDID");
-	return true;
-}
-
-static bool
-is_a_display (int fd, struct error **e)
-{
-	struct stat st;
-	if (fstat (fd, &st) < 0)
-		return error_set (e, "%s: %s", "fstat", strerror (errno));
-
-	unsigned long funcs;
-	if (!(st.st_mode & S_IFCHR)
-	 || ioctl (fd, I2C_FUNCS, &funcs) < 0
-	 || !(funcs & I2C_FUNC_I2C))
-		return error_set (e, "not an I2C device");
-
-	return check_edid (fd, e);
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static bool
-ddc_send (int fd, unsigned command, void *args, size_t args_len,
-	struct error **e)
-{
-	struct str buf;
-	str_init (&buf);
-	str_pack_u8 (&buf, DDC_ADDRESS_HOST | I2C_READ);
-	str_pack_u8 (&buf, DDC_LENGTH_XOR | (args_len + 1));
-	str_pack_u8 (&buf, command);
-	str_append_data (&buf, args, args_len);
-
-	unsigned xor = DDC_ADDRESS_DISPLAY;
-	for (size_t i = 0; i < buf.len; i++)
-		xor ^= buf.str[i];
-	str_pack_u8 (&buf, xor);
-
-	struct i2c_msg msg =
-	{
-		// The driver unshifts it back
-		.addr = DDC_ADDRESS_DISPLAY >> 1, .flags = 0,
-		.len = buf.len, .buf = (uint8_t *) buf.str,
-	};
-
-	struct i2c_rdwr_ioctl_data data;
-	data.msgs = &msg;
-	data.nmsgs = 1;
-
-	bool failed = ioctl (fd, I2C_RDWR, &data) < 0;
-	str_free (&buf);
-	if (failed)
-		return error_set (e, "%s: %s", "ioctl", strerror (errno));
-	return true;
-}
-
-static bool
-ddc_read (int fd, unsigned *command, void *out_buf, size_t *n_read,
-	struct error **e)
-{
-	uint8_t buf[128] = "";
-	struct i2c_msg msg =
-	{
-		// The driver unshifts it back
-		.addr = DDC_ADDRESS_DISPLAY >> 1, .flags = I2C_M_RD,
-		.len = sizeof buf, .buf = buf,
-	};
-
-	struct i2c_rdwr_ioctl_data data;
-	data.msgs = &msg;
-	data.nmsgs = 1;
-
-	if (ioctl (fd, I2C_RDWR, &data) < 0)
-		return error_set (e, "%s: %s", "ioctl", strerror (errno));
-
-	struct msg_unpacker unpacker;
-	msg_unpacker_init (&unpacker, buf, sizeof buf);
-
-	uint8_t sender, length, cmd;
-	(void) msg_unpacker_u8 (&unpacker, &sender);
-	(void) msg_unpacker_u8 (&unpacker, &length);
-	(void) msg_unpacker_u8 (&unpacker, &cmd);
-
-	if (sender != (DDC_ADDRESS_DISPLAY | I2C_WRITE) || !(length & 0x80))
-		return error_set (e, "invalid response");
-	if (!(length ^= 0x80))
-		return error_set (e, "NULL response");
-
-	// TODO: also check the checksum
-
-	*command = cmd;
-	memcpy (out_buf, unpacker.data + unpacker.offset, (*n_read = length - 1));
-	return true;
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
 static bool
 set_brightness (int fd, long diff, struct error **e)
 {
-	uint8_t get_req[] = { VCP_BRIGHTNESS };
-	if (!ddc_send (fd, DDC_GET_VCP_FEATURE, get_req, sizeof get_req, e))
+	struct vcp_feature_readout readout;
+	if (!vcp_get_feature (fd, VCP_BRIGHTNESS, &readout, e))
 		return false;
 
-	wait_ms (40);
-
-	unsigned command = 0;
-	uint8_t buf[128] = "";
-	size_t len = 0;
-	if (!ddc_read (fd, &command, buf, &len, e))
-		return false;
-
-	if (command != DDC_GET_VCP_FEATURE_REPLY || len != 7)
-		return error_set (e, "invalid response");
-
-	struct msg_unpacker unpacker;
-	msg_unpacker_init (&unpacker, buf, len);
-
-	uint8_t result;     msg_unpacker_u8  (&unpacker, &result);
-	uint8_t vcp_opcode; msg_unpacker_u8  (&unpacker, &vcp_opcode);
-	uint8_t type;       msg_unpacker_u8  (&unpacker, &type);
-	int16_t max;        msg_unpacker_i16 (&unpacker, &max);
-	int16_t cur;        msg_unpacker_i16 (&unpacker, &cur);
-
-	if (result == 0x01)
-		return error_set (e, "error reported by monitor");
-
-	if (result != 0x00
-	 || vcp_opcode != VCP_BRIGHTNESS)
-		return error_set (e, "invalid response");
-
-	// These are unsigned but usually just one byte long
-	if (max < 0 || cur < 0)
-		return error_set (e, "capability range overflow");
-
-	int16_t req = (cur * 100 + diff * max + 50) / 100;
-	if (req > max) req = max;
-	if (req < 0)   req = 0;
+	int16_t req = (readout.cur * 100 + diff * readout.max + 50) / 100;
+	req = MIN (req, readout.max);
+	req = MAX (req, 0);
 
 	uint8_t set_req[] = { VCP_BRIGHTNESS, req >> 8, req };
 	if (!ddc_send (fd, DDC_SET_VCP_FEATURE, set_req, sizeof set_req, e))
@@ -263,7 +59,7 @@ set_brightness (int fd, long diff, struct error **e)
 
 	wait_ms (50);
 
-	printf ("brightness set to %.2f%%\n", 100. * req / max);
+	printf ("brightness set to %.2f%%\n", 100. * req / readout.max);
 	return true;
 }
 
diff --git a/ddc-ci.c b/ddc-ci.c
new file mode 100644
index 0000000..0edce8a
--- /dev/null
+++ b/ddc-ci.c
@@ -0,0 +1,246 @@
+/*
+ * ddc-ci.c: DDC-CI utilities, Linux-only
+ *
+ * Copyright (c) 2015 - 2017, Přemysl Janouch 
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ */
+
+// Sources: ddcciv1r1.pdf, i2c-dev.c in Linux, ddccontrol source code,
+//   http://www.boichat.ch/nicolas/ddcci/specs.html was also helpful
+
+#include 
+
+#include 
+#ifndef I2C_FUNC_I2C
+// Fuck you, openSUSE, for fucking up the previous file, see e.g.
+// https://github.com/solettaproject/soletta/commit/427f47f
+#include 
+#endif
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+log_message_custom (void *user_data, const char *quote, const char *fmt,
+	va_list ap)
+{
+	(void) user_data;
+	FILE *stream = stdout;
+
+	fprintf (stream, PROGRAM_NAME ": ");
+	fputs (quote, stream);
+	vfprintf (stream, fmt, ap);
+	fputs ("\n", stream);
+}
+
+static void
+wait_ms (long ms)
+{
+	struct timespec ts = { 0, ms * 1000 * 1000 };
+	nanosleep (&ts, NULL);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#define DDC_LENGTH_XOR 0x80
+
+enum
+{
+	DDC_ADDRESS_HOST          = 0x50,   ///< Bus master's base address
+	DDC_ADDRESS_DISPLAY       = 0x6E    ///< The display's base address
+};
+
+enum { I2C_WRITE, I2C_READ };
+
+enum
+{
+	DDC_GET_VCP_FEATURE       = 0x01,   ///< Request info about a feature
+	DDC_GET_VCP_FEATURE_REPLY = 0x02,   ///< Feature info response
+	DDC_SET_VCP_FEATURE       = 0x03    ///< Set or activate a feature
+};
+
+enum
+{
+	VCP_BRIGHTNESS            = 0x10,   ///< Standard VCP opcode for brightness
+	VCP_CONTRAST              = 0x12,   ///< Standard VCP opcode for contrast
+	VCP_INPUT_SOURCE          = 0x60    ///< Standard VCP opcode for input
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+check_edid (int fd, struct error **e)
+{
+	uint8_t edid_req[] = { 0x00 };
+	uint8_t buf[8] = "";
+	struct i2c_msg bufs[] =
+	{
+		{ .addr = 0x50, .flags = 0,
+		  .len = 1, .buf = edid_req },
+		{ .addr = 0x50, .flags = I2C_M_RD,
+		  .len = sizeof buf, .buf = buf },
+	};
+
+	struct i2c_rdwr_ioctl_data data;
+	data.msgs = bufs;
+	data.nmsgs = 2;
+
+	if (ioctl (fd, I2C_RDWR, &data) < 0)
+		return error_set (e, "%s: %s", "ioctl", strerror (errno));
+	if (memcmp ("\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00", buf, sizeof buf))
+		return error_set (e, "invalid EDID");
+	return true;
+}
+
+static bool
+is_a_display (int fd, struct error **e)
+{
+	struct stat st;
+	if (fstat (fd, &st) < 0)
+		return error_set (e, "%s: %s", "fstat", strerror (errno));
+
+	unsigned long funcs;
+	if (!(st.st_mode & S_IFCHR)
+	 || ioctl (fd, I2C_FUNCS, &funcs) < 0
+	 || !(funcs & I2C_FUNC_I2C))
+		return error_set (e, "not an I2C device");
+
+	return check_edid (fd, e);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+ddc_send (int fd, unsigned command, void *args, size_t args_len,
+	struct error **e)
+{
+	struct str buf;
+	str_init (&buf);
+	str_pack_u8 (&buf, DDC_ADDRESS_HOST | I2C_READ);
+	str_pack_u8 (&buf, DDC_LENGTH_XOR | (args_len + 1));
+	str_pack_u8 (&buf, command);
+	str_append_data (&buf, args, args_len);
+
+	unsigned xor = DDC_ADDRESS_DISPLAY;
+	for (size_t i = 0; i < buf.len; i++)
+		xor ^= buf.str[i];
+	str_pack_u8 (&buf, xor);
+
+	struct i2c_msg msg =
+	{
+		// The driver unshifts it back
+		.addr = DDC_ADDRESS_DISPLAY >> 1, .flags = 0,
+		.len = buf.len, .buf = (uint8_t *) buf.str,
+	};
+
+	struct i2c_rdwr_ioctl_data data;
+	data.msgs = &msg;
+	data.nmsgs = 1;
+
+	bool failed = ioctl (fd, I2C_RDWR, &data) < 0;
+	str_free (&buf);
+	if (failed)
+		return error_set (e, "%s: %s", "ioctl", strerror (errno));
+	return true;
+}
+
+static bool
+ddc_read (int fd, unsigned *command, void *out_buf, size_t *n_read,
+	struct error **e)
+{
+	uint8_t buf[128] = "";
+	struct i2c_msg msg =
+	{
+		// The driver unshifts it back
+		.addr = DDC_ADDRESS_DISPLAY >> 1, .flags = I2C_M_RD,
+		.len = sizeof buf, .buf = buf,
+	};
+
+	struct i2c_rdwr_ioctl_data data;
+	data.msgs = &msg;
+	data.nmsgs = 1;
+
+	if (ioctl (fd, I2C_RDWR, &data) < 0)
+		return error_set (e, "%s: %s", "ioctl", strerror (errno));
+
+	struct msg_unpacker unpacker;
+	msg_unpacker_init (&unpacker, buf, sizeof buf);
+
+	uint8_t sender, length, cmd;
+	(void) msg_unpacker_u8 (&unpacker, &sender);
+	(void) msg_unpacker_u8 (&unpacker, &length);
+	(void) msg_unpacker_u8 (&unpacker, &cmd);
+
+	if (sender != (DDC_ADDRESS_DISPLAY | I2C_WRITE) || !(length & 0x80))
+		return error_set (e, "invalid response");
+	if (!(length ^= 0x80))
+		return error_set (e, "NULL response");
+
+	// TODO: also check the checksum
+
+	*command = cmd;
+	memcpy (out_buf, unpacker.data + unpacker.offset, (*n_read = length - 1));
+	return true;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct vcp_feature_readout
+{
+	uint8_t type;                       ///< Type of the value
+	int16_t max;                        ///< Maximum value
+	int16_t cur;                        ///< Current value
+};
+
+static bool
+vcp_get_feature (int fd, uint8_t feature, struct vcp_feature_readout *out,
+	struct error **e)
+{
+	uint8_t get_req[] = { feature };
+	if (!ddc_send (fd, DDC_GET_VCP_FEATURE, get_req, sizeof get_req, e))
+		return false;
+
+	wait_ms (40);
+
+	unsigned command = 0;
+	uint8_t buf[128] = "";
+	size_t len = 0;
+	if (!ddc_read (fd, &command, buf, &len, e))
+		return false;
+
+	if (command != DDC_GET_VCP_FEATURE_REPLY || len != 7)
+		return error_set (e, "invalid response");
+
+	struct msg_unpacker unpacker;
+	msg_unpacker_init (&unpacker, buf, len);
+
+	uint8_t result;     msg_unpacker_u8  (&unpacker, &result);
+	uint8_t vcp_opcode; msg_unpacker_u8  (&unpacker, &vcp_opcode);
+	msg_unpacker_u8  (&unpacker, &out->type);
+	msg_unpacker_i16 (&unpacker, &out->max);
+	msg_unpacker_i16 (&unpacker, &out->cur);
+
+	if (result == 0x01)
+		return error_set (e, "error reported by monitor");
+
+	if (result != 0x00
+	 || vcp_opcode != feature)
+		return error_set (e, "invalid response");
+
+	// These are unsigned but usually just one byte long
+	if (out->max < 0 || out->cur < 0)
+		return error_set (e, "capability range overflow");
+	return true;
+}
+
diff --git a/input-switch.c b/input-switch.c
new file mode 100644
index 0000000..352c71c
--- /dev/null
+++ b/input-switch.c
@@ -0,0 +1,153 @@
+/*
+ * input-switch.c: switches display input via DDC/CI
+ *
+ * Copyright (c) 2017, Přemysl Janouch 
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ */
+
+// This makes openat() available even though I set _POSIX_C_SOURCE and
+// _XOPEN_SOURCE to a version of POSIX older than 2008
+#define _GNU_SOURCE
+
+#include "config.h"
+#undef PROGRAM_NAME
+#define PROGRAM_NAME "input-switch"
+#include "liberty/liberty.c"
+
+#include "ddc-ci.c"
+#include 
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+set_input_source (int fd, int input, struct error **e)
+{
+	struct vcp_feature_readout readout;
+	if (!vcp_get_feature (fd, VCP_INPUT_SOURCE, &readout, e))
+		return false;
+	if (input < 0 || input > readout.max)
+		return error_set (e, "input index out of range");
+
+	uint8_t set_req[] = { VCP_INPUT_SOURCE, input >> 8, input };
+	if (!ddc_send (fd, DDC_SET_VCP_FEATURE, set_req, sizeof set_req, e))
+		return false;
+
+	wait_ms (50);
+
+	printf ("input set from %d to %d of %d\n", readout.cur, input, readout.max);
+	return true;
+}
+
+static void
+i2c (int input)
+{
+	DIR *dev = opendir ("/dev");
+	if (!dev)
+	{
+		print_error ("cannot access %s: %s: %s",
+			"/dev", "opendir", strerror (errno));
+		return;
+	}
+
+	struct dirent *entry;
+	while ((entry = readdir (dev)))
+	{
+		if (strncmp (entry->d_name, "i2c-", 4))
+			continue;
+
+		printf ("Trying %s... ", entry->d_name);
+		int fd = openat (dirfd (dev), entry->d_name, O_RDONLY);
+		if (fd < 0)
+		{
+			print_error ("%s: %s", "openat", strerror (errno));
+			continue;
+		}
+
+		struct error *e = NULL;
+		if (!is_a_display (fd, &e)
+		 || !set_input_source (fd, input, &e))
+		{
+			printf ("%s\n", e->message);
+			error_free (e);
+		}
+
+		close (fd);
+	}
+	closedir (dev);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// This list is from the MCCS 2.2a specification
+struct
+{
+	int code;                           ///< Input code
+	const char *name;                   ///< Input name
+	int index;                          ///< Input index
+}
+g_inputs[] =
+{
+	{ 0x01, "vga",       1, },          // Analog video (R/G/B) 1
+	{ 0x02, "vga",       2, },          // Analog video (R/G/B) 2
+	{ 0x03, "dvi",       1, },          // Digital video (TMDS) 1 DVI 1
+	{ 0x04, "dvi",       2, },          // Digital video (TMDS) 2 DVI 2
+	{ 0x05, "composite", 1, },          // Composite video 1
+	{ 0x06, "composite", 2, },          // Composite video 2
+	{ 0x07, "s-video",   1, },          // S-video 1
+	{ 0x08, "s-video",   2, },          // S-video 2
+	{ 0x09, "tuner",     1, },          // Tuner 1
+	{ 0x0A, "tuner",     2, },          // Tuner 2
+	{ 0x0B, "tuner",     3, },          // Tuner 3
+	{ 0x0C, "component", 1, },          // Component video (YPbPr/YCbCr) 1
+	{ 0x0D, "component", 2, },          // Component video (YPbPr/YCbCr) 2
+	{ 0x0E, "component", 3, },          // Component video (YPbPr/YCbCr) 3
+	{ 0x0F, "dp",        1, },          // DisplayPort 1
+	{ 0x10, "dp",        2, },          // DisplayPort 2
+	{ 0x11, "hdmi",      1, },          // Digital Video (TMDS) 3 HDMI 1
+	{ 0x12, "hdmi",      2, },          // Digital Video (TMDS) 4 HDMI 2
+};
+
+int
+main (int argc, char *argv[])
+{
+	g_log_message_real = log_message_custom;
+
+	if (argc <= 1)
+	{
+		printf ("Usage: %s  []\n", argv[0]);
+		exit (EXIT_FAILURE);
+	}
+
+	unsigned long input_source = 0;
+	if (xstrtoul (&input_source, argv[1], 10))
+	{
+		i2c (input_source);
+		exit (EXIT_SUCCESS);
+	}
+
+	unsigned long index = 1;
+	if (argc > 2 && !xstrtoul (&index, argv[2], 10))
+		exit_fatal ("given index is not a number: %s", argv[2]);
+	for (size_t i = 0; i < N_ELEMENTS (g_inputs); i++)
+		if (!strcasecmp_ascii (g_inputs[i].name, argv[1])
+		 && g_inputs[i].index == (int) index)
+			input_source = g_inputs[i].code;
+	if (!input_source)
+		exit_fatal ("unknown input_source: %s %lu", argv[1], index);
+
+	i2c (input_source);
+	return 0;
+}
+
-- 
cgit v1.2.3-70-g09d2