Compare commits

..

18 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
21 changed files with 1659 additions and 47 deletions

View File

@@ -25,7 +25,7 @@ include_directories (
link_directories ( link_directories (
${x_LIBRARY_DIRS} ${pulse_LIBRARY_DIRS} ${dbus_LIBRARY_DIRS}) ${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 # Generate a configuration file
configure_file (${PROJECT_SOURCE_DIR}/config.h.in 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}) include_directories (${PROJECT_BINARY_DIR})
# Build # Build
set (targets wmstatus paswitch siprandom) set (targets wmstatus paswitch siprandom genpass)
if ("${CMAKE_SYSTEM_NAME}" STREQUAL Linux) if ("${CMAKE_SYSTEM_NAME}" STREQUAL Linux)
# These use Linux i2c APIs, but can be made to work on macOS # These use Linux i2c APIs, but can be made to work on macOS
list (APPEND targets brightness input-switch) list (APPEND targets brightness input-switch)
@@ -58,9 +58,10 @@ target_link_libraries (wmstatus
add_threads (wmstatus) add_threads (wmstatus)
if (WITH_GDM) if (WITH_GDM)
include_directories (${gdm_INCLUDE_DIRS}) list (APPEND targets gdm-switch-user)
link_directories (${gdm_LIBRARY_DIRS})
add_executable (gdm-switch-user gdm-switch-user.c) 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}) target_link_libraries (gdm-switch-user ${gdm_LIBRARIES})
endif () endif ()
@@ -95,6 +96,7 @@ endif ()
# These should be accessible by users, but need to touch system devices. # These should be accessible by users, but need to touch system devices.
# Use the setuid bit, for simplicity. # Use the setuid bit, for simplicity.
set (SETUID "SETUID" CACHE STRING "Set this empty on permission issues")
foreach (target brightness input-switch) foreach (target brightness input-switch)
if (${target} IN_LIST targets) if (${target} IN_LIST targets)
list (REMOVE_ITEM targets ${target}) list (REMOVE_ITEM targets ${target})
@@ -103,7 +105,7 @@ foreach (target brightness input-switch)
OWNER_WRITE OWNER_READ OWNER_EXECUTE OWNER_WRITE OWNER_READ OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE WORLD_READ WORLD_EXECUTE
SETUID) ${SETUID})
endif () endif ()
endforeach () 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 Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted. 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 Building
-------- --------
Build dependencies: CMake, pkg-config, liberty (included) + 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 $ git clone --recursive https://git.janouch.name/p/desktop-tools.git
$ mkdir desktop-tools/build $ mkdir desktop-tools/build

View File

