Last active
January 28, 2026 11:25
-
-
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 )
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | |
| /// |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Clone https://gitlab.archlinux.org/archlinux/packaging/packages/helix.git, place
layout-remap.patchandupdate.shinside, and runupdate.sh.This will build helix with the layout-remap patch. Install it with
makepkg -i.Don't forget to add
helixto pacman'sIgnorePkgso it wouldn't replace it automatically.