44import platform
55import sys
66import threading
7- import tkinter as tk
8- from tkinter import ttk
9- from typing import TYPE_CHECKING , Callable , Optional
7+ from typing import TYPE_CHECKING , Any , Callable
108
119if TYPE_CHECKING :
1210 from flask import Flask
1311
1412logger = logging .getLogger (__name__ )
1513
14+ # Check if tkinter is available (it won't be in Docker/headless environments)
15+ _tkinter_available = True
16+ tk : Any = None
17+ ttk : Any = None
18+ try :
19+ import tkinter as tk # noqa: F811
20+ from tkinter import ttk # noqa: F811
21+ except ImportError :
22+ _tkinter_available = False
23+ logger .debug ("tkinter not available - GUI will be disabled" )
24+
1625
1726class ServerStatusWindow :
1827 """A simple status window for displaying server information and shutdown control.
@@ -33,73 +42,76 @@ def __init__(self, host: str, port: int, shutdown_callback: Callable[[], None]):
3342 port: Server port number
3443 shutdown_callback: Function to call when server should be shut down
3544 """
45+ if not _tkinter_available :
46+ raise RuntimeError ("tkinter is not available - GUI cannot be used" )
47+
3648 self .host = host
3749 self .port = port
3850 self .shutdown_callback = shutdown_callback
39- self .root : Optional [ tk . Tk ] = None
51+ self .root : Any = None
4052 self .is_running = False
41- self .logs_window : Optional [ tk . Toplevel ] = None
42- self .logs_text : Optional [ tk . Text ] = None
53+ self .logs_window : Any = None
54+ self .logs_text : Any = None
4355
4456 def create_window (self ) -> None :
4557 """Create and configure the tkinter window."""
46- self .root = tk .Tk ()
47- self .root .title ("ttmp32gme Server" )
48- self .root .geometry ("400x250" )
49- self .root .resizable (False , False )
58+ self .root = tk .Tk () # type: ignore[union-attr]
59+ self .root .title ("ttmp32gme Server" ) # type: ignore[union-attr]
60+ self .root .geometry ("400x250" ) # type: ignore[union-attr]
61+ self .root .resizable (False , False ) # type: ignore[union-attr]
5062
5163 # Handle window close event
52- self .root .protocol ("WM_DELETE_WINDOW" , self .on_close )
64+ self .root .protocol ("WM_DELETE_WINDOW" , self .on_close ) # type: ignore[union-attr]
5365
5466 # Create main frame with padding
55- main_frame = ttk .Frame (self .root , padding = "20" )
67+ main_frame = ttk .Frame (self .root , padding = "20" ) # type: ignore[union-attr]
5668 main_frame .grid (row = 0 , column = 0 , sticky = "wens" )
5769
5870 # Title label
59- title_label = ttk .Label (
71+ title_label = ttk .Label ( # type: ignore[union-attr]
6072 main_frame , text = "TipToi MP3 GME Converter" , font = ("Helvetica" , 16 , "bold" )
6173 )
6274 title_label .grid (row = 0 , column = 0 , columnspan = 2 , pady = (0 , 20 ))
6375
6476 # Server status section
65- status_label = ttk .Label (
77+ status_label = ttk .Label ( # type: ignore[union-attr]
6678 main_frame , text = "Server is running" , font = ("Helvetica" , 12 )
6779 )
6880 status_label .grid (row = 1 , column = 0 , columnspan = 2 , pady = (0 , 10 ))
6981
7082 # URL display
7183 url = f"http://{ self .host } :{ self .port } /"
72- url_frame = ttk .Frame (main_frame )
84+ url_frame = ttk .Frame (main_frame ) # type: ignore[union-attr]
7385 url_frame .grid (row = 2 , column = 0 , columnspan = 2 , pady = (0 , 20 ))
7486
75- ttk .Label (url_frame , text = "URL:" ).grid (row = 0 , column = 0 , padx = (0 , 5 ))
76- url_entry = ttk .Entry (url_frame , width = 30 )
87+ ttk .Label (url_frame , text = "URL:" ).grid (row = 0 , column = 0 , padx = (0 , 5 )) # type: ignore[union-attr]
88+ url_entry = ttk .Entry (url_frame , width = 30 ) # type: ignore[union-attr]
7789 url_entry .insert (0 , url )
7890 url_entry .config (state = "readonly" )
7991 url_entry .grid (row = 0 , column = 1 )
8092
8193 # Buttons frame
82- button_frame = ttk .Frame (main_frame )
94+ button_frame = ttk .Frame (main_frame ) # type: ignore[union-attr]
8395 button_frame .grid (row = 3 , column = 0 , columnspan = 2 , pady = (10 , 0 ))
8496
8597 # Open Browser button
86- open_button = ttk .Button (
98+ open_button = ttk .Button ( # type: ignore[union-attr]
8799 button_frame , text = "Open Browser" , command = self .open_browser
88100 )
89101 open_button .grid (row = 0 , column = 0 , padx = (0 , 5 ))
90102
91103 # Show Logs button
92- logs_button = ttk .Button (button_frame , text = "Show Logs" , command = self .show_logs )
104+ logs_button = ttk .Button (button_frame , text = "Show Logs" , command = self .show_logs ) # type: ignore[union-attr]
93105 logs_button .grid (row = 0 , column = 1 , padx = (0 , 5 ))
94106
95107 # Stop Server button
96- stop_button = ttk .Button (
108+ stop_button = ttk .Button ( # type: ignore[union-attr]
97109 button_frame , text = "Stop Server" , command = self .on_close
98110 )
99111 stop_button .grid (row = 0 , column = 2 )
100112
101113 # Info label at the bottom
102- info_label = ttk .Label (
114+ info_label = ttk .Label ( # type: ignore[union-attr]
103115 main_frame ,
104116 text = "Close this window to stop the server" ,
105117 font = ("Helvetica" , 9 ),
@@ -115,65 +127,65 @@ def open_browser(self) -> None:
115127
116128 def show_logs (self ) -> None :
117129 """Open a window showing server logs."""
118- if self .logs_window and tk .Toplevel .winfo_exists (self .logs_window ):
130+ if self .logs_window and tk .Toplevel .winfo_exists (self .logs_window ): # type: ignore[union-attr]
119131 # Window already exists, just raise it
120- self .logs_window .lift ()
121- self .logs_window .focus_force ()
132+ self .logs_window .lift () # type: ignore[union-attr]
133+ self .logs_window .focus_force () # type: ignore[union-attr]
122134 return
123135
124136 # Create logs window
125- self .logs_window = tk .Toplevel (self .root )
126- self .logs_window .title ("Server Logs" )
127- self .logs_window .geometry ("700x500" )
137+ self .logs_window = tk .Toplevel (self .root ) # type: ignore[union-attr]
138+ self .logs_window .title ("Server Logs" ) # type: ignore[union-attr]
139+ self .logs_window .geometry ("700x500" ) # type: ignore[union-attr]
128140
129141 # Create frame for logs
130- logs_frame = ttk .Frame (self .logs_window , padding = "10" )
142+ logs_frame = ttk .Frame (self .logs_window , padding = "10" ) # type: ignore[union-attr]
131143 logs_frame .grid (row = 0 , column = 0 , sticky = "nsew" )
132- self .logs_window .grid_rowconfigure (0 , weight = 1 )
133- self .logs_window .grid_columnconfigure (0 , weight = 1 )
144+ self .logs_window .grid_rowconfigure (0 , weight = 1 ) # type: ignore[union-attr]
145+ self .logs_window .grid_columnconfigure (0 , weight = 1 ) # type: ignore[union-attr]
134146
135147 # Create scrolled text widget for logs
136- logs_text_frame = ttk .Frame (logs_frame )
148+ logs_text_frame = ttk .Frame (logs_frame ) # type: ignore[union-attr]
137149 logs_text_frame .grid (row = 0 , column = 0 , sticky = "nsew" )
138150 logs_frame .grid_rowconfigure (0 , weight = 1 )
139151 logs_frame .grid_columnconfigure (0 , weight = 1 )
140152
141153 # Text widget with scrollbar
142- scrollbar = ttk .Scrollbar (logs_text_frame )
143- scrollbar .pack (side = tk .RIGHT , fill = tk .Y )
154+ scrollbar = ttk .Scrollbar (logs_text_frame ) # type: ignore[union-attr]
155+ scrollbar .pack (side = tk .RIGHT , fill = tk .Y ) # type: ignore[union-attr]
144156
145- self .logs_text = tk .Text (
157+ self .logs_text = tk .Text ( # type: ignore[union-attr]
146158 logs_text_frame ,
147- wrap = tk .WORD ,
159+ wrap = tk .WORD , # type: ignore[union-attr]
148160 yscrollcommand = scrollbar .set ,
149161 font = ("Courier" , 10 ),
150162 bg = "white" ,
151163 fg = "black" ,
152164 )
153- self .logs_text .pack (side = tk .LEFT , fill = tk .BOTH , expand = True )
154- scrollbar .config (command = self .logs_text .yview )
165+ self .logs_text .pack (side = tk .LEFT , fill = tk .BOTH , expand = True ) # type: ignore[union-attr]
166+ scrollbar .config (command = self .logs_text .yview ) # type: ignore[union-attr]
155167
156168 # Buttons frame at bottom
157- button_frame = ttk .Frame (logs_frame )
169+ button_frame = ttk .Frame (logs_frame ) # type: ignore[union-attr]
158170 button_frame .grid (row = 1 , column = 0 , pady = (10 , 0 ))
159171
160172 # Refresh button
161- refresh_button = ttk .Button (
173+ refresh_button = ttk .Button ( # type: ignore[union-attr]
162174 button_frame , text = "Refresh" , command = self .refresh_logs
163175 )
164- refresh_button .pack (side = tk .LEFT , padx = (0 , 5 ))
176+ refresh_button .pack (side = tk .LEFT , padx = (0 , 5 )) # type: ignore[union-attr]
165177
166178 # Clear button
167- clear_button = ttk .Button (
179+ clear_button = ttk .Button ( # type: ignore[union-attr]
168180 button_frame , text = "Clear" , command = self .clear_logs_display
169181 )
170- clear_button .pack (side = tk .LEFT , padx = (0 , 5 ))
182+ clear_button .pack (side = tk .LEFT , padx = (0 , 5 )) # type: ignore[union-attr]
171183
172184 # Close button
173- close_button = ttk .Button (
174- button_frame , text = "Close" , command = self .logs_window .destroy
185+ close_button = ttk .Button ( # type: ignore[union-attr]
186+ button_frame , text = "Close" , command = self .logs_window .destroy # type: ignore[union-attr]
175187 )
176- close_button .pack (side = tk .LEFT )
188+ close_button .pack (side = tk .LEFT ) # type: ignore[union-attr]
177189
178190 # Load initial logs
179191 self .refresh_logs ()
@@ -241,11 +253,16 @@ def should_use_gui() -> bool:
241253 - Running on macOS
242254 - Running from PyInstaller bundle
243255 - Not in development mode
256+ - tkinter is available
244257
245258 Returns:
246259 True if GUI should be used, False otherwise
247260 """
248- return platform .system () == "Darwin" and getattr (sys , "frozen" , False )
261+ return (
262+ _tkinter_available
263+ and platform .system () == "Darwin"
264+ and getattr (sys , "frozen" , False )
265+ )
249266
250267
251268def run_server_with_gui (app : "Flask" , host : str , port : int ) -> None :
0 commit comments