Layout Indicator: The Fix for Windows’ Annoying Language Switch Glitch

You know the drill…

You’re in the zone. You switch keyboard layouts to type a quick message, slam out a sentence, look up, and see gibberish on the screen. Instead of Spanish, you’ve typed a bunch of random symbols. Instead of French, you’ve got a mess.

This tiny, infuriating problem drove me nuts for years. It breaks your flow, forcing you to stop, delete, and re-type. The standard language indicator in the corner of the screen is useless—you have to consciously look for it, which defeats the whole purpose of typing fast.

I didn’t just want an indicator; I needed a reflex action. Something my brain would see without even trying. So, I decided to build it.

 

From a Simple Idea to a Full-Featured App

The concept, inspired by macOS, was simple: when you switch layouts, a large, semi-transparent overlay should pop up in the center of the screen—EN, ES, FR—and then fade away. You can’t miss it, and after a while, you don’t even have to think about it.

It started as a basic Python script and slowly evolved into a polished, professional tool:

  • System Tray Icon: The app lives quietly in your tray, out of the way.

 

  • Smart Language Detection: The indicator isn’t hard-coded. It automatically scans your system and supports whatever layouts you have installed—Spanish, French, Hebrew, Arabic, Chinese, you name it.
  • Custom Font Size: On a 4K monitor or a small laptop screen? You can scale the indicator’s font size from 25% to 100% to fit your setup.

  • Persistent Settings: The app remembers your font size preference after a restart.
  • Start with Windows: The essential “set it and forget it” feature. The app can launch itself at startup, so it’s always ready.

 

Let’s Talk Security: Open Source & Antivirus Paranoia

Now for the important part. When you install a tool that monitors keystrokes and can add itself to startup, you have every right to be skeptical.

Full Transparency: It’s Open Source I believe in 100% transparency. That’s why the entire source code for this utility is open.

# Filename: layout_indicator.py (Version 1.0.0 - Stable Release)
# Description: A stable system tray utility to display the current keyboard layout.

import tkinter as tk
import ctypes
import threading
from pynput import keyboard
import pystray
from PIL import Image
import os
import sys
import winshell
from pathlib import Path
import json

# --- APP & SETTINGS CONFIGURATION ---
APP_NAME = "LayoutIndicator"
APP_DATA_FOLDER = Path.home() / "AppData" / "Roaming" / APP_NAME
SETTINGS_FILE = APP_DATA_FOLDER / "settings.json"

# --- FONT SIZE SETTINGS ---
BASE_FONT_SIZE = 250
current_font_scale = 1.0
CURRENT_FONT_SIZE = BASE_FONT_SIZE

# --- GENERAL SETTINGS ---
TIMER_DELAY = 0.2
DISPLAY_TIME_MS = 900
WINDOW_BG_COLOR = "#2B2B2B"
FONT_FAMILY = "Arial Black"
COLORS = ['#87CEEB', '#FF6347', '#98FB98', '#FFD700', '#DDA0DD', '#F08080']

# --- AUTO-START SETUP ---
STARTUP_FOLDER = winshell.startup()
SHORTCUT_PATH = os.path.join(STARTUP_FOLDER, f"{APP_NAME}.lnk")

# --- HELPER FUNCTION TO FIND ASSETS ---
def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

# --- CORE LOGIC ---
LAYOUTS = {}
def build_layouts_map():
    global LAYOUTS
    user32 = ctypes.WinDLL('user32', use_last_error=True)
    kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
    num_layouts = user32.GetKeyboardLayoutList(0, None)
    layout_handles = (ctypes.c_void_p * num_layouts)()
    user32.GetKeyboardLayoutList(num_layouts, layout_handles)
    temp_layouts = {}
    color_index = 0
    for handle in layout_handles:
        lcid = handle & 0xFFFF
        layout_hex = f'{lcid:04x}'.upper()
        full_id = '0000' + layout_hex
        lang_name_buffer = ctypes.create_unicode_buffer(3)
        kernel32.GetLocaleInfoW(lcid, 0x00000059, lang_name_buffer, 3)
        temp_layouts[full_id] = {'name': lang_name_buffer.value.upper(), 'color': COLORS[color_index % len(COLORS)]}
        color_index += 1
    LAYOUTS = temp_layouts
    print("Layouts reloaded:", LAYOUTS)

def get_current_layout_info():
    user32 = ctypes.WinDLL('user32', use_last_error=True)
    hwnd = user32.GetForegroundWindow()
    thread_id = user32.GetWindowThreadProcessId(hwnd, 0)
    layout_id = user32.GetKeyboardLayout(thread_id)
    layout_hex = f'{layout_id & 0xFFFF:04x}'.upper()
    full_id = '0000' + layout_hex
    return LAYOUTS.get(full_id, {'name': '??', 'color': '#FFFFFF'})

def show_indicator():
    layout_info = get_current_layout_info()
    text_to_show = layout_info['name']
    text_color = layout_info['color']
    root = tk.Tk()
    root.withdraw()
    indicator = tk.Toplevel(root)
    indicator.attributes("-topmost", True)
    indicator.overrideredirect(True)
    indicator.attributes("-alpha", 0.85)
    indicator.config(bg=WINDOW_BG_COLOR)
    label = tk.Label(indicator, text=text_to_show, font=(FONT_FAMILY, CURRENT_FONT_SIZE, "bold"), fg=text_color, bg=WINDOW_BG_COLOR)
    label.pack(padx=40, pady=20)
    indicator.update_idletasks()
    screen_width = indicator.winfo_screenwidth()
    screen_height = indicator.winfo_screenheight()
    x = (screen_width // 2) - (indicator.winfo_width() // 2)
    y = (screen_height // 2) - (indicator.winfo_height() // 2)
    indicator.geometry(f"+{x}+{y}")
    indicator.after(DISPLAY_TIME_MS, root.destroy)
    root.mainloop()

def start_keyboard_listener():
    SWITCH_COMBINATIONS = [ {keyboard.Key.alt_l, keyboard.Key.shift}, {keyboard.Key.cmd, keyboard.Key.space} ]
    current_keys = set()
    def on_press(key):
        if key in {keyboard.Key.alt_l, keyboard.Key.shift, keyboard.Key.cmd, keyboard.Key.space}:
            current_keys.add(key)
            for combination in SWITCH_COMBINATIONS:
                if all(k in current_keys for k in combination):
                    threading.Timer(TIMER_DELAY, show_indicator).start()
                    break
    def on_release(key):
        if key in current_keys:
            try:
                current_keys.remove(key)
            except KeyError:
                pass
    with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
        listener.join()

# --- SETTINGS MANAGEMENT ---
def save_settings():
    try:
        os.makedirs(APP_DATA_FOLDER, exist_ok=True)
        settings = {'font_scale': current_font_scale}
        with open(SETTINGS_FILE, 'w') as f:
            json.dump(settings, f)
        print(f"Settings saved to {SETTINGS_FILE}")
    except Exception as e:
        print(f"Error saving settings: {e}")

def load_settings():
    global current_font_scale
    try:
        if os.path.exists(SETTINGS_FILE):
            with open(SETTINGS_FILE, 'r') as f:
                settings = json.load(f)
                if 'font_scale' in settings:
                    current_font_scale = settings['font_scale']
            print(f"Settings loaded from {SETTINGS_FILE}")
    except Exception as e:
        print(f"Error loading settings, using defaults: {e}")
    set_font_size(current_font_scale, save=False)

# --- SYSTEM TRAY MENU ACTIONS ---
def set_font_size(scale, save=True):
    global CURRENT_FONT_SIZE, current_font_scale
    current_font_scale = scale
    CURRENT_FONT_SIZE = int(BASE_FONT_SIZE * scale)
    print(f"Font size set to {CURRENT_FONT_SIZE} ({scale*100}%)")
    if save:
        save_settings()

def toggle_autostart():
    if os.path.exists(SHORTCUT_PATH):
        os.remove(SHORTCUT_PATH)
        print("Autostart disabled.")
    else:
        with winshell.shortcut(SHORTCUT_PATH) as shortcut:
            shortcut.path = sys.executable
            shortcut.description = "Keyboard Layout Indicator"
            shortcut.working_directory = str(Path(sys.executable).parent)
        print("Autostart enabled.")

def is_autostart_enabled():
    return os.path.exists(SHORTCUT_PATH)

def reload_layouts(icon):
    build_layouts_map()

def quit_program(icon):
    icon.stop()
    os._exit(0)

# --- MAIN EXECUTION BLOCK ---
if __name__ == "__main__":
    load_settings()
    build_layouts_map()
    listener_thread = threading.Thread(target=start_keyboard_listener, daemon=True)
    listener_thread.start()
    
    try:
        icon_path = resource_path("icon.png")
        image = Image.open(icon_path)
    except FileNotFoundError:
        print("Error: icon.png not found! Make sure it's in the same folder.")
        sys.exit(1)
        
    # Final, simplified menu
    menu = pystray.Menu(
        pystray.MenuItem('Reload Layouts', reload_layouts),
        pystray.MenuItem('Font Size', pystray.Menu(
            pystray.MenuItem('100% (Default)', lambda: set_font_size(1.0), checked=lambda item: current_font_scale == 1.0, radio=True),
            pystray.MenuItem('75%', lambda: set_font_size(0.75), checked=lambda item: current_font_scale == 0.75, radio=True),
            pystray.MenuItem('50%', lambda: set_font_size(0.50), checked=lambda item: current_font_scale == 0.50, radio=True),
            pystray.MenuItem('25%', lambda: set_font_size(0.25), checked=lambda item: current_font_scale == 0.25, radio=True)
        )),
        pystray.MenuItem('Start with Windows', toggle_autostart, checked=lambda item: is_autostart_enabled()),
        pystray.MenuItem('Exit', quit_program)
    )
    
    icon = pystray.Icon("LayoutIndicator", image, "Layout Indicator", menu)
    
    print("Program started and is running in the system tray.")
    icon.run()

 

Anyone, from a curious user to a cybersecurity expert, can review every single line of code. You can verify for yourself that the app does exactly what it says: it watches for Alt+Shift (or your custom hotkey) and shows an overlay. That’s it. No data collection, no network activity, no shady business.

Why Do Some Antiviruses Get Nervous? If you run the app’s .exe through VirusTotal, you’ll see something interesting: 60+ industry giants like Kaspersky, Microsoft, Google, Avast, ESET, and Malwarebytes give it a clean bill of health. But a handful of lesser-known engines flag it. Why?

It’s a classic case of a “false positive.” These engines act like overzealous, rookie security guards. They see a combination of behaviors that, while perfectly legitimate, fit a suspicious pattern:

  1. It’s a compiled .exe: Malware often uses “packers” to hide its code. PyInstaller, which we use, does the same thing for legitimate reasons.
  2. It monitors keyboard input: This is how keylogger trojans steal passwords.
  3. It can add itself to the startup folder: This is how persistent malware works.

A simple algorithm sees these three things and rings the alarm bell without understanding the context. Meanwhile, the pros at the major antivirus labs see that the app only listens for system keys, sends zero data, and only enables autostart when you explicitly tell it to via the menu.

The verdict is clear: trust the 60+ experts, not the few paranoid bots. The VirusTotal report speaks for itself.

The Bottom Line

Layout Indicator is a clean, honest solution to a real-world annoyance. It’s a lightweight utility that stays out of your way and does one job perfectly.

I hope it makes your daily workflow just a little bit smoother.

 

Download Layout Indicator

⚠️ Heads up:
– The .exe is unsigned, so Windows might show a warning.
Click “More info” → “Run anyway” — you’re good.

Follow the Vibe


Leave a Reply

Your email address will not be published. Required fields are marked *

Which device is primarily used to input text?