Replies: 3 comments 2 replies
-
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.
-
@arkanoryn :: I know I'm late to the party, but... For many years I worked in Windows all day and Mac at night and weekends. Learning to use the correct chords for the current OS rewired my brain to adapt quickly to changes from devs, from me, or learning new apps; much like those able to type equally well in different alternate layouts. This made me quick to learn new chords and unlearn them when they change (I made many changes). I encourage you to not lean on the board to help with this. Make the effort and reap the benefits [later]. Just sayin'. Though, many of my chord changes in apps and macOS align chords. For example, cycling through tabs in any tab-enabled app on my Macs is the same: ⌘⌥J/L. Whether I'm in Safari, Finder, VSCode, etc. they all use the same chord. Same idea for history, toggling sidebar, etc. I also repeat chords. For example, ⌘P opens print dialog, but once in the print dialog, the same key press invokes "Save to PDF..." (I rarely ever print to paper). So I get the impulse to change the keeb to make chords easier between OSs. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
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