Compare commits

..

24 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
9e91058ed9 Add elksmart-comm for transceiving infrared codes
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
The receive functionality is quite unstable,
however useful enough for something that is officially unsupported.

The gadget is picky about cables,
but it has ridiculous reach when it works.
2024-08-30 02:55:35 +02:00
fbc7454647 wmstatus: cleanup 2024-08-10 08:51:43 +02:00
94bc8c251c wmstatus: improve Sway forwards of xkb-lock-group
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2024-08-08 14:58:02 +02:00
e83cfa3c15 Fix calloc argument order, add some consts 2024-08-08 14:39:28 +02:00
29c89942ce Bump liberty
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2024-08-08 08:58:36 +02:00
9042aeaa93 wmstatus: fix the binding parser
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2024-08-07 18:30:46 +02:00
180b16faee wmstatus: add an option to import bindings to Sway
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
We still want to retain the ability to bind them on our own under X11.

With this, the Wayland situation has considerably improved,
but the activity watch and keyboard layout switching are still broken.
2024-08-07 17:01:35 +02:00
674ea6d9a6 wmstatus: add IPC capabilities
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
And remove the odd prefix functionality.
2024-08-07 12:32:33 +02:00
9ccdc3430c wmstatus: make bindings configurable 2024-08-07 11:51:14 +02:00
4a22708f52 wmstatus: move to libertyconf 2024-08-07 11:51:14 +02:00
128ef14c39 wmstatus: round in noise playback setting
All checks were successful
Alpine 3.19 Success
OpenBSD 7.3 Success
2024-04-17 00:42:55 +02:00
9def673a2b Install some binaries with the setuid bit
All checks were successful
Alpine 3.19 Success
OpenBSD 7.3 Success
2024-03-27 08:26:49 +01:00
36df9cc6c9 Fix gdm-switch-user build 2024-02-25 01:56:09 +01:00
22 changed files with 2333 additions and 257 deletions

View File

@@ -25,7 +25,7 @@ include_directories (
link_directories (
${x_LIBRARY_DIRS} ${pulse_LIBRARY_DIRS} ${dbus_LIBRARY_DIRS})
option (WITH_GDM "Compile with GDM support" ${gdm_FOUND})
option (WITH_GDM "Compile with GDM utilities" ${gdm_FOUND})
# Generate a configuration file
configure_file (${PROJECT_SOURCE_DIR}/config.h.in
@@ -33,7 +33,7 @@ configure_file (${PROJECT_SOURCE_DIR}/config.h.in
include_directories (${PROJECT_BINARY_DIR})
# Build
set (targets wmstatus paswitch siprandom big-brother)
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)
@@ -47,7 +47,7 @@ elseif (APPLE)
add_definitions (-D_DARWIN_C_SOURCE)
endif ()
foreach (name ${targets})
foreach (name big-brother ${targets})
add_executable (${name} ${name}.c)
endforeach ()
@@ -58,9 +58,10 @@ target_link_libraries (wmstatus
add_threads (wmstatus)
if (WITH_GDM)
include_directories (${gdm_INCLUDE_DIRS})
link_directories (${gdm_LIBRARY_DIRS})
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})
target_link_libraries (gdm-switch-user ${gdm_LIBRARIES})
endif ()
@@ -93,7 +94,21 @@ if (WITH_GDM)
install (TARGETS gdm-switch-user DESTINATION ${CMAKE_INSTALL_BINDIR})
endif ()
list (REMOVE_ITEM targets big-brother)
# These should be accessible by users, but need to touch system devices.
# Use the setuid bit, for simplicity.
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}
PERMISSIONS
OWNER_WRITE OWNER_READ OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE
${SETUID})
endif ()
endforeach ()
install (TARGETS ${targets} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (PROGRAMS shellify DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})

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

@@ -37,7 +37,8 @@ Regular releases are sporadic. git master should be stable enough.
Building
--------
Build dependencies: CMake, pkg-config, liberty (included) +
Runtime dependencies: libpulse, libx11, dbus-1, libgdm (optional)
Runtime dependencies: libpulse, libx11, dbus-1 +
Optional runtime dependencies: libgdm (gdm-switch-user)
$ git clone --recursive https://git.janouch.name/p/desktop-tools.git
$ mkdir desktop-tools/build

View File

@@ -46,7 +46,7 @@ log_message_custom (void *user_data, const char *quote, const char *fmt,
static void
wait_ms (long ms)
{
struct timespec ts = { 0, ms * 1000 * 1000 };
struct timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 };
nanosleep (&ts, NULL);
}

View File

