#!/usr/bin/env python3 import tkinter as tk from tkinter import ttk import threading import time from Xlib import X, display from Xlib.ext import shape import re class Tooltip: def __init__(self, widget): self.widget = widget self.tipwindow = None self.id = None self.x = self.y = 0 def showtip(self, text): "Display text in tooltip window" self.text = text if self.tipwindow or not self.text: return x = self.widget.winfo_rootx() + 20 y = self.widget.winfo_rooty() + 20 self.tipwindow = tw = tk.Toplevel(self.widget) tw.wm_overrideredirect(1) tw.wm_geometry("+%d+%d" % (x, y)) label = tk.Label(tw, text=self.text, justify=tk.LEFT, background="#ffffe0", relief=tk.SOLID, borderwidth=1, font=("tahoma", "8", "normal")) label.pack(ipadx=1) def hidetip(self): tw = self.tipwindow self.tipwindow = None if tw: tw.destroy() def CreateTooltip(widget, text): tooltip = Tooltip(widget) def enter(event): tooltip.showtip(text) def leave(event): tooltip.hidetip() widget.bind('', enter) widget.bind('', leave) class WindowSwitcher: def __init__(self): self.root = tk.Tk() self.root.title("Window Switcher") self.root.overrideredirect(True) # No border self.root.attributes('-topmost', True) # Always on top self.root.configure(bg='#2c2c2c') # Set up window style self.setup_ui() # Initialize window list self.current_windows = [] self.buttons = [] # Create X11 connection self.d = display.Display() self.root_win = self.d.screen().root # Enable event listening (with lower permission mask) self.enable_event_listening() # Initial window list self.update_window_list() # Bind drag events self.is_dragging = False self.drag_threshold = 5 # Minimum pixel movement to consider as drag self.start_x = 0 self.start_y = 0 self.drag_start_x = 0 self.drag_start_y = 0 self.bind_drag_events() def setup_ui(self): # Button container self.button_frame = tk.Frame(self.root, bg='#2c2c2c') self.button_frame.pack(fill='both', expand=True, padx=1, pady=1) def enable_event_listening(self): """Enable X11 event listening (with lower permission mask)""" try: # Use PropertyChangeMask and FocusChangeMask self.root_win.change_attributes( event_mask=X.PropertyChangeMask | X.FocusChangeMask ) # Start event listening thread self.event_thread = threading.Thread(target=self.event_loop, daemon=True) self.event_thread.start() except Exception as e: print(f"Cannot set up event listening: {e}") print("Using polling mode as fallback") # If event listening fails, start polling thread self.polling_thread = threading.Thread(target=self.polling_loop, daemon=True) self.polling_thread.start() def bind_drag_events(self): def start_move(event): # Only start dragging if clicked on the background, not on a button if str(event.widget) == str(self.root): self.is_dragging = False # Reset drag state self.start_x = event.x_root self.start_y = event.y_root self.drag_start_x = self.root.winfo_x() self.drag_start_y = self.root.winfo_y() def do_move(event): # Calculate distance moved current_x = event.x_root current_y = event.y_root dx = abs(current_x - self.start_x) dy = abs(current_y - self.start_y) # If movement exceeds threshold, start dragging if not self.is_dragging and (dx > self.drag_threshold or dy > self.drag_threshold): self.is_dragging = True if self.is_dragging: x = self.drag_start_x + (current_x - self.start_x) y = self.drag_start_y + (current_y - self.start_y) self.root.geometry(f"+{x}+{y}") def end_move(event): # After drag is complete, reset dragging state self.is_dragging = False # Bind events to root window self.root.bind("", start_move) self.root.bind("", do_move) self.root.bind("", end_move) def get_current_desktop_windows(self): """Get all windows on current desktop (including minimized windows)""" try: # Get current desktop number current_desktop_prop = self.root_win.get_full_property( self.d.intern_atom("_NET_CURRENT_DESKTOP"), X.AnyPropertyType ) current_desktop = current_desktop_prop.value[0] if current_desktop_prop else 0 # Get current active window active_window_prop = self.root_win.get_full_property( self.d.intern_atom("_NET_ACTIVE_WINDOW"), X.AnyPropertyType ) active_wid = active_window_prop.value[0] if active_window_prop else None # Get all client windows client_list_prop = self.root_win.get_full_property( self.d.intern_atom("_NET_CLIENT_LIST"), X.AnyPropertyType ) if not client_list_prop: return [] window_ids = client_list_prop.value windows = [] for wid in window_ids: try: win = self.d.create_resource_object('window', wid) # Skip current active window if wid == active_wid: continue # Get window type, exclude dock, desktop, etc. wtype_prop = win.get_full_property( self.d.intern_atom("_NET_WM_WINDOW_TYPE"), X.AnyPropertyType ) if wtype_prop: wtype_atoms = wtype_prop.value skip_types = [ self.d.intern_atom("_NET_WM_WINDOW_TYPE_DOCK"), self.d.intern_atom("_NET_WM_WINDOW_TYPE_DESKTOP"), self.d.intern_atom("_NET_WM_WINDOW_TYPE_SPLASH"), self.d.intern_atom("_NET_WM_WINDOW_TYPE_TOOLBAR"), self.d.intern_atom("_NET_WM_WINDOW_TYPE_MENU") ] if any(atom in wtype_atoms for atom in skip_types): continue # Get window's desktop desktop_prop = win.get_full_property( self.d.intern_atom("_NET_WM_DESKTOP"), X.AnyPropertyType ) if desktop_prop and desktop_prop.value[0] != current_desktop: continue # Get window name name_prop = win.get_full_property( self.d.intern_atom("_NET_WM_NAME"), X.AnyPropertyType ) or win.get_full_property( self.d.intern_atom("WM_NAME"), X.AnyPropertyType ) title = name_prop.value.decode() if name_prop and name_prop.value else "Unknown" # Check if window is minimized state_prop = win.get_full_property( self.d.intern_atom("_NET_WM_STATE"), X.AnyPropertyType ) is_minimized = False if state_prop: state_atoms = state_prop.value minimized_atom = self.d.intern_atom("_NET_WM_STATE_HIDDEN") if minimized_atom in state_atoms: is_minimized = True # Show non-active windows (including minimized windows) windows.append((wid, title, is_minimized)) except Exception as e: print(f"Error processing window {wid}: {e}") continue # Return at most 10 windows return windows[:10] except Exception as e: print(f"Error getting window list: {e}") return [] def update_window_list(self): """Update window list""" new_windows = self.get_current_desktop_windows() if new_windows != self.current_windows: self.current_windows = new_windows # Update UI in main thread self.root.after(0, self.refresh_ui) def activate_window(self, wid): """Activate specified window only if not dragging""" # Activate only if not dragging if not self.is_dragging: try: win = self.d.create_resource_object('window', wid) # Build ClientMessage event from Xlib.protocol import event atom = self.d.intern_atom("_NET_ACTIVE_WINDOW") # Create ClientMessage event msg = event.ClientMessage( window=win, client_type=atom, data=(32, [1, X.CurrentTime, 0, 0, 0]) ) # Send activate window message self.root_win.send_event( msg, event_mask=X.SubstructureRedirectMask | X.SubstructureNotifyMask ) self.d.flush() # Bring window to top win.configure(stack_mode=X.Above) self.root.attributes('-topmost', True) # Keep switcher on top except Exception as e: print(f"Error activating window: {e}") def get_display_text(self, title): """Determine display text based on character type""" if not title: return "?" # Check if it contains Chinese characters has_chinese = bool(re.search(r'[\u4e00-\u9fff]', title)) if has_chinese: # If contains Chinese, show first character only return title[0] else: # If only English, show first 2-3 characters, but not exceeding title length return title[:3] def refresh_ui(self): """Refresh UI, update window list""" # Clear old buttons for btn in self.buttons: btn.destroy() self.buttons.clear() # Adjust window size based on number of windows num_windows = len(self.current_windows) if num_windows == 0: # If no windows, hide entire window self.root.withdraw() else: # Calculate window width (each cell 30px, plus borders and spacing) width = num_windows * 30 + (num_windows - 1) * 2 # 30px cell + 2px border self.root.geometry(f"{width}x30") self.root.deiconify() # Ensure window is visible # Create new buttons for wid, title, is_minimized in self.current_windows: # Get text to display display_text = self.get_display_text(title) # Create fixed-size frame container container = tk.Frame(self.button_frame, width=30, height=30, bg='#4a4a4a') container.pack_propagate(False) # Prevent container size from changing with content container.pack(side='left', padx=1, pady=1) # Create button inside container btn = tk.Button( container, text=display_text, command=lambda w=wid: self.activate_window(w), bg='#4a4a4a', fg='#ffffff', activebackground='#5a5a5a', activeforeground='#ffffff', relief='flat', font=('Arial', 10, 'bold'), bd=0 # No border ) # Add tooltip CreateTooltip(btn, f"{title} {'(Minimized)' if is_minimized else ''}") # Fill container with button btn.pack(fill='both', expand=True) self.buttons.append(container) def event_loop(self): """Event listening loop""" # Event mask constants PROPERTY_CHANGE = 28 # X.PropertyChangeMask FOCUS_IN = 9 # X.FocusIn FOCUS_OUT = 10 # X.FocusOut while True: try: event = self.d.next_event() # Check if window list update is needed should_update = False # Compare using event type values if event.type == PROPERTY_CHANGE: # Window property changed, check if it's window state or title change atom_name = self.d.get_atom_name(event.atom) if atom_name in ["_NET_WM_NAME", "WM_NAME", "_NET_WM_STATE", "_NET_ACTIVE_WINDOW", "_NET_CLIENT_LIST"]: should_update = True elif event.type == FOCUS_IN or event.type == FOCUS_OUT: # Focus changed should_update = True if should_update: # Add delay to prevent continuous triggering time.sleep(0.1) self.update_window_list() except Exception as e: print(f"Event loop error: {e}") time.sleep(0.1) # Delay on error def polling_loop(self): """Polling loop (fallback)""" while True: try: self.update_window_list() time.sleep(2) # Poll every 2 seconds except Exception as e: print(f"Polling loop error: {e}") time.sleep(2) def run(self): self.root.mainloop() if __name__ == "__main__": app = WindowSwitcher() app.run()