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:
- It’s a compiled
.exe
: Malware often uses “packers” to hide its code. PyInstaller, which we use, does the same thing for legitimate reasons. - It monitors keyboard input: This is how keylogger trojans steal passwords.
- 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.
Leave a Reply