
Layout Indicator v 1.0 (python)





# 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()
Follow the Vibe
