Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions i18n/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,8 @@ move_up = "Move up"
move_down = "Move down"
text = "Text"
types_text_characters_one_by_one = "Types text characters one by one"
host_text = "Unicode"
host_text_requires_entropy_running = "Types Unicode text through Entropy; Entropy and its Universal Symbols backend must be running"
tap = "Tap"
press_and_release_a_key = "Press and release a key"
down = "Down"
Expand All @@ -956,13 +958,16 @@ delay = "Delay"
wait_before_next_action = "Wait before next action"
type_text_here = "Type text here"
characters_to_type_when_this_macro_runs = "Characters to type when this macro runs"
type_unicode_text_here = "Type Unicode text here"
unicode_text_requires_entropy_running = "Unicode text requires Entropy and its Universal Symbols backend to be running"
click_to_change_key_press_and_release_this_key = "Click to change key — press and release this key"
click_to_change_key_holds_down_until_up = "Click to change key — holds down until Up"
click_to_change_key_releases_this_key = "Click to change key — releases this key"
delay_is_in_milliseconds = "Delay is in milliseconds"
remove_this_action = "Remove this action"
plus_text = "+ Text"
type_characters = "Type characters"
plus_host_text = "+ Unicode"
plus_tap = "+ Tap"
plus_down = "+ Down"
hold_a_key = "Hold a key"
Expand Down
5 changes: 5 additions & 0 deletions i18n/ru.toml
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,8 @@ move_up = "Выше"
move_down = "Ниже"
text = "Текст"
types_text_characters_one_by_one = "Вводит символы текста по одному"
host_text = "Unicode"
host_text_requires_entropy_running = "Вводит Unicode-текст через Entropy; Entropy и бэкенд Universal Symbols должны быть запущены"
tap = "Tap"
press_and_release_a_key = "Нажать и отпустить клавишу"
down = "Down"
Expand All @@ -956,13 +958,16 @@ delay = "Задержка"
wait_before_next_action = "Пауза перед следующим действием"
type_text_here = "Введите текст здесь"
characters_to_type_when_this_macro_runs = "Символы, которые макрос будет вводить"
type_unicode_text_here = "Введите Unicode-текст здесь"
unicode_text_requires_entropy_running = "Для Unicode-текста Entropy и бэкенд Universal Symbols должны быть запущены"
click_to_change_key_press_and_release_this_key = "Нажмите, чтобы сменить клавишу — press/release"
click_to_change_key_holds_down_until_up = "Нажмите, чтобы сменить клавишу — удерживать до Up"
click_to_change_key_releases_this_key = "Нажмите, чтобы сменить клавишу — отпустить"
delay_is_in_milliseconds = "Задержка в миллисекундах"
remove_this_action = "Удалить это действие"
plus_text = "+ Текст"
type_characters = "Ввести символы"
plus_host_text = "+ Unicode"
plus_tap = "+ Tap"
plus_down = "+ Down"
hold_a_key = "Удерживать клавишу"
Expand Down
113 changes: 113 additions & 0 deletions linux/fcitx5/src/entropyuniversalsymbols.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "fcitx-utils/handlertable.h"
#include "fcitx-utils/key.h"
#include "fcitx-utils/keysym.h"
Expand All @@ -25,6 +26,12 @@ constexpr uint16_t MOD_CTRL = 0x0100;
constexpr uint16_t MOD_SHIFT = 0x0200;
constexpr uint16_t MOD_ALT = 0x0400;
constexpr uint16_t MOD_GUI = 0x0800;
constexpr uint16_t HOST_TEXT_START_TRIGGER =
MOD_CTRL | MOD_SHIFT | MOD_ALT | MOD_GUI | (KC_F13 + 7);
constexpr uint16_t HOST_TEXT_END_TRIGGER =
MOD_CTRL | MOD_SHIFT | MOD_ALT | MOD_GUI | (KC_F13 + 6);
constexpr uint16_t HOST_TEXT_DATA_MODIFIERS = MOD_CTRL | MOD_ALT | MOD_GUI;
constexpr size_t HOST_TEXT_MAX_DIGITS = 3 * 1024;

