2929from tui .gdtf_share .gdtf import GDTFScreen
3030from pathlib import Path
3131
32+ try :
33+ import keyring
34+ from keyring .errors import KeyringError
35+ except ImportError : # pragma: no cover - optional dependency at runtime
36+ keyring = None
37+ KeyringError = Exception
38+
3239
3340class MVRDisplay (VerticalScroll ):
3441 def update_items (self , items ):
@@ -118,6 +125,8 @@ def on_select_changed(self, event: Select.Changed):
118125class PollToMVR (App ):
119126 """A Textual app to manage Uptime Kuma MVR."""
120127
128+ APP_NAME = "PollToMVR"
129+ TITLE = APP_NAME
121130 CSS_PATH = [
122131 "app.css" ,
123132 "quit_screen.css" ,
@@ -138,6 +147,9 @@ class PollToMVR(App):
138147 ]
139148
140149 CONFIG_FILE = "config.json"
150+ KEYRING_SERVICE = APP_NAME
151+ KEYRING_USERNAME_KEY = "gdtf_username"
152+ KEYRING_PASSWORD_KEY = "gdtf_password"
141153 configuration = SimpleNamespace (
142154 artnet_timeout = "1" , show_debug = False , gdtf_username = "" , gdtf_password = ""
143155 )
@@ -172,15 +184,22 @@ def on_mount(self) -> None:
172184 """Load the configuration from the JSON file when the app starts."""
173185 path = Path ("gdtf_files" )
174186 path .mkdir (parents = True , exist_ok = True )
187+ config_data = {}
175188 if os .path .exists (self .CONFIG_FILE ):
176189 with open (self .CONFIG_FILE , "r" ) as f :
177190 try :
178- vars (self .configuration ).update (json .load (f ))
191+ config_data = json .load (f )
192+ vars (self .configuration ).update (config_data )
179193 self .notify ("Configuration loaded..." , timeout = 1 )
180194
181195 except json .JSONDecodeError :
182196 # Handle empty or invalid JSON file
183197 pass
198+ migrated = self ._load_credentials (config_data )
199+ if migrated :
200+ with open (self .CONFIG_FILE , "w" ) as f :
201+ json .dump (config_data , f , indent = 4 )
202+ self .notify ("Credentials moved to system keyring." , timeout = 2 )
184203
185204 def on_button_pressed (self , event : Button .Pressed ) -> None :
186205 """Called when a button is pressed."""
@@ -236,8 +255,10 @@ def check_quit(quit_confirmed: bool) -> None:
236255
237256 def action_save_config (self ) -> None :
238257 """Save the configuration to the JSON file."""
258+ config_data = vars (self .app .configuration ).copy ()
259+ self ._persist_credentials (config_data )
239260 with open (self .CONFIG_FILE , "w" ) as f :
240- json .dump (vars ( self . app . configuration ) , f , indent = 4 )
261+ json .dump (config_data , f , indent = 4 )
241262
242263 def action_quit (self ) -> None :
243264 """Save the configuration to the JSON file when the app closes."""
@@ -248,6 +269,69 @@ def get_layer_name(self, uuid):
248269 for layer_name , layer_id in self .mvr_layers :
249270 if layer_id == uuid :
250271 return layer_name
272+ return None
273+
274+ def _keyring_get (self , key ):
275+ if not keyring :
276+ return None
277+ try :
278+ return keyring .get_password (self .KEYRING_SERVICE , key )
279+ except KeyringError :
280+ return None
281+
282+ def _keyring_set (self , key , value ):
283+ if not keyring :
284+ return False
285+ try :
286+ if value :
287+ keyring .set_password (self .KEYRING_SERVICE , key , value )
288+ else :
289+ try :
290+ keyring .delete_password (self .KEYRING_SERVICE , key )
291+ except KeyringError :
292+ pass
293+ return True
294+ except KeyringError :
295+ return False
296+
297+ def _load_credentials (self , config_data ):
298+ migrated = False
299+ username = self ._keyring_get (self .KEYRING_USERNAME_KEY )
300+ password = self ._keyring_get (self .KEYRING_PASSWORD_KEY )
301+ config_username = config_data .get ("gdtf_username" , "" )
302+ config_password = config_data .get ("gdtf_password" , "" )
303+ if (config_username or config_password ) and (
304+ username is None or password is None
305+ ):
306+ stored_user = self ._keyring_set (self .KEYRING_USERNAME_KEY , config_username )
307+ stored_pass = self ._keyring_set (self .KEYRING_PASSWORD_KEY , config_password )
308+ if stored_user and stored_pass :
309+ config_data .pop ("gdtf_username" , None )
310+ config_data .pop ("gdtf_password" , None )
311+ migrated = True
312+ username = self ._keyring_get (self .KEYRING_USERNAME_KEY )
313+ password = self ._keyring_get (self .KEYRING_PASSWORD_KEY )
314+ if username is None :
315+ username = config_username
316+ if password is None :
317+ password = config_password
318+ self .configuration .gdtf_username = username or ""
319+ self .configuration .gdtf_password = password or ""
320+ return migrated
321+
322+ def _persist_credentials (self , config_data ):
323+ stored_user = self ._keyring_set (
324+ self .KEYRING_USERNAME_KEY , self .configuration .gdtf_username
325+ )
326+ stored_pass = self ._keyring_set (
327+ self .KEYRING_PASSWORD_KEY , self .configuration .gdtf_password
328+ )
329+ if stored_user and stored_pass :
330+ config_data .pop ("gdtf_username" , None )
331+ config_data .pop ("gdtf_password" , None )
332+ else :
333+ config_data ["gdtf_username" ] = self .configuration .gdtf_username
334+ config_data ["gdtf_password" ] = self .configuration .gdtf_password
251335
252336 @on (Button .Pressed )
253337 @work
0 commit comments