@@ -46,7 +46,7 @@ log_message_custom (void *user_data, const char *quote, const char *fmt,
static void static void
wait_ms (long ms) wait_ms (long ms)
{ {
struct timespec ts = { 0, ms * 1000 * 1000 }; struct timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 };
nanosleep (&ts, NULL); 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"); 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", { .name = "name",
.comment = "Device identifier", .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", { .name = "temp",
.comment = "Path to temperature sensor output", .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. // 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. // Thus the reset to defaults on invalid values is effectively disabled here.
static bool 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 **e)
{ {
struct error *warning = NULL, *error = NULL; struct error *warning = NULL, *error = NULL;
@@ -445,7 +445,7 @@ static bool
check_device_configuration (struct config_item *subtree, struct error **e) check_device_configuration (struct config_item *subtree, struct error **e)
{ {
// Check regular fields in the device object // 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)) if (!apply_schema (s, subtree, e))
return false; return false;
@@ -465,7 +465,7 @@ check_device_configuration (struct config_item *subtree, struct error **e)
while ((pwm = str_map_iter_next (&iter))) while ((pwm = str_map_iter_next (&iter)))
{ {
const char *subpath = iter.link->key; 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)) if (!apply_schema (s, pwm, &error))
{ {
error_set (e, "PWM `%s': %s", subpath, error->message); error_set (e, "PWM `%s': %s", subpath, error->message);

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++; sink->ports_len++;
struct port *port = sink->ports = 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++) for (struct pa_sink_port_info **iter = info->ports; *iter; iter++)
{ {
port->name = xstrdup ((*iter)->name); port->name = xstrdup ((*iter)->name);

View File

@@ -1,7 +1,7 @@
/* /*
* wmstatus.c: simple PulseAudio-enabled status setter for dwm and i3/sway * 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 * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted. * purpose with or without fee is hereby granted.
@@ -70,6 +70,20 @@ log_message_custom (void *user_data, const char *quote, const char *fmt,
fputs ("\n", stream); fputs ("\n", stream);
} }
static void
shell_quote (const char *str, struct str *output)
{
// See SUSv3 Shell and Utilities, 2.2.3 Double-Quotes
str_append_c (output, '"');
for (const char *p = str; *p; p++)
{
if (strchr ("`$\"\\", *p))
str_append_c (output, '\\');
str_append_c (output, *p);
}
str_append_c (output, '"');
}
// --- NUT --------------------------------------------------------------------- // --- NUT ---------------------------------------------------------------------
// More or less copied and pasted from the MPD client. This code doesn't even // More or less copied and pasted from the MPD client. This code doesn't even
@@ -782,8 +796,12 @@ backend_i3_new (void)
// --- Configuration ----------------------------------------------------------- // --- Configuration -----------------------------------------------------------
static struct config_schema g_config_general[] = 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", { .name = "command",
.comment = "Command to run for more info", .comment = "Command to run for more info",
.type = CONFIG_ITEM_STRING }, .type = CONFIG_ITEM_STRING },
@@ -793,12 +811,12 @@ static struct config_schema g_config_general[] =
{} {}
}; };
static struct config_schema g_config_mpd[] = static const struct config_schema g_config_mpd[] =
{ {
// XXX: We might want to allow config item defaults to not disable nulls.
{ .name = "address", { .name = "address",
.comment = "MPD host or socket", .comment = "MPD host or socket",
.type = CONFIG_ITEM_STRING, .type = CONFIG_ITEM_STRING },
.default_ = "\"localhost\"" },
{ .name = "service", { .name = "service",
.comment = "MPD service name or port", .comment = "MPD service name or port",
.type = CONFIG_ITEM_STRING, .type = CONFIG_ITEM_STRING,
@@ -809,7 +827,7 @@ static struct config_schema g_config_mpd[] =
{} {}
}; };
static struct config_schema g_config_nut[] = static const struct config_schema g_config_nut[] =
{ {
{ .name = "enabled", { .name = "enabled",
.comment = "NUT UPS status reading enabled", .comment = "NUT UPS status reading enabled",
@@ -925,7 +943,7 @@ parse_binding (const char *line, struct strv *out)
{ {
case 0: edge = table[state][0]; break; case 0: edge = table[state][0]; break;
case '\t': case '\t':
case '\n': edge = table[state][4]; break; case '\n':
case ' ': edge = table[state][1]; break; case ' ': edge = table[state][1]; break;
case '\'': edge = table[state][2]; break; case '\'': edge = table[state][2]; break;
case '\\': edge = table[state][3]; break; case '\\': edge = table[state][3]; break;
@@ -1361,7 +1379,7 @@ make_time_status (const char *fmt)
if (local == NULL) if (local == NULL)
exit_fatal ("%s: %s", "localtime", strerror (errno)); 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"); exit_fatal ("strftime == 0");
return xstrdup (buf); return xstrdup (buf);
@@ -1433,7 +1451,9 @@ refresh_status (struct app_context *ctx)
for (size_t i = 0; i < ctx->command_current.len; i++) for (size_t i = 0; i < ctx->command_current.len; i++)
ctx->backend->add (ctx->backend, ctx->command_current.vector[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"); char *times = make_time_status
(get_config_string (ctx->config.root, "general.time_format"));
if (*times)
ctx->backend->add (ctx->backend, times); ctx->backend->add (ctx->backend, times);
free (times); free (times);
@@ -1795,8 +1815,12 @@ mpd_on_io_hook (void *user_data, bool outgoing, const char *line)
static void static void
on_mpd_reconnect (void *user_data) on_mpd_reconnect (void *user_data)
{ {
// FIXME: the user should be able to disable MPD
struct app_context *ctx = user_data; 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; struct mpd_client *c = &ctx->mpd_client;
c->user_data = ctx; c->user_data = ctx;
@@ -1806,10 +1830,7 @@ on_mpd_reconnect (void *user_data)
c->on_io_hook = mpd_on_io_hook; c->on_io_hook = mpd_on_io_hook;
struct error *e = NULL; struct error *e = NULL;
struct config_item *root = ctx->config.root; if (!mpd_client_connect (&ctx->mpd_client, address, service, &e))
if (!mpd_client_connect (&ctx->mpd_client,
get_config_string (root, "mpd.address"),
get_config_string (root, "mpd.service"), &e))
{ {
print_error ("%s: %s", "cannot connect to MPD", e->message); print_error ("%s: %s", "cannot connect to MPD", e->message);
error_free (e); error_free (e);
@@ -2235,7 +2256,7 @@ action_noise_adjust (struct app_context *ctx, const struct strv *args)
long arg = strtol (args->vector[0], NULL, 10); long arg = strtol (args->vector[0], NULL, 10);
ctx->noise_fadeout_samples = 0; ctx->noise_fadeout_samples = 0;
ctx->noise_fadeout_iterator = 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; return;
time_t now = time (NULL); time_t now = time (NULL);
@@ -2567,7 +2588,7 @@ action_xkb_lock_group (struct app_context *ctx, const struct strv *args)
return; return;
} }
long group = strtol (args->vector[0], NULL, 10) - 1; long group = strtol (args->vector[0], NULL, 10);
if (group < XkbGroup1Index || group > XkbGroup4Index) if (group < XkbGroup1Index || group > XkbGroup4Index)
print_warning ("invalid XKB group index: %s", args->vector[0]); print_warning ("invalid XKB group index: %s", args->vector[0]);
else else
@@ -2992,6 +3013,156 @@ app_save_configuration (struct app_context *ctx, const char *path_hint)
free (filename); free (filename);
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
sway_command_argument_needs_quoting (const char *word)
{
while (*word)
if (!isalnum_ascii (*word++))
return true;
return false;
}
static void
sway_append_command_argument (struct str *out, const char *word)
{
if (out->len)
str_append_c (out, ' ');
if (!sway_command_argument_needs_quoting (word))
{
str_append (out, word);
return;
}
str_append_c (out, '\'');
for (const char *p = word; *p; p++)
{
if (*p == '\'' || *p == '\\')
str_append_c (out, '\\');
str_append_c (out, *p);
}
str_append_c (out, '\'');
}
static char *
sway_shell_command_for_action (const struct strv *args)
{
// The i3/Sway quoting is properly fucked up,
// and its exec command forwards to `sh -c`.
struct str shell_command = str_make ();
if (strcmp (args->vector[0], "exec"))
{
// argv[0] would need realpath() applied on it.
shell_quote (PROGRAM_NAME, &shell_command);
str_append (&shell_command, " -- ");
shell_quote (args->vector[0], &shell_command);
str_append_c (&shell_command, ' ');
}
for (size_t i = 1; i < args->len; i++)
{
shell_quote (args->vector[i], &shell_command);
str_append_c (&shell_command, ' ');
}
if (shell_command.len)
shell_command.str[--shell_command.len] = 0;
return str_steal (&shell_command);
}
static const char *
sway_bindsym (const char *combination, const char *action)
{
const char *error = NULL;
struct strv keys = strv_make ();
struct strv args = strv_make ();
if (!parse_binding (combination, &keys))
{
error = "parsing key combination failed";
goto out;
}
if (!parse_binding (action, &args) || !args.len)
{
error = "parsing the binding failed";
goto out;
}
struct action handler = action_by_name (args.vector[0]);
if (!handler.name)
{
error = "unknown action";
goto out;
}
// This command name may not be quoted.
// Note that i3-msg doesn't accept bindsym at all, only swaymsg does.
struct str sway_command = str_make ();
sway_append_command_argument (&sway_command, "bindsym");
char *recombined = strv_join (&keys, "+");
sway_append_command_argument (&sway_command, recombined);
free (recombined);
if (handler.handler == action_xkb_lock_group)
{
// This should also switch the XWayland layout,
// though it has been observed to not take effect immediately.
sway_append_command_argument (&sway_command, "input");
sway_append_command_argument (&sway_command, "type:keyboard");
sway_append_command_argument (&sway_command, "xkb_switch_layout");
for (size_t i = 1; i < args.len; i++)
sway_append_command_argument (&sway_command, args.vector[i]);
}
else
{
sway_append_command_argument (&sway_command, "exec");
char *shell_command = sway_shell_command_for_action (&args);
sway_append_command_argument (&sway_command, shell_command);
free (shell_command);
}
struct strv argv = strv_make ();
strv_append (&argv, "swaymsg");
strv_append_owned (&argv, str_steal (&sway_command));
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init (&actions);
posix_spawnp (NULL, argv.vector[0], &actions, NULL, argv.vector, environ);
posix_spawn_file_actions_destroy (&actions);
strv_free (&argv);
out:
strv_free (&keys);
strv_free (&args);
return error;
}
static void
sway_forward_bindings (void)
{
// app_context_init() has side-effects.
struct app_context ctx = { .config = app_make_config () };
app_load_configuration (&ctx);
struct str_map *keys =
&config_item_get (ctx.config.root, "keys", NULL)->value.object;
struct str_map_iter iter = str_map_iter_make (keys);
struct config_item *action;
while ((action = str_map_iter_next (&iter)))
{
const char *combination = iter.link->key, *err = NULL;
if (action->type != CONFIG_ITEM_NULL)
{
if (action->type != CONFIG_ITEM_STRING)
err = "expected a string";
else
err = sway_bindsym (combination, action->value.string.str);
}
if (err)
print_warning ("configuration: key `%s': %s", combination, err);
}
}
// --- Signals ----------------------------------------------------------------- // --- Signals -----------------------------------------------------------------
static int g_signal_pipe[2]; ///< A pipe used to signal... signals static int g_signal_pipe[2]; ///< A pipe used to signal... signals
@@ -3079,15 +3250,16 @@ main (int argc, char *argv[])
{ 'd', "debug", NULL, 0, "run in debug mode" }, { 'd', "debug", NULL, 0, "run in debug mode" },
{ 'h', "help", NULL, 0, "display this help and exit" }, { 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" }, { 'V', "version", NULL, 0, "output version information and exit" },
{ '3', "i3bar", NULL, 0, "print output for i3bar/sway-bar instead" }, { '3', "i3bar", NULL, 0, "print output for i3-bar/swaybar instead" },
{ 's', "bind-sway", NULL, 0, "import bindings over swaymsg" },
{ 'w', "write-default-cfg", "FILENAME", { 'w', "write-default-cfg", "FILENAME",
OPT_OPTIONAL_ARG | OPT_LONG_ONLY, OPT_OPTIONAL_ARG | OPT_LONG_ONLY,
"write a default configuration file and exit" }, "write a default configuration file and exit" },
{ 0, NULL, NULL, 0, NULL } { 0, NULL, NULL, 0, NULL }
}; };
struct opt_handler oh = struct opt_handler oh = opt_handler_make (argc, argv, opts, "[ACTION...]",
opt_handler_make (argc, argv, opts, NULL, "Set root window name."); "Set root window name.");
bool i3bar = false; bool i3bar = false;
int c; int c;
@@ -3106,6 +3278,9 @@ main (int argc, char *argv[])
case '3': case '3':
i3bar = true; i3bar = true;
break; break;
case 's':
sway_forward_bindings ();
exit (EXIT_SUCCESS);
case 'w': case 'w':
{ {
// app_context_init() has side-effects. // app_context_init() has side-effects.

View File

@@ -16,10 +16,10 @@ keys = {
"XF86AudioPrev" = "mpd previous" "XF86AudioPrev" = "mpd previous"
"XF86AudioNext" = "mpd next" "XF86AudioNext" = "mpd next"
"Mod4 F1" = "xkb-lock-group 1" "Mod4 F1" = "xkb-lock-group 0"
"Mod4 F2" = "xkb-lock-group 2" "Mod4 F2" = "xkb-lock-group 1"
"Mod4 F3" = "xkb-lock-group 3" "Mod4 F3" = "xkb-lock-group 2"
"Mod4 F4" = "xkb-lock-group 4" "Mod4 F4" = "xkb-lock-group 3"
"Mod4 Control F1" = "exec input-switch vga 1" "Mod4 Control F1" = "exec input-switch vga 1"
"Mod4 Control Shift F1" = "exec input-switch vga 2" "Mod4 Control Shift F1" = "exec input-switch vga 2"
@@ -57,4 +57,7 @@ keys = {
"Control XF86AudioRaiseVolume" = "noise-adjust +1" "Control XF86AudioRaiseVolume" = "noise-adjust +1"
"Control XF86AudioLowerVolume" = "noise-adjust -1" "Control XF86AudioLowerVolume" = "noise-adjust -1"
# Turns on or off Pioneer integrated amplifiers
"Mod4 Control Delete" = "exec elksmart-comm --nec A538"
} }