@@ -16,6 +16,7 @@ Install: pip install pynput pywin32
1616
1717import json
1818import os
19+ import queue
1920import sys
2021import threading
2122import time
@@ -464,9 +465,12 @@ def launch_item(item: LaunchItem):
464465 if item .item_type == 'document' :
465466 subprocess .Popen (['xdg-open' , item .path ])
466467 else :
467- subprocess .Popen ([item .path ] + item .args )
468+ subprocess .Popen (
469+ [item .path ] + item .args ,
470+ start_new_session = True ,
471+ )
468472 except Exception as e :
469- print (f"Error launching { item .name } : { e } " )
473+ print (f"Error launching { item .name } : { e } " , flush = True )
470474
471475
472476# ============== Clipboard ==============
@@ -835,25 +839,26 @@ class LauncherPopup:
835839 SEPARATOR_COLOR = "#3c3c46"
836840
837841 def __init__ (self , position : tuple , config : Config , usage_tracker : UsageTracker ,
838- clipboard_manager : ClipboardManager , mouse_listener = None ):
842+ clipboard_manager : ClipboardManager , mouse_listener = None , master = None ):
839843 self .config = config
840844 self .usage_tracker = usage_tracker
841845 self .clipboard = clipboard_manager
842846 self .position = position
843847 self ._mouse_listener = mouse_listener
848+ self ._master = master
844849 self ._shown_time = 0.0
845850 self ._last_checked_press = 0.0
846851 self ._tkinter_click_time = 0.0
847852
848- self .root : Optional [tk .Tk ] = None
853+ self .root : Optional [tk .Toplevel ] = None
849854 self .shortcut_num = 1
850855 self ._numbered_items : List [LaunchItem ] = []
851856 self .clipboard_search_var : Optional [tk .StringVar ] = None
852857 self .clipboard_frame : Optional [tk .Frame ] = None
853858
854859 def show (self ):
855860 """Show the popup window"""
856- self .root = tk .Tk ( )
861+ self .root = tk .Toplevel ( self . _master )
857862 self .root .title ("Launcher" )
858863
859864 # Remove window decorations
@@ -925,9 +930,6 @@ class LauncherPopup:
925930 self ._last_checked_press = 0.0
926931 self .root .after (100 , self ._check_click_outside )
927932
928- # Start main loop
929- self .root .mainloop ()
930-
931933 def _build_content (self , parent : ttk .Frame ):
932934 """Build the popup content"""
933935 self .shortcut_num = 1
@@ -1323,6 +1325,8 @@ class Launcher:
13231325 self .usage_tracker = UsageTracker ()
13241326 self .clipboard = ClipboardManager (self .config .max_clipboard_history )
13251327 self .popup : Optional [LauncherPopup ] = None
1328+ self ._trigger_queue : queue .Queue = queue .Queue ()
1329+ self ._root : Optional [tk .Tk ] = None
13261330
13271331 ListenerClass = EvdevMouseListener if _INPUT_BACKEND == 'evdev' else MouseInputListener
13281332 self .input_listener = ListenerClass (
@@ -1333,18 +1337,28 @@ class Launcher:
13331337 print (f"Input backend: { _INPUT_BACKEND } " )
13341338
13351339 def _on_trigger (self , position : tuple ):
1336- """Handle trigger event"""
1337- print (f"Trigger at { position } " )
1340+ """Called from evdev thread — post to main thread via queue."""
1341+ self ._trigger_queue .put (position )
1342+
1343+ def _poll_triggers (self ):
1344+ """Poll trigger queue on main thread."""
1345+ try :
1346+ while True :
1347+ pos = self ._trigger_queue .get_nowait ()
1348+ self ._handle_trigger (pos )
1349+ except queue .Empty :
1350+ pass
1351+ if self ._root :
1352+ self ._root .after (50 , self ._poll_triggers )
13381353
1339- # Close existing popup if any
1354+ def _handle_trigger (self , position : tuple ):
1355+ """Handle trigger on main thread — safe for tkinter."""
1356+ print (f"Trigger at { position } " )
13401357 if self .popup and self .popup .root :
13411358 self .popup .close ()
1342-
1343- # Show new popup
1344- self .popup = LauncherPopup (position , self .config , self .usage_tracker , self .clipboard , self .input_listener )
1345-
1346- # Run in thread to not block input listener
1347- threading .Thread (target = self .popup .show , daemon = True ).start ()
1359+ self .popup = LauncherPopup (position , self .config , self .usage_tracker ,
1360+ self .clipboard , self .input_listener , self ._root )
1361+ self .popup .show ()
13481362
13491363 def run (self ):
13501364 """Start the launcher"""
@@ -1355,15 +1369,26 @@ class Launcher:
13551369 _write_helper_scripts ()
13561370 self .input_listener .start ()
13571371
1358- try :
1359- # Keep main thread alive
1360- while True :
1361- time . sleep ( 1 )
1362- except KeyboardInterrupt :
1372+ import signal
1373+ self . _root = tk . Tk ()
1374+ self . _root . withdraw ()
1375+
1376+ def on_sigint ( sig , frame ) :
13631377 print ("\n Shutting down..." )
13641378 self .input_listener .stop ()
1379+ self ._root .quit ()
1380+
1381+ signal .signal (signal .SIGINT , on_sigint )
1382+ self ._root .after (50 , self ._poll_triggers )
1383+ self ._root .mainloop ()
13651384
13661385
13671386if __name__ == "__main__" :
1387+ # Daemonize: redirect output to log when not on a terminal
1388+ if not sys .stdout .isatty ():
1389+ _log = os .path .expanduser ("~/.cache/launcher.log" )
1390+ _f = open (_log , "a" )
1391+ sys .stdout = sys .stderr = _f
1392+
13681393 launcher = Launcher ()
13691394 launcher .run ()
0 commit comments