Skip to content

Instantly share code, notes, and snippets.

@ElishaAz
Last active January 28, 2026 11:25
Show Gist options
  • Select an option

  • Save ElishaAz/0ca47ab0f7dc210b9e071eddfd022760 to your computer and use it in GitHub Desktop.

Select an option

Save ElishaAz/0ca47ab0f7dc210b9e071eddfd022760 to your computer and use it in GitHub Desktop.
Add layout-remaping to Helix (as in https://github.com/helix-editor/helix/pull/5046 )
diff -ura helix-25.07.1/helix-term/src/application.rs helix-25.07.1-patched/helix-term/src/application.rs
--- helix-25.07.1/helix-term/src/application.rs 2025-07-18 17:31:37.000000000 +0300
+++ helix-25.07.1-patched/helix-term/src/application.rs 2026-01-28 11:22:10.647758731 +0200
@@ -25,7 +25,7 @@
config::Config,
handlers,
job::Jobs,
- keymap::Keymaps,
+ keymap::{Keymaps, LayoutRemap},
ui::{self, overlay::overlaid},
};
@@ -128,7 +128,13 @@
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.keys
}));
- let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys)));
+ let layout_remap = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
+ &config.editor.layout_remap
+ }));
+ let editor_view = Box::new(ui::EditorView::new(
+ Keymaps::new(keys),
+ LayoutRemap::new(layout_remap),
+ ));
compositor.push(editor_view);
if args.load_tutor {
diff -ura helix-25.07.1/helix-term/src/keymap.rs helix-25.07.1-patched/helix-term/src/keymap.rs
--- helix-25.07.1/helix-term/src/keymap.rs 2025-07-18 17:31:37.000000000 +0300
+++ helix-25.07.1-patched/helix-term/src/keymap.rs 2026-01-28 11:41:45.332191489 +0200
@@ -398,6 +398,35 @@
}
}
+pub struct LayoutRemap {
+ pub map: Box<dyn DynAccess<HashMap<char, char>>>,
+}
+
+impl LayoutRemap {
+ pub fn new(map: Box<dyn DynAccess<HashMap<char, char>>>) -> Self {
+ Self { map }
+ }
+
+ pub fn translate(&self, event: &KeyEvent) -> KeyEvent {
+ use helix_view::keyboard::KeyCode;
+ let remap = &*self.map.load();
+ if let KeyCode::Char(c) = event.code {
+ KeyEvent {
+ code: KeyCode::Char(*remap.get(&c).unwrap_or(&c)),
+ modifiers: event.modifiers,
+ }
+ } else {
+ *event
+ }
+ }
+}
+
+impl Default for LayoutRemap {
+ fn default() -> Self {
+ Self::new(Box::new(ArcSwap::new(Arc::new(HashMap::new()))))
+ }
+}
+
#[cfg(test)]
mod tests {
use super::macros::keymap;
diff -ura helix-25.07.1/helix-term/src/ui/editor.rs helix-25.07.1-patched/helix-term/src/ui/editor.rs
--- helix-25.07.1/helix-term/src/ui/editor.rs 2025-07-18 17:31:37.000000000 +0300
+++ helix-25.07.1-patched/helix-term/src/ui/editor.rs 2026-01-28 11:30:06.741060302 +0200
@@ -4,7 +4,7 @@
events::{OnModeSwitch, PostCommand},
handlers::completion::CompletionItem,
key,
- keymap::{KeymapResult, Keymaps},
+ keymap::{KeymapResult, Keymaps, LayoutRemap},
ui::{
document::{render_document, LinePos, TextRenderer},
statusline,
@@ -37,6 +37,7 @@
pub struct EditorView {
pub keymaps: Keymaps,
+ layout_remap: LayoutRemap,
on_next_key: Option<(OnKeyCallback, OnKeyCallbackKind)>,
pseudo_pending: Vec<KeyEvent>,
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
@@ -58,9 +59,10 @@
}
impl EditorView {
- pub fn new(keymaps: Keymaps) -> Self {
+ pub fn new(keymaps: Keymaps, layout_remap: LayoutRemap) -> Self {
Self {
keymaps,
+ layout_remap,
on_next_key: None,
pseudo_pending: Vec::new(),
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
@@ -837,6 +839,12 @@
) -> Option<KeymapResult> {
let mut last_mode = mode;
self.pseudo_pending.extend(self.keymaps.pending());
+ // Translate Normal and Select mode keys using layout remap table.
+ let event = if !matches!(last_mode, Mode::Insert) {
+ self.layout_remap.translate(&event)
+ } else {
+ event
+ };
let key_result = self.keymaps.get(mode, event);
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
diff -ura helix-25.07.1/helix-view/src/editor.rs helix-25.07.1-patched/helix-view/src/editor.rs
--- helix-25.07.1/helix-view/src/editor.rs 2025-07-18 17:31:37.000000000 +0300
+++ helix-25.07.1-patched/helix-view/src/editor.rs 2026-01-28 12:59:50.122962752 +0200
@@ -84,6 +84,40 @@
)
}
+#[derive(Serialize, Deserialize, Default, Eq, PartialEq)]
+#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
+struct LayoutMap {
+ from: String,
+ into: String,
+}
+
+fn deserialize_layout_remap<'de, D>(deserializer: D) -> Result<HashMap<char, char>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ // Make (from, to) char pairs from `Vec` of `LayoutMap`s filtering
+ // cases where `from == to` which we don't need to translate.
+ let pairs = Vec::<LayoutMap>::deserialize(deserializer)?
+ .iter()
+ .flat_map(|layout_map| layout_map.from.chars().zip(layout_map.into.chars()))
+ .filter(|(from, to)| from != to)
+ .collect::<HashMap<char, char>>();
+ Ok(pairs)
+}
+
+fn serialize_layout_remap<S>(map: &HashMap<char, char>, serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ let mut from = String::new();
+ let mut into = String::new();
+ map.iter().for_each(|(f, t)| {
+ from.push(*f);
+ into.push(*t);
+ });
+ vec![LayoutMap { from, into }].serialize(serializer)
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct GutterConfig {
@@ -341,6 +375,12 @@
pub soft_wrap: SoftWrap,
/// Workspace specific lsp ceiling dirs
pub workspace_lsp_roots: Vec<PathBuf>,
+ /// Translate pressed keys in Normal and Select modes when a keyboard layout different from Latin is chosen in the system.
+ #[serde(
+ serialize_with = "serialize_layout_remap",
+ deserialize_with = "deserialize_layout_remap"
+ )]
+ pub layout_remap: HashMap<char, char>,
/// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
pub default_line_ending: LineEndingConfig,
/// Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true`.
@@ -1022,6 +1062,7 @@
completion_replace: false,
continue_comments: true,
workspace_lsp_roots: Vec::new(),
+ layout_remap: HashMap::new(),
default_line_ending: LineEndingConfig::default(),
insert_final_newline: true,
atomic_save: true,
diff -ura helix-25.07.1/helix-view/src/input.rs helix-25.07.1-patched/helix-view/src/input.rs
--- helix-25.07.1/helix-view/src/input.rs 2025-07-18 17:31:37.000000000 +0300
+++ helix-25.07.1-patched/helix-view/src/input.rs 2026-01-28 11:33:39.963232949 +0200
@@ -77,6 +77,24 @@
}
}
+ /// Translates a `KeyCode` using the `f` function. Only
+ /// `KeyCode::Char(c)` are translated. This method should be used
+ /// whenever a translation for the characters is desired, e.g. when
+ /// remapping a keyboard layout from non-Latin to Latin.
+ pub fn translate<F>(&self, f: F) -> KeyEvent
+ where
+ F: Fn(char) -> char,
+ {
+ if let KeyCode::Char(c) = self.code {
+ KeyEvent {
+ code: KeyCode::Char(f(c)),
+ modifiers: self.modifiers,
+ }
+ } else {
+ *self
+ }
+ }
+
/// Format the key in such a way that a concatenated sequence
/// of keys can be read easily.
///
#!/bin/sh
git reset --hard HEAD
git pull
# # Rename to `helix-custom` (needs more changes)
# sed -i -e 's/pkgname=helix/pkgname=helix-custom/' PKGBUILD
# mv helix.install helix-custom.install
# sed -i '/prepare() {/a mv "helix-$pkgver" "$pkgname-$pkgver" || true' PKGBUILD
# sed -i -e 's@"contrib/$pkgname.png"@"contrib/helix.png"@' PKGBUILD
sed -i -e 's/source=(/source=("layout-remap.patch" /' PKGBUILD
sed -i -e 's/b2sums=(/b2sums=("eea5ee0bc3e921ced0feb7bb0c80e5f4fda050ddb04e4637f371fbc293289c8dd62c200e5990cb502d974005e2bdeaa4eaed936d854ac3e519f4402b304ea943" /' PKGBUILD
sed -i '/ cargo fetch --locked --target "$(rustc --print host-tuple)"/i patch -p1 -i ..\/layout-remap.patch' PKGBUILD
# Skip checks
sed -i -e 's/cargo test --frozen/#cargo test --frozen/' PKGBUILD
makepkg -s
@ElishaAz

Copy link
Copy Markdown
Author

Clone https://gitlab.archlinux.org/archlinux/packaging/packages/helix.git, place layout-remap.patch and update.sh inside, and run update.sh.

This will build helix with the layout-remap patch. Install it with makepkg -i.

Don't forget to add helix to pacman's IgnorePkg so it wouldn't replace it automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment