- Introduced .python-version for Python version management. - Added AGENTS.md for documentation on agent usage and best practices. - Created alembic.ini for database migration configurations. - Implemented main.py as the entry point for the application. - Established pyproject.toml for project dependencies and configurations. - Initialized README.md for project overview. - Generated uv.lock for dependency locking. - Documented milestones and specifications in docs/milestones.md and docs/spec.md. - Created logs/status_line.json for logging status information. - Added initial spike implementations for UI tray hotkeys, audio capture, ASR latency, and encryption validation. - Set up NoteFlow core structure in src/noteflow with necessary modules and services. - Developed test suite in tests directory for application, domain, infrastructure, and integration testing. - Included initial migration scripts in infrastructure/persistence/migrations for database setup. - Established security protocols in infrastructure/security for key management and encryption. - Implemented audio infrastructure for capturing and processing audio data. - Created converters for ASR and ORM in infrastructure/converters. - Added export functionality for different formats in infrastructure/export. - Ensured all new files are included in the repository for future development.
254 lines
7.5 KiB
Python
254 lines
7.5 KiB
Python
"""Interactive UI + Tray + Hotkeys demo for Spike 1.
|
|
|
|
Run with: python -m spikes.spike_01_ui_tray_hotkeys.demo
|
|
|
|
Features:
|
|
- Flet window with Start/Stop buttons
|
|
- System tray icon with context menu
|
|
- Global hotkey support (Ctrl+Shift+R)
|
|
- Notifications on state changes
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import queue
|
|
import sys
|
|
import threading
|
|
from enum import Enum, auto
|
|
|
|
import flet as ft
|
|
|
|
from .hotkey_impl import PynputHotkeyManager
|
|
from .protocols import TrayIcon, TrayMenuItem
|
|
from .tray_impl import PystrayController
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AppState(Enum):
|
|
"""Application state."""
|
|
|
|
IDLE = auto()
|
|
RECORDING = auto()
|
|
|
|
|
|
class NoteFlowDemo:
|
|
"""Demo application combining Flet UI, system tray, and hotkeys."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize the demo application."""
|
|
self.state = AppState.IDLE
|
|
self.tray = PystrayController(app_name="NoteFlow Demo")
|
|
self.hotkey_manager = PynputHotkeyManager()
|
|
|
|
# Queue for cross-thread communication
|
|
self._event_queue: queue.Queue[str] = queue.Queue()
|
|
|
|
# Flet page reference (set when app starts)
|
|
self._page: ft.Page | None = None
|
|
self._status_text: ft.Text | None = None
|
|
self._toggle_button: ft.ElevatedButton | None = None
|
|
|
|
def _update_ui(self) -> None:
|
|
"""Update UI elements based on current state."""
|
|
if self._page is None:
|
|
return
|
|
|
|
if self.state == AppState.RECORDING:
|
|
if self._status_text:
|
|
self._status_text.value = "Recording..."
|
|
self._status_text.color = ft.Colors.RED
|
|
if self._toggle_button:
|
|
self._toggle_button.text = "Stop Recording"
|
|
self._toggle_button.bgcolor = ft.Colors.RED
|
|
self.tray.set_icon(TrayIcon.RECORDING)
|
|
self.tray.set_tooltip("NoteFlow - Recording")
|
|
else:
|
|
if self._status_text:
|
|
self._status_text.value = "Idle"
|
|
self._status_text.color = ft.Colors.GREY
|
|
if self._toggle_button:
|
|
self._toggle_button.text = "Start Recording"
|
|
self._toggle_button.bgcolor = ft.Colors.BLUE
|
|
self.tray.set_icon(TrayIcon.IDLE)
|
|
self.tray.set_tooltip("NoteFlow - Idle")
|
|
|
|
self._page.update()
|
|
|
|
def _toggle_recording(self) -> None:
|
|
"""Toggle recording state."""
|
|
if self.state == AppState.IDLE:
|
|
self.state = AppState.RECORDING
|
|
logger.info("Started recording")
|
|
self.tray.notify("NoteFlow", "Recording started")
|
|
else:
|
|
self.state = AppState.IDLE
|
|
logger.info("Stopped recording")
|
|
self.tray.notify("NoteFlow", "Recording stopped")
|
|
|
|
self._update_ui()
|
|
|
|
def _on_toggle_click(self, e: ft.ControlEvent) -> None:
|
|
"""Handle toggle button click."""
|
|
self._toggle_recording()
|
|
|
|
def _on_hotkey(self) -> None:
|
|
"""Handle global hotkey press."""
|
|
logger.info("Hotkey pressed!")
|
|
# Queue event for main thread
|
|
self._event_queue.put("toggle")
|
|
|
|
def _process_events(self) -> None:
|
|
"""Process queued events (called periodically from UI thread)."""
|
|
try:
|
|
while True:
|
|
event = self._event_queue.get_nowait()
|
|
if event == "toggle":
|
|
self._toggle_recording()
|
|
elif event == "quit":
|
|
self._cleanup()
|
|
if self._page:
|
|
self._page.window.close()
|
|
except queue.Empty:
|
|
pass
|
|
|
|
def _setup_tray_menu(self) -> None:
|
|
"""Set up the system tray context menu."""
|
|
menu_items = [
|
|
TrayMenuItem(
|
|
label="Start Recording" if self.state == AppState.IDLE else "Stop Recording",
|
|
callback=self._toggle_recording,
|
|
),
|
|
TrayMenuItem(label="", callback=lambda: None, separator=True),
|
|
TrayMenuItem(
|
|
label="Show Window",
|
|
callback=lambda: self._event_queue.put("show"),
|
|
),
|
|
TrayMenuItem(label="", callback=lambda: None, separator=True),
|
|
TrayMenuItem(
|
|
label="Quit",
|
|
callback=lambda: self._event_queue.put("quit"),
|
|
),
|
|
]
|
|
self.tray.set_menu(menu_items)
|
|
|
|
def _cleanup(self) -> None:
|
|
"""Clean up resources."""
|
|
self.hotkey_manager.unregister_all()
|
|
self.tray.stop()
|
|
|
|
def _build_ui(self, page: ft.Page) -> None:
|
|
"""Build the Flet UI."""
|
|
self._page = page
|
|
page.title = "NoteFlow Demo - Spike 1"
|
|
page.window.width = 400
|
|
page.window.height = 300
|
|
page.theme_mode = ft.ThemeMode.DARK
|
|
|
|
# Status text
|
|
self._status_text = ft.Text(
|
|
value="Idle",
|
|
size=24,
|
|
weight=ft.FontWeight.BOLD,
|
|
color=ft.Colors.GREY,
|
|
)
|
|
|
|
# Toggle button
|
|
self._toggle_button = ft.ElevatedButton(
|
|
text="Start Recording",
|
|
icon=ft.Icons.MIC,
|
|
on_click=self._on_toggle_click,
|
|
bgcolor=ft.Colors.BLUE,
|
|
color=ft.Colors.WHITE,
|
|
width=200,
|
|
height=50,
|
|
)
|
|
|
|
# Hotkey info
|
|
hotkey_text = ft.Text(
|
|
value="Hotkey: Ctrl+Shift+R",
|
|
size=14,
|
|
color=ft.Colors.GREY_400,
|
|
)
|
|
|
|
# Layout
|
|
page.add(
|
|
ft.Column(
|
|
controls=[
|
|
ft.Container(height=30),
|
|
self._status_text,
|
|
ft.Container(height=20),
|
|
self._toggle_button,
|
|
ft.Container(height=30),
|
|
hotkey_text,
|
|
ft.Text(
|
|
value="System tray icon is active",
|
|
size=12,
|
|
color=ft.Colors.GREY_600,
|
|
),
|
|
],
|
|
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
|
alignment=ft.MainAxisAlignment.CENTER,
|
|
)
|
|
)
|
|
|
|
# Set up event polling
|
|
def poll_events() -> None:
|
|
self._process_events()
|
|
|
|
# Poll events every 100ms
|
|
page.run_task(self._poll_loop)
|
|
|
|
async def _poll_loop(self) -> None:
|
|
"""Async loop to poll events."""
|
|
import asyncio
|
|
|
|
while True:
|
|
self._process_events()
|
|
await asyncio.sleep(0.1)
|
|
|
|
def run(self) -> None:
|
|
"""Run the demo application."""
|
|
logger.info("Starting NoteFlow Demo")
|
|
|
|
# Start system tray
|
|
self.tray.start()
|
|
self._setup_tray_menu()
|
|
|
|
# Register global hotkey
|
|
try:
|
|
self.hotkey_manager.register("ctrl+shift+r", self._on_hotkey)
|
|
logger.info("Registered hotkey: Ctrl+Shift+R")
|
|
except Exception as e:
|
|
logger.warning("Failed to register hotkey: %s", e)
|
|
|
|
try:
|
|
# Run Flet app
|
|
ft.app(target=self._build_ui)
|
|
finally:
|
|
self._cleanup()
|
|
logger.info("Demo ended")
|
|
|
|
|
|
def main() -> None:
|
|
"""Run the UI + Tray + Hotkeys demo."""
|
|
print("=== NoteFlow Demo - Spike 1 ===")
|
|
print("Features:")
|
|
print(" - Flet window with Start/Stop buttons")
|
|
print(" - System tray icon with context menu")
|
|
print(" - Global hotkey: Ctrl+Shift+R")
|
|
print()
|
|
|
|
demo = NoteFlowDemo()
|
|
demo.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|