keyboard: add haptic feedback

This commit is contained in:
Vladimir Stoiakin 2025-11-26 12:34:09 +03:00
parent 271cdf8534
commit 6b3477dc78
19 changed files with 211 additions and 15 deletions

View file

@ -1,3 +1,6 @@
[keyboard]
haptic_feedback=true
[theme] [theme]
default=breezy-light default=breezy-light

View file

@ -40,7 +40,13 @@ static int parsing_handler(void* user_data, const char* section, const char* key
static int parsing_handler(void* user_data, const char* section, const char* key, const char* value) { static int parsing_handler(void* user_data, const char* section, const char* key, const char* value) {
bb_config_opts *opts = (bb_config_opts *)user_data; bb_config_opts *opts = (bb_config_opts *)user_data;
if (strcmp(section, "theme") == 0) { if (strcmp(section, "keyboard") == 0) {
if (strcmp(key, "haptic_feedback") == 0) {
if (bbx_config_parse_bool(value, &(opts->keyboard.haptic_feedback))) {
return 1;
}
}
} else if (strcmp(section, "theme") == 0) {
if (strcmp(key, "default") == 0) { if (strcmp(key, "default") == 0) {
bbx_themes_theme_id_t id = bbx_themes_find_theme_with_name(value); bbx_themes_theme_id_t id = bbx_themes_find_theme_with_name(value);
if (id != BBX_THEMES_THEME_NONE) { if (id != BBX_THEMES_THEME_NONE) {
@ -80,6 +86,7 @@ static int parsing_handler(void* user_data, const char* section, const char* key
*/ */
void bb_config_init_opts(bb_config_opts *opts) { void bb_config_init_opts(bb_config_opts *opts) {
opts->keyboard.haptic_feedback = true;
opts->theme.default_id = BBX_THEMES_THEME_BREEZY_DARK; opts->theme.default_id = BBX_THEMES_THEME_BREEZY_DARK;
opts->input.pointer = true; opts->input.pointer = true;
opts->input.touchscreen = true; opts->input.touchscreen = true;

View file

@ -11,6 +11,14 @@
#include "sq2lv_layouts.h" #include "sq2lv_layouts.h"
/**
* Options related to the keyboard
*/
typedef struct {
/* If true, vibrate on key presses */
bool haptic_feedback;
} bb_config_opts_keyboard;
/** /**
* Options related to the theme * Options related to the theme
*/ */
@ -43,6 +51,8 @@ typedef struct {
* Options parsed from config file(s) * Options parsed from config file(s)
*/ */
typedef struct { typedef struct {
/* Options related to the keyboard */
bb_config_opts_keyboard keyboard;
/* Options related to the theme */ /* Options related to the theme */
bb_config_opts_theme theme; bb_config_opts_theme theme;
/* Options related to input devices */ /* Options related to input devices */

View file

@ -12,6 +12,7 @@
#include "lvgl/lvgl.h" #include "lvgl/lvgl.h"
#include "../shared/force_feedback.h"
#include "../shared/indev.h" #include "../shared/indev.h"
#include "../shared/log.h" #include "../shared/log.h"
#include "../shared/theme.h" #include "../shared/theme.h"
@ -93,6 +94,8 @@ static void keyboard_value_changed_cb(lv_event_t *event) {
return; return;
} }
bbx_force_feedback_play();
if (sq2lv_is_layer_switcher(kb, btn_id)) { if (sq2lv_is_layer_switcher(kb, btn_id)) {
pop_checked_modifier_keys(); pop_checked_modifier_keys();
sq2lv_switch_layer(kb, btn_id); sq2lv_switch_layer(kb, btn_id);
@ -296,7 +299,8 @@ int main(int argc, char *argv[]) {
/* Attach input devices and start monitoring for new ones */ /* Attach input devices and start monitoring for new ones */
struct bbx_indev_opts input_config = { struct bbx_indev_opts input_config = {
.pointer = conf_opts.input.pointer, .pointer = conf_opts.input.pointer,
.touchscreen = conf_opts.input.touchscreen .touchscreen = conf_opts.input.touchscreen,
.force_feedback = conf_opts.keyboard.haptic_feedback
}; };
if (bbx_indev_init(fd_epoll, &input_config) == 0) if (bbx_indev_init(fd_epoll, &input_config) == 0)
return EXIT_FAILURE; return EXIT_FAILURE;

View file

@ -59,6 +59,10 @@ static int parsing_handler(void* user_data, const char* section, const char* key
if (bbx_config_parse_bool(value, &(opts->keyboard.popovers))) { if (bbx_config_parse_bool(value, &(opts->keyboard.popovers))) {
return 1; return 1;
} }
} else if (strcmp(key, "haptic_feedback") == 0) {
if (bbx_config_parse_bool(value, &(opts->keyboard.haptic_feedback))) {
return 1;
}
} }
} else if (strcmp(section, "theme") == 0) { } else if (strcmp(section, "theme") == 0) {
if (strcmp(key, "default") == 0) { if (strcmp(key, "default") == 0) {
@ -190,6 +194,7 @@ void f0_config_init_opts(f0_config_opts *opts) {
opts->keyboard.autohide = true; opts->keyboard.autohide = true;
opts->keyboard.layout_id = SQ2LV_LAYOUT_US; opts->keyboard.layout_id = SQ2LV_LAYOUT_US;
opts->keyboard.popovers = true; opts->keyboard.popovers = true;
opts->keyboard.haptic_feedback = true;
opts->textarea.obscured = true; opts->textarea.obscured = true;
opts->textarea.bullet = LV_SYMBOL_BULLET; opts->textarea.bullet = LV_SYMBOL_BULLET;
opts->input.keyboard = true; opts->input.keyboard = true;

View file

@ -33,6 +33,8 @@ typedef struct {
sq2lv_layout_id_t layout_id; sq2lv_layout_id_t layout_id;
/* If true, display key popovers on press */ /* If true, display key popovers on press */
bool popovers; bool popovers;
/* If true, vibrate on key presses */
bool haptic_feedback;
} f0_config_opts_keyboard; } f0_config_opts_keyboard;
/** /**

View file

@ -8,6 +8,7 @@
#include "../shared/backends.h" #include "../shared/backends.h"
#include "../shared/display.h" #include "../shared/display.h"
#include "../shared/force_feedback.h"
#include "../shared/header.h" #include "../shared/header.h"
#include "../shared/indev.h" #include "../shared/indev.h"
#include "../shared/keyboard.h" #include "../shared/keyboard.h"
@ -37,19 +38,19 @@
f0_cli_opts cli_opts; f0_cli_opts cli_opts;
f0_config_opts conf_opts; f0_config_opts conf_opts;
bool is_alternate_theme = false; static bool is_alternate_theme = false;
bool is_keyboard_hidden = false; static bool is_keyboard_hidden = false;
static char **field_values = NULL; static char **field_values = NULL;
int current_field_index = 0; static int current_field_index = 0;
lv_obj_t *form_container = NULL; static lv_obj_t *form_container = NULL;
lv_obj_t *form_textarea = NULL; static lv_obj_t *form_textarea = NULL;
lv_obj_t *keyboard = NULL; static lv_obj_t *keyboard = NULL;
int32_t content_height_with_kb; static int32_t content_height_with_kb;
int32_t content_height_without_kb; static int32_t content_height_without_kb;
/** /**
* Static prototypes * Static prototypes
@ -265,7 +266,7 @@ static void exit_failure();
* Static functions * Static functions
*/ */
static void intro_key_cb(lv_event_t *event) { static void intro_key_cb(lv_event_t *event) {
uint32_t key = lv_indev_get_key(lv_indev_active()); uint32_t key = lv_indev_get_key(lv_indev_active());
if (key == LV_KEY_ENTER) { if (key == LV_KEY_ENTER) {
get_started_btn_clicked_cb(event); get_started_btn_clicked_cb(event);
@ -409,6 +410,8 @@ static void keyboard_value_changed_cb(lv_event_t *event) {
return; return;
} }
bbx_force_feedback_play();
if (sq2lv_is_layer_switcher(kb, btn_id)) { if (sq2lv_is_layer_switcher(kb, btn_id)) {
sq2lv_switch_layer(kb, btn_id); sq2lv_switch_layer(kb, btn_id);
return; return;
@ -889,7 +892,8 @@ int main(int argc, char *argv[]) {
.keymap = &conf_opts.hw_keyboard, .keymap = &conf_opts.hw_keyboard,
.keyboard = conf_opts.input.keyboard, .keyboard = conf_opts.input.keyboard,
.pointer = conf_opts.input.pointer, .pointer = conf_opts.input.pointer,
.touchscreen = conf_opts.input.touchscreen .touchscreen = conf_opts.input.touchscreen,
.force_feedback = conf_opts.keyboard.haptic_feedback
}; };
if (bbx_indev_init(fd_epoll, &input_config) == 0) if (bbx_indev_init(fd_epoll, &input_config) == 0)
exit_failure(); exit_failure();

View file

@ -54,7 +54,7 @@ result.
# NOTES # 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: Some terminal commands, like _clear_ or _setfont_, can erase the keyboard or break 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;/usr/bin/kill -s SIGUSR1 buffyboard

View file

@ -25,6 +25,11 @@ for and, if found, merged in the following order:
# OPTIONS # OPTIONS
## Keyboard
*haptic_feedback* = <true|false>
Enable or disable vibrations when pressing keys.
Default: true.
## Theme ## Theme
*default* = <adwaita-light|adwaita-dark|breezy-light|breezy-dark|nord-light|nord-dark|pmos-light|pmos-dark> *default* = <adwaita-light|adwaita-dark|breezy-light|breezy-dark|nord-light|nord-dark|pmos-light|pmos-dark>
Selects the default theme on boot. Can be changed at runtime to the Selects the default theme on boot. Can be changed at runtime to the

View file

@ -48,7 +48,11 @@ for and, if found, merged in the following order:
*popovers* = <true|false> *popovers* = <true|false>
Enable or disable key press popovers showing the selected key. Enable or disable key press popovers showing the selected key.
default: true. Default: true.
*haptic_feedback* = <true|false>
Enable or disable vibrations when pressing keys.
Default: true.
## Textarea ## Textarea
*obscured* = <true|false> *obscured* = <true|false>

View file

@ -26,6 +26,7 @@ shared_sources = files(
'shared/fonts/font_32.c', 'shared/fonts/font_32.c',
'shared/cli_common.c', 'shared/cli_common.c',
'shared/config.c', 'shared/config.c',
'shared/force_feedback.c',
'shared/indev.c', 'shared/indev.c',
'shared/log.c', 'shared/log.c',
'shared/theme.c', 'shared/theme.c',

109
shared/force_feedback.c Normal file
View file

@ -0,0 +1,109 @@
/**
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "force_feedback.h"
#include "log.h"
#include <linux/input.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <libudev.h>
#include <stdint.h>
#include <unistd.h>
#define bit_is_set(array, bit) ((array[bit / 8] >> bit % 8) & 0x01)
/**
* Static variables
*/
static int fd = -1;
static struct input_event ff_event = {
.type = EV_FF,
.code = 0,
.value = 1
};
/**
* Public functions
*/
void bbx_force_feedback_connect(struct udev_device* device) {
if (fd >= 0)
return;
const char* node = udev_device_get_devnode(device);
int fd_event = open(node, O_RDWR);
if (fd_event < 0) {
bbx_log(BBX_LOG_LEVEL_ERROR, "Can't open %s", node);
return;
}
int n_effects;
if (ioctl(fd_event, EVIOCGEFFECTS, &n_effects) == -1 || n_effects == 0) {
close(fd_event);
return;
}
bbx_log(BBX_LOG_LEVEL_VERBOSE, "Found a force feedback device: %s", node);
uint8_t features[(FF_CNT + 7) / 8];
if (ioctl(fd_event, EVIOCGBIT(EV_FF, sizeof(features)), features) == -1) {
bbx_log(BBX_LOG_LEVEL_ERROR, "Can't request features of %s", node);
close(fd_event);
return;
}
if (!bit_is_set(features, FF_RUMBLE)) {
bbx_log(BBX_LOG_LEVEL_VERBOSE, "%s does not support FF_RUMBLE", node);
close(fd_event);
return;
}
struct ff_effect effect = {
.type = FF_RUMBLE,
.id = -1,
.direction = 0x0000,
.trigger.button = 0,
.trigger.interval = 0,
.replay.length = 50,
.replay.delay = 0,
.u.rumble.strong_magnitude = 0xC000,
.u.rumble.weak_magnitude = 0xC000
};
if (ioctl(fd_event, EVIOCSFF, &effect) == -1) {
bbx_log(BBX_LOG_LEVEL_ERROR, "Can't upload the effect");
close(fd_event);
return;
}
ff_event.code = effect.id;
if (bit_is_set(features, FF_GAIN)) {
struct input_event gain = {
.type = EV_FF,
.code = FF_GAIN,
.value = 0xFFFF
};
if (write(fd_event, &gain, sizeof(gain)) != sizeof(gain))
bbx_log(BBX_LOG_LEVEL_WARNING, "Can't set the gain");
}
fd = fd_event;
}
void bbx_force_feedback_play() {
if (fd < 0)
return;
if (write(fd, &ff_event, sizeof(ff_event)) != sizeof(ff_event)) {
// Assume that the force feedback device was disconnected
close(fd);
fd = -1;
}
}

20
shared/force_feedback.h Normal file
View file

@ -0,0 +1,20 @@
/**
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#ifndef BBX_FORCE_FEEDBACK_H
#define BBX_FORCE_FEEDBACK_H
#include <libudev.h>
/**
* Try to connect a force feedback device.
*/
void bbx_force_feedback_connect(struct udev_device* device);
/**
* Play a force feedback effect.
*/
void bbx_force_feedback_play();
#endif /* BBX_FORCE_FEEDBACK_H */

View file

@ -6,6 +6,7 @@
#include "indev.h" #include "indev.h"
#include "cursor/cursor.h" #include "cursor/cursor.h"
#include "force_feedback.h"
#include "log.h" #include "log.h"
#include "lvgl/lvgl.h" #include "lvgl/lvgl.h"
@ -93,6 +94,7 @@ static struct {
#endif #endif
uint8_t pointer : 1; uint8_t pointer : 1;
uint8_t touchscreen : 1; uint8_t touchscreen : 1;
uint8_t force_feedback : 1;
} options; } options;
/** /**
@ -641,6 +643,12 @@ static void attach_input_device(struct udev_device* device) {
break; break;
} }
/* A device can be a force feedback device and an input device independently,
so we check for both capabilities. */
if (options.force_feedback)
bbx_force_feedback_connect(device);
struct libinput_device* dev = libinput_path_add_device(context_libinput, node); struct libinput_device* dev = libinput_path_add_device(context_libinput, node);
if (!dev) { if (!dev) {
bbx_log(BBX_LOG_LEVEL_WARNING, "libinput can't use %s", node); bbx_log(BBX_LOG_LEVEL_WARNING, "libinput can't use %s", node);
@ -795,6 +803,7 @@ uint8_t bbx_indev_init(int fd_epoll, const struct bbx_indev_opts* opts) {
#endif #endif
options.pointer = opts->pointer; options.pointer = opts->pointer;
options.touchscreen = opts->touchscreen; options.touchscreen = opts->touchscreen;
options.force_feedback = opts->force_feedback;
attach_input_devices(); attach_input_devices();

View file

@ -20,6 +20,7 @@ struct bbx_indev_opts {
#endif #endif
uint8_t pointer : 1; uint8_t pointer : 1;
uint8_t touchscreen : 1; uint8_t touchscreen : 1;
uint8_t force_feedback : 1;
}; };
#ifndef BBX_APP_BUFFYBOARD #ifndef BBX_APP_BUFFYBOARD

View file

@ -71,6 +71,10 @@ static int parsing_handler(void* user_data, const char* section, const char* key
if (bbx_config_parse_bool(value, &(opts->keyboard.popovers))) { if (bbx_config_parse_bool(value, &(opts->keyboard.popovers))) {
return 1; return 1;
} }
} else if (strcmp(key, "haptic_feedback") == 0) {
if (bbx_config_parse_bool(value, &(opts->keyboard.haptic_feedback))) {
return 1;
}
} }
} else if (strcmp(section, "textarea") == 0) { } else if (strcmp(section, "textarea") == 0) {
if (strcmp(key, "obscured") == 0) { if (strcmp(key, "obscured") == 0) {
@ -161,6 +165,7 @@ void ul_config_init_opts(ul_config_opts *opts) {
opts->keyboard.autohide = true; opts->keyboard.autohide = true;
opts->keyboard.layout_id = SQ2LV_LAYOUT_US; opts->keyboard.layout_id = SQ2LV_LAYOUT_US;
opts->keyboard.popovers = true; opts->keyboard.popovers = true;
opts->keyboard.haptic_feedback = true;
opts->textarea.obscured = true; opts->textarea.obscured = true;
opts->textarea.bullet = LV_SYMBOL_BULLET; opts->textarea.bullet = LV_SYMBOL_BULLET;
opts->theme.default_id = BBX_THEMES_THEME_BREEZY_DARK; opts->theme.default_id = BBX_THEMES_THEME_BREEZY_DARK;

View file

@ -39,6 +39,8 @@ typedef struct {
sq2lv_layout_id_t layout_id; sq2lv_layout_id_t layout_id;
/* If true, display key popovers on press */ /* If true, display key popovers on press */
bool popovers; bool popovers;
/* If true, vibrate on key presses */
bool haptic_feedback;
} ul_config_opts_keyboard; } ul_config_opts_keyboard;
/** /**

View file

@ -10,6 +10,7 @@
#include "../shared/backends.h" #include "../shared/backends.h"
#include "../shared/display.h" #include "../shared/display.h"
#include "../shared/force_feedback.h"
#include "../shared/header.h" #include "../shared/header.h"
#include "../shared/indev.h" #include "../shared/indev.h"
#include "../shared/keyboard.h" #include "../shared/keyboard.h"
@ -334,6 +335,8 @@ static void keyboard_value_changed_cb(lv_event_t *event) {
return; return;
} }
bbx_force_feedback_play();
if (sq2lv_is_layer_switcher(kb, btn_id)) { if (sq2lv_is_layer_switcher(kb, btn_id)) {
sq2lv_switch_layer(kb, btn_id); sq2lv_switch_layer(kb, btn_id);
return; return;
@ -447,7 +450,8 @@ int main(int argc, char *argv[]) {
.keymap = &conf_opts.hw_keyboard, .keymap = &conf_opts.hw_keyboard,
.keyboard = conf_opts.input.keyboard, .keyboard = conf_opts.input.keyboard,
.pointer = conf_opts.input.pointer, .pointer = conf_opts.input.pointer,
.touchscreen = conf_opts.input.touchscreen .touchscreen = conf_opts.input.touchscreen,
.force_feedback = conf_opts.keyboard.haptic_feedback
}; };
if (bbx_indev_init(fd_epoll, &input_config) == 0) if (bbx_indev_init(fd_epoll, &input_config) == 0)
exit_failure(); exit_failure();

View file

@ -7,6 +7,7 @@ animations=true
autohide=false autohide=false
layout=us layout=us
popovers=true popovers=true
haptic_feedback=true
[textarea] [textarea]
obscured=true obscured=true