buffyboard: track virtual terminals with /sys/class/tty/tty0/active

This commit is contained in:
Vladimir Stoiakin 2025-07-03 14:19:10 +00:00 committed by Johannes Marbach
parent d0ec4777cc
commit c4b3729047
12 changed files with 283 additions and 335 deletions

View file

@ -16,6 +16,8 @@ If a change only affects particular applications, they are listed in parentheses
- fix: Do not hang if graphics backend is not available (!57, thanks @vstoiakin)
- fix!(unl0kr): Disable software rotation due to regressed performance (!56)
- feat(buffyboard): Rotate the keyboard according to /sys/class/graphics/fbcon/rotate by default (!60, thanks @vstoiakin)
- fix(buffyboard): Prevent overlap of the keyboard and the terminal (!58, thanks @vstoiakin)
- feat(buffyboard): Add a handler for SIGUSR1 to redraw the keyboard (!58, thanks @vstoiakin)
## 3.3.0 (2025-04-15)

View file

@ -7,3 +7,4 @@ default=breezy-light
#[quirks]
#fbdev_force_refresh=true
#ignore_unused_terminals=false

View file

@ -1,5 +1,6 @@
[Unit]
Documentation=https://gitlab.postmarketos.org/postmarketOS/buffybox
After=getty.target
[Service]
ExecStart=@bindir@/buffyboard
@ -36,3 +37,6 @@ SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged
SystemCallFilter=~@resources
[Install]
WantedBy=multi-user.target

View file

