From 5ec6e414636ce99e0f0842ac6677224232c27834 Mon Sep 17 00:00:00 2001 From: nguyen kim chung <25049282+itshare4u@users.noreply.github.com> Date: Thu, 21 Aug 2025 11:10:17 +0700 Subject: [PATCH 1/7] feat: add per-window/per-buffer input method context management --- lua/im_select.lua | 105 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 11 deletions(-) diff --git a/lua/im_select.lua b/lua/im_select.lua index 670ad5b..e73ba13 100644 --- a/lua/im_select.lua +++ b/lua/im_select.lua @@ -185,6 +185,37 @@ local function restore_previous_im() end end +-- ===== Per-window / Per-buffer addon (stores the "real" IM per context) ===== +-- Priority when restoring: buffer pinned > window > buffer (fallback) +local function save_context_im() + local current = get_current_select(C.default_command) + if current and current ~= "" then + -- always remember last IM for the current WINDOW + vim.w.im_select_context = current + -- if buffer is "pinned", keep its own IM too + if vim.b.im_select_pin then + vim.b.im_select_context = current + end + end +end + +local function restore_context_im() + local target + if vim.b.im_select_pin and vim.b.im_select_context and vim.b.im_select_context ~= "" then + target = vim.b.im_select_context + elseif vim.w.im_select_context and vim.w.im_select_context ~= "" then + target = vim.w.im_select_context + elseif vim.b.im_select_context and vim.b.im_select_context ~= "" then + target = vim.b.im_select_context + end + if target and target ~= "" then + local current = get_current_select(C.default_command) + if current ~= target then + change_im_select(C.default_command, target) + end + end +end + M.setup = function(opts) if not is_supported() then return @@ -203,19 +234,71 @@ M.setup = function(opts) -- set autocmd local group_id = vim.api.nvim_create_augroup("im-select", { clear = true }) - if #C.set_previous_events > 0 then - vim.api.nvim_create_autocmd(C.set_previous_events, { - callback = restore_previous_im, - group = group_id, - }) + -- ===== Per-window / Per-buffer behavior ===== + -- Save the *actual* IM right BEFORE InsertLeave (so we don't accidentally save English) + vim.api.nvim_create_autocmd("InsertLeavePre", { + group = group_id, + callback = save_context_im, + }) + + -- When entering Insert/Cmdline, restore per-context IM then snapshot it again + local function restore_then_save() + -- small defer lets other UI/plugins settle first + vim.defer_fn(function() + restore_context_im() + save_context_im() + end, 25) -- 20–40ms works well end - if #C.set_default_events > 0 then - vim.api.nvim_create_autocmd(C.set_default_events, { - callback = restore_default_im, - group = group_id, - }) - end + vim.api.nvim_create_autocmd({ "InsertEnter", "CmdlineEnter" }, { + group = group_id, + callback = restore_then_save, + }) + + -- If window changes while in Insert, apply the window's IM + vim.api.nvim_create_autocmd("WinEnter", { + group = group_id, + callback = function() + if vim.fn.mode() == "i" then + restore_then_save() + end + end, + }) + + -- ===== Always English in Normal mode ===== + -- Mirror im-select.nvim behavior: set default IM on these "normal-ish" events. + vim.api.nvim_create_autocmd({ "VimEnter", "FocusGained", "InsertLeave", "CmdlineLeave" }, { + group = group_id, + callback = function() + -- Only force when we're actually in Normal; otherwise defer briefly. + if vim.fn.mode() == "n" then + restore_default_im() + else + vim.defer_fn(function() + if vim.fn.mode() == "n" then + restore_default_im() + end + end, 20) + end + end, + }) + + -- User commands to pin/unpin buffer IM + vim.api.nvim_create_user_command("IMPinBuffer", function() + local cur = get_current_select(C.default_command) + if not cur or cur == "" then + vim.notify("[im-select] cannot detect current IM", vim.log.levels.WARN) + return + end + vim.b.im_select_context = cur + vim.b.im_select_pin = true + vim.notify("[im-select] pinned buffer IM = " .. cur) + end, {}) + + vim.api.nvim_create_user_command("IMUnpinBuffer", function() + vim.b.im_select_pin = false + vim.notify("[im-select] buffer unpinned (window IM will be used)") + end, {}) end return M From 78c6ccd7560e26152f1ed9f8509f95c444227ca5 Mon Sep 17 00:00:00 2001 From: nguyen kim chung <25049282+itshare4u@users.noreply.github.com> Date: Thu, 21 Aug 2025 11:40:25 +0700 Subject: [PATCH 2/7] feat: enhance per-window/per-buffer input method context management --- lua/im_select.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lua/im_select.lua b/lua/im_select.lua index e73ba13..2e7592d 100644 --- a/lua/im_select.lua +++ b/lua/im_select.lua @@ -190,11 +190,11 @@ end local function save_context_im() local current = get_current_select(C.default_command) if current and current ~= "" then - -- always remember last IM for the current WINDOW - vim.w.im_select_context = current - -- if buffer is "pinned", keep its own IM too - if vim.b.im_select_pin then - vim.b.im_select_context = current + -- Always remember per-BUFFER IM (so different buffers in the same window can differ) + vim.b.im_select_context = current + -- Keep a per-WINDOW default only if none exists yet (used as fallback for new buffers) + if vim.w.im_select_context == nil or vim.w.im_select_context == "" then + vim.w.im_select_context = current end end end @@ -203,10 +203,10 @@ local function restore_context_im() local target if vim.b.im_select_pin and vim.b.im_select_context and vim.b.im_select_context ~= "" then target = vim.b.im_select_context - elseif vim.w.im_select_context and vim.w.im_select_context ~= "" then - target = vim.w.im_select_context elseif vim.b.im_select_context and vim.b.im_select_context ~= "" then target = vim.b.im_select_context + elseif vim.w.im_select_context and vim.w.im_select_context ~= "" then + target = vim.w.im_select_context end if target and target ~= "" then local current = get_current_select(C.default_command) From 4d14cdb114b58f654e86a9d022b6bf86b5cd6843 Mon Sep 17 00:00:00 2001 From: nguyen kim chung <25049282+itshare4u@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:22:05 +0700 Subject: [PATCH 3/7] feat: add lualine component for input method display - Add lualine component to show IM that will be used in insert mode - Display name is computed and stored by im_select.lua for consistency - Component shows context-aware IM: buffer pinned > buffer > window - Add get_display_name() function to convert IM identifiers to readable names - Store display names in vim.b.im_select_display_name and vim.w.im_select_display_name - Support customizable colors and icons in lualine component - Add comprehensive examples and documentation - Component only displays when context is available (no fallback assumptions) Files: - lua/im_select.lua: Add display name logic and storage - lua/im_select/lualine.lua: Lualine component implementation - examples/lualine_config.lua: Usage examples - README.md: Documentation for lualine integration --- README.md | 67 +++++++++++++++++++++++ examples/lualine_config.lua | 58 ++++++++++++++++++++ lua/im_select.lua | 44 ++++++++++++++- lua/im_select/lualine.lua | 106 ++++++++++++++++++++++++++++++++++++ 4 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 examples/lualine_config.lua create mode 100644 lua/im_select/lualine.lua diff --git a/README.md b/README.md index b31aeee..28862f7 100644 --- a/README.md +++ b/README.md @@ -199,3 +199,70 @@ Options with its default values end, } ``` + +## 5. Lualine Integration + +`im-select.nvim` provides a lualine component to display the input method that will be used when entering insert mode. This helps you know which language you'll be typing before actually entering insert mode. + +### 5.1 Basic Usage + +```lua +require('lualine').setup { + sections = { + lualine_x = { + 'encoding', + 'fileformat', + 'filetype', + require('im_select.lualine').get_component() + }, + } +} +``` + +### 5.2 Custom Configuration + +```lua +require('lualine').setup { + sections = { + lualine_x = { + require('im_select.lualine').get_component({ + -- Custom display names + display_names = { + ["com.apple.keylayout.ABC"] = "πŸ‡ΊπŸ‡Έ EN", + ["com.apple.inputmethod.VietnameseIM.VietnameseSimpleTelex"] = "πŸ‡»πŸ‡³ VI", + ["com.apple.inputmethod.Chinese.Pinyin"] = "πŸ‡¨πŸ‡³ CN", + }, + + -- Colors + color_english = { fg = "#98be65", gui = "bold" }, + color_other = { fg = "#ECBE7B", gui = "bold" }, + + -- Icon settings + icon = "⌨️", + show_icon = true, + + -- Update frequency (ms) + update_interval = 200, + }) + }, + } +} +``` + +### 5.3 Available Options + +- `display_names`: Custom mapping of IM identifiers to display names +- `color_english`: Color for English input method +- `color_other`: Color for non-English input methods +- `icon`: Icon to show before IM name (default: `⌨️`) +- `show_icon`: Whether to show icon (default: `true`) +- `update_interval`: How often to refresh in milliseconds (default: `200`) + +### 5.4 How It Works + +The component displays the input method that will be used when you enter insert mode: + +- **Per-buffer context**: Shows the saved IM for current buffer/window +- **Always visible**: No need to enter insert mode to see which IM will be used +- **Smart fallback**: Shows default English IM if no context is saved +- **Performance optimized**: Uses caching to avoid frequent system calls diff --git a/examples/lualine_config.lua b/examples/lualine_config.lua new file mode 100644 index 0000000..a86ec3d --- /dev/null +++ b/examples/lualine_config.lua @@ -0,0 +1,58 @@ +-- Example configuration for lualine with im-select component + +-- Method 1: Basic usage +require('lualine').setup { + sections = { + lualine_c = { + 'filename', + require('im_select.lualine').get_component() + }, + } +} + +-- Method 2: With custom configuration +require('lualine').setup { + sections = { + lualine_x = { + 'encoding', + 'fileformat', + 'filetype', + require('im_select.lualine').get_component({ + -- Custom display names + display_names = { + ["com.apple.keylayout.ABC"] = "πŸ‡ΊπŸ‡Έ EN", + ["com.apple.inputmethod.VietnameseIM.VietnameseSimpleTelex"] = "πŸ‡»πŸ‡³ VI", + ["com.apple.inputmethod.Chinese.Pinyin"] = "πŸ‡¨πŸ‡³ CN", + }, + + -- Colors + color_english = { fg = "#98be65", gui = "bold" }, + color_other = { fg = "#ECBE7B", gui = "bold" }, + + -- Icon settings + icon = "⌨️", + show_icon = true, + + -- Update frequency (ms) + update_interval = 200, + }) + }, + } +} + +-- Method 3: Separate setup then use +require('im_select.lualine').setup({ + display_names = { + ["com.apple.keylayout.ABC"] = "EN", + ["com.apple.inputmethod.VietnameseIM.VietnameseSimpleTelex"] = "VI", + }, + icon = "⌨️", +}) + +require('lualine').setup { + sections = { + lualine_y = { + require('im_select.lualine').get_component() + }, + } +} diff --git a/lua/im_select.lua b/lua/im_select.lua index 2e7592d..cd19bd1 100644 --- a/lua/im_select.lua +++ b/lua/im_select.lua @@ -6,6 +6,41 @@ local function all_trim(s) return s:match("^%s*(.-)%s*$") end +local function get_display_name(im) + if not im or im == "" then + return "?" + end + + -- For macOS + if im == "com.apple.keylayout.ABC" then + return "EN" + elseif im:match("Vietnamese") then + return "VI" + elseif im:match("Chinese") or im:match("Pinyin") then + return "CN" + elseif im:match("Korean") then + return "KO" + elseif im:match("Japanese") or im:match("Hiragana") or im:match("Katakana") then + return "JP" + end + + -- For Windows + if im == "1033" then + return "EN" + end + + -- For Linux fcitx/ibus + if im == "keyboard-us" or im == "1" or im == "xkb:us::eng" then + return "EN" + elseif im:match("rime") or im:match("pinyin") then + return "CN" + end + + -- Fallback: show last part of identifier + local parts = vim.split(im, "[%.%-_]") + return parts[#parts]:upper():sub(1, 3) +end + local function determine_os() if vim.fn.has("macunix") == 1 then return "macOS" @@ -190,11 +225,16 @@ end local function save_context_im() local current = get_current_select(C.default_command) if current and current ~= "" then + local display_name = get_display_name(current) + -- Always remember per-BUFFER IM (so different buffers in the same window can differ) vim.b.im_select_context = current + vim.b.im_select_display_name = display_name + -- Keep a per-WINDOW default only if none exists yet (used as fallback for new buffers) if vim.w.im_select_context == nil or vim.w.im_select_context == "" then vim.w.im_select_context = current + vim.w.im_select_display_name = display_name end end end @@ -290,9 +330,11 @@ M.setup = function(opts) vim.notify("[im-select] cannot detect current IM", vim.log.levels.WARN) return end + local display_name = get_display_name(cur) vim.b.im_select_context = cur + vim.b.im_select_display_name = display_name vim.b.im_select_pin = true - vim.notify("[im-select] pinned buffer IM = " .. cur) + vim.notify("[im-select] pinned buffer IM = " .. display_name .. " (" .. cur .. ")") end, {}) vim.api.nvim_create_user_command("IMUnpinBuffer", function() diff --git a/lua/im_select/lualine.lua b/lua/im_select/lualine.lua new file mode 100644 index 0000000..b9beda7 --- /dev/null +++ b/lua/im_select/lualine.lua @@ -0,0 +1,106 @@ +-- Lualine component for im-select.nvim +-- Shows input method that will be used in insert mode + +local M = {} + +-- Config for display +local config = { + -- Colors + color_english = { fg = "#98be65" }, -- Green + color_other = { fg = "#ECBE7B" }, -- Yellow + -- Icons + icon = "⌨️", + show_icon = true, +} + +local function get_saved_display_name() + -- Use the same priority logic as im_select.lua: buffer pinned > buffer > window + if vim.b.im_select_pin and vim.b.im_select_display_name and vim.b.im_select_display_name ~= "" then + return vim.b.im_select_display_name + elseif vim.b.im_select_display_name and vim.b.im_select_display_name ~= "" then + return vim.b.im_select_display_name + elseif vim.w.im_select_display_name and vim.w.im_select_display_name ~= "" then + return vim.w.im_select_display_name + end + + return nil +end + +local function get_saved_im_for_color() + -- Get the IM identifier for color determination + if vim.b.im_select_pin and vim.b.im_select_context and vim.b.im_select_context ~= "" then + return vim.b.im_select_context + elseif vim.b.im_select_context and vim.b.im_select_context ~= "" then + return vim.b.im_select_context + elseif vim.w.im_select_context and vim.w.im_select_context ~= "" then + return vim.w.im_select_context + end + + return nil +end + +local function get_color(im) + if not im then + return config.color_english + end + + -- Default English IM identifiers + local english_ims = { + "com.apple.keylayout.ABC", + "1033", + "keyboard-us", + "1", + "xkb:us::eng" + } + + for _, eng_im in ipairs(english_ims) do + if im == eng_im then + return config.color_english + end + end + + return config.color_other +end + +-- Main component function +function M.component() + return { + function() + -- Get the display name that's already computed by im_select + local display_name = get_saved_display_name() + + -- If no context saved, don't show anything + if not display_name or display_name == "" then + return "" + end + + if config.show_icon then + return config.icon .. " " .. display_name + else + return display_name + end + end, + + color = function() + local context_im = get_saved_im_for_color() + return get_color(context_im) + end, + } +end + +-- Setup function to configure the component +function M.setup(opts) + if opts then + config = vim.tbl_deep_extend("force", config, opts) + end +end + +-- Get component for lualine config +function M.get_component(opts) + if opts then + M.setup(opts) + end + return M.component() +end + +return M From 3af6e3b05f00aaa5106a5282c4e8a10b028f37f9 Mon Sep 17 00:00:00 2001 From: nguyen kim chung <25049282+itshare4u@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:07:59 +0700 Subject: [PATCH 4/7] refactor: simplify context retrieval logic in lualine component --- lua/im_select.lua | 6 ++++-- lua/im_select/lualine.lua | 24 ++++-------------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/lua/im_select.lua b/lua/im_select.lua index cd19bd1..5f953c1 100644 --- a/lua/im_select.lua +++ b/lua/im_select.lua @@ -244,7 +244,7 @@ local function restore_context_im() if vim.b.im_select_pin and vim.b.im_select_context and vim.b.im_select_context ~= "" then target = vim.b.im_select_context elseif vim.b.im_select_context and vim.b.im_select_context ~= "" then - target = vim.b.im_select_context + target = vim.b.im_select_context elseif vim.w.im_select_context and vim.w.im_select_context ~= "" then target = vim.w.im_select_context end @@ -339,7 +339,9 @@ M.setup = function(opts) vim.api.nvim_create_user_command("IMUnpinBuffer", function() vim.b.im_select_pin = false - vim.notify("[im-select] buffer unpinned (window IM will be used)") + vim.b.im_select_context = nil + vim.b.im_select_display_name = nil + vim.notify("[im-select] buffer unpinned and context cleared (window IM will be used)") end, {}) end diff --git a/lua/im_select/lualine.lua b/lua/im_select/lualine.lua index b9beda7..1ad5ec0 100644 --- a/lua/im_select/lualine.lua +++ b/lua/im_select/lualine.lua @@ -14,29 +14,13 @@ local config = { } local function get_saved_display_name() - -- Use the same priority logic as im_select.lua: buffer pinned > buffer > window - if vim.b.im_select_pin and vim.b.im_select_display_name and vim.b.im_select_display_name ~= "" then - return vim.b.im_select_display_name - elseif vim.b.im_select_display_name and vim.b.im_select_display_name ~= "" then - return vim.b.im_select_display_name - elseif vim.w.im_select_display_name and vim.w.im_select_display_name ~= "" then - return vim.w.im_select_display_name - end - - return nil + -- Priority: buffer > window + return vim.b.im_select_display_name or vim.w.im_select_display_name end local function get_saved_im_for_color() - -- Get the IM identifier for color determination - if vim.b.im_select_pin and vim.b.im_select_context and vim.b.im_select_context ~= "" then - return vim.b.im_select_context - elseif vim.b.im_select_context and vim.b.im_select_context ~= "" then - return vim.b.im_select_context - elseif vim.w.im_select_context and vim.w.im_select_context ~= "" then - return vim.w.im_select_context - end - - return nil + -- Priority: buffer > window + return vim.b.im_select_context or vim.w.im_select_context end local function get_color(im) From 2eae29b284c9563a8a391351485467e431de0d2c Mon Sep 17 00:00:00 2001 From: nguyen kim chung <25049282+itshare4u@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:22:38 +0700 Subject: [PATCH 5/7] feat: optimize IM context saving to prevent display flicker --- lua/im_select.lua | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lua/im_select.lua b/lua/im_select.lua index 5f953c1..fe630ca 100644 --- a/lua/im_select.lua +++ b/lua/im_select.lua @@ -222,19 +222,25 @@ end -- ===== Per-window / Per-buffer addon (stores the "real" IM per context) ===== -- Priority when restoring: buffer pinned > window > buffer (fallback) -local function save_context_im() +local function save_context_im(update_display) local current = get_current_select(C.default_command) if current and current ~= "" then - local display_name = get_display_name(current) - - -- Always remember per-BUFFER IM (so different buffers in the same window can differ) + -- Always save the IM context vim.b.im_select_context = current - vim.b.im_select_display_name = display_name + + -- Only update display name when requested (default: true for backward compatibility) + if update_display ~= false then + local display_name = get_display_name(current) + vim.b.im_select_display_name = display_name + end -- Keep a per-WINDOW default only if none exists yet (used as fallback for new buffers) if vim.w.im_select_context == nil or vim.w.im_select_context == "" then vim.w.im_select_context = current - vim.w.im_select_display_name = display_name + if update_display ~= false then + local display_name = get_display_name(current) + vim.w.im_select_display_name = display_name + end end end end @@ -286,7 +292,7 @@ M.setup = function(opts) -- small defer lets other UI/plugins settle first vim.defer_fn(function() restore_context_im() - save_context_im() + save_context_im(false) -- Don't update display name to prevent lualine flicker end, 25) -- 20–40ms works well end From 347f95deb54e55b72b7545e8adb92f86396a56d7 Mon Sep 17 00:00:00 2001 From: nguyen kim chung <25049282+itshare4u@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:29:47 +0700 Subject: [PATCH 6/7] refactor: streamline color determination logic in lualine component --- lua/im_select/lualine.lua | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/lua/im_select/lualine.lua b/lua/im_select/lualine.lua index 1ad5ec0..0264abb 100644 --- a/lua/im_select/lualine.lua +++ b/lua/im_select/lualine.lua @@ -18,32 +18,12 @@ local function get_saved_display_name() return vim.b.im_select_display_name or vim.w.im_select_display_name end -local function get_saved_im_for_color() - -- Priority: buffer > window - return vim.b.im_select_context or vim.w.im_select_context -end - -local function get_color(im) - if not im then +local function get_color(display_name) + if not display_name or display_name == "" or display_name == "EN" then return config.color_english + else + return config.color_other end - - -- Default English IM identifiers - local english_ims = { - "com.apple.keylayout.ABC", - "1033", - "keyboard-us", - "1", - "xkb:us::eng" - } - - for _, eng_im in ipairs(english_ims) do - if im == eng_im then - return config.color_english - end - end - - return config.color_other end -- Main component function @@ -66,8 +46,8 @@ function M.component() end, color = function() - local context_im = get_saved_im_for_color() - return get_color(context_im) + local display_name = get_saved_display_name() + return get_color(display_name) end, } end From 49a0bcbda6f2d287bac1906a062dae4769fc44e1 Mon Sep 17 00:00:00 2001 From: nguyen kim chung <25049282+itshare4u@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:33:52 +0700 Subject: [PATCH 7/7] feat: initialize input method context on buffer enter and enhance display name retrieval logic --- lua/im_select.lua | 11 +++++++++++ lua/im_select/lualine.lua | 13 +++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lua/im_select.lua b/lua/im_select.lua index fe630ca..45817d9 100644 --- a/lua/im_select.lua +++ b/lua/im_select.lua @@ -281,6 +281,17 @@ M.setup = function(opts) local group_id = vim.api.nvim_create_augroup("im-select", { clear = true }) -- ===== Per-window / Per-buffer behavior ===== + -- Initialize context for new buffers + vim.api.nvim_create_autocmd("BufEnter", { + group = group_id, + callback = function() + -- Only initialize if no context exists for this buffer + if not vim.b.im_select_display_name then + save_context_im() + end + end, + }) + -- Save the *actual* IM right BEFORE InsertLeave (so we don't accidentally save English) vim.api.nvim_create_autocmd("InsertLeavePre", { group = group_id, diff --git a/lua/im_select/lualine.lua b/lua/im_select/lualine.lua index 0264abb..ac01379 100644 --- a/lua/im_select/lualine.lua +++ b/lua/im_select/lualine.lua @@ -14,8 +14,17 @@ local config = { } local function get_saved_display_name() - -- Priority: buffer > window - return vim.b.im_select_display_name or vim.w.im_select_display_name + -- Priority: buffer > window > fallback to current system IM + local display = vim.b.im_select_display_name or vim.w.im_select_display_name + + -- If no saved context, try to get current IM and convert to display name + if not display then + -- Simple fallback - we can't call main plugin functions here + -- so just assume English as default when no context + return "EN" + end + + return display end local function get_color(display_name)