66
77from __future__ import annotations
88
9+ import os
10+ from pathlib import Path
911from typing import Any
1012
1113from loguru import logger
1517from .base import BaseScreen
1618
1719
20+ def _is_docker_setup () -> bool :
21+ """Check if running inside the Docker setup container."""
22+ return os .environ .get ('THOTH_DOCKER_SETUP' ) == '1'
23+
24+
25+ def _host_to_container_path (host_path : Path ) -> Path :
26+ """Translate a host filesystem path to the equivalent container path."""
27+ host_home = os .environ .get ('THOTH_HOST_HOME' , '' )
28+ if not host_home :
29+ return host_path
30+ container_home = str (Path .home ())
31+ path_str = str (host_path )
32+ if path_str .startswith (host_home ):
33+ return Path (container_home + path_str [len (host_home ) :])
34+ return host_path
35+
36+
37+ def _container_to_host_path (container_path : Path ) -> Path :
38+ """Translate a container path back to the host filesystem path."""
39+ host_home = os .environ .get ('THOTH_HOST_HOME' , '' )
40+ if not host_home :
41+ return container_path
42+ container_home = str (Path .home ())
43+ path_str = str (container_path )
44+ if path_str .startswith (container_home ):
45+ return Path (host_home + path_str [len (container_home ) :])
46+ return container_path
47+
48+
1849class WelcomeScreen (BaseScreen ):
1950 """Welcome screen for Thoth setup wizard."""
2051
@@ -24,6 +55,116 @@ def __init__(self) -> None:
2455 title = 'Welcome to Thoth Setup' ,
2556 subtitle = 'AI-Powered Research Assistant for Obsidian' ,
2657 )
58+ self .existing_config : dict [str , Any ] | None = None
59+ self .has_existing_installation = False
60+
61+ def on_mount (self ) -> None :
62+ """Run when screen is mounted - detect existing installation."""
63+ self ._detect_existing_installation ()
64+
65+ def _detect_existing_installation (self ) -> None :
66+ """Check if there's already a valid Thoth installation."""
67+ vault_path_str = os .environ .get ('OBSIDIAN_VAULT_PATH' , '' ).strip ()
68+ if not vault_path_str :
69+ logger .info ('No OBSIDIAN_VAULT_PATH in environment' )
70+ return
71+
72+ try :
73+ # Resolve the vault path
74+ vault_path = Path (vault_path_str ).expanduser ().resolve ()
75+
76+ # Handle Docker path translation
77+ if _is_docker_setup () and not vault_path .exists ():
78+ vault_path = _host_to_container_path (vault_path )
79+
80+ if not vault_path .exists () or not vault_path .is_dir ():
81+ logger .info (f'Vault path does not exist: { vault_path } ' )
82+ return
83+
84+ # Check for settings.json (new and legacy locations)
85+ settings_path = vault_path / 'thoth' / '_thoth' / 'settings.json'
86+ legacy_path = vault_path / '_thoth' / 'settings.json'
87+
88+ if not settings_path .exists () and legacy_path .exists ():
89+ settings_path = legacy_path
90+
91+ if not settings_path .exists ():
92+ logger .info (f'No settings.json found in vault: { vault_path } ' )
93+ return
94+
95+ # Try to load settings with Pydantic validation
96+ from thoth .config import Settings
97+
98+ settings = Settings .from_json_file (settings_path )
99+
100+ # If we got here, we have a valid config
101+ self .has_existing_installation = True
102+
103+ # Store config info for the update path
104+ vault_path_host = vault_path
105+ if _is_docker_setup ():
106+ vault_path_host = _container_to_host_path (vault_path )
107+
108+ self .existing_config = {
109+ 'vault_path' : vault_path ,
110+ 'vault_path_host' : vault_path_host ,
111+ 'settings_path' : settings_path ,
112+ 'settings' : settings ,
113+ 'version' : settings .version or 'unknown' ,
114+ }
115+
116+ logger .info (
117+ f'Detected existing installation at { vault_path } (version: { settings .version } )'
118+ )
119+
120+ # Update UI to show the update option
121+ self ._show_update_option ()
122+
123+ except Exception as e :
124+ logger .debug (f'Error detecting existing installation: { e } ' )
125+ # Not fatal - user can still do full setup
126+
127+ def _show_update_option (self ) -> None :
128+ """Update the UI to show the update option."""
129+ if not self .existing_config :
130+ return
131+
132+ try :
133+ # Update the welcome text to show detected config
134+ welcome_widget = self .query_one ('.welcome-content' , Static )
135+ vault_display = self .existing_config ['vault_path_host' ]
136+ version = self .existing_config ['version' ]
137+
138+ new_text = f"""
139+ [bold cyan]Welcome to Thoth![/bold cyan]
140+
141+ [bold green]Existing installation detected![/bold green]
142+
143+ Vault: [cyan]{ vault_display } [/cyan]
144+ Version: [cyan]{ version } [/cyan]
145+
146+ [bold]Choose how to proceed:[/bold]
147+
148+ • [yellow]Update Software[/yellow] - Keep your settings, update templates & plugin
149+ • [cyan]Begin Setup[/cyan] - Reconfigure everything (will preserve existing files)
150+
151+ [bold cyan]Navigation:[/bold cyan]
152+ • [dim]Tab to switch between buttons[/dim]
153+ • [dim]Enter to select[/dim]
154+ • [dim]ESC to exit wizard[/dim]
155+ """
156+
157+ welcome_widget .update (new_text .strip ())
158+
159+ # Show the update button
160+ try :
161+ update_btn = self .query_one ('#update' , Button )
162+ update_btn .styles .display = 'block'
163+ except Exception :
164+ pass # Button might not exist yet
165+
166+ except Exception as e :
167+ logger .debug (f'Could not update UI: { e } ' )
27168
28169 def compose_content (self ) -> ComposeResult :
29170 """
@@ -76,8 +217,62 @@ def compose_buttons(self) -> ComposeResult:
76217 Button widgets
77218 """
78219 yield Button ('Exit' , id = 'cancel' , variant = 'error' )
220+
221+ # Update button (hidden by default, shown when existing config detected)
222+ update_btn = Button ('Update Software' , id = 'update' , variant = 'primary' )
223+ update_btn .styles .display = 'none'
224+ yield update_btn
225+
79226 yield Button ('Begin Setup' , id = 'next' , variant = 'success' )
80227
228+ async def on_button_pressed (self , event : Button .Pressed ) -> None :
229+ """Handle button press events.
230+
231+ Args:
232+ event: Button pressed event
233+ """
234+ button_id = event .button .id
235+
236+ if button_id == 'update' :
237+ await self ._handle_update_path ()
238+ else :
239+ # Delegate to base class for cancel/next
240+ await super ().on_button_pressed (event )
241+
242+ async def _handle_update_path (self ) -> None :
243+ """Handle the update-only path - skip config and go to installation."""
244+ if not self .existing_config :
245+ self .show_error ('No existing configuration detected' )
246+ return
247+
248+ logger .info ('User chose update-only path' )
249+
250+ # Extract paths from existing settings
251+ settings = self .existing_config ['settings' ]
252+ paths_config = {
253+ 'workspace' : settings .paths .workspace ,
254+ 'pdf' : settings .paths .pdf ,
255+ 'markdown' : settings .paths .markdown ,
256+ 'notes' : settings .paths .notes ,
257+ }
258+
259+ # Populate wizard_data with minimal info needed for installation
260+ if hasattr (self .app , 'wizard_data' ):
261+ self .app .wizard_data .update (
262+ {
263+ 'vault_path' : self .existing_config ['vault_path' ],
264+ 'vault_path_host' : self .existing_config ['vault_path_host' ],
265+ 'paths_config' : paths_config ,
266+ 'update_only' : True ,
267+ }
268+ )
269+
270+ # Jump directly to installation
271+ from .installation import InstallationScreen
272+
273+ logger .info ('Jumping to installation for update-only' )
274+ await self .app .push_screen (InstallationScreen ())
275+
81276 async def validate_and_proceed (self ) -> dict [str , Any ] | None :
82277 """
83278 Validate welcome screen (always passes).
0 commit comments