Skip to content
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
58 changes: 58 additions & 0 deletions examples/lualine_config.lua
Original file line number Diff line number Diff line change
@@ -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()
},
}
}
166 changes: 155 additions & 11 deletions lua/im_select.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -185,6 +220,48 @@ 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(update_display)
local current = get_current_select(C.default_command)
if current and current ~= "" then
-- Always save the IM context
vim.b.im_select_context = current

-- 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
if update_display ~= false then
local display_name = get_display_name(current)
vim.w.im_select_display_name = display_name
end
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.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)
if current ~= target then
change_im_select(C.default_command, target)
end
end
end

M.setup = function(opts)
if not is_supported() then
return
Expand All @@ -203,19 +280,86 @@ 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 =====
-- 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,
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(false) -- Don't update display name to prevent lualine flicker
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
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 = " .. display_name .. " (" .. cur .. ")")
end, {})

vim.api.nvim_create_user_command("IMUnpinBuffer", function()
vim.b.im_select_pin = false
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

return M
79 changes: 79 additions & 0 deletions lua/im_select/lualine.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
-- 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()
-- 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)
if not display_name or display_name == "" or display_name == "EN" then
return config.color_english
else
return config.color_other
end
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 display_name = get_saved_display_name()
return get_color(display_name)
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