Replies: 2 comments
-
Thanks for the post! These are great points.
No, this is a fair question. I don't have a compelling reason to say that forward select word is better, just that this is what fit into my workflow. I tend to navigate around when editing, and will first move to the beginning of where I want to begin a selection, then use the (forward) Select Word macro to select what I want. You may well be right that backward select word is better (or perhaps a pair of macros for both forward and backward?).
Again, no compelling reason. I wrote Select Word before OS detection was part of QMK. It would be a nice update to make use of it. I don't know when I'll get back to this, but I would like to make updates to Select Word to incorporate the enhancements you suggest here. Thanks again for the suggestions. |
Beta Was this translation helpful? Give feedback.
-
Hi @getreuer ! Thank you for the kind reply! I went forward with my proposition in the meantime. This is the solution I went with, with my constraints. If you have any feedback, I'd be happy to read about it! [Personal] ConstraintsMy main constraint is that I have 2 keyboards running QMK. One being older than the other and having 10 times less memory: so I need to be really careful as to what I implement in it. At the same time, I don't want to code everything twice, so all my codebase is in my At the same time, I don't like big functions and huge files, so I try to work around them, or break them, as much as it makes sense to me. Finally, I'm neither a C developer nor a native English speaker. I'm even not a programer anymore for the past 3 years, so please bear with me while reading through my words and loc 😅 My ImplementationActions / Shortcuts
// mykeyboard.h
// CKC is to be used in your keymap, if you want to use the action determined below CKC stands for: [my_]Custom_KeyCode
#ifndef CKC
# define CKC(x) (SAFE_RANGE + x) // generate custom keycode from enum
#endif // CKC
# define NEW_SAFE_RANGE SAFE_RANGE + _LAST_SHORTCUT_ID
enum custom_keycodes {
LAYER_LOCK = NEW_SAFE_RANGE, // or whatever custom keycode you wanna use
// ...
}; // shortcuts/shortcuts.h
typedef enum {
ALFRED = _FIRST_SHORTCUT_ID,
LINE_JUMPL,
LINE_JUMPR,
LINE_SELECTL,
LINE_SELECTR,
WORD_JUMPL,
WORD_JUMPR,
WORD_SELECTL,
WORD_SELECTR,
// ...
_LAST_SHORTCUT_ID
} shortcuts_id_e;
// clang-format on // shortcuts/shortcuts.c
// L for Left, R for Right
const action_s actions[] = {
[LINE_JUMPL] = {.on_macOS = SS_LCMD(SS_TAP(X_LEFT)), .on_winOS = SS_TAP(X_HOME)},
[LINE_JUMPR] = {.on_macOS = SS_LCMD(SS_TAP(X_RIGHT)), .on_winOS = SS_TAP(X_END)},
[LINE_SELECTL] = {.on_macOS = SS_LCMD(SS_LSFT(SS_TAP(X_LEFT))), .on_winOS = SS_LSFT(SS_TAP(X_HOME))},
[LINE_SELECTR] = {.on_macOS = SS_LCMD(SS_LSFT(SS_TAP(X_RIGHT))), .on_winOS = SS_LSFT(SS_TAP(X_END))},
[WORD_JUMPL] = {.on_macOS = SS_LALT(SS_TAP(X_LEFT)), .on_winOS = SS_LCTL(SS_TAP(X_LEFT))},
[WORD_JUMPR] = {.on_macOS = SS_LALT(SS_TAP(X_RIGHT)), .on_winOS = SS_LCTL(SS_TAP(X_RIGHT))},
[WORD_SELECTL] = {.on_macOS = SS_LALT(SS_LSFT(SS_TAP(X_LEFT))), .on_winOS = SS_LSFT(SS_TAP(X_LEFT))},
[WORD_SELECTR] = {.on_macOS = SS_LALT(SS_LSFT(SS_TAP(X_RIGHT))), .on_winOS = SS_LSFT(SS_TAP(X_RIGHT))},
};
static_assert(ARRAY_SIZE(actions) == _LAST_SHORTCUT_ID, "Mismatch"); // ensure that we have all the actions from the enum from the header file
void send_action(shortcuts_id_e id) {
send_string(detected_host_os() == OS_MACOS ? actions[id].on_macOS : actions[id].on_winOS);
};
bool send_shortcut(shortcuts_id_e id, keyrecord_t *record) {
assert(id < _LAST_SHORTCUT_ID);
switch (id) {
// we ignore the APP_* here, but it's to do the ALT-TABing
// case APP_NEXT:
// case APP_PREV:
// process_tabbing(id, record);
// return false;
default: // clang-format off
if (!record->event.pressed) { return true; } // clang-format on
send_action(id);
return false;
}
return true;
};
bool process_shortcuts(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case CKC(_FIRST_SHORTCUT_ID)... CKC(_LAST_SHORTCUT_ID) - 1:
return send_shortcut(keycode - CKC(_FIRST_SHORTCUT_ID), record);
}
// alt tab is a special shortcuts and we want to unregister the ALT / CMD
// if the feature is active and the layer-key is released
// if (alt_tab_state.active && !record->event.pressed) {
// unregister_code(CMD_OR_ALT);
// alt_tab_state.active = false;
// }
return true;
};
Selection
// selection/selection.h
typedef enum {
SELECTION_FORWARD = 0,
SELECTION_BACKWARD,
} direction_e;
typedef enum {
SELECTION_WORD = 0,
SELECTION_LINE,
SELECTION_WHOLE_LINE,
} type_e;
typedef struct {
bool is_active;
direction_e direction;
type_e type;
deferred_token token;
uint8_t rep_count;
} selection_state_s;
void td_process_selection_backward(tap_dance_state_t *td_state, void *user_data);
void td_process_selection_forward(tap_dance_state_t *td_state, void *user_data);
bool process_selection(uint16_t keycode, keyrecord_t *record);
void select_current_line(direction_e direction);
void select_current_word(direction_e direction); // selection/selection.c
#include "helpers/helpers.h" // some helper functions, like "is_shifted()"
#include "selection.h"
#include "shortcuts/shortcuts.h"
// I want to jump faster, 'cause I'm not patient and the default is tooooo slow.
static const uint8_t INIT_DELAY_MS = 250;
static const uint8_t REP_DELAY_MS[] PROGMEM = {238, 238, 199, 199, 168, 168, 132, 132, 132, 99, 99, 79, 79, 79, 79, 65, 65, 65, 65, 57, 49, 43};
static selection_state_s state = {
.is_active = false,
.direction = SELECTION_FORWARD,
.type = SELECTION_WORD,
.token = INVALID_DEFERRED_TOKEN,
.rep_count = 0,
};
static void move_to_word(direction_e direction) {
direction == SELECTION_FORWARD ? send_action(WORD_JUMPR) : send_action(WORD_JUMPL);
};
static void move_to_next_line(direction_e direction) {
direction == SELECTION_FORWARD ? tap_code16(KC_DOWN) : tap_code16(KC_UP);
};
// I differenciate between whole line (start to end) and just lines (current cursor position)
static void move_to_next_whole_line(direction_e direction) {
if (direction == SELECTION_FORWARD) {
tap_code16(KC_DOWN);
send_action(LINE_JUMPR);
} else {
tap_code16(KC_UP);
send_action(LINE_JUMPL);
}
};
static void select_line(direction_e direction) {
direction == SELECTION_FORWARD ? send_action(LINE_JUMPR) : send_action(LINE_JUMPL);
};
void select_current_line(direction_e direction) {
direction == SELECTION_FORWARD ? send_action(LINE_JUMPL) : send_action(LINE_JUMPR);
register_mods(MOD_BIT(KC_LSFT));
select_line(direction);
};
void select_current_word(direction_e direction) {
direction == SELECTION_FORWARD ? send_action(WORD_JUMPL) : send_action(WORD_JUMPR);
register_mods(MOD_BIT(KC_LSFT));
move_to_word(direction);
};
// requires DEFFERED_TOKEN
uint32_t selection_callback(uint32_t trigger_time, void *cb_arg) {
switch (state.type) {
case SELECTION_WORD:
move_to_word(state.direction);
break;
case SELECTION_LINE:
move_to_next_line(state.direction);
break;
case SELECTION_WHOLE_LINE:
move_to_next_whole_line(state.direction);
break;
default:
break;
}
if (state.rep_count < sizeof(REP_DELAY_MS)) {
++state.rep_count;
}
return pgm_read_byte(REP_DELAY_MS - 1 + state.rep_count);
}
static void process_active_selection(tap_dance_state_t *td_state, direction_e direction) {
td_state_t cur_state = cur_dance(td_state);
state.direction = direction;
if (cur_state == TD_1_TAP || cur_state == TD_2_TAP || cur_state == TD_2_HOLD) { // 1 and 2 Taps are by words selection
state.type = SELECTION_WORD;
move_to_word(direction);
} else if (cur_state == TD_1_HOLD) { // whole line selection
state.type = SELECTION_WHOLE_LINE;
move_to_next_whole_line(direction);
} else if (cur_state == TD_3_TAP || cur_state == TD_3_HOLD) { // line selection (below current cursor position)
state.type = SELECTION_LINE;
move_to_next_line(direction);
} else { // else we don't manage it and deactive the state. Could be that we will ignore it later? TBD
state.is_active = false;
}
// held key trigger a repeat of the selection, each tick faster than the previous.
if (cur_state == TD_1_HOLD || cur_state == TD_2_HOLD || cur_state == TD_3_HOLD) {
state.token = defer_exec(INIT_DELAY_MS, selection_callback, NULL);
}
};
static void start_selection(tap_dance_state_t *td_state, direction_e direction) {
td_state_t cur_state = cur_dance(td_state); // comes with tap-dance. It's basically the default function found in the documentation
state.is_active = true;
state.direction = direction;
send_keyboard_report();
if (cur_state == TD_1_TAP) { // current place to end / beginning of word and be ready to move into direction
state.type = SELECTION_WORD;
register_mods(MOD_BIT(KC_LSFT));
move_to_word(direction);
} else if (cur_state == TD_1_HOLD) { // select whole line and select in direction
state.type = SELECTION_WHOLE_LINE;
select_current_line(direction);
} else if (cur_state == TD_2_TAP || cur_state == TD_2_HOLD) { // select current word and be ready to move in direction
state.type = SELECTION_WORD;
select_current_word(direction);
} else if (cur_state == TD_3_TAP || cur_state == TD_3_HOLD) { // select line from current position and be ready to move
state.type = SELECTION_LINE;
register_mods(MOD_BIT(KC_LSFT));
select_line(direction);
} else {
// else we don't manage it and deactive the state. Could be that we will ignore it later? TBD
state.is_active = false;
}
if (cur_state == TD_1_HOLD || cur_state == TD_2_HOLD || cur_state == TD_3_HOLD) {
state.token = defer_exec(INIT_DELAY_MS, selection_callback, NULL);
}
};
void td_process_selection(tap_dance_state_t *td_state, direction_e direction) {
state.direction = direction;
state.rep_count = 0;
if (state.is_active) {
process_active_selection(td_state, direction);
} else {
disable_shift(); // from helpers/helpers.c -> `del_mods(MOD_MASK_SHIFT); del_oneshot_mods(MOD_MASK_SHIFT);`
start_selection(td_state, direction);
}
};
void td_process_selection_backward(tap_dance_state_t *td_state, void *user_data) {
td_process_selection(td_state, SELECTION_BACKWARD);
};
void td_process_selection_forward(tap_dance_state_t *td_state, void *user_data) {
td_process_selection(td_state, SELECTION_FORWARD);
};
// add this to your `bool process_record_user(uint16_t keycode, keyrecord_t *record)`
bool process_selection(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case TD(TD_SELECTION_BACKWARD):
case TD(TD_SELECTION_FORWARD):
if (!record->event.pressed) {
cancel_deferred_exec(state.token);
}
break;
default:
if (state.is_active && record->event.pressed) {
unregister_mods(MOD_BIT(KC_LSFT));
state.is_active = false;
}
}
return true;
}; Tap DanceThen somewhere, you should have: tap_dance_action_t tap_dance_actions[] = {
// clang-format off
[TD_JUMP_BACKWARD] = ACTION_TAP_DANCE_FN(td_process_jump_backward),
[TD_JUMP_FORWARD] = ACTION_TAP_DANCE_FN(td_process_jump_forward),
[TD_SELECTION_BACKWARD] = ACTION_TAP_DANCE_FN(td_process_selection_backward),
[TD_SELECTION_FORWARD] = ACTION_TAP_DANCE_FN(td_process_selection_forward),
} Jump
// jump_cursor/jump.h
#pragma once
#include "selection/selection.h" // I don't want to re-define the direction. feels like a waste. I could put it in an higher level, I don't know.
typedef struct {
deferred_token token;
uint8_t rep_count;
direction_e direction;
} jump_state_s;
bool process_jump_cursor(uint16_t keycode, keyrecord_t *record);
void td_process_jump_forward(tap_dance_state_t *td_state, void *user_data);
void td_process_jump_backward(tap_dance_state_t *td_state, void *user_data);
void cancel_jump_state_token(void); jump_cursor/jump.c
#include QMK_KEYBOARD_H
#include "<my_keyboard>.h"
#include "shortcuts/shortcuts.h"
#include "selection/selection.h"
#include "jump.h"
jump_state_s jump_state = {
.token = INVALID_DEFERRED_TOKEN,
.rep_count = 0,
.direction = SELECTION_FORWARD,
};
// again: impatience is king
static const uint8_t INIT_DELAY_MS = 250;
static const uint8_t REP_DELAY_MS[] PROGMEM = {238, 238, 238, 199, 199, 199, 168, 168, 168, 134};
direction_e get_jump_state_direction(void) {
return jump_state.direction;
}
void cancel_jump_state_token(void) {
jump_state.token = INVALID_DEFERRED_TOKEN;
}
uint32_t jump_callback(uint32_t trigger_time, void *cb_arg) {
get_jump_state_direction() == SELECTION_FORWARD ? send_action(WORD_JUMPR) : send_action(WORD_JUMPL);
if (jump_state.rep_count < sizeof(REP_DELAY_MS)) {
++jump_state.rep_count;
}
return pgm_read_byte(REP_DELAY_MS - 1 + jump_state.rep_count);
};
static void td_process_jump(tap_dance_state_t *td_state, direction_e direction) {
td_state_t cur_state = cur_dance(td_state);
if (cur_state == TD_1_TAP) {
direction == SELECTION_FORWARD ? send_action(WORD_JUMPR) : send_action(WORD_JUMPL);
} else if (cur_state == TD_1_HOLD) {
direction == SELECTION_FORWARD ? send_action(WORD_JUMPR) : send_action(WORD_JUMPL); // we should move and then start the repeat
jump_state.rep_count = 0;
jump_state.direction = direction;
jump_state.token = defer_exec(INIT_DELAY_MS, jump_callback, NULL);
} else {
direction == SELECTION_FORWARD ? send_action(LINE_JUMPR) : send_action(LINE_JUMPL);
}
};
void td_process_jump_backward(tap_dance_state_t *td_state, void *user_data) {
td_process_jump(td_state, SELECTION_BACKWARD);
};
void td_process_jump_forward(tap_dance_state_t *td_state, void *user_data) {
td_process_jump(td_state, SELECTION_FORWARD);
};
bool process_jump_cursor(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case TD(TD_JUMP_BACKWARD):
case TD(TD_JUMP_FORWARD):
if (!record->event.pressed) {
cancel_deferred_exec(jump_state.token);
cancel_jump_state_token();
}
}
return true;
}; Keymapfinally, keymap.c bool process_record_user(uint16_t keycode, keyrecord_t *record) { // clang-format off
if (!process_selection(keycode, record)) { return false; }
// ...
if (!process_jump_cursor(keycode, record)) { return false; } // clang-format on
}; Done!Ok so, it's the first time I writing so much code in a conversation like that, I don't know what it will look like. Let me know what you think. |
Beta Was this translation helpful? Give feedback.
-
Hello everyone!
First thank you for your blog posts and your publicly available scripts! They really helped me jump start my customization of my QMK keyboard. 🙇
I posted my question in the QMK Discord and filterpaper proposed me to write it here. I hope this is the right category.
I "installed" the Word Selection and I feel a bit off about it: the script selects FORWARD. But most of the time, I am in a situation where I want to select BACKWARD.
So I was considering changing it a bit, and make it so that if
SFT enabled -> move backward
.However shift is already used to "select line".
So I was thinking about going with:
on first "activation", if held = select lines (set status or smtg), otherwise select words ; nxt taps = move forward ; nxt taps + shift = move backwards
Since I'm new here, and probably the dumbest of the bunch: is there a reason not to do it / why it was not done?
Simultaneously, I was wondering if there is a reason why we use a define "MAC_OS" instead of using the built-in OS detection?
Btw for the record: OS detection works for me on both mac and windows. (cf. your blog post)
Thank you for your time and consideration!
Beta Was this translation helpful? Give feedback.
All reactions