@ -63,6 +63,10 @@ static int parsing_handler(void* user_data, const char* section, const char* key
if (bbx_config_parse_bool(value, &(opts->quirks.fbdev_force_refresh))) {
return 1;
}
} else if (strcmp(key, "ignore_unused_terminals") == 0) {
if (bbx_config_parse_bool(value, &(opts->quirks.ignore_unused_terminals))) {
return 1;
}
}
}
@ -80,6 +84,7 @@ void bb_config_init_opts(bb_config_opts *opts) {
opts->input.pointer = true;
opts->input.touchscreen = true;
opts->quirks.fbdev_force_refresh = false;
opts->quirks.ignore_unused_terminals = true;
}
void bb_config_parse_directory(const char *path, bb_config_opts *opts) {

View file

@ -35,6 +35,8 @@ typedef struct {
typedef struct {
/* If true and using the framebuffer backend, force a refresh on every draw operation */
bool fbdev_force_refresh;
/* If true, do not automatically update the layout of new terminals and wait for SIGUSR1 */
bool ignore_unused_terminals;
} bb_config_opts_quirks;
/**

View file

@ -0,0 +1,3 @@
[Service]
ExecStartPost=-/usr/bin/kill -s SIGUSR1 buffyboard
StandardError=journal

View file

@ -19,13 +19,14 @@
#include "../shared/themes.h"
#include "../squeek2lvgl/sq2lv.h"
#include <limits.h>
#include <sys/epoll.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
/**
@ -35,37 +36,19 @@
bb_cli_opts cli_opts;
bb_config_opts conf_opts;
static bool resize_terminals = false;
static lv_obj_t *keyboard = NULL;
static sig_atomic_t redraw_requested = false;
/**
* Static prototypes
*/
/**
* Compute the denominator of the keyboard height factor. The keyboard height is calculated
* by dividing the display height by the denominator.
*
* @param width display width
* @param height display height
* @return denominator
*/
static int keyboard_height_denominator(int32_t width, int32_t height);
/**
* Handle termination signals sent to the process.
* Handle signals sent to the process.
*
* @param signum the signal's number
*/
static void sigaction_handler(int signum);
/**
* Callback for the terminal resizing timer.
*
* @param timer the timer object
*/
static void terminal_resize_timer_cb(lv_timer_t *timer);
static void signal_handler(int signum);
/**
* Handle LV_EVENT_VALUE_CHANGED events from the keyboard widget.
@ -93,25 +76,16 @@ static void pop_checked_modifier_keys(void);
* Static functions
*/
static int keyboard_height_denominator(int32_t width, int32_t height) {
return (height > width) ? 3 : 2;
}
static void sigaction_handler(int signum) {
LV_UNUSED(signum);
if (resize_terminals) {
bb_terminal_reset_all();
static void signal_handler(int signum) {
if (signum == SIGUSR1) {
redraw_requested = true;
return;
}
bb_terminal_reset_all();
exit(0);
}
static void terminal_resize_timer_cb(lv_timer_t *timer) {
LV_UNUSED(timer);
if (resize_terminals) {
bb_terminal_shrink_current();
}
}
static void keyboard_value_changed_cb(lv_event_t *event) {
lv_obj_t *kb = lv_event_get_target(event);
@ -196,23 +170,9 @@ int main(int argc, char *argv[]) {
bb_config_parse_directory("/etc/buffyboard.conf.d", &conf_opts);
bb_config_parse_files(cli_opts.config_files, cli_opts.num_config_files, &conf_opts);
/* Prepare for terminal resizing and reset */
resize_terminals = bb_terminal_init(2.0f / 3.0f);
if (resize_terminals) {
/* Clean up on termination */
struct sigaction action;
lv_memset(&action, 0, sizeof(action));
action.sa_handler = sigaction_handler;
sigaction(SIGINT, &action, NULL);
sigaction(SIGTERM, &action, NULL);
/* Resize current terminal */
bb_terminal_shrink_current();
}
/* Set up uinput device */
if (!bb_uinput_device_init(sq2lv_unique_scancodes, sq2lv_num_unique_scancodes)) {
return 1;
return EXIT_FAILURE;
}
/* Initialise LVGL and set up logging callback */
@ -223,17 +183,19 @@ int main(int argc, char *argv[]) {
lv_display_t *disp = lv_linux_fbdev_create();
if (access("/dev/fb0", F_OK) != 0) {
bbx_log(BBX_LOG_LEVEL_ERROR, "/dev/fb0 is not available");
sigaction_handler(SIGTERM);
return EXIT_FAILURE;
}
lv_linux_fbdev_set_file(disp, "/dev/fb0");
if (conf_opts.quirks.fbdev_force_refresh) {
lv_linux_fbdev_set_force_refresh(disp, true);
}
lv_display_set_physical_resolution(disp,
lv_display_get_horizontal_resolution(disp),
lv_display_get_vertical_resolution(disp));
/* Override display properties with command line options if necessary */
lv_display_set_offset(disp, cli_opts.x_offset, cli_opts.y_offset);
if (cli_opts.hor_res > 0 || cli_opts.ver_res > 0) {
lv_display_set_physical_resolution(disp, lv_display_get_horizontal_resolution(disp), lv_display_get_vertical_resolution(disp));
if (cli_opts.hor_res > 0 && cli_opts.ver_res > 0) {
lv_display_set_resolution(disp, cli_opts.hor_res, cli_opts.ver_res);
}
if (cli_opts.dpi > 0) {
@ -241,27 +203,29 @@ int main(int argc, char *argv[]) {
}
/* Set up display rotation */
int32_t hor_res_phys = lv_display_get_horizontal_resolution(disp);
int32_t ver_res_phys = lv_display_get_vertical_resolution(disp);
lv_display_set_physical_resolution(disp, hor_res_phys, ver_res_phys);
lv_display_set_rotation(disp, cli_opts.rotation);
const int32_t hor_res = lv_display_get_horizontal_resolution(disp);
const int32_t ver_res = lv_display_get_vertical_resolution(disp);
const int32_t keyboard_height = ver_res > hor_res ? ver_res / 3 : ver_res / 2;
const int32_t tty_height = ver_res - keyboard_height;
switch (cli_opts.rotation) {
case LV_DISPLAY_ROTATION_0:
case LV_DISPLAY_ROTATION_180: {
int32_t denom = keyboard_height_denominator(hor_res_phys, ver_res_phys);
lv_display_set_resolution(disp, hor_res_phys, ver_res_phys / denom);
lv_display_set_offset(disp, 0, (cli_opts.rotation == LV_DISPLAY_ROTATION_0) ? (denom - 1) * ver_res_phys / denom : 0);
break;
}
case LV_DISPLAY_ROTATION_90:
case LV_DISPLAY_ROTATION_270: {
int32_t denom = keyboard_height_denominator(ver_res_phys, hor_res_phys);
lv_display_set_resolution(disp, hor_res_phys / denom, ver_res_phys);
lv_display_set_offset(disp, 0, (cli_opts.rotation == LV_DISPLAY_ROTATION_90) ? (denom - 1) * hor_res_phys / denom : 0);
break;
}
case LV_DISPLAY_ROTATION_0:
case LV_DISPLAY_ROTATION_180:
lv_display_set_resolution(disp, hor_res, keyboard_height);
lv_display_set_offset(disp, 0, cli_opts.rotation == LV_DISPLAY_ROTATION_0? tty_height : 0);
break;
case LV_DISPLAY_ROTATION_90:
case LV_DISPLAY_ROTATION_270:
lv_display_set_resolution(disp, keyboard_height, hor_res);
lv_display_set_offset(disp, cli_opts.rotation == LV_DISPLAY_ROTATION_90? tty_height : 0, 0);
break;
}
/* Prepare for terminal resizing and reset */
bb_terminal_init(tty_height - 8, hor_res, ver_res);
/* Start input device monitor and auto-connect available devices */
bbx_indev_start_monitor_and_autoconnect(false, conf_opts.input.pointer, conf_opts.input.touchscreen);
@ -286,13 +250,89 @@ int main(int argc, char *argv[]) {
/* Apply default keyboard layout */
sq2lv_switch_layout(keyboard, SQ2LV_LAYOUT_TERMINAL_US);
/* Start timer for periodically resizing terminals */
lv_timer_create(terminal_resize_timer_cb, 1000, NULL);
/* Open the file to track virtual terminals */
int fd_tty = open("/sys/class/tty/tty0/active", O_RDONLY|O_NOCTTY|O_CLOEXEC);
if (fd_tty < 0) {
perror("Can't open /sys/class/tty/tty0/active");
return EXIT_FAILURE;
}
int fd_epoll = epoll_create1(EPOLL_CLOEXEC);
if (fd_epoll == -1) {
perror("epoll_create1() is failed");
return EXIT_FAILURE;
}
struct epoll_event event;
event.events = EPOLLIN|EPOLLET;
event.data.fd = fd_tty;
int r = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd_tty, &event);
if (r == -1) {
perror("epoll_ctl() is failed");
return EXIT_FAILURE;
}
/* Set signal handlers */
struct sigaction action;
action.sa_handler = signal_handler;
action.sa_flags = 0;
sigfillset(&action.sa_mask);
sigaction(SIGINT, &action, NULL);
sigaction(SIGTERM, &action, NULL);
action.sa_flags = SA_RESTART;
sigemptyset(&action.sa_mask);
sigaction(SIGUSR1, &action, NULL);
sigset_t sigmask;
sigemptyset(&sigmask);
sigaddset(&sigmask, SIGUSR1);
sigprocmask(SIG_BLOCK, &sigmask, NULL);
sigemptyset(&sigmask);
/* Periodically run timer / task handler */
while(1) {
uint32_t time_till_next = lv_timer_handler();
usleep(time_till_next * 1000);
int r = epoll_pwait(fd_epoll, &event, 1, time_till_next, &sigmask);
if (r == 0)
continue;
if (r < 0) {
if (errno != EINTR) {
perror("epoll_wait() is failed");
return EXIT_FAILURE;
}
if (!redraw_requested)
continue;
redraw_requested = false;
} else if (conf_opts.quirks.ignore_unused_terminals) {
lseek(fd_tty, 0, SEEK_SET);
char buffer[8];
ssize_t size = read(fd_tty, buffer, sizeof(buffer));
if (size <= 0) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Can't read /sys/class/tty/tty0/active");
continue;
}
buffer[size] = 0;
unsigned int tty;
if (sscanf(buffer, "tty%u", &tty) != 1) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Unexpected value of /sys/class/tty/tty0/active");
continue;
}
if (!bb_terminal_is_busy(tty)) {
bbx_log(BBX_LOG_LEVEL_VERBOSE, "Terminal %u isn't used, skip automatic update.", tty);
continue;
}
}
bb_terminal_shrink_current();
lv_obj_invalidate(keyboard);
}
return 0;

View file

@ -40,4 +40,10 @@ if depsystemd.found()
install_dir: system_unit_dir,
install_tag: 'buffyboard'
)
install_data('getty-buffyboard.conf',
rename: 'buffyboard.conf',
install_dir: system_unit_dir / 'getty@.service.d',
install_tag: 'buffyboard'
)
endif

View file

@ -3,16 +3,14 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "terminal.h"
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <linux/kd.h>
#include <linux/vt.h>
#include <sys/ioctl.h>
@ -22,260 +20,121 @@
* Static variables
*/
static int current_fd = -1;
static int current_vt = -1;
static bool resized_vts[MAX_NR_CONSOLES];
static float height_factor = 1;
/**
* Static prototypes
*/
/**
* Close the current file descriptor and reopen /dev/tty0.
*
* @return true if opening was successful, false otherwise
*/
static bool reopen_current_terminal(void);
/**
* Close the current file descriptor.
*/
static void close_current_terminal(void);
/**
* Get the currently active virtual terminal.
*
* @return number of the active VT (e.g. 7 for /dev/tty7)
*/
static int get_active_terminal(void);
/**
* Retrieve a terminal's size.
*
* @param fd TTY file descriptor
* @param size pointer to winsize struct for writing the size into
* @return true if the operation was successful, false otherwise. On failure, errno will be set
* to the value set by the failed system call.
*/
static bool get_terminal_size(int fd, struct winsize *size);
/**
* Update a terminal's size.
*
* @param fd TTY file descriptor
* @param size pointer to winsize struct for reading the new size from
* @return true if the operation was successful, false otherwise. On failure, errno will be set
* to the value set by the failed system call.
*/
static bool set_terminal_size(int fd, struct winsize *size);
/**
* Shrink the height of a terminal by the current factor.
*
* @param fd TTY file descriptor
* @return true if the operation was successful, false otherwise
*/
static bool shrink_terminal(int fd);
/**
* Reset the height of a terminal to the maximum.
*
* @param fd TTY file descriptor
* @param size pointer to winsize struct for writing the final size into
* @return true if the operation was successful, false otherwise
*/
static bool reset_terminal(int fd, struct winsize *size);
/**
* Static functions
*/
static bool reopen_current_terminal(void) {
close_current_terminal();
current_fd = open("/dev/tty0", O_RDWR | O_NOCTTY);
if (current_fd < 0) {
perror("Could not open /dev/tty0");
return false;
}
return true;
}
static void close_current_terminal(void) {
if (current_fd < 0) {
return;
}
close(current_fd);
current_fd = -1;
}
static int get_active_terminal(void) {
struct vt_stat stat;
if (ioctl(current_fd, VT_GETSTATE, &stat) != 0) {
perror("Could not retrieve current termimal state");
return -1;
}
return stat.v_active;
}
static bool get_terminal_size(int fd, struct winsize *size) {
if (ioctl(fd, TIOCGWINSZ, size) != 0) {
int errsv = errno;
perror("Could not retrieve current terminal size");
errno = errsv;
return false;
}
return true;
}
static bool set_terminal_size(int fd, struct winsize *size) {
if (ioctl(fd, TIOCSWINSZ, size) != 0) {
int errsv = errno;
perror("Could not update current terminal size");
errno = errsv;
return false;
}
return true;
}
static bool shrink_terminal(int fd) {
struct winsize size = { 0, 0, 0, 0 };
if (!reset_terminal(fd, &size)) {
perror("Could not shrink terminal size");
return false;
}
size.ws_row = floor((float)size.ws_row * height_factor);
if (!set_terminal_size(fd, &size)) {
perror("Could not shrink terminal size");
return false;
}
return true;
}
static bool reset_terminal(int fd, struct winsize *size) {
if (!get_terminal_size(fd, size)) {
perror("Could not reset terminal size");
return false;
}
/* Test-resize by two rows. If the terminal is already maximised, this will fail and we can exit early. */
size->ws_row += 2;
if (!set_terminal_size(fd, size)) {
bool is_max = (errno == EINVAL);
size->ws_row -= 2;
return is_max;
}
size->ws_row = floor((float)size->ws_row / height_factor);
if (!set_terminal_size(fd, size)) {
if (errno != EINVAL) {
perror("Could not reset terminal size");
return false;
}
/* Size too large. Reduce by one row until it fits. */
do {
size->ws_row -= 1;
} while (size->ws_row > 0 && !set_terminal_size(fd, size) && errno == EINVAL);
if (errno != EINVAL || size->ws_row == 0) {
perror("Could not reset terminal size");
return false;
}
} else {
/* Size fits but may not max out available space. Increase by one row until it doesn't fit anymore. */
do {
size->ws_row += 1;
} while (set_terminal_size(fd, size));
if (errno != EINVAL) {
perror("Could not reset terminal size");
return false;
}
size->ws_row -= 1;
}
return true;
}
static int32_t _tty_size;
static int32_t _h_display_size;
static int32_t _v_display_size;
/**
* Public functions
*/
bool bb_terminal_init(float factor) {
if (!reopen_current_terminal()) {
perror("Could not prepare for terminal resizing");
void bb_terminal_init(int32_t tty_size, int32_t h_display_size, int32_t v_display_size) {
_tty_size = tty_size;
_h_display_size = h_display_size;
_v_display_size = v_display_size;
}
void bb_terminal_shrink_current() {
int fd = open("/dev/tty0", O_RDONLY|O_NOCTTY);
if (fd < 0)
return;
/* KDFONTOP returns EINVAL if we are not in the text mode,
so we can skip this check */
/*
int mode;
if (ioctl(fd, KDGETMODE, &mode) != 0)
goto end;
if (mode != KD_TEXT)
goto end;
*/
struct console_font_op cfo = {
.op = KD_FONT_OP_GET,
.width = UINT_MAX,
.height = UINT_MAX,
.charcount = UINT_MAX,
.data = NULL
};
if (ioctl(fd, KDFONTOP, &cfo) != 0)
goto end;
struct winsize size;
size.ws_row = _tty_size / cfo.height;
size.ws_col = _h_display_size / cfo.width;
ioctl(fd, TIOCSWINSZ, &size);
end:
close(fd);
}
void bb_terminal_reset_all() {
int fd = open("/dev/tty0", O_RDONLY|O_NOCTTY);
if (fd < 0)
return;
struct vt_stat state;
if (ioctl(fd, VT_GETSTATE, &state) != 0) {
close(fd);
return;
}
close(fd);
char buffer[sizeof("/dev/tty") + 2];
unsigned short mask = state.v_state >> 1;
unsigned int tty = 1;
for (; mask; mask >>= 1, tty++) {
if (mask & 0x01) {
sprintf(buffer, "/dev/tty%u", tty);
int tty_fd = open(buffer, O_RDONLY|O_NOCTTY);
if (tty_fd < 0)
continue;
/* KDFONTOP returns EINVAL if we are not in the text mode,
so we can skip this check */
/*
int mode;
if (ioctl(tty_fd, KDGETMODE, &mode) != 0)
goto end;
if (mode != KD_TEXT)
goto end;
*/
struct console_font_op cfo = {
.op = KD_FONT_OP_GET,
.width = UINT_MAX,
.height = UINT_MAX,
.charcount = UINT_MAX,
.data = NULL
};
if (ioctl(tty_fd, KDFONTOP, &cfo) != 0)
goto end;
struct winsize size;
size.ws_row = _v_display_size / cfo.height;
size.ws_col = _h_display_size / cfo.width;
ioctl(tty_fd, TIOCSWINSZ, &size);
end:
close(tty_fd);
}
}
}
bool bb_terminal_is_busy(unsigned int tty) {
/* We can use any other terminal than `tty` here. */
int fd = open(tty == 1? "/dev/tty2" : "/dev/tty1", O_RDONLY|O_NOCTTY);
if (fd < 0)
return false;
}
current_vt = get_active_terminal();
if (current_vt < 0) {
perror("Could not prepare for terminal resizing");
struct vt_stat state;
int r = ioctl(fd, VT_GETSTATE, &state);
close(fd);
if (r != 0)
return false;
}
height_factor = factor;
return true;
}
void bb_terminal_shrink_current(void) {
int active_vt = get_active_terminal();
if (active_vt < 0) {
perror("Could not resize current terminal");
return;
}
if (active_vt < 0 || active_vt > MAX_NR_CONSOLES - 1) {
perror("Could not resize current terminal, index is out of bounds");
return;
}
if (resized_vts[active_vt - 1]) {
return; /* Already resized */
}
if (active_vt != current_vt) {
if (!reopen_current_terminal()) {
perror("Could not resize current terminal");
return;
}
current_vt = active_vt;
}
if (!shrink_terminal(current_fd)) {
perror("Could not resize current terminal");
return;
}
resized_vts[current_vt - 1] = true;
}
void bb_terminal_reset_all(void) {
char device[16];
struct winsize size = { 0, 0, 0, 0 };
for (int i = 0; i < MAX_NR_CONSOLES; ++i) {
if (!resized_vts[i]) {
continue;
}
snprintf(device, 16, "/dev/tty%d", i + 1);
int fd = open(device, O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("Could not reset TTY, unable to open TTY");
continue;
}
reset_terminal(fd, &size);
}
return (state.v_state >> tty) & 0x01;
}

View file

@ -8,24 +8,30 @@
#define BB_TERMINAL_H
#include <stdbool.h>
#include <stdint.h>
/**
* Prepare for resizing terminals by opening the current one.
* Prepare for resizing terminals.
*
* @param factor factor (between 0 and 1) by which to adapt terminal sizes
* @return true if the operation was successful, false otherwise. No other bb_terminal_* functions
* must be called if false is returned.
* @param tty_size vertical size of the tty area in pixels
* @param h_display_size horizontal resolution of the display in pixels
* @param v_display_size vertical resolution of the display in pixels
*/
bool bb_terminal_init(float factor);
void bb_terminal_init(int32_t tty_size, int32_t h_display_size, int32_t v_display_size);
/**
* Shrink the height of the active terminal by the current factor.
* Shrink the height of the active terminal.
*/
void bb_terminal_shrink_current(void);
void bb_terminal_shrink_current();
/**
* Re-maximise the height of all previously resized terminals.
* Re-maximise the size of all active terminals.
*/
void bb_terminal_reset_all(void);
void bb_terminal_reset_all();
/**
* Check whether the terminal is opened by some process
*/
bool bb_terminal_is_busy(unsigned int tty);
#endif /* BB_TERMINAL_H */

View file

@ -52,6 +52,15 @@ result.
*-V, --version*
Print the unl0kr version and exit.
# NOTES
Some terminal commands, like _clear_ or _setfont_, can erase the keyboard or brake the layout of the terminal. In this case you should send a signal to Buffyboard or switch to another terminal to update the screen:
```
setfont solar24x32;/usr/bin/kill -s SIGUSR1 buffyboard
setfont solar24x32;chvt $((`fgconsole`+1));chvt $((`fgconsole`-1))
```
# EXAMPLES
*Execute Buffyboard using the default config*

View file

@ -45,6 +45,17 @@ for and, if found, merged in the following order:
after every draw operation. This has a negative performance impact.
Default: false.
*ignore_unused_terminals* = <true|false>
If true, buffyboard won't automatically update the layout of a new terminal and
draw the keyboard, if the terminal is not opened by any process. In this case
SIGUSR1 should be sent to buffyboard to update the layout. This quirk was introduced to resolve a race between buffyboard and systemd-logind according to the following scenario:
- A user switches to a new virtual terminal
- Buffyboard opens the terminal and changes the number of rows
- systemd-logind sees that the terminal is opened by some other process and don't start getty@.service
The race is resolved by enabling this option and installing a drop-in file
for getty@.service that sends SIGUSR1 to buffyboard. Default: true.
# SEE ALSO
*buffyboard*(1)