struct SmartSymbol {
uint16_t trigger;
Expand Down Expand Up @@ -166,6 +173,78 @@ std::optional<std::string> symbolForKey(const Key &key) {
return std::nullopt;
}

bool isHostTextTransportTrigger(uint16_t trigger) {
const auto base = trigger & 0x00ff;
const auto modifiers = trigger & 0xff00;
return trigger == HOST_TEXT_START_TRIGGER || trigger == HOST_TEXT_END_TRIGGER ||
(modifiers == HOST_TEXT_DATA_MODIFIERS && base >= KC_F13 && base <= KC_F13 + 7);
}

bool isTransportModifierKey(KeySym sym) {
switch (sym) {
case FcitxKey_Shift_L:
case FcitxKey_Shift_R:
case FcitxKey_Control_L:
case FcitxKey_Control_R:
case FcitxKey_Alt_L:
case FcitxKey_Alt_R:
case FcitxKey_Super_L:
case FcitxKey_Super_R:
return true;
default:
return false;
}
}

bool isValidUtf8(const std::string &text) {
for (size_t index = 0; index < text.size();) {
const auto byte = static_cast<uint8_t>(text[index]);
size_t width = 0;
if (byte <= 0x7f) {
width = 1;
} else if (byte >= 0xc2 && byte <= 0xdf) {
width = 2;
} else if (byte >= 0xe0 && byte <= 0xef) {
width = 3;
} else if (byte >= 0xf0 && byte <= 0xf4) {
width = 4;
} else {
return false;
}
if (index + width > text.size()) {
return false;
}
for (size_t continuation = 1; continuation < width; ++continuation) {
if ((static_cast<uint8_t>(text[index + continuation]) & 0xc0) != 0x80) {
return false;
}
}
if ((byte == 0xe0 && static_cast<uint8_t>(text[index + 1]) < 0xa0) ||
(byte == 0xed && static_cast<uint8_t>(text[index + 1]) >= 0xa0) ||
(byte == 0xf0 && static_cast<uint8_t>(text[index + 1]) < 0x90) ||
(byte == 0xf4 && static_cast<uint8_t>(text[index + 1]) >= 0x90)) {
return false;
}
index += width;
}
return true;
}

std::optional<std::string> decodeHostTextDigits(const std::vector<uint8_t> &digits) {
if (digits.size() % 3 != 0) {
return std::nullopt;
}
std::string text;
text.reserve(digits.size() / 3);
for (size_t index = 0; index < digits.size(); index += 3) {
if (digits[index] > 3 || digits[index + 1] > 7 || digits[index + 2] > 7) {
return std::nullopt;
}
text.push_back(char((digits[index] << 6) | (digits[index + 1] << 3) | digits[index + 2]));
}
return isValidUtf8(text) ? std::optional<std::string>(text) : std::nullopt;
}

} // namespace

class EntropyUniversalSymbols final : public AddonInstance {
Expand All @@ -179,6 +258,38 @@ class EntropyUniversalSymbols final : public AddonInstance {
private:
void handleKeyEvent(Event &event) {
auto &keyEvent = static_cast<KeyEvent &>(event);
const auto base = baseKeycodeForSym(keyEvent.key().sym());
if (base) {
const auto trigger = uint16_t(*base | transportModifiers(keyEvent.key().states()));
if (isHostTextTransportTrigger(trigger)) {
if (!keyEvent.isRelease()) {
if (trigger == HOST_TEXT_START_TRIGGER) {
hostTextActive_ = true;
hostTextDigits_.clear();
} else if (trigger == HOST_TEXT_END_TRIGGER && hostTextActive_) {
hostTextActive_ = false;
if (const auto text = decodeHostTextDigits(hostTextDigits_)) {
keyEvent.inputContext()->commitString(*text);
}
hostTextDigits_.clear();
} else if (hostTextActive_ &&
(trigger & 0xff00) == HOST_TEXT_DATA_MODIFIERS) {
if (hostTextDigits_.size() < HOST_TEXT_MAX_DIGITS) {
hostTextDigits_.push_back(uint8_t((trigger & 0x00ff) - KC_F13));
} else {
hostTextActive_ = false;
hostTextDigits_.clear();
}
}
}
keyEvent.filterAndAccept();
return;
}
}
if (!keyEvent.isRelease() && !isTransportModifierKey(keyEvent.key().sym())) {
hostTextActive_ = false;
hostTextDigits_.clear();
}
const auto symbol = symbolForKey(keyEvent.key());
if (!symbol) {
return;
Expand All @@ -194,6 +305,8 @@ class EntropyUniversalSymbols final : public AddonInstance {

Instance *instance_;
std::unique_ptr<HandlerTableEntry<EventHandler>> eventHandler_;
bool hostTextActive_ = false;
std::vector<uint8_t> hostTextDigits_;
};

class EntropyUniversalSymbolsFactory final : public AddonFactory {
Expand Down
74 changes: 74 additions & 0 deletions linux/ibus/entropy-ibus-engine
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ MOD_CTRL = 0x0100
MOD_SHIFT = 0x0200
MOD_ALT = 0x0400
MOD_GUI = 0x0800
HOST_TEXT_START_TRIGGER = MOD_CTRL | MOD_SHIFT | MOD_ALT | MOD_GUI | (KC_F13 + 7)
HOST_TEXT_END_TRIGGER = MOD_CTRL | MOD_SHIFT | MOD_ALT | MOD_GUI | (KC_F13 + 6)
HOST_TEXT_DATA_MODIFIERS = MOD_CTRL | MOD_ALT | MOD_GUI
HOST_TEXT_MAX_DIGITS = 3 * 1024
EVDEV_F13_KEYCODE = 183
XKB_F13_KEYCODE = EVDEV_F13_KEYCODE + 8
TERMINAL_CLIENT_MARKERS = (
Expand Down Expand Up @@ -666,13 +670,53 @@ def _transport_modifiers(state):
return modifiers


def _host_text_transport_trigger(base_keycode, modifiers):
trigger = base_keycode | modifiers
return (
trigger == HOST_TEXT_START_TRIGGER
or trigger == HOST_TEXT_END_TRIGGER
or (
modifiers == HOST_TEXT_DATA_MODIFIERS
and KC_F13 <= base_keycode <= KC_F13 + 7
)
)


def _transport_modifier_key(keyval):
return keyval in {
IBus.KEY_Shift_L,
IBus.KEY_Shift_R,
IBus.KEY_Control_L,
IBus.KEY_Control_R,
IBus.KEY_Alt_L,
IBus.KEY_Alt_R,
IBus.KEY_Super_L,
IBus.KEY_Super_R,
}


def _decode_host_text_digits(digits):
if len(digits) % 3:
return None
payload = []
for high, middle, low in zip(digits[::3], digits[1::3], digits[2::3]):
if high > 3 or middle > 7 or low > 7:
return None
payload.append((high << 6) | (middle << 3) | low)
try:
return bytes(payload).decode("utf-8")
except UnicodeDecodeError:
return None


class EntropyEngine(IBus.Engine):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._text_expander = SHARED_TEXT_EXPANDER_RUNTIME
self._client = ""
self._client_capabilities = 0
self._input_purpose = IBus.InputPurpose.FREE_FORM
self._host_text_digits = None

def do_focus_in_id(self, object_path, client):
del object_path
Expand All @@ -681,6 +725,7 @@ class EntropyEngine(IBus.Engine):
def do_focus_out_id(self, object_path):
del object_path
self._client = ""
self._host_text_digits = None

def do_set_capabilities(self, caps):
self._client_capabilities = int(caps)
Expand All @@ -693,6 +738,14 @@ class EntropyEngine(IBus.Engine):
base_keycode, source = _transport_keycode_from_event(keyval, keycode)
if base_keycode is not None:
modifiers = _transport_modifiers(state)
if _host_text_transport_trigger(base_keycode, modifiers):
if not (state & IBus.ModifierType.RELEASE_MASK):
text = self._process_host_text_transport(base_keycode | modifiers)
if text:
self.commit_text(IBus.Text.new_from_string(text))
return True
if not (state & IBus.ModifierType.RELEASE_MASK):
self._host_text_digits = None
symbol = SMART_SYMBOLS.get((base_keycode, modifiers))
del source
if symbol is not None:
Expand All @@ -701,8 +754,29 @@ class EntropyEngine(IBus.Engine):
self._commit_text_expander_aware_text(symbol)
return True

if not (state & IBus.ModifierType.RELEASE_MASK) and not _transport_modifier_key(keyval):
self._host_text_digits = None
return self._process_text_expander_key(keyval, keycode, state)

def _process_host_text_transport(self, trigger):
if trigger == HOST_TEXT_START_TRIGGER:
self._host_text_digits = []
return None
if self._host_text_digits is None:
return None
if trigger == HOST_TEXT_END_TRIGGER:
digits = self._host_text_digits
self._host_text_digits = None
return _decode_host_text_digits(digits)
if trigger & ~0xFF == HOST_TEXT_DATA_MODIFIERS:
if len(self._host_text_digits) >= HOST_TEXT_MAX_DIGITS:
self._host_text_digits = None
return None
self._host_text_digits.append((trigger & 0xFF) - KC_F13)
return None
self._host_text_digits = None
return None

def _process_text_expander_key(self, keyval, keycode, state):
del keycode
if state & IBus.ModifierType.RELEASE_MASK:
Expand Down
10 changes: 10 additions & 0 deletions src/hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,9 @@ impl HidDevice {

let mut response = [0; MSG_LEN];
match (request[0], request[1], request[2]) {
(CMD_VIA_MACRO_GET_BUFFER_SIZE, _, _) => {
response[1..3].copy_from_slice(&1024u16.to_be_bytes());
}
(CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP, DYNAMIC_VIAL_COMBO_SET) => {
let mut keys = [0; 4];
for (index, key) in keys.iter_mut().enumerate() {
Expand Down Expand Up @@ -1047,6 +1050,13 @@ fn hex_nibble(byte: u8) -> Result<u8> {
mod tests {
use super::*;

#[test]
fn test_hid_reports_macro_buffer_capacity() {
let (device, _) = HidDevice::test_device();

assert_eq!(device.get_macro_buffer_size().unwrap(), 1024);
}

fn qmk_settings_command(subcommand: u8, qsid: u16) -> [u8; MSG_LEN] {
let mut command = [0u8; MSG_LEN];
command[0] = CMD_VIA_VIAL_PREFIX;
Expand Down
23 changes: 23 additions & 0 deletions src/hid_macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,27 @@ impl HidDevice {
pub fn encode_macros(macros: &[Vec<u8>], buf_size: u16) -> Vec<u8> {
encode_macro_buffer(macros, buf_size)
}

/// Number of bytes required to write all macro entries without truncation.
pub fn encoded_macros_len(macros: &[Vec<u8>]) -> usize {
macros
.iter()
.map(|macro_bytes| macro_bytes.len() + 1)
.sum::<usize>()
.max(1)
}
}

#[cfg(test)]
mod tests {
use super::HidDevice;

#[test]
fn encoded_macros_len_includes_each_terminator() {
assert_eq!(
HidDevice::encoded_macros_len(&[b"a".to_vec(), b"bc".to_vec()]),
5
);
assert_eq!(HidDevice::encoded_macros_len(&[]), 1);
}
}
5 changes: 5 additions & 0 deletions src/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,8 @@ fn static_catalog_key(text: &str) -> Option<&'static str> {
"Move down" => Some("macro_editor.move_down"),
"Text" => Some("macro_editor.text"),
"Types text characters one by one" => Some("macro_editor.types_text_characters_one_by_one"),
"Unicode" => Some("macro_editor.host_text"),
"Types Unicode text through Entropy; Entropy and its Universal Symbols backend must be running" => Some("macro_editor.host_text_requires_entropy_running"),
"Tap" => Some("macro_editor.tap"),
"Press and release a key" => Some("macro_editor.press_and_release_a_key"),
"Down" => Some("macro_editor.down"),
Expand All @@ -697,13 +699,16 @@ fn static_catalog_key(text: &str) -> Option<&'static str> {
"Wait before next action" => Some("macro_editor.wait_before_next_action"),
"Type text here" => Some("macro_editor.type_text_here"),
"Characters to type when this macro runs" => Some("macro_editor.characters_to_type_when_this_macro_runs"),
"Type Unicode text here" => Some("macro_editor.type_unicode_text_here"),
"Unicode text requires Entropy and its Universal Symbols backend to be running" => Some("macro_editor.unicode_text_requires_entropy_running"),
"Click to change key — press and release this key" => Some("macro_editor.click_to_change_key_press_and_release_this_key"),
"Click to change key — holds down until Up" => Some("macro_editor.click_to_change_key_holds_down_until_up"),
"Click to change key — releases this key" => Some("macro_editor.click_to_change_key_releases_this_key"),
"Delay is in milliseconds" => Some("macro_editor.delay_is_in_milliseconds"),
"Remove this action" => Some("macro_editor.remove_this_action"),
"+ Text" => Some("macro_editor.plus_text"),
"Type characters" => Some("macro_editor.type_characters"),
"+ Unicode" => Some("macro_editor.plus_host_text"),
"+ Tap" => Some("macro_editor.plus_tap"),
"+ Down" => Some("macro_editor.plus_down"),
"Hold a key" => Some("macro_editor.hold_a_key"),
Expand Down
Loading
Loading