@@ -124,7 +124,7 @@ config_validate_nonnegative (const struct config_item *item, struct error **e)
return error_set (e, "must be non-negative");
}
static struct config_schema g_config_device[] =
static const struct config_schema g_config_device[] =
{
{ .name = "name",
.comment = "Device identifier",
@@ -137,7 +137,7 @@ static struct config_schema g_config_device[] =
{}
};
static struct config_schema g_config_pwm[] =
static const struct config_schema g_config_pwm[] =
{
{ .name = "temp",
.comment = "Path to temperature sensor output",
@@ -415,7 +415,7 @@ device_create (struct app_context *ctx, const char *path,
// There is no room for errors in the configuration, everything must be valid.
// Thus the reset to defaults on invalid values is effectively disabled here.
static bool
apply_schema (struct config_schema *schema, struct config_item *object,
apply_schema (const struct config_schema *schema, struct config_item *object,
struct error **e)
{
struct error *warning = NULL, *error = NULL;
@@ -445,7 +445,7 @@ static bool
check_device_configuration (struct config_item *subtree, struct error **e)
{
// Check regular fields in the device object
for (struct config_schema *s = g_config_device; s->name; s++)
for (const struct config_schema *s = g_config_device; s->name; s++)
if (!apply_schema (s, subtree, e))
return false;
@@ -465,7 +465,7 @@ check_device_configuration (struct config_item *subtree, struct error **e)
while ((pwm = str_map_iter_next (&iter)))
{
const char *subpath = iter.link->key;
for (struct config_schema *s = g_config_pwm; s->name; s++)
for (const struct config_schema *s = g_config_pwm; s->name; s++)
if (!apply_schema (s, pwm, &error))
{
error_set (e, "PWM `%s': %s", subpath, error->message);

View File

@@ -1,5 +1,5 @@
// Public domain
#include <gdm-user-switching.h>
#include <gdm/gdm-user-switching.h>
int
main (int argc, char *argv[])

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;
}
}

Submodule liberty updated: ad5b2fb8cd...75fc6f1c37

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

@@ -290,7 +290,7 @@ on_sink_info (pa_context *context, const pa_sink_info *info, int eol,
sink->ports_len++;
struct port *port = sink->ports =
xcalloc (sizeof *sink->ports, sink->ports_len);
xcalloc (sink->ports_len, sizeof *sink->ports);
for (struct pa_sink_port_info **iter = info->ports; *iter; iter++)
{
port->name = xstrdup ((*iter)->name);

1046
wmstatus.c

File diff suppressed because it is too large Load Diff

63
wmstatus.conf.example Normal file
View File

@@ -0,0 +1,63 @@
# vim: set ft=libertyconf:
keys = {
# This key should be labeled L on normal Qwert[yz] layouts
"Mod4 n" = "exec dm-tool lock" # gdm-switch-user
# xmodmap grep -e Alt_R -e Meta_R -e ISO_Level3_Shift -e Mode_switch
# can be used to figure out which modifier is AltGr
"Mod4 Up" = "mpd-play-toggle"
"Mod4 Down" = "mpd stop"
"Mod4 Left" = "mpd previous"
"Mod4 Right" = "mpd next"
"Mod4 Shift Left" = "mpd seekcur -10"
"Mod4 Shift Right" = "mpd seekcur +10"
"XF86AudioPlay" = "mpd-play-toggle"
"XF86AudioPrev" = "mpd previous"
"XF86AudioNext" = "mpd next"
"Mod4 F1" = "xkb-lock-group 0"
"Mod4 F2" = "xkb-lock-group 1"
"Mod4 F3" = "xkb-lock-group 2"
"Mod4 F4" = "xkb-lock-group 3"
"Mod4 Control F1" = "exec input-switch vga 1"
"Mod4 Control Shift F1" = "exec input-switch vga 2"
"Mod4 Control F2" = "exec input-switch dvi 1"
"Mod4 Control Shift F2" = "exec input-switch dvi 2"
"Mod4 Control F3" = "exec input-switch hdmi 1"
"Mod4 Control Shift F3" = "exec input-switch hdmi 2"
"Mod4 Control F4" = "exec input-switch dp 1"
"Mod4 Control Shift F4" = "exec input-switch dp 2"
"Mod4 Home" = "exec brightness +10"
"Mod4 End" = "exec brightness -10"
"XF86MonBrightnessUp" = "exec brightness +10"
"XF86MonBrightnessDown" = "exec brightness -10"
# We need to wait a little while until user releases the key
"Mod4 F5" = "exec sh -c 'sleep 1; xset dpms force standby'"
"Mod4 Shift F5" = "insomnia"
"Mod4 Pause" = "exec sh -c 'sleep 1; xset dpms force standby'"
"Mod4 Shift Pause" = "insomnia"
"Mod4 Insert" = "audio-switch"
"Mod4 Delete" = "audio-mute"
"Mod4 Shift Delete" = "audio-mic-mute"
"Mod4 Page_Up" = "audio-volume +5"
"Mod4 Shift Page_Up" = "audio-volume +1"
"Mod4 Page_Down" = "audio-volume -5"
"Mod4 Shift Page_Down" = "audio-volume -1"
" XF86AudioRaiseVolume" = "audio-volume +5"
"Shift XF86AudioRaiseVolume" = "audio-volume +1"
" XF86AudioLowerVolume" = "audio-volume -5"
"Shift XF86AudioLowerVolume" = "audio-volume -1"
" XF86AudioMute" = "audio-mute"
" XF86AudioMicMute" = "audio-mic-mute"
"Control XF86AudioRaiseVolume" = "noise-adjust +1"
"Control XF86AudioLowerVolume" = "noise-adjust -1"
# Turns on or off Pioneer integrated amplifiers
"Mod4 Control Delete" = "exec elksmart-comm --nec A538"
}