wubi
This guide will walk you through setting up the modern Fcitx5 input method framework with Wubi (五笔) tables. It is fully optimized for Arch Linux running Hyprland using the modern Lua configuration API layout.
Prerequisites
Ensure your system packages are up to date before starting:
sudo pacman -SyuInstall fonts (optional):
sudo pacman -S adobe-source-han-sans-cn-fonts adobe-source-han-serif-cn-fonts wqy-microhei wqy-zenhei noto-fonts-cjk noto-fonts-emojiStep 1: Install Framework & Wubi Tables
Install the Fcitx5 framework, the core Chinese component addons (which include Wubi), and the graphical configuration tool. Run this command in your terminal:
sudo pacman -S fcitx5-im fcitx5-chinese-addons fcitx5-configtoolfcitx5-im: Installs the core daemon along with GTK and Qt binding modules.fcitx5-chinese-addons: Provides native engines for Wubi, Pinyin, and Cangjie.fcitx5-configtool: The graphical interface used to manage your input methods.
Step 2: Configure Environment Variables (modules/env.lua)
For Wayland and GUI applications to recognize and forward text input through Fcitx5, you must set system environment variables. Do not use global system files like /etc/profile. Instead, inject them directly via Hyprland's Lua API. Open your ~/.config/hypr/modules/env.lua file and add the following block:
-- Input Method Configurations for Fcitx5
hl.env("GTK_IM_MODULE", "fcitx")
hl.env("QT_IM_MODULE", "fcitx")
hl.env("XMODIFIERS", "@im=fcitx")
hl.env("SDL_IM_MODULE", "fcitx")
hl.env("GLFW_IM_MODULE", "ibus")⚠️ Important Syntax Rules:
- Keep values exactly as
"fcitx". Appending a5(e.g.,fcitx5) will break input functionality across many flatpaks, web browsers, and Electron applications. GLFW_IM_MODULEintentionally requires"ibus"to remain compatible with Fcitx5 downstream.
Step 3: Configure Daemon Autostart (modules/autostart.lua)
Fcitx5 must run as a background service when your graphical environment launches. Open your ~/.config/hypr/modules/autostart.lua file. Wrap the asynchronous execution string inside the hyprland.start event listener:
-- Background Services Initialization
hl.on("hyprland.start", function ()
hl.exec_cmd("fcitx5 -d")
-- Your other autostart modules (like waybar, hyprpaper) go here
end)hl.exec_cmd: Spawns the software completely asynchronously in the background. You do not need to add trailing bash operands like&.-d: Runs Fcitx5 as a background daemon.
Step 4: Add Wubi to Your Engine Array
To apply your environmental variables and trigger the newly declared background service, log out of your system and log back into your desktop environment.
Open your terminal or application runner (like Rofi) and launch the configuration screen:
fcitx5-configtoolUncheck
Only Show Current Languageif it is enabled to expand your visibility search options.Scroll through the left column or type to search for
Wubi(五笔).Highlight
Wubiand click the>arrow button to add it to your active layout list in the right column.Click
Applyin the bottom-right corner to save your preferences and exit the wizard.
Step 5: Test and Switch
- Open an application (e.g., your browser, terminal, or text editor).
- Press
Ctrl+Spaceto trigger the language input method engine. - Type your desired structural Wubi configuration layouts. The interactive context dropdown menu should overlay on top of your cursor screen immediately.
If you are using a status bar module like Waybar or AGS, let me know if you would like help implementing a custom module config to track your active Wubi / English layout state visually!
On my macOS, I use AeroSpace as my window manager, and following is the config file:
# AeroSpace tries to find the custom config in two locations:
# ~/.aerospace.toml
# ${XDG_CONFIG_HOME}/aerospace/aerospace.toml
# (environment variable XDG_CONFIG_HOME fallbacks to ~/.config if the variable
# is not presented)
# Config version for compatibility and deprecations
# Fallback value (if you omit the key): config-version = 1
config-version = 2
# You can use it to add commands that run after AeroSpace startup.
# Available commands : https://nikitabobko.github.io/AeroSpace/commands
# Run SketchyBar and JankyBorders together with AeroSpace.
after-startup-command = [
# 'exec-and-forget sketchybar', # I use macOS original statusbar
'exec-and-forget borders active_color=0xffe1e3e4 inactive_color=0xff494d64 width=2.0 blacklist=Finder',
]
# Start AeroSpace at login
start-at-login = true
# Normalizations. See: https://nikitabobko.github.io/AeroSpace/guide#normalization
enable-normalization-flatten-containers = true
enable-normalization-opposite-orientation-for-nested-containers = true
# See: https://nikitabobko.github.io/AeroSpace/guide#layouts
# The 'accordion-padding' specifies the size of accordion padding
# You can set 0 to disable the padding feature
accordion-padding = 20
# Possible values: tiles|accordion
default-root-container-layout = 'tiles'
# Possible values: horizontal|vertical|auto
# 'auto' means: wide monitor (anything wider than high) gets horizontal orientation,
# tall monitor (anything higher than wide) gets vertical orientation
default-root-container-orientation = 'auto'
# You can effectively turn off macOS "Hide application" (cmd-h) feature by toggling this flag
# Useful if you don't use this macOS feature, but accidentally hit cmd-h or cmd-alt-h key
# Also see: https://nikitabobko.github.io/AeroSpace/goodies#disable-hide-app
automatically-unhide-macos-hidden-apps = false
# List of workspaces that should stay alive even when they contain no windows,
# even when they are invisible.
# This config version is only available since 'config-version = 2'
# Fallback value (if you omit the key): persistent-workspaces = []
# This is the looong menu on your original macOS status bar
persistent-workspaces = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "B", "C", "D", "E", "F", "G", "I", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
# exec-on-workspace-change callback allows to run arbitrary process when focused
# workspace changes. It may be useful for integrating with bars.
# Besides the exec.env-vars, the process has access to the following environment variables:
# AEROSPACE_FOCUSED_WORKSPACE - the workspace user switched to
# AEROSPACE_PREV_WORKSPACE - the workspace user switched from
# Notify Sketchybar about workspace change
exec-on-workspace-change = ['/bin/bash', '-c', 'sketchybar --trigger aerospace_workspace_change FOCUSED_WORKSPACE=$AEROSPACE_FOCUSED_WORKSPACE']
# A callback that runs every time binding mode changes
# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes
# See: https://nikitabobko.github.io/AeroSpace/commands#mode
on-mode-changed = ['exec-and-forget sh -lc "aerospace list-modes --current > /tmp/aerospace_mode_state.txt"', 'exec-and-forget sketchybar --trigger aerospace_mode_changed']
# Drop it from your config, if you don't like this behavior
# See https://nikitabobko.github.io/AeroSpace/guide#on-focus-changed-callbacks
# See https://nikitabobko.github.io/AeroSpace/commands#move-mouse
# Fallback value (if you omit the key): on-focused-monitor-changed = []
on-focused-monitor-changed = ['move-mouse monitor-lazy-center']
on-focus-changed = ['move-mouse window-lazy-center']
# Gaps between windows (inner-*) and between monitor edges (outer-*).
# Possible values:
# - Constant: gaps.outer.top = 8
# - Per monitor: gaps.outer.top = [{ monitor.main = 16 }, { monitor."some-pattern" = 32 }, 24]
# In this example, 24 is a default value when there is no match.
# Monitor pattern is the same as for 'workspace-to-monitor-force-assignment'.
# See:
# https://nikitabobko.github.io/AeroSpace/guide#assign-workspaces-to-monitors
[gaps]
inner.horizontal = 4
inner.vertical = 4
outer.left = 4
outer.bottom = 4
outer.top = 0
outer.right = 4
# Possible values: (qwerty|dvorak|colemak)
# See https://nikitabobko.github.io/AeroSpace/guide#key-mapping
[key-mapping]
preset = 'qwerty'
# 'main' binding mode declaration
# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes
# 'main' binding mode must be always presented
# Fallback value (if you omit the key): mode.main.binding = {}
[mode.main.binding]
alt-enter = 'exec-and-forget open -n /Applications/Ghostty.app'
# A: alt-enter: opens the terminal (Ghostty) ttys000
# B: ctrl-` toggles on/off quick terminal (Ghostty) ttys001
# C: cmd-n: When already in Ghostty, opens terminal (Ghostty) ttys001
# Explain the numbering logic of ttys000/001/002
# See: https://nikitabobko.github.io/AeroSpace/commands#layout
alt-slash = 'layout tiles horizontal vertical'
alt-comma = 'layout accordion horizontal vertical'
# See: https://nikitabobko.github.io/AeroSpace/commands#focus
# Move cursor
alt-h = 'focus left'
# alt-j = 'focus down' # Reserved for NeoVim - moving lines down
# alt-k = 'focus up' # Reserved for NeoVim - moving lines up
alt-l = 'focus right'
# See: https://nikitabobko.github.io/AeroSpace/commands#move
# Swap windows
alt-shift-h = 'move left'
alt-shift-j = 'move down'
alt-shift-k = 'move up'
alt-shift-l = 'move right'
# See: https://nikitabobko.github.io/AeroSpace/commands#resize
alt-minus = 'resize smart -50'
alt-equal = 'resize smart +50'
# See: https://nikitabobko.github.io/AeroSpace/commands#workspace
alt-1 = 'workspace 1'
alt-2 = 'workspace 2'
alt-3 = 'workspace 3'
alt-4 = 'workspace 4'
alt-5 = 'workspace 5'
alt-6 = 'workspace 6'
alt-7 = 'workspace 7'
alt-8 = 'workspace 8'
alt-9 = 'workspace 9'
# Reserved this keybind for NeoVim - insert three lines
# alt-a = 'workspace A'
alt-b = 'workspace B'
alt-c = 'workspace C'
alt-d = 'workspace D'
alt-e = 'workspace E'
alt-f = ['workspace F', '''exec-and-forget osascript -e 'tell application "Figma" to activate'''']
alt-g = 'workspace G'
alt-i = 'workspace I'
alt-m = 'workspace M'
alt-n = 'workspace N'
alt-o = 'workspace O'
alt-p = 'workspace P'
alt-q = 'workspace Q'
alt-r = 'workspace R'
alt-s = 'workspace S'
alt-t = 'workspace T'
alt-u = 'workspace U'
alt-v = 'workspace V'
alt-w = ['workspace W', '''exec-and-forget osascript -e 'tell application "WeChat" to activate'''']
alt-x = 'workspace X'
alt-y = 'workspace Y'
alt-z = 'workspace Z'
# See: https://nikitabobko.github.io/AeroSpace/commands#move-node-to-workspace
alt-shift-1 = 'move-node-to-workspace 1'
alt-shift-2 = 'move-node-to-workspace 2'
alt-shift-3 = 'move-node-to-workspace 3'
alt-shift-4 = 'move-node-to-workspace 4'
alt-shift-5 = 'move-node-to-workspace 5'
alt-shift-6 = 'move-node-to-workspace 6'
alt-shift-7 = 'move-node-to-workspace 7'
alt-shift-8 = 'move-node-to-workspace 8'
alt-shift-9 = 'move-node-to-workspace 9'
# alt-shift-a = 'move-node-to-workspace A'
alt-shift-b = 'move-node-to-workspace B'
alt-shift-c = 'move-node-to-workspace C'
alt-shift-d = 'move-node-to-workspace D'
alt-shift-e = 'move-node-to-workspace E'
alt-shift-f = 'move-node-to-workspace F'
alt-shift-g = 'move-node-to-workspace G'
alt-shift-i = 'move-node-to-workspace I'
alt-shift-m = 'move-node-to-workspace M'
alt-shift-n = 'move-node-to-workspace N'
alt-shift-o = 'move-node-to-workspace O'
alt-shift-p = 'move-node-to-workspace P'
alt-shift-q = 'move-node-to-workspace Q'
alt-shift-r = 'move-node-to-workspace R'
alt-shift-s = 'move-node-to-workspace S'
alt-shift-t = 'move-node-to-workspace T'
alt-shift-u = 'move-node-to-workspace U'
alt-shift-v = 'move-node-to-workspace V'
alt-shift-w = 'move-node-to-workspace W'
alt-shift-x = 'move-node-to-workspace X'
alt-shift-y = 'move-node-to-workspace Y'
alt-shift-z = 'move-node-to-workspace Z'
# See: https://nikitabobko.github.io/AeroSpace/commands#workspace-back-and-forth
alt-tab = 'workspace-back-and-forth'
# See: https://nikitabobko.github.io/AeroSpace/commands#move-workspace-to-monitor
alt-shift-tab = 'move-workspace-to-monitor --wrap-around next'
# See: https://nikitabobko.github.io/AeroSpace/commands#mode
alt-shift-semicolon = 'mode service'
# 'service' binding mode declaration.
# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes
[mode.service.binding]
esc = ['reload-config', 'mode main']
r = ['flatten-workspace-tree', 'mode main'] # reset all tiles at the same width
f = ['layout floating tiling', 'mode main'] # Toggle between floating and tiling layout
backspace = ['close-all-windows-but-current', 'mode main']
# sticky is not yet supported https://github.com/nikitabobko/AeroSpace/issues/2
#s = ['layout sticky tiling', 'mode main']
alt-shift-h = ['join-with left', 'mode main']
alt-shift-j = ['join-with down', 'mode main']
alt-shift-k = ['join-with up', 'mode main']
alt-shift-l = ['join-with right', 'mode main']
# Terminal
[[on-window-detected]]
if.app-id = 'net.kovidgoyal.kitty'
run = 'move-node-to-workspace T'
[[on-window-detected]]
if.app-id = 'com.googlecode.iterm2'
run = 'move-node-to-workspace T'
[[on-window-detected]]
if.app-id = 'com.mitchellh.ghostty'
run = 'move-node-to-workspace T'
# Browser
[[on-window-detected]]
if.app-id = 'com.brave.Browser'
run = 'move-node-to-workspace B'
[[on-window-detected]]
if.app-id = 'com.google.Chrome'
run = 'move-node-to-workspace G'
[[on-window-detected]]
if.app-id = 'com.apple.Safari'
run = 'move-node-to-workspace S'
# Floating
[[on-window-detected]]
if.app-id = 'com.figma.Desktop'
run = 'move-node-to-workspace F'
[[on-window-detected]]
if.app-id = 'com.apple.finder'
run = ['layout floating', 'move-node-to-workspace F']
# Mail
[[on-window-detected]]
if.app-id = 'com.microsoft.Outlook'
run = 'move-node-to-workspace M'
[[on-window-detected]]
if.app-id = 'com.apple.mail'
run = 'move-node-to-workspace M'
# Can't see the app UI? So disabled it
# [[on-window-detected]]
# if.window-title-regex-substring = "Fonts"
# run = ['layout floating']
# After disabled Skechybar, `System Setting` is also on floating layout
# without any explicit configuration
# Is this a default setting?
# vim: ft=tomlFrom what you described above about the special workspace (scratchpad) on Arch Linux, can I say it is a very similar concept of the letter-named workspaces on macOS/AeroSpace? If that is the case, help me define a couple of special workspaces with letter-named, so that I can paste into my Lua file. Lastly, is there a possible means for the user to real-time view the special workspaces that they are currently on?
Special Workspace vs AeroSpace Letter-Named Workspaces
Not exactly the same! There's an important distinction:
AeroSpace letter-named workspaces (B, C, D, etc.)
- Persistent workspaces - Always exist, always visible in workspace indicators
- Normal workspaces - Just like numbered ones, but with letter names
- Always accessible - You can switch to them like any other workspace
Hyprland Special Workspaces (scratchpads)
- Hidden by default - Don't appear in workspace indicators
- Toggle behavior - Same key shows/hides
- Overlay mode - Appears ON TOP of your current workspace, doesn't switch workspaces
- Temporary feeling - Feels like a dropdown/popup
Better AeroSpace analogy: Special workspaces are like AeroSpace's floating windows + toggle or a dropdown terminal, not letter-named workspaces.
Letter-Named Workspaces in Hyprland (Non-special)
If you want true letter-named workspaces like AeroSpace, you can just use regular workspaces:
-- Switch to letter-named workspaces (AeroSpace style)
hl.bind(mainMod .. " + G", hl.dsp.focus({ workspace = "name:G" })) -- Git/Code
hl.bind(mainMod .. " + M", hl.dsp.focus({ workspace = "name:M" })) -- Music/Mail
hl.bind(mainMod .. " + W", hl.dsp.focus({ workspace = "name:W" })) -- WeChat/WhatsApp
hl.bind(mainMod .. " + Z", hl.dsp.focus({ workspace = "name:Z" })) -- Zen
-- Move windows to letter-named workspaces
hl.bind(secondMod .. " + G", hl.dsp.window.move({ workspace = "name:G" }))
hl.bind(secondMod .. " + M", hl.dsp.window.move({ workspace = "name:M" }))
hl.bind(secondMod .. " + W", hl.dsp.window.move({ workspace = "name:W" }))
hl.bind(secondMod .. " + Z", hl.dsp.window.move({ workspace = "name:Z" }))Auto-move apps to specific workspaces (like your AeroSpace config)
-- Terminal apps to workspace T
hl.on("window", function(win)
if win.class == "kitty" or win.class == "Ghostty" then
hl.dsp.window.move({ workspace = "name:T" })
elseif win.class == "Brave-browser" then
hl.dsp.window.move({ workspace = "name:B" })
elseif win.class == "Google-chrome" then
hl.dsp.window.move({ workspace = "name:G" })
elseif win.class == "Figma" then
hl.dsp.window.move({ workspace = "name:F" })
elseif win.class == "WeChat" then
hl.dsp.window.move({ workspace = "name:W" })
elseif win.class == "Mail" or win.class == "NeoMutt" then
hl.dsp.window.move({ workspace = "name:M" })
end
end)This concept is still under experimental and does not work yet.
Special Workspaces (Scratchpad) for specific apps
If you prefer the scratchpad behavior for certain apps:
-- Create special workspaces for different purposes
local special_workspaces = {
terminal = "special:term",
calculator = "special:calc",
music = "special:music",
messaging = "special:chat"
}
-- Toggle keybinds for special workspaces
hl.bind(mainMod .. " + grave", hl.dsp.workspace.toggle_special("term")) -- ` key for terminal scratchpad
hl.bind(mainMod .. " + C", hl.dsp.workspace.toggle_special("calc")) -- Calculator scratchpad
hl.bind(mainMod .. " + M", hl.dsp.workspace.toggle_special("music")) -- Music scratchpad
-- Send specific apps to special workspaces automatically
hl.on("window", function(win)
if win.class == "kitty" and win.title:find("scratch") then
hl.dsp.window.move({ workspace = "special:term" })
elseif win.class == "Gnome-calculator" then
hl.dsp.window.move({ workspace = "special:calc" })
elseif win.class == "Spotify" then
hl.dsp.window.move({ workspace = "special:music" })
elseif win.class == "WeChat" or win.class == "Discord" then
hl.dsp.window.move({ workspace = "special:chat" })
end
end)Real-time View of Special Workspaces
Yes! Several ways to see your special workspaces:
1. Waybar module (most common)
Add to your Waybar config (~/.config/waybar/config):
"hyprland/workspaces": {
"format": "{icon}",
"format-icons": {
"1": "",
"2": "",
"special:magic": "龍",
"special:term": "",
"special:calc": "",
"special:music": ""
},
"show-special": true
}2. Eww widget (more customizable)
-- In your Eww config
(deflisten special_workspaces "hyprctl workspaces -j | jq '.[] | select(.name | startswith(\"special\")) | .name'")
(defwidget workspaces_special []
(eventbox :onscroll "scroll" (label :text "${special_workspaces}")))3. CLI command (manual check)
# List all workspaces including special ones
hyprctl workspaces -j | jq '.[] | select(.name | contains("special"))'
# Or watch in real-time
watch -n 1 'hyprctl workspaces -j | jq ".[] | select(.name | startswith(\"special\")) | {name, windows}"'4. Custom script for any bar
Create ~/.config/hypr/scripts/special-workspaces.sh:
#!/bin/bash
while true; do
specials=$(hyprctl workspaces -j | jq -r '.[] | select(.name | startswith("special")) | .name' | tr '\n' ' ')
echo "Special: [$specials]"
sleep 1
done5. Hyprland notification (shows when toggled)
-- Show notification when toggling special workspace
hl.bind(mainMod .. " + S", function()
hl.dsp.workspace.toggle_special("magic")
hl.exec_cmd('notify-send "Scratchpad" "Toggled special workspace"')
end)Recommendation for you
Since you're coming from AeroSpace with letter workspaces, I'd suggest:
- Use regular letter-named workspaces (not special) for main apps - more similar to AeroSpace
- Use special workspaces ONLY for temporary/dropdown things like:
- Quick terminal (toggle with `)
- Calculator
- Music controls
- Clipboard history
This hybrid approach gives you the best of both worlds!
