diff --git a/src/cli/sync.py b/src/cli/sync.py index ffbb23f..2352aef 100644 --- a/src/cli/sync.py +++ b/src/cli/sync.py @@ -801,6 +801,59 @@ def status(): os.unlink(pid_file) +@sync.command(name="interactive") +@click.option( + "--org", + help="Specify the organization name for the subfolder to store emails and calendar events", + default="corteva", +) +@click.option( + "--vdir", + help="Output calendar events in vdir format to the specified directory", + default="~/Calendar", +) +@click.option( + "--notify/--no-notify", + help="Send macOS notifications for new email messages", + default=True, +) +@click.option( + "--dry-run", + is_flag=True, + help="Run in dry-run mode without making changes.", + default=False, +) +@click.option( + "--demo", + is_flag=True, + help="Run with simulated sync (demo mode)", + default=False, +) +def interactive(org, vdir, notify, dry_run, demo): + """Launch interactive TUI dashboard for sync operations.""" + from .sync_dashboard import run_dashboard_sync + + sync_config = { + "org": org, + "vdir": vdir, + "notify": notify, + "dry_run": dry_run, + "days_back": 1, + "days_forward": 30, + "download_attachments": False, + "two_way_calendar": False, + "continue_iteration": False, + "icsfile": None, + } + asyncio.run( + run_dashboard_sync(notify=notify, sync_config=sync_config, demo_mode=demo) + ) + + +# Alias 'i' for 'interactive' +sync.add_command(interactive, name="i") + + def check_calendar_changes(vdir_path, org): """ Check if there are local calendar changes that need syncing. diff --git a/src/cli/sync_dashboard.py b/src/cli/sync_dashboard.py index 5acd181..439f542 100644 --- a/src/cli/sync_dashboard.py +++ b/src/cli/sync_dashboard.py @@ -529,13 +529,17 @@ class SyncDashboard(App): def action_daemonize(self) -> None: """Start sync daemon in background and exit TUI.""" + import subprocess from src.cli.sync_daemon import SyncDaemon, create_daemon_config + # Build config from sync_config, adding sync_interval + daemon_config = { + **self._sync_config, + "sync_interval": self.sync_interval, + } + # Check if daemon is already running - config = create_daemon_config( - sync_interval=self.sync_interval, - notify=True, # Enable notifications for daemon - ) + config = create_daemon_config(**daemon_config) daemon = SyncDaemon(config) if daemon.is_running(): @@ -548,26 +552,51 @@ class SyncDashboard(App): # Start daemon and exit self._log_to_task(self.selected_task, "Starting background daemon...") - # Fork the daemon process + # Use subprocess to start the daemon via CLI command + # This properly handles daemonization without conflicting with TUI try: - pid = os.fork() - if pid == 0: - # Child process - become the daemon - os.setsid() - # Second fork to prevent zombie processes - pid2 = os.fork() - if pid2 == 0: - # Grandchild - this becomes the daemon - daemon.start() - else: - os._exit(0) - else: - # Parent process - wait briefly then exit TUI - import time + # Build the command with current config + cmd = [ + sys.executable, + "-m", + "src.cli", + "sync", + "run", + "--daemon", + "--org", + self._sync_config.get("org", "corteva"), + "--vdir", + self._sync_config.get("vdir", "~/Calendar"), + ] + if self._sync_config.get("notify", True): + cmd.append("--notify") + if self._sync_config.get("dry_run", False): + cmd.append("--dry-run") + if self._sync_config.get("two_way_calendar", False): + cmd.append("--two-way-calendar") + if self._sync_config.get("download_attachments", False): + cmd.append("--download-attachments") - time.sleep(0.5) - self.exit(message="Daemon started. Sync continues in background.") - except OSError as e: + # Start the daemon process detached + subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + start_new_session=True, + ) + + # Give it a moment to start + time.sleep(0.5) + + # Verify it started + if daemon.is_running(): + self.exit( + message=f"Daemon started (PID {daemon.get_pid()}). Sync continues in background." + ) + else: + self._log_to_task(self.selected_task, "Failed to start daemon") + except Exception as e: self._log_to_task(self.selected_task, f"Failed to daemonize: {e}") def set_sync_callback(self, callback: Callable) -> None: