Compare commits

...

11 Commits

Author SHA1 Message Date
6387329316 liustsim: adjust colours
All checks were successful
Alpine 3.22 Success
OpenBSD 7.8 Success
2025-12-07 16:39:45 +01:00
4c51a7f325 wmstatus: fix up configuration comment
All checks were successful
Alpine 3.22 Success
OpenBSD 7.8 Success
2025-12-03 02:01:35 +01:00
ced5aaeff1 wmstatus: make changing the time format possible
All checks were successful
Alpine 3.22 Success
OpenBSD 7.8 Success
As well as disabling the field altogether.
2025-12-03 01:56:31 +01:00
de4379ca2c Add Toshiba Tec LIUST-A00 utilities
All checks were successful
Alpine 3.22 Success
OpenBSD 7.8 Success
2025-11-17 15:16:21 +01:00
f26cfd3bb5 wmstatus: don't spam X session logs without MPD
All checks were successful
Alpine 3.21 Success
OpenBSD 7.6 Success
Allow and default to setting the MPD address to null.
2025-08-10 00:22:22 +02:00
f2ec611c26 Add genpass: a tool to generate passwords
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2025-03-31 21:06:21 +02:00
5b64c639ac CMakeLists.txt: don't enforce setuid bit
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2024-11-25 06:12:20 +01:00
8096a1b2c9 Move elksmart-comm to another repository 2024-11-25 03:31:07 +01:00
bb4fdcd936 wmstatus: fix noise adjustment logic
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
It was possible to trigger an untracked playback stream.
2024-10-12 15:36:34 +02:00
d06beedcaa CMakeLists.txt: install optional targets
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2024-10-12 14:20:31 +02:00
dc3f0d6d05 elksmart-comm: add support for EKX5S-T
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
This device seems to be very picky about USB ports,
but at least learning is reliable,
and it uses the same protocol as EKX4S.
2024-10-12 14:02:30 +02:00
17 changed files with 1457 additions and 723 deletions

View File

@@ -19,14 +19,12 @@ find_package (PkgConfig REQUIRED)
pkg_check_modules (x REQUIRED x11 xext xextproto)
pkg_check_modules (pulse REQUIRED libpulse)
pkg_check_modules (dbus REQUIRED dbus-1)
pkg_check_modules (libusb libusb-1.0)
pkg_check_modules (gdm gdm glib-2.0 gio-2.0)
include_directories (
${x_INCLUDE_DIRS} ${pulse_INCLUDE_DIRS} ${dbus_INCLUDE_DIRS})
link_directories (
${x_LIBRARY_DIRS} ${pulse_LIBRARY_DIRS} ${dbus_LIBRARY_DIRS})
option (WITH_USB "Compile with USB utilities" ${libusb_FOUND})
option (WITH_GDM "Compile with GDM utilities" ${gdm_FOUND})
# Generate a configuration file
@@ -35,7 +33,7 @@ configure_file (${PROJECT_SOURCE_DIR}/config.h.in
include_directories (${PROJECT_BINARY_DIR})
# Build
set (targets wmstatus paswitch siprandom)
set (targets wmstatus paswitch siprandom genpass)
if ("${CMAKE_SYSTEM_NAME}" STREQUAL Linux)
# These use Linux i2c APIs, but can be made to work on macOS
list (APPEND targets brightness input-switch)
@@ -59,13 +57,8 @@ target_link_libraries (wmstatus
${x_LIBRARIES} ${pulse_LIBRARIES} ${dbus_LIBRARIES})
add_threads (wmstatus)
if (WITH_USB)
add_executable (elksmart-comm elksmart-comm.c)
target_include_directories (elksmart-comm PUBLIC ${libusb_INCLUDE_DIRS})
target_link_directories (elksmart-comm PUBLIC ${libusb_LIBRARY_DIRS})
target_link_libraries (elksmart-comm ${libusb_LIBRARIES})
endif ()
if (WITH_GDM)
list (APPEND targets gdm-switch-user)
add_executable (gdm-switch-user gdm-switch-user.c)
target_include_directories (gdm-switch-user PUBLIC ${gdm_INCLUDE_DIRS})
target_link_directories (gdm-switch-user PUBLIC ${gdm_LIBRARY_DIRS})
@@ -103,7 +96,8 @@ endif ()
# These should be accessible by users, but need to touch system devices.
# Use the setuid bit, for simplicity.
foreach (target brightness input-switch elksmart-comm)
set (SETUID "SETUID" CACHE STRING "Set this empty on permission issues")
foreach (target brightness input-switch)
if (${target} IN_LIST targets)
list (REMOVE_ITEM targets ${target})
install (TARGETS ${target} DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -111,7 +105,7 @@ foreach (target brightness input-switch elksmart-comm)
OWNER_WRITE OWNER_READ OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE
SETUID)
${SETUID})
endif ()
endforeach ()

View File

@@ -1,4 +1,4 @@
Copyright (c) 2015 - 2024, Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2015 - 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.

View File

@@ -27,8 +27,6 @@ to other people as well:
pseudo-random data; it should be fast enough to saturate most devices
- 'big-brother' tracks the title of the active window and the idle state of
the user and writes these events to standard output.
- 'elksmart-comm' interfaces with ELK Smart infrared dongles,
partially reimplementing the Ocrustar mobile app.
Few of them are useful outside of Linux.
@@ -40,8 +38,7 @@ Building
--------
Build dependencies: CMake, pkg-config, liberty (included) +
Runtime dependencies: libpulse, libx11, dbus-1 +
Optional runtime dependencies:
libusb-1.0 (elksmart-comm), libgdm (gdm-switch-user)
Optional runtime dependencies: libgdm (gdm-switch-user)
$ git clone --recursive https://git.janouch.name/p/desktop-tools.git
$ mkdir desktop-tools/build

View File

@@ -1,695 +0,0 @@
/*
* elksmart-comm.c: ELK Smart infrared dongle tool (for the 4th generation)
*
* Copyright (c) 2024, 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.
*
* 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.
*
*/
#include "config.h"
#undef PROGRAM_NAME
#define PROGRAM_NAME "elksmart-comm"
#include "liberty/liberty.c"
#include <libusb.h>
// --- Utilities ---------------------------------------------------------------
/// Search for a device with given vendor and product ID.
/// This is quite similar to libusb_open_device_with_vid_pid().
static libusb_device_handle *
find_device (int vendor, int product, int *error)
{
libusb_device **list = NULL;
libusb_device_handle *handle = NULL;
int result = 0;
ssize_t len = libusb_get_device_list (NULL, &list);
if (len < 0)
{
result = len;
goto out;
}
for (ssize_t i = 0; i < len; i++)
{
libusb_device *device = list[i];
struct libusb_device_descriptor desc = {};
if ((result = libusb_get_device_descriptor (device, &desc)))
print_debug ("%s", libusb_strerror (result));
else if (desc.idVendor != vendor || desc.idProduct != product)
continue;
else if (!(result = libusb_open (device, &handle)))
break;
}
libusb_free_device_list (list, true);
out:
if (error != NULL && result != 0)
*error = result;
return handle;
}
static void
wait_ms (long ms)
{
struct timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 };
nanosleep (&ts, NULL);
}
static void
dump_hex (const unsigned char *buf, size_t len)
{
for (size_t i = 0; i < len; i++)
printf ("%02x", buf[i]);
printf ("\n");
}
static bool
read_hex (const char *string, struct str *out)
{
static const char *alphabet = "0123456789abcdef";
str_reset (out);
while (true)
{
while (*string && strchr (" \t\n\r\v\f", *string))
string++;
if (!*string)
return true;
const char *hi, *lo;
if (!(hi = strchr (alphabet, tolower_ascii (*string++))) || !*string
|| !(lo = strchr (alphabet, tolower_ascii (*string++))))
return false;
str_pack_u8 (out, (hi - alphabet) << 4 | (lo - alphabet));
}
}
// --- Coding ------------------------------------------------------------------
// Values are in microseconds.
struct pulse { unsigned on, off; };
static bool
pulse_equal (struct pulse a, struct pulse b)
{
return a.on == b.on && a.off == b.off;
}
static size_t
decode_learned_direct (const uint8_t *b, size_t b_len, struct pulse *pulses)
{
size_t pulses_len = 0;
for (size_t i = 0; i < b_len; )
{
struct pulse *pulse = &pulses[pulses_len++];
while (b[i] == 0xff)
{
pulse->on += 4080;
if (++i == b_len)
return 0;
}
pulse->on += b[i++] * 16;
// Who cares, presumably it stays off.
if (i == b_len)
break;
while (b[i] == 0xff)
{
pulse->off += 4080;
if (++i == b_len)
return 0;
}
pulse->off += b[i++] * 16;
}
return pulses_len;
}
static struct pulse *
decode_learned (const struct str *code, size_t *len, struct error **e)
{
// This conveniently has an upper bound.
struct pulse *pulses = xcalloc (code->len, sizeof *pulses);
if (!(*len = decode_learned_direct
((const uint8_t *) code->str, code->len, pulses)))
{
error_set (e, "code ends unexpectedly");
free (pulses);
return NULL;
}
return pulses;
}
static struct pulse *
encode_nec_byte (struct pulse *p, uint8_t byte)
{
for (int i = 7; i >= 0; i--)
*p++ = (struct pulse)
{ .on = 550, .off = ((byte >> i) & 1) ? 1650 : 550 };
return p;
}
static struct pulse *
encode_nec (const struct str *code, size_t *len, struct error **e)
{
if (code->len % 2)
{
error_set (e, "NEC transmission format requires pairs");
return NULL;
}
// The timings seem to be rather tolerant.
*len = code->len / 2 * (1 /* leader */ + 32 + 1 /* stop */);
struct pulse *pulses = xcalloc (*len, sizeof *pulses), *p = pulses;
for (size_t i = 0; i < code->len; i += 2)
{
*p++ = (struct pulse) { .on = 8500, .off = 4250 };
p = encode_nec_byte (p, code->str[i + 0]);
p = encode_nec_byte (p, ~code->str[i + 0]);
p = encode_nec_byte (p, code->str[i + 1]);
p = encode_nec_byte (p, ~code->str[i + 1]);
*p++ = (struct pulse) { .on = 550, .off = 25000 };
}
return pulses;
}
static void
compress_value (unsigned value, struct str *encoded)
{
if (value <= 2032)
{
// We fix a minor problem in the original Ocrustar algorithm.
uint8_t v = value / 16. + .5;
str_pack_u8 (encoded, MAX (2, v));
}
else
do
{
uint8_t v = value & 0x7f;
if ((value >>= 7))
v |= 0x80;
str_pack_u8 (encoded, v);
} while (value);
}
static void
compress_pulses (const struct pulse *pulses, size_t len, struct str *encoded)
{
unsigned counts[len] = {};
for (size_t i = 0; i < len; i++)
for (size_t k = 0; k < len; k++)
if (pulse_equal (pulses[i], pulses[k]))
counts[i]++;
struct pulse p1 = {}, p2 = {};
size_t top1 = 0, top2 = 0;
for (size_t i = 0; i < len; i++)
if (counts[i] > counts[top1])
p1 = pulses[top1 = i];
for (size_t i = 0; i < len; i++)
if (counts[i] < counts[top1]
&& counts[i] > counts[top2])
p2 = pulses[top2 = i];
else if (counts[top2] == counts[top1])
p2 = pulses[top2 = i];
// Although I haven't really tried it, something tells me that
// this will work even in the degenerated case of len <= 2.
// XXX: The receiver might not like multibyte values here,
// Ocrustar also oddly replaces 0xff with 0xfe for these fields.
compress_value (p2.on, encoded);
compress_value (p2.off, encoded);
compress_value (p1.on, encoded);
compress_value (p1.off, encoded);
str_pack_u8 (encoded, -1);
str_pack_u8 (encoded, -1);
str_pack_u8 (encoded, -1);
for (size_t i = 0; i < len; i++)
{
if (pulse_equal (pulses[i], p1))
str_pack_u8 (encoded, 0);
else if (pulse_equal (pulses[i], p2))
str_pack_u8 (encoded, 1);
else
{
compress_value (pulses[i].on, encoded);
compress_value (pulses[i].off, encoded);
}
}
}
// --- Device interaction ------------------------------------------------------
enum
{
USB_VENDOR_SMTCTL = 0x045c,
// 0x134 (EKX5S ~ 5s, 5th generation remote)
// 0x195 (EKX4S ~ 4s, 4th generation remote)
// 0x184 (international edition)
USB_PRODUCT_SMTCTL_SMART = 0x0195,
// There should only ever be one interface.
USB_INTERFACE = 0,
};
static uint8_t
c_transmit[] = { -1, -1, -1, -1 },
c_learn[] = { -2, -2, -2, -2 },
c_stop[] = { -3, -3, -3, -3 },
c_identify[] = { -4, -4, -4, -4 };
static struct
{
unsigned char endpoint_out; ///< Outgoing endpoint
unsigned char endpoint_in; ///< Incoming endpoint
}
g;
static bool
init_device_from_desc (struct libusb_config_descriptor *desc, struct error **e)
{
// We're not being particuarly strict in here.
if (desc->bNumInterfaces != 1)
return error_set (e, "unexpected USB interface count");
if (desc->interface->num_altsetting != 1)
return error_set (e, "unexpected alternate setting count");
const struct libusb_interface_descriptor *asd = desc->interface->altsetting;
if (asd->bInterfaceClass != LIBUSB_CLASS_COMM)
return error_set (e, "unexpected USB interface class");
if (asd->bNumEndpoints != 2)
return error_set (e, "unexpected endpoint count");
bool have_out = false, have_in = false;
for (uint8_t i = 0; i < asd->bNumEndpoints; i++)
{
const struct libusb_endpoint_descriptor *epd = asd->endpoint + i;
if ((epd->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK)
!= LIBUSB_ENDPOINT_TRANSFER_TYPE_BULK)
return error_set (e, "unexpected endpoint transfer type");
switch ((epd->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK))
{
break; case LIBUSB_ENDPOINT_OUT:
have_out = true;
g.endpoint_out = epd->bEndpointAddress;
break; case LIBUSB_ENDPOINT_IN:
have_in = true;
g.endpoint_in = epd->bEndpointAddress;
}
}
if (!have_out || !have_in)
return error_set (e, "USB interface is not bidirectional");
return true;
}
static bool
init_device (libusb_device_handle *device, struct error **e)
{
struct libusb_config_descriptor *desc = NULL;
int result =
libusb_get_active_config_descriptor (libusb_get_device (device), &desc);
if (result)
return error_set (e, "%s", libusb_strerror (result));
bool ok = true;
if ((result = libusb_kernel_driver_active (device, USB_INTERFACE)) == 1)
ok = error_set (e, "device is claimed by a kernel driver");
else if (result)
ok = error_set (e, "%s", libusb_strerror (result));
else
ok = init_device_from_desc (desc, e);
libusb_free_config_descriptor (desc);
return ok;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static uint8_t
mangle (uint8_t value)
{
uint8_t reversed = 0;
for (int i = 0; i < 8; i++)
{
reversed = (reversed << 1) | (value & 1);
value >>= 1;
}
return ~reversed;
}
static uint8_t
checksum (const uint8_t *b, size_t len)
{
uint32_t sum = 0;
for (size_t i = 0; i < len; i++)
sum += b[i];
return mangle ((sum & 0xF0) | ((sum >> 8) & 0x0F));
}
static bool
send_transmit (libusb_device_handle *device, unsigned long frequency,
const struct pulse *pulses, size_t pulses_len, struct error **e)
{
if (g_debug_mode)
for (size_t i = 0; i < pulses_len; )
{
printf ("%u,%u", pulses[i].on, pulses[i].off);
putchar (++i == pulses_len ? '\n' : ',');
}
struct str compressed = str_make ();
compress_pulses (pulses, pulses_len, &compressed);
struct str message = str_make ();
str_append_data (&message, c_transmit, sizeof c_transmit);
frequency += 0x7ffff;
str_pack_u8 (&message, mangle (frequency >> 8));
str_pack_u8 (&message, mangle (frequency >> 16));
str_pack_u8 (&message, mangle (frequency));
str_pack_u8 (&message, mangle (compressed.len >> 8));
str_pack_u8 (&message, mangle (compressed.len));
str_append_str (&message, &compressed);
str_free (&compressed);
size_t i = 0;
uint8_t buffer[64];
bool ok = true;
while (i != message.len)
{
size_t chunk = MIN (62, message.len - i);
memcpy (buffer, message.str + i, chunk);
i += chunk;
if (chunk == 62)
{
buffer[chunk] = checksum (buffer, chunk);
chunk++;
}
int result = 0, len = 0;
if ((result = libusb_bulk_transfer (device, g.endpoint_out,
buffer, chunk, &len, 100)))
{
ok = error_set (e, "send: %s", libusb_strerror (result));
break;
}
wait_ms (2);
}
str_free (&message);
return ok;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
pulse_is_likely_leader (const struct pulse *p)
{
return p->on >= 2048 && p->off >= 2048;
}
static void
try_to_depulse (const struct str *code)
{
size_t len = 0;
struct pulse *pulses = decode_learned (code, &len, NULL);
if (!pulses)
return;
struct pulse *p = pulses, *end = p + len;
while (p != end && pulse_is_likely_leader (p))
{
p++;
printf ("Attempted pulse decode:\n");
uint8_t bits = 0, nibble = 0;
for (; p != end && !pulse_is_likely_leader (p); p++)
{
nibble = nibble << 1 | (p->off > 2 * p->on);
if (++bits == 4)
{
putchar ("0123456789abcdef"[nibble]);
bits = nibble = 0;
}
}
putchar ('\n');
}
free (pulses);
}
static bool
recv_learn (libusb_device_handle *device, struct str *data, struct error **e)
{
uint8_t buffer[64] = {};
int result = 0, len = 0;
while ((result = libusb_bulk_transfer (device, g.endpoint_in,
buffer, sizeof buffer, &len, 100)))
{
if (result != LIBUSB_ERROR_TIMEOUT)
return error_set (e, "learn/recv: %s", libusb_strerror (result));
print_debug ("learn/recv: %s", libusb_strerror (result));
}
if (len < 6 || memcmp (buffer, c_learn, sizeof c_learn))
return error_set (e, "learn/recv: %s", "unexpected response");
// This field might only make sense for a later device,
// because it doesn't always correspond with how much data we receive.
// Nonetheless, it does match exactly often enough.
size_t size = buffer[4] << 8 | buffer[5];
print_debug ("learn: code size: %zu", size);
str_append_data (data, buffer + 6, len - 6);
dump_hex ((const unsigned char *) data->str, data->len);
while (data->len < size)
{
if (!(result = libusb_bulk_transfer (device, g.endpoint_in,
buffer, sizeof buffer, &len, 100)))
{
dump_hex (buffer, len);
str_append_data (data, buffer, len);
print_debug ("learn: received %d (have %zu of %zu)",
len, data->len, size);
continue;
}
if (result != LIBUSB_ERROR_TIMEOUT)
return error_set (e, "learn/recv: %s", libusb_strerror (result));
// The device seems to queue up its output with pauses.
print_debug ("learn/recv: %s", libusb_strerror (result));
}
// As far as I know, this doesn't do anything,
// and the device doesn't accept it while scanning infrared codes either.
if ((result = libusb_bulk_transfer (device, g.endpoint_out,
c_stop, sizeof c_stop, &len, 100)))
return error_set (e, "learn/send: %s", libusb_strerror (result));
return true;
}
static bool
send_learn (libusb_device_handle *device, struct error **e)
{
int result = 0, len = 0;
if ((result = libusb_bulk_transfer (device, g.endpoint_out,
c_learn, sizeof c_learn, &len, 100)))
return error_set (e, "learn/send: %s", libusb_strerror (result));
printf ("Reading remote control codes.\n");
printf ("Press a remote control button from less than a centimeter.\n");
printf ("The dongle may be unusable until it returns some data.\n");
// ... Resetting the device using libusb_reset_device() doesn't help then.
printf ("If the code fails to replay, retry the capture.\n");
struct str data = str_make ();
bool ok = recv_learn (device, &data, e);
if (ok)
{
printf ("Full command:\n");
dump_hex ((const unsigned char *) data.str, data.len);
try_to_depulse (&data);
}
str_free (&data);
return ok;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
send_identify (libusb_device_handle *device, struct error **e)
{
uint8_t buffer[64] = {};
int result = 0, len = 0;
while (!(result = libusb_bulk_transfer (device, g.endpoint_in,
buffer, sizeof buffer, &len, 10)))
/* Flush buffers. */;
if ((result = libusb_bulk_transfer (device, g.endpoint_out,
c_identify, sizeof c_identify, &len, 100)))
return error_set (e, "identify/send: %s", libusb_strerror (result));
if ((result = libusb_bulk_transfer (device, g.endpoint_in,
buffer, sizeof buffer, &len, 100)))
return error_set (e, "identify/recv: %s", libusb_strerror (result));
// XXX: Sometimes, the device doesn't send any identification values.
if (len != 6 || memcmp (buffer, c_identify, sizeof c_identify)
|| buffer[4] != 0x70 || buffer[5] != 0x01)
return error_set (e, "device busy or not supported");
#if 0
// My device does not respond to this request.
static uint8_t c_serial[] = { -5, -5, -5, -5 };
if ((result = libusb_bulk_transfer (device, g.endpoint_out,
c_serial, sizeof c_serial, &len, 100)))
return error_set (e, "serial/send: %s", libusb_strerror (result));
if ((result = libusb_bulk_transfer (device, g.endpoint_in,
buffer, sizeof buffer, &len, 100)))
return error_set (e, "serial/recv: %s", libusb_strerror (result));
if (len < (int) sizeof c_serial
|| memcmp (buffer, c_serial, sizeof c_serial))
return error_set (e, "serial retrieval failed");
#endif
return true;
}
static bool
run (libusb_device_handle *device, unsigned long frequency, bool nec,
char **codes, size_t codes_len, struct error **e)
{
if (!send_identify (device, e))
return false;
if (!codes_len)
return send_learn (device, e);
struct str code = str_make ();
bool ok = true;
for (size_t i = 0; i < codes_len; i++)
{
if (!read_hex (codes[i], &code))
{
ok = error_set (e, "invalid hex string");
break;
}
size_t pulses_len = 0;
struct pulse *pulses = nec
? encode_nec (&code, &pulses_len, e)
: decode_learned (&code, &pulses_len, e);
ok = pulses && send_transmit (device, frequency, pulses, pulses_len, e);
free (pulses);
if (!ok)
break;
wait_ms (100);
}
str_free (&code);
return ok;
}
// --- Main --------------------------------------------------------------------
int
main (int argc, char *argv[])
{
unsigned long frequency = 38000;
bool nec = false;
static const struct opt opts[] =
{
{ 'd', "debug", NULL, 0, "run in debug mode" },
{ 'f', "frequency", "HZ", 0, "frequency (38000 Hz by default)" },
{ 'n', "nec", NULL, 0, "use the NEC transmission format" },
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 0, NULL, NULL, 0, NULL }
};
struct opt_handler oh = opt_handler_make (argc, argv, opts, "[COMMAND...]",
"Transmit or receive infrared commands.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
switch (c)
{
case 'd':
g_debug_mode = true;
break;
case 'f':
if (!xstrtoul (&frequency, optarg, 10) || !frequency)
exit_fatal ("invalid frequency");
break;
case 'n':
nec = true;
break;
case 'h':
opt_handler_usage (&oh, stdout);
exit (EXIT_SUCCESS);
case 'V':
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS);
default:
print_error ("wrong options");
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
argc -= optind;
argv += optind;
opt_handler_free (&oh);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#if LIBUSB_API_VERSION >= 0x0100010A
const struct libusb_init_option option =
{
.option = LIBUSB_OPTION_LOG_LEVEL,
.value.ival = LIBUSB_LOG_LEVEL_DEBUG,
};
int result = libusb_init_context (NULL, &option, g_debug_mode);
#else
int result = libusb_init (NULL);
#endif
if (result)
exit_fatal ("libusb: %s", libusb_strerror (result));
libusb_device_handle *device =
find_device (USB_VENDOR_SMTCTL, USB_PRODUCT_SMTCTL_SMART, &result);
if (result)
exit_fatal ("couldn't open device: %s", libusb_strerror (result));
else if (!device)
exit_fatal ("no suitable device found");
struct error *e = NULL;
if (!init_device (device, &e))
exit_fatal ("%s", e->message);
if ((result = libusb_claim_interface (device, USB_INTERFACE)) == 1)
exit_fatal ("couldn't claim interface: %s", libusb_strerror (result));
if (!run (device, frequency, nec, argv, argc, &e))
{
print_error ("%s", e->message);
error_free (e);
}
if ((result = libusb_release_interface (device, USB_INTERFACE)) == 1)
exit_fatal ("couldn't release interface: %s", libusb_strerror (result));
libusb_close (device);
libusb_exit (NULL);
return 0;
}

151
genpass.c Normal file
View File

@@ -0,0 +1,151 @@
/*
* genpass.c: password generator
*
* 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.
*
* 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.
*
*/
#include "config.h"
#undef PROGRAM_NAME
#define PROGRAM_NAME "genpass"
#include "liberty/liberty.c"
static struct str
parse_group (const char *group)
{
bool present[0x100] = {};
for (size_t i = 0; group[i]; i++)
{
unsigned char c = group[i];
if (!i || c != '-' || !group[i + 1])
present[c] = true;
else if (group[i + 1] < group[i - 1])
exit_fatal ("character ranges must be increasing");
else
for (c = group[i - 1]; ++c <= group[i + 1]; )
present[c] = true;
}
struct str alphabet = str_make ();
for (size_t i = 1; i < N_ELEMENTS (present); i++)
if (present[i])
str_append_c (&alphabet, i);
if (!alphabet.len)
exit_fatal ("empty group");
return alphabet;
}
static void
parse_program_arguments (int argc, char **argv,
unsigned long *length, struct strv *groups, struct str *alphabet)
{
static const struct opt opts[] =
{
{ 'l', "length", "CHARACTERS", 0, "set password length" },
{ 'd', "debug", NULL, 0, "run in debug mode" },
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 0, NULL, NULL, 0, NULL }
};
struct opt_handler oh =
opt_handler_make (argc, argv, opts, "GROUP...", "Password generator.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
switch (c)
{
case 'l':
if (!xstrtoul (length, optarg, 10) || *length <= 0)
print_fatal ("invalid length argument");
break;
case 'd':
g_debug_mode = true;
break;
case 'h':
opt_handler_usage (&oh, stdout);
exit (EXIT_SUCCESS);
case 'V':
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS);
default:
print_error ("wrong options");
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
argc -= optind;
argv += optind;
for (int i = 0; i < argc; i++)
{
struct str alphabet = parse_group (argv[i]);
strv_append_owned (groups, str_steal (&alphabet));
}
bool present[0x100] = {};
for (size_t i = 0; i < groups->len; i++)
for (size_t k = 0; groups->vector[i][k]; k++)
{
unsigned char c = groups->vector[i][k];
if (present[c])
exit_fatal ("groups are not disjunct");
present[c] = true;
}
for (size_t i = 1; i < N_ELEMENTS (present); i++)
if (present[i])
str_append_c (alphabet, i);
if (groups->len > *length)
exit_fatal ("the requested length is less than the number of groups");
if (!groups->len)
{
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
opt_handler_free (&oh);
}
int
main (int argc, char *argv[])
{
unsigned long length = 8;
struct strv groups = strv_make ();
struct str alphabet = str_make ();
parse_program_arguments (argc, argv, &length, &groups, &alphabet);
unsigned seed = 0;
if (!random_bytes (&seed, sizeof seed, NULL))
exit_fatal ("failed to initialize random numbers");
srand (seed);
// Select from a joined alphabet, but make sure all groups are represented.
struct str candidate = str_make ();
while (true)
{
restart:
for (size_t i = length; i--; )
str_append_c (&candidate, alphabet.str[rand () % alphabet.len]);
for (size_t i = 0; i < groups.len; i++)
if (!strpbrk (candidate.str, groups.vector[i]))
{
str_reset (&candidate);
goto restart;
}
printf ("%s\n", candidate.str);
return 0;
}
}

18
liust-50/README.adoc Normal file
View File

@@ -0,0 +1,18 @@
LIUST-50
========
Included here are a simulator for the Toshiba Tec LIUST-A00 (LIUST-50)
VFD line display, and a status program sending data to the device.
For device documentation, see https://github.com/boricha/M202MD12D which is
seemingly a project for the later LIUST-A10.
Device setup
------------
# stty -F /dev/ttyS0 9600 parenb oddp -crtscts -cstopb cs8
Running
-------
# liustatus > /dev/ttyS0
$ liustatus | liustsim

211
liust-50/charset/charset.go Normal file
View File

@@ -0,0 +1,211 @@
package charset
import (
"bytes"
_ "embed"
"image"
_ "image/png"
"log"
)
// Charsets are loosely based on CP 437 and JIS X 0201.
var runesJapan2 = [256]rune{
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
0x58, 0x59, 0x5A, 0x5B, '¥', 0x5D, 0x5E, 0x5F,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,
0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, '⌂',
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
'▒', '。', '「', '」', '、', '・', 'ヲ', 'ァ',
'ィ', 'ゥ', 'ェ', 'ォ', 'ャ', 'ュ', 'ョ', 'ッ',
'ー', 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ',
'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ',
'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ',
'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ',
'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ',
'リ', 'ル', 'レ', 'ロ', 'ワ', 'ン', '゙', '゚',
'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ',
'Φ', 'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩',
'→', '←', '↓', '↑', '½', '¼', '★', '◊',
'㎏', '℔', '<27>', '×', '▾', '▴', '日', ' ',
}
var runesInternational = [256]rune{
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,
0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, '⌂',
'Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç',
'ê', 'ë', 'è', 'ï', 'î', 'ì', 'Ä', 'Å',
'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù',
'ÿ', 'Ö', 'Ü', '¢', '£', '¥', '₧', 'ƒ',
'á', 'í', 'ó', 'ú', 'ñ', 'Ñ', 'ª', 'º',
'¿', '⌐', '¬', '½', '¼', '¡', '«', '»',
'░', '▒', '▓' - 1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, '█', '▄', '▌', '▐', '▀',
'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ',
'Φ', 'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩',
'≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈',
'°', '∙', '·', '√', 'ⁿ', '²', '■', ' ',
}
var runesInternationalVariants = []string{
"#$@[\\]^`{|}~", // USA
"#$à·ç§^`éùè╍", // France
"#$§ÄÖÜ^`äöüß", // Germany
"£$@[\\]^`{|}~", // UK
"#$@ÆØÅ^`æøå~", // Denmark 1
"#¤ÉÄÖÅÜéäöåü", // Sweden
"#$@·\\é^ùàòèì", // Italy
"₧$@¡Ñ¿^`╍ñ}~", // Spain
"#$@[¥]^`{|}~", // Japan
"#¤ÉÆØÅÜéæøåü", // Norway
"#$ÉÆØÅÜéæøåü", // Denmark 2
"#$á¡Ñ¿é`íñóú", // Spain 2
"#$á¡Ñ¿éüíñóú", // Latin America
}
var internationalVariantsChars = []byte{
0x23, 0x24, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x60, 0x7B, 0x7C, 0x7D, 0x7E}
// ResolveCharToRune tries to decode a character into a Unicode rune.
// It may return rune(-1) if the character is deemed to have no representation.
func ResolveCharToRune(char, charset uint8) rune {
if charset == 0x63 {
return runesJapan2[char]
}
if int(charset) >= len(runesInternationalVariants) {
return -1
}
for i, b := range internationalVariantsChars {
if char == b {
return []rune(runesInternationalVariants[charset])[i]
}
}
return runesInternational[char]
}
// ResolveRune tries to find a corresponding character for a Unicode rune.
func ResolveRune(r rune, charset uint8) (uint8, bool) {
if charset == 0x63 {
for i, ch := range runesJapan2 {
if ch == r {
return uint8(i), true
}
}
return 0, false
}
if int(charset) >= len(runesInternationalVariants) {
return 0, false
}
variantRunes := []rune(runesInternationalVariants[charset])
for i, ch := range variantRunes {
if ch == r {
return internationalVariantsChars[i], true
}
}
for i, ch := range runesInternational {
if ch == r {
return uint8(i), true
}
}
return 0, false
}
//go:embed japan.png
var pngJapan2 []byte
var imageJapan2 image.Image
//go:embed germany.png
var pngGermany []byte
var imageGermany image.Image
//go:embed international.png
var pngInternational []byte
var imageInternational image.Image
func init() {
var err error
imageJapan2, _, err = image.Decode(bytes.NewReader(pngJapan2))
if err != nil {
log.Fatalln(err)
}
imageGermany, _, err = image.Decode(bytes.NewReader(pngGermany))
if err != nil {
log.Fatalln(err)
}
imageInternational, _, err = image.Decode(bytes.NewReader(pngInternational))
if err != nil {
log.Fatalln(err)
}
}
// ResolveCharToImage tries to decode a character into a 5x7 bitmap image
// (white on black).
func ResolveCharToImage(char, charset uint8) image.Image {
const (
gridWidth = 6
gridHeight = 8
)
var src image.Image
var col, row int
if charset == 0x63 {
src, col, row = imageJapan2, int(char)/16, int(char)%16
} else if int(charset) < len(runesInternationalVariants) {
src, col, row = imageGermany, int(char)/16, int(char)%16
for i, b := range internationalVariantsChars {
if char == b {
src, col, row = imageInternational, i, int(charset)
}
}
} else {
return nil
}
x0 := col * gridWidth
y0 := row * gridHeight
return src.(interface {
SubImage(r image.Rectangle) image.Image
}).SubImage(image.Rect(
x0,
y0,
x0+gridWidth-1,
y0+gridHeight-1,
))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

BIN
liust-50/charset/japan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,242 @@
package main
import (
"math/rand"
"strings"
"time"
)
type kaomojiKind int
const (
kaomojiKindAwake kaomojiKind = iota
kaomojiKindBlink
kaomojiKindFace
kaomojiKindChase
kaomojiKindHappy
kaomojiKindSleep
kaomojiKindSnore
kaomojiKindPeek
)
type kaomojiState struct {
kind kaomojiKind
face string
message string
delay int
}
func (ks *kaomojiState) Format() string {
line := []rune(strings.Repeat(" ", displayWidth))
face := []rune(ks.face)
if x := (len(line) - len(face) + 1) / 2; x < 0 {
copy(line, face)
} else {
copy(line[x:], face)
}
if ks.message != "" {
copy(line[14:], []rune(ks.message))
}
return string(line)
}
func (ks *kaomojiState) Duration() time.Duration {
return time.Millisecond * time.Duration(ks.delay)
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
func kaomojiNewAwake() kaomojiState {
return kaomojiState{
kind: kaomojiKindAwake,
face: "(o_o)",
message: "",
delay: 2_000 + rand.Intn(4_000),
}
}
func kaomojiNewBlink() kaomojiState {
return kaomojiState{
kind: kaomojiKindBlink,
face: "(-_-)",
message: "",
delay: 100 + rand.Intn(50),
}
}
func kaomojiNewFace() kaomojiState {
faces := []struct {
face, message string
}{
{"(x_x)", "ズキズキ"},
{"(T_T)", "ズーン"},
{"=^.^=", "ニャー"},
{"(>_<)", "ゲップ"},
{"(O_O)", "ジー"},
}
x := faces[rand.Intn(len(faces))]
return kaomojiState{
kind: kaomojiKindFace,
face: x.face,
message: x.message,
delay: 10_000,
}
}
func kaomojiNewChase() kaomojiState {
faces := []string{"(゚ロ゚)", "(゚∩゚)"}
return kaomojiState{
kind: kaomojiKindChase,
face: faces[rand.Intn(len(faces))],
message: "",
delay: 125,
}
}
func kaomojiNewHappy() kaomojiState {
return kaomojiState{
kind: kaomojiKindHappy,
face: "(^_^)",
message: "",
delay: 500,
}
}
func kaomojiNewSleep() kaomojiState {
return kaomojiState{
kind: kaomojiKindSleep,
face: "(-_-)",
message: "",
delay: 10_000,
}
}
func kaomojiNewSnore() kaomojiState {
return kaomojiState{
kind: kaomojiKindSnore,
face: "(-_-)",
message: "グーグー",
delay: 10_000,
}
}
func kaomojiNewPeek() kaomojiState {
faces := []string{"(o_-)", "(-_o)"}
return kaomojiState{
kind: kaomojiKindPeek,
face: faces[rand.Intn(len(faces))],
message: "",
delay: 3_000,
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
func kaomojiAnimateChase(state kaomojiState) (lines []string) {
// The main character is fixed and of fixed width.
var (
normal = []rune("(o_o)")
alert = []rune("(O_O)")
centre = (displayWidth - 4) / 2
chaserLen = len([]rune(state.face))
)
// For simplicity, let the animation run off-screen.
for chaserX := chaserLen + displayWidth; chaserX >= 0; chaserX-- {
line := []rune(strings.Repeat(" ", chaserLen+displayWidth))
chased, chasedX := normal, chaserLen+centre
if chasedX > chaserX-7 {
chased, chasedX = alert, chaserX-7
}
if chasedX >= 0 {
copy(line[chasedX:], chased)
}
copy(line[chaserX:], []rune(state.face))
lines = append(lines, string(line[chaserLen:]))
}
// Return our main character back.
for chasedX := displayWidth; chasedX >= centre; chasedX-- {
line := []rune(strings.Repeat(" ", displayWidth))
copy(line[chasedX:], normal)
lines = append(lines, string(line))
}
return
}
func kaomojiProducer(lines chan<- string) {
state := kaomojiNewAwake()
execute := func() {
lines <- state.Format()
time.Sleep(state.Duration())
}
for {
switch state.kind {
case kaomojiKindAwake:
execute()
switch f := rand.Float32(); {
case f < 0.025:
state = kaomojiNewFace()
case f < 0.050:
state = kaomojiNewChase()
case f < 0.075:
state = kaomojiNewHappy()
case f < 0.100:
state = kaomojiNewSleep()
default:
state = kaomojiNewBlink()
}
case kaomojiKindBlink, kaomojiKindFace:
execute()
state = kaomojiNewAwake()
case kaomojiKindHappy:
face := state.face
execute()
state.face = " " + face
execute()
state.face = face
execute()
state.face = face + " "
execute()
state.face = face
execute()
state = kaomojiNewAwake()
case kaomojiKindChase:
for _, line := range kaomojiAnimateChase(state) {
lines <- line
time.Sleep(state.Duration())
}
state = kaomojiNewAwake()
case kaomojiKindSleep:
execute()
switch f := rand.Float32(); {
case f < 0.10:
state = kaomojiNewAwake()
case f < 0.20:
state = kaomojiNewPeek()
case f < 0.60:
state = kaomojiNewSnore()
default:
state = kaomojiNewSleep()
}
case kaomojiKindSnore:
execute()
state = kaomojiNewSleep()
case kaomojiKindPeek:
execute()
state = kaomojiNewSleep()
}
}
}

View File

@@ -0,0 +1,146 @@
package main
import (
"fmt"
"math/rand"
"strings"
"time"
"janouch.name/desktop-tools/liust-50/charset"
)
const (
displayWidth = 20
displayHeight = 2
targetCharset = 0x63
)
type DisplayState struct {
Display [displayHeight][displayWidth]uint8
}
type Display struct {
Current, Last DisplayState
}
func NewDisplay() *Display {
t := &Display{}
for y := 0; y < displayHeight; y++ {
for x := 0; x < displayWidth; x++ {
t.Current.Display[y][x] = ' '
t.Last.Display[y][x] = ' '
}
}
return t
}
func (t *Display) SetLine(row int, content string) {
if row < 0 || row >= displayHeight {
return
}
runes := []rune(content)
for x := 0; x < displayWidth; x++ {
if x < len(runes) {
b, ok := charset.ResolveRune(runes[x], targetCharset)
if ok {
t.Current.Display[row][x] = b
} else {
t.Current.Display[row][x] = '?'
}
} else {
t.Current.Display[row][x] = ' '
}
}
}
func (t *Display) HasChanges() bool {
for y := 0; y < displayHeight; y++ {
for x := 0; x < displayWidth; x++ {
if t.Current.Display[y][x] != t.Last.Display[y][x] {
return true
}
}
}
return false
}
func (t *Display) Update() {
for y := 0; y < displayHeight; y++ {
start := -1
for x := 0; x < displayWidth; x++ {
if t.Current.Display[y][x] != t.Last.Display[y][x] {
start = x
break
}
}
if start >= 0 {
fmt.Printf("\x1b[%d;%dH%s",
y+1, start+1, []byte(t.Current.Display[y][start:]))
copy(t.Last.Display[y][start:], t.Current.Display[y][start:])
}
}
}
func statusProducer(lines chan<- string) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
temperature, fetcher := "", NewWeatherFetcher()
temperatureChan := make(chan string)
go fetcher.Run(5*time.Minute, temperatureChan)
for {
select {
case newTemperature := <-temperatureChan:
temperature = newTemperature
default:
}
now := time.Now()
status := fmt.Sprintf("%s %3s %s",
now.Format("Mon _2 Jan"), temperature, now.Format("15:04"))
// Ensure exactly 20 characters.
runes := []rune(status)
if len(runes) > displayWidth {
status = string(runes[:displayWidth])
} else if len(runes) < displayWidth {
status = status + strings.Repeat(" ", displayWidth-len(runes))
}
lines <- status
<-ticker.C
}
}
func main() {
rand.Seed(time.Now().UTC().UnixNano())
terminal := NewDisplay()
kaomojiChan := make(chan string, 1)
statusChan := make(chan string, 1)
go func() {
kaomojiChan <- strings.Repeat(" ", displayWidth)
statusChan <- strings.Repeat(" ", displayWidth)
}()
go kaomojiProducer(kaomojiChan)
go statusProducer(statusChan)
// TODO(p): And we might want to disable cursor visibility as well.
fmt.Printf("\x1bR%c", targetCharset)
fmt.Print("\x1b[2J") // Clear display
for {
select {
case line := <-kaomojiChan:
terminal.SetLine(0, line)
case line := <-statusChan:
terminal.SetLine(1, line)
}
if terminal.HasChanges() {
terminal.Update()
}
}
}

View File

@@ -0,0 +1,132 @@
package main
import (
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"strconv"
"time"
)
const (
baseURL = "https://api.met.no/weatherapi"
userAgent = "liustatus/1.0"
// Prague coordinates.
lat = 50.08804
lon = 14.42076
altitude = 202
)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
type Weatherdata struct {
XMLName xml.Name `xml:"weatherdata"`
Product Product `xml:"product"`
}
type Product struct {
Times []Time `xml:"time"`
}
type Time struct {
From string `xml:"from,attr"`
To string `xml:"to,attr"`
Location Location `xml:"location"`
}
type Location struct {
Temperature *Temperature `xml:"temperature"`
}
type Temperature struct {
Unit string `xml:"unit,attr"`
Value string `xml:"value,attr"`
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// WeatherFetcher handles weather data retrieval.
type WeatherFetcher struct {
client *http.Client
}
// NewWeatherFetcher creates a new weather fetcher instance.
func NewWeatherFetcher() *WeatherFetcher {
return &WeatherFetcher{
client: &http.Client{Timeout: 30 * time.Second},
}
}
// fetchWeather retrieves the current temperature from the API.
func (w *WeatherFetcher) fetchWeather() (string, error) {
url := fmt.Sprintf(
"%s/locationforecast/2.0/classic?lat=%.5f&lon=%.5f&altitude=%d",
baseURL, lat, lon, altitude)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", userAgent)
resp, err := w.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var weatherData Weatherdata
if err := xml.Unmarshal(body, &weatherData); err != nil {
return "", err
}
now := time.Now().UTC()
for _, t := range weatherData.Product.Times {
toTime, err := time.Parse("2006-01-02T15:04:05Z", t.To)
if err != nil || toTime.Before(now) {
continue
}
if t.Location.Temperature != nil {
temp, err := strconv.ParseFloat(t.Location.Temperature.Value, 64)
if err != nil {
continue
}
return fmt.Sprintf("%d゚", int(temp)), nil
}
}
return "", fmt.Errorf("no usable temperature data found")
}
// update fetches new weather data and returns it.
func (w *WeatherFetcher) update() string {
temp, err := w.fetchWeather()
if err != nil {
log.Printf("Error fetching weather: %v", err)
}
return temp
}
// Run runs as a goroutine to periodically fetch weather data.
func (w *WeatherFetcher) Run(interval time.Duration, output chan<- string) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
output <- w.update()
for range ticker.C {
output <- w.update()
}
}

View File

@@ -0,0 +1,411 @@
package main
import (
"bufio"
"image"
"image/color"
"log"
"os"
"strconv"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"janouch.name/desktop-tools/liust-50/charset"
)
// --- Display emulation -------------------------------------------------------
const (
displayWidth = 20
displayHeight = 2
charWidth = 5 + 1
charHeight = 7 + 1
)
// TODO(p): See how this works exactly, and implement it.
const (
cursorModeOff = iota
cursorModeBlink
cursorModeLightUp
)
type Display struct {
chars [displayHeight][displayWidth]uint8
charset uint8
cursorX int
cursorY int
cursorMode int
}
func NewDisplay() *Display {
return &Display{charset: 2}
}
func (d *Display) Clear() {
for y := 0; y < displayHeight; y++ {
for x := 0; x < displayWidth; x++ {
d.chars[y][x] = 0x20 // space
}
}
}
func (d *Display) ClearToEnd() {
for x := d.cursorX; x < displayWidth; x++ {
d.chars[d.cursorY][x] = 0x20 // space
}
}
func (d *Display) drawCharacter(
img *image.RGBA, character image.Image, cx, cy int) {
if character == nil {
return
}
bounds := character.Bounds()
width, height := bounds.Dx(), bounds.Dy()
for dy := 0; dy < height; dy++ {
for dx := 0; dx < width; dx++ {
var c color.RGBA
if r, _, _, _ := character.At(
bounds.Min.X+dx, bounds.Min.Y+dy).RGBA(); r >= 0x8000 {
c = color.RGBA{0x00, 0xFF, 0xB0, 0xFF}
} else {
c = color.RGBA{0x18, 0x18, 0x18, 0xFF}
}
img.SetRGBA(1+cx*charWidth+dx, 1+cy*charHeight+dy, c)
}
}
}
func (d *Display) Render() image.Image {
width := 1 + displayWidth*charWidth
height := 1 + displayHeight*charHeight
// XXX: Not sure if we rather don't want to provide double buffering,
// meaning we would cycle between two internal buffers.
img := image.NewRGBA(image.Rect(0, 0, width, height))
black := [4]uint8{0x00, 0x00, 0x00, 0xFF}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
copy(img.Pix[img.PixOffset(x, y):], black[:])
}
}
for cy := 0; cy < displayHeight; cy++ {
for cx := 0; cx < displayWidth; cx++ {
charImg := charset.ResolveCharToImage(d.chars[cy][cx], d.charset)
d.drawCharacter(img, charImg, cx, cy)
}
}
return img
}
func (d *Display) PutChar(ch uint8) {
if d.cursorX >= displayWidth || d.cursorY >= displayHeight {
return
}
d.chars[d.cursorY][d.cursorX] = ch
d.cursorX++
if d.cursorX >= displayWidth {
d.cursorX = displayWidth - 1
}
}
func (d *Display) LineFeed() {
d.cursorY++
if d.cursorY >= displayHeight {
d.cursorY = displayHeight - 1
y := 0
for ; y < displayHeight-1; y++ {
d.chars[y] = d.chars[y+1]
}
for x := 0; x < displayWidth; x++ {
d.chars[y][x] = 0x20
}
}
}
func (d *Display) CarriageReturn() {
d.cursorX = 0
}
func (d *Display) Backspace() {
if d.cursorX > 0 {
d.cursorX--
}
}
func (d *Display) SetCursor(x, y int) {
if x >= 0 && x < displayWidth {
d.cursorX = x
}
if y >= 0 && y < displayHeight {
d.cursorY = y
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
func parseANSI(input string) (command string, params []int) {
if !strings.HasPrefix(input, "\x1b[") {
return "", nil
}
input = input[2:]
if len(input) == 0 {
return "", nil
}
cmdIdx := len(input) - 1
paramStr, command := input[:cmdIdx], input[cmdIdx:]
if paramStr != "" {
for _, p := range strings.Split(paramStr, ";") {
if p = strings.TrimSpace(p); p == "" {
params = append(params, 0)
} else if value, err := strconv.Atoi(p); err == nil {
params = append(params, value)
}
}
}
return command, params
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
type protocolParser struct {
seq strings.Builder
inEsc bool
inCSI bool
display *Display
}
func newProtocolParser(d *Display) *protocolParser {
return &protocolParser{display: d}
}
func (pp *protocolParser) reset() {
pp.inEsc = false
pp.inCSI = false
pp.seq.Reset()
}
func (pp *protocolParser) handleCSICommand() bool {
cmd, params := parseANSI(pp.seq.String())
switch cmd {
case "J": // Clear display
// XXX: The no params case is unverified.
if len(params) == 0 || params[0] == 2 {
pp.display.Clear()
}
case "K": // Delete to end of line
// XXX: The no params case is unverified (but it should work).
if len(params) == 0 || params[0] == 0 {
pp.display.ClearToEnd()
}
case "H": // Cursor position
y, x := 0, 0
if len(params) >= 1 {
y = params[0] - 1 // 1-indexed to 0-indexed
}
if len(params) >= 2 {
x = params[1] - 1
}
pp.display.SetCursor(x, y)
}
return true
}
func (pp *protocolParser) handleEscapeSequence(b byte) bool {
pp.seq.WriteByte(b)
if pp.seq.Len() == 2 && b == '[' {
pp.inCSI = true
return false
}
if pp.seq.Len() == 3 && pp.seq.String()[1] == 'R' {
pp.display.charset = b
pp.reset()
return true
}
if pp.inCSI && (b >= 'A' && b <= 'Z' || b >= 'a' && b <= 'z') {
refresh := pp.handleCSICommand()
pp.reset()
return refresh
}
if pp.seq.Len() == 6 && pp.seq.String()[1:5] == "\\?LC" {
pp.display.cursorMode = int(pp.seq.String()[5])
return true
}
return false
}
func (pp *protocolParser) handleCharacter(b byte) bool {
switch b {
case 0x0A: // LF
pp.display.LineFeed()
return true
case 0x0D: // CR
pp.display.CarriageReturn()
return true
case 0x08: // BS
pp.display.Backspace()
return true
default:
if b >= 0x20 {
pp.display.PutChar(b)
return true
}
}
return false
}
func (pp *protocolParser) handleByte(b byte) (needsRefresh bool) {
if b == 0x1b { // ESC
pp.reset()
pp.inEsc = true
pp.seq.WriteByte(b)
return false
}
if pp.inEsc {
return pp.handleEscapeSequence(b)
}
return pp.handleCharacter(b)
}
// --- Display widget ----------------------------------------------------------
type DisplayRenderer struct {
image *canvas.Image
label *canvas.Text
objects []fyne.CanvasObject
displayWidget *DisplayWidget
}
func (r *DisplayRenderer) Destroy() {}
func (r *DisplayRenderer) Layout(size fyne.Size) {
minSize := r.MinSize()
aspectRatio := minSize.Width / minSize.Height
var areaX, areaY, areaWidth, areaHeight float32
if size.Width/size.Height > aspectRatio {
areaHeight = size.Height
areaWidth = areaHeight * aspectRatio
areaX = (size.Width - areaWidth) / 2
} else {
areaWidth = size.Width
areaHeight = areaWidth / aspectRatio
areaY = (size.Height - areaHeight) / 2
}
imageHeight := areaHeight * (minSize.Height - 5) / minSize.Height
r.image.Move(fyne.NewPos(areaX, areaY))
r.image.Resize(fyne.NewSize(areaWidth, imageHeight))
// The appropriate TextSize for the desired label height is guesswork.
// In theory, we could figure out the relation between TextSize
// and measured height in our MinSize.
r.label.TextSize = (areaHeight - imageHeight) * 0.75
labelSize := r.label.MinSize()
// The VFD display is not mounted exactly in the centre of the device.
r.label.Move(fyne.NewPos(
areaX+(areaWidth-labelSize.Width)*0.525,
areaY+imageHeight))
r.label.Resize(labelSize)
}
func (r *DisplayRenderer) MinSize() fyne.Size {
// The VFD display doesn't have rectangular pixels,
// they are rather elongated in a roughly 3:4 ratio.
//
// Add space for the bottom label.
bounds := r.image.Image.Bounds()
return fyne.NewSize(float32(bounds.Dx()), float32(bounds.Dy())*1.25).
AddWidthHeight(0, 5)
}
func (r *DisplayRenderer) Objects() []fyne.CanvasObject { return r.objects }
func (r *DisplayRenderer) Refresh() {
r.image.Image = r.displayWidget.display.Render()
r.image.Refresh()
r.label.Refresh()
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
type DisplayWidget struct {
widget.BaseWidget
display *Display
}
func NewDisplayWidget(display *Display) *DisplayWidget {
dw := &DisplayWidget{display: display}
dw.ExtendBaseWidget(dw)
return dw
}
func (dw *DisplayWidget) CreateRenderer() fyne.WidgetRenderer {
image := canvas.NewImageFromImage(dw.display.Render())
image.ScaleMode = canvas.ImageScalePixels
label := canvas.NewText("TOSHIBA", color.Gray{0x99})
label.TextStyle.Bold = true
return &DisplayRenderer{
image: image,
label: label,
objects: []fyne.CanvasObject{image, label},
displayWidget: dw,
}
}
// --- Main --------------------------------------------------------------------
func main() {
a := app.New()
a.Settings().SetTheme(theme.DarkTheme())
window := a.NewWindow("Toshiba Tec LIUST-50 Simulator")
display := NewDisplay()
display.Clear()
dw := NewDisplayWidget(display)
window.SetContent(dw)
window.Resize(fyne.NewSize(600, 150))
go func() {
reader := bufio.NewReader(os.Stdin)
parser := newProtocolParser(display)
for {
b, err := reader.ReadByte()
if err != nil {
log.Println(err)
return
}
if parser.handleByte(b) {
fyne.DoAndWait(func() { dw.Refresh() })
}
}
}()
window.ShowAndRun()
}

40
liust-50/go.mod Normal file
View File

@@ -0,0 +1,40 @@
module janouch.name/desktop-tools/liust-50
go 1.25.1
require fyne.io/fyne/v2 v2.7.1
require (
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.2.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.3.0 // indirect
github.com/godbus/dbus/v5 v5.2.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.1 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.1.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.4.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
golang.org/x/image v0.33.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

80
liust-50/go.sum Normal file
View File

@@ -0,0 +1,80 @@
fyne.io/fyne/v2 v2.7.1 h1:ja7rNHWWEooha4XBIZNnPP8tVFwmTfwMJdpZmLxm2Zc=
fyne.io/fyne/v2 v2.7.1/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI=
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4=
github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,7 +1,7 @@
/*
* wmstatus.c: simple PulseAudio-enabled status setter for dwm and i3/sway
*
* Copyright (c) 2015 - 2024, Přemysl Eric Janouch <p@janouch.name>
* Copyright (c) 2015 - 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.
@@ -798,6 +798,10 @@ backend_i3_new (void)
static const struct config_schema g_config_general[] =
{
{ .name = "time_format",
.comment = "strftime format specification string",
.type = CONFIG_ITEM_STRING,
.default_ = "\"Week %V, %a %d %b %Y %H:%M %Z\"" },
{ .name = "command",
.comment = "Command to run for more info",
.type = CONFIG_ITEM_STRING },
@@ -809,10 +813,10 @@ static const struct config_schema g_config_general[] =
static const struct config_schema g_config_mpd[] =
{
// XXX: We might want to allow config item defaults to not disable nulls.
{ .name = "address",
.comment = "MPD host or socket",
.type = CONFIG_ITEM_STRING,
.default_ = "\"localhost\"" },
.type = CONFIG_ITEM_STRING },
{ .name = "service",
.comment = "MPD service name or port",
.type = CONFIG_ITEM_STRING,
@@ -1375,7 +1379,7 @@ make_time_status (const char *fmt)
if (local == NULL)
exit_fatal ("%s: %s", "localtime", strerror (errno));
if (!strftime (buf, sizeof buf, fmt, local))
if (*fmt && !strftime (buf, sizeof buf, fmt, local))
exit_fatal ("strftime == 0");
return xstrdup (buf);
@@ -1447,8 +1451,10 @@ refresh_status (struct app_context *ctx)
for (size_t i = 0; i < ctx->command_current.len; i++)
ctx->backend->add (ctx->backend, ctx->command_current.vector[i]);
char *times = make_time_status ("Week %V, %a %d %b %Y %H:%M %Z");
ctx->backend->add (ctx->backend, times);
char *times = make_time_status
(get_config_string (ctx->config.root, "general.time_format"));
if (*times)
ctx->backend->add (ctx->backend, times);
free (times);
ctx->backend->flush (ctx->backend);
@@ -1809,8 +1815,12 @@ mpd_on_io_hook (void *user_data, bool outgoing, const char *line)
static void
on_mpd_reconnect (void *user_data)
{
// FIXME: the user should be able to disable MPD
struct app_context *ctx = user_data;
struct config_item *root = ctx->config.root;
const char *address = get_config_string (root, "mpd.address");
const char *service = get_config_string (root, "mpd.service");
if (!address)
return;
struct mpd_client *c = &ctx->mpd_client;
c->user_data = ctx;
@@ -1820,10 +1830,7 @@ on_mpd_reconnect (void *user_data)
c->on_io_hook = mpd_on_io_hook;
struct error *e = NULL;
struct config_item *root = ctx->config.root;
if (!mpd_client_connect (&ctx->mpd_client,
get_config_string (root, "mpd.address"),
get_config_string (root, "mpd.service"), &e))
if (!mpd_client_connect (&ctx->mpd_client, address, service, &e))
{
print_error ("%s: %s", "cannot connect to MPD", e->message);
error_free (e);
@@ -2249,7 +2256,7 @@ action_noise_adjust (struct app_context *ctx, const struct strv *args)
long arg = strtol (args->vector[0], NULL, 10);
ctx->noise_fadeout_samples = 0;
ctx->noise_fadeout_iterator = 0;
if (!ctx->noise_end_time && (arg < 0 || !noise_start (ctx)))
if (!ctx->noise_end_time && (arg <= 0 || !noise_start (ctx)))
return;
time_t now = time (NULL);