quantum-framework 0.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terminal Engine - Builder/Orchestrator
|
|
3
|
+
|
|
4
|
+
Orchestrates the compilation pipeline:
|
|
5
|
+
ApplicationNode (type="terminal") -> extract screens/keybindings/css -> TerminalCodeGenerator -> .py file
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
builder = TerminalBuilder()
|
|
9
|
+
python_code = builder.build(app_node)
|
|
10
|
+
builder.build_to_file(app_node, "output.py")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from quantum.core.ast_nodes import ApplicationNode
|
|
18
|
+
from quantum.core.features.terminal_engine.src.ast_nodes import (
|
|
19
|
+
ScreenNode, KeybindingNode, ServiceNode, CssNode,
|
|
20
|
+
)
|
|
21
|
+
from quantum.runtime.terminal_code_generator import TerminalCodeGenerator
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TerminalBuildError(Exception):
|
|
25
|
+
"""Error during terminal app build."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TerminalBuilder:
|
|
30
|
+
"""Builds a standalone Python TUI app from a Quantum terminal ApplicationNode."""
|
|
31
|
+
|
|
32
|
+
def build(self, app: ApplicationNode) -> str:
|
|
33
|
+
"""Build Python source string from an ApplicationNode with type='terminal'.
|
|
34
|
+
|
|
35
|
+
The ApplicationNode is expected to have:
|
|
36
|
+
- app.screens: list of ScreenNode
|
|
37
|
+
- app.terminal_css: str (optional)
|
|
38
|
+
- app.keybindings: list of KeybindingNode (optional)
|
|
39
|
+
- app.services: list of ServiceNode (optional)
|
|
40
|
+
"""
|
|
41
|
+
screens = getattr(app, 'screens', [])
|
|
42
|
+
terminal_css = getattr(app, 'terminal_css', '')
|
|
43
|
+
keybindings = getattr(app, 'keybindings', [])
|
|
44
|
+
services = getattr(app, 'services', [])
|
|
45
|
+
|
|
46
|
+
if not screens:
|
|
47
|
+
raise TerminalBuildError("No screens found in terminal application")
|
|
48
|
+
|
|
49
|
+
valid_screens = [s for s in screens if isinstance(s, ScreenNode)]
|
|
50
|
+
if not valid_screens:
|
|
51
|
+
raise TerminalBuildError("No valid ScreenNode found in terminal application")
|
|
52
|
+
|
|
53
|
+
generator = TerminalCodeGenerator()
|
|
54
|
+
python_code = generator.generate(
|
|
55
|
+
screens=valid_screens,
|
|
56
|
+
title=app.app_id,
|
|
57
|
+
terminal_css=terminal_css,
|
|
58
|
+
keybindings=[k for k in keybindings if isinstance(k, KeybindingNode)],
|
|
59
|
+
services=[s for s in services if isinstance(s, ServiceNode)],
|
|
60
|
+
functions=getattr(app, 'functions', None) or [],
|
|
61
|
+
state_vars=getattr(app, 'state_vars', None) or [],
|
|
62
|
+
)
|
|
63
|
+
return python_code
|
|
64
|
+
|
|
65
|
+
def build_to_file(self, app: ApplicationNode, output_path: Optional[str] = None) -> str:
|
|
66
|
+
"""Build and write .py file. Returns the output file path."""
|
|
67
|
+
python_code = self.build(app)
|
|
68
|
+
|
|
69
|
+
if output_path is None:
|
|
70
|
+
output_path = f"{app.app_id}.py"
|
|
71
|
+
|
|
72
|
+
path = Path(output_path)
|
|
73
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
path.write_text(python_code, encoding='utf-8')
|
|
75
|
+
|
|
76
|
+
return str(path.resolve())
|
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terminal Engine - Code Generator
|
|
3
|
+
|
|
4
|
+
Transforms Terminal AST nodes into Python code that runs with Textual.
|
|
5
|
+
This is a compiler/transpiler: Python generates Python TUI apps.
|
|
6
|
+
|
|
7
|
+
Uses PyBuilder for structured, safe Python generation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from typing import List, Dict, Any, Optional
|
|
13
|
+
|
|
14
|
+
from quantum.core.features.terminal_engine.src.ast_nodes import (
|
|
15
|
+
ScreenNode, PanelNode, LayoutNode, TableNode, ColumnNode,
|
|
16
|
+
TerminalInputNode, ButtonNode, MenuNode, OptionNode,
|
|
17
|
+
TextNode_Terminal, ProgressNode, TreeNode, TabsNode, TabNode,
|
|
18
|
+
LogNode_Terminal, HeaderNode_Terminal, FooterNode, StatusNode,
|
|
19
|
+
KeybindingNode, TimerNode_Terminal, ServiceNode, CssNode,
|
|
20
|
+
OnEventNode_Terminal, RawCodeNode_Terminal,
|
|
21
|
+
)
|
|
22
|
+
from quantum.core.ast_nodes import QuantumNode, QueryNode
|
|
23
|
+
from quantum.core.features.conditionals.src.ast_node import IfNode
|
|
24
|
+
from quantum.core.features.loops.src.ast_node import LoopNode
|
|
25
|
+
from quantum.core.features.state_management.src.ast_node import SetNode
|
|
26
|
+
from quantum.core.features.functions.src.ast_node import FunctionNode
|
|
27
|
+
|
|
28
|
+
from quantum.runtime.terminal_templates import (
|
|
29
|
+
APP_TEMPLATE, PyBuilder, py_string, py_id, py_bool, split_call,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TerminalCodeGenerator:
|
|
34
|
+
"""Generates standalone Python Textual app from a terminal AST."""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self._state_vars: List[Dict] = [] # {name, value, type}
|
|
38
|
+
self._functions: List[FunctionNode] = []
|
|
39
|
+
self._keybindings: List[Dict] = []
|
|
40
|
+
self._timers: List[Dict] = []
|
|
41
|
+
self._services: List[Dict] = []
|
|
42
|
+
self._css_blocks: List[str] = []
|
|
43
|
+
self._queries: List[Dict] = []
|
|
44
|
+
self._screens: List[ScreenNode] = []
|
|
45
|
+
self._extra_imports: List[str] = []
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# Public API
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def generate(self, screens: List[ScreenNode], title: str = "Quantum TUI",
|
|
52
|
+
terminal_css: str = "", keybindings: List = None,
|
|
53
|
+
services: List = None, functions: List = None,
|
|
54
|
+
state_vars: List = None) -> str:
|
|
55
|
+
"""Generate full Python app from ScreenNodes.
|
|
56
|
+
|
|
57
|
+
`functions` e `state_vars` sao os <q:function>/<q:set> escritos no
|
|
58
|
+
topo do <q:application>. So os declarados DENTRO de um <qt:screen>
|
|
59
|
+
chegavam ate aqui, entao um `on-click` apontando para uma funcao
|
|
60
|
+
global caia no ramo "acao desconhecida" e virava um
|
|
61
|
+
`self.notify("Action: salvar")`: o botao anunciava a acao em vez de
|
|
62
|
+
executa-la.
|
|
63
|
+
"""
|
|
64
|
+
for func in (functions or []):
|
|
65
|
+
if func not in self._functions:
|
|
66
|
+
self._functions.append(func)
|
|
67
|
+
for var in (state_vars or []):
|
|
68
|
+
self._state_vars.append({
|
|
69
|
+
'name': var.name,
|
|
70
|
+
'value': var.value,
|
|
71
|
+
'type': getattr(var, 'type', 'string'),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
if terminal_css:
|
|
75
|
+
self._css_blocks.append(terminal_css)
|
|
76
|
+
|
|
77
|
+
# Process top-level keybindings/services
|
|
78
|
+
for kb in (keybindings or []):
|
|
79
|
+
if isinstance(kb, KeybindingNode):
|
|
80
|
+
self._keybindings.append({
|
|
81
|
+
'key': kb.key,
|
|
82
|
+
'action': kb.action,
|
|
83
|
+
'description': kb.description or kb.action,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
for svc in (services or []):
|
|
87
|
+
if isinstance(svc, ServiceNode):
|
|
88
|
+
self._services.append({
|
|
89
|
+
'id': svc.service_id,
|
|
90
|
+
'handler': svc.handler,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
self._screens = screens
|
|
94
|
+
|
|
95
|
+
# Process all screens
|
|
96
|
+
for screen in screens:
|
|
97
|
+
self._process_screen_children(screen.children)
|
|
98
|
+
|
|
99
|
+
# Build app code
|
|
100
|
+
py = PyBuilder()
|
|
101
|
+
self._build_app_class(py, screens, title)
|
|
102
|
+
app_code = py.build()
|
|
103
|
+
|
|
104
|
+
# Determine app class name
|
|
105
|
+
app_class = py_id(title.replace(' ', '')) + "App"
|
|
106
|
+
|
|
107
|
+
# Determine filename
|
|
108
|
+
filename = py_id(title.lower().replace(' ', '_')) + ".py"
|
|
109
|
+
|
|
110
|
+
# Extra imports
|
|
111
|
+
extra_imports = '\n'.join(self._extra_imports)
|
|
112
|
+
|
|
113
|
+
return APP_TEMPLATE.format(
|
|
114
|
+
title=title,
|
|
115
|
+
filename=filename,
|
|
116
|
+
extra_imports=extra_imports,
|
|
117
|
+
app_code=app_code,
|
|
118
|
+
app_class=app_class,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
# Process AST children (collect metadata)
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def _process_screen_children(self, children: List[QuantumNode]):
|
|
126
|
+
"""Walk screen children to collect state vars, functions, etc."""
|
|
127
|
+
for child in children:
|
|
128
|
+
if isinstance(child, SetNode):
|
|
129
|
+
self._state_vars.append({
|
|
130
|
+
'name': child.name,
|
|
131
|
+
'value': child.value,
|
|
132
|
+
'type': getattr(child, 'type', 'string'),
|
|
133
|
+
})
|
|
134
|
+
elif isinstance(child, FunctionNode):
|
|
135
|
+
self._functions.append(child)
|
|
136
|
+
elif isinstance(child, KeybindingNode):
|
|
137
|
+
self._keybindings.append({
|
|
138
|
+
'key': child.key,
|
|
139
|
+
'action': child.action,
|
|
140
|
+
'description': child.description or child.action,
|
|
141
|
+
})
|
|
142
|
+
elif isinstance(child, TimerNode_Terminal):
|
|
143
|
+
self._timers.append({
|
|
144
|
+
'id': child.timer_id,
|
|
145
|
+
'interval': child.interval,
|
|
146
|
+
'action': child.action,
|
|
147
|
+
})
|
|
148
|
+
elif isinstance(child, ServiceNode):
|
|
149
|
+
self._services.append({
|
|
150
|
+
'id': child.service_id,
|
|
151
|
+
'handler': child.handler,
|
|
152
|
+
})
|
|
153
|
+
elif isinstance(child, CssNode):
|
|
154
|
+
self._css_blocks.append(child.content)
|
|
155
|
+
elif isinstance(child, QueryNode):
|
|
156
|
+
self._queries.append({
|
|
157
|
+
'name': child.name,
|
|
158
|
+
'datasource': getattr(child, 'datasource', ''),
|
|
159
|
+
'sql': getattr(child, 'sql', ''),
|
|
160
|
+
})
|
|
161
|
+
if 'import sqlite3' not in self._extra_imports:
|
|
162
|
+
self._extra_imports.append('import sqlite3')
|
|
163
|
+
elif hasattr(child, 'children'):
|
|
164
|
+
self._process_screen_children(child.children)
|
|
165
|
+
|
|
166
|
+
# ------------------------------------------------------------------
|
|
167
|
+
# Build the App class
|
|
168
|
+
# ------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
def _build_app_class(self, py: PyBuilder, screens: List[ScreenNode], title: str):
|
|
171
|
+
"""Build the main Textual App class."""
|
|
172
|
+
app_class = py_id(title.replace(' ', '')) + "App"
|
|
173
|
+
|
|
174
|
+
py.blank()
|
|
175
|
+
py.class_def(app_class, 'App')
|
|
176
|
+
|
|
177
|
+
# TITLE
|
|
178
|
+
py.assign('TITLE', py_string(title))
|
|
179
|
+
|
|
180
|
+
# CSS
|
|
181
|
+
if self._css_blocks:
|
|
182
|
+
css_combined = '\n'.join(self._css_blocks)
|
|
183
|
+
py.assign('CSS', f'"""{css_combined}"""')
|
|
184
|
+
py.blank()
|
|
185
|
+
|
|
186
|
+
# BINDINGS
|
|
187
|
+
if self._keybindings:
|
|
188
|
+
bindings_list = []
|
|
189
|
+
for kb in self._keybindings:
|
|
190
|
+
desc = kb.get('description', kb['action'])
|
|
191
|
+
bindings_list.append(
|
|
192
|
+
f'Binding({py_string(kb["key"])}, {py_string(kb["action"])}, {py_string(desc)})'
|
|
193
|
+
)
|
|
194
|
+
py.assign('BINDINGS', f'[\n ' + ',\n '.join(bindings_list) + ',\n ]')
|
|
195
|
+
py.blank()
|
|
196
|
+
|
|
197
|
+
# Reactive state vars
|
|
198
|
+
for var in self._state_vars:
|
|
199
|
+
default = self._get_default_value(var)
|
|
200
|
+
py.assign(var['name'], f'reactive({default})')
|
|
201
|
+
if self._state_vars:
|
|
202
|
+
py.blank()
|
|
203
|
+
|
|
204
|
+
# compose() method
|
|
205
|
+
self._emit_compose(py, screens)
|
|
206
|
+
|
|
207
|
+
# on_mount() for timers
|
|
208
|
+
if self._timers:
|
|
209
|
+
py.blank()
|
|
210
|
+
py.method_def('on_mount', 'self')
|
|
211
|
+
py.docstring('Set up timers on app mount.')
|
|
212
|
+
for timer in self._timers:
|
|
213
|
+
action = timer['action']
|
|
214
|
+
interval = timer['interval']
|
|
215
|
+
py.line(f'self.set_interval({interval}, '
|
|
216
|
+
f'self.action_{py_id(split_call(action)[0])})')
|
|
217
|
+
py.end_block()
|
|
218
|
+
|
|
219
|
+
# Event handlers
|
|
220
|
+
self._emit_event_handlers(py)
|
|
221
|
+
|
|
222
|
+
# User-defined functions
|
|
223
|
+
self._emit_functions(py)
|
|
224
|
+
|
|
225
|
+
# Query functions
|
|
226
|
+
self._emit_queries(py)
|
|
227
|
+
|
|
228
|
+
# Action methods for keybindings/timers
|
|
229
|
+
self._emit_action_methods(py)
|
|
230
|
+
|
|
231
|
+
py.end_block() # Close class
|
|
232
|
+
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
# compose() - Widget tree
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
def _emit_compose(self, py: PyBuilder, screens: List[ScreenNode]):
|
|
238
|
+
"""Emit the compose() method that yields the widget tree."""
|
|
239
|
+
py.line('def compose(self) -> ComposeResult:')
|
|
240
|
+
py.indent()
|
|
241
|
+
py.docstring('Build the widget tree.')
|
|
242
|
+
|
|
243
|
+
# Use first screen as main compose
|
|
244
|
+
if screens:
|
|
245
|
+
screen = screens[0]
|
|
246
|
+
for child in screen.children:
|
|
247
|
+
self._emit_widget(py, child)
|
|
248
|
+
|
|
249
|
+
py.end_block()
|
|
250
|
+
|
|
251
|
+
def _emit_widget(self, py: PyBuilder, node: QuantumNode):
|
|
252
|
+
"""Emit a yield statement for a widget node."""
|
|
253
|
+
|
|
254
|
+
if isinstance(node, HeaderNode_Terminal):
|
|
255
|
+
if node.show_clock:
|
|
256
|
+
py.line('yield Header(show_clock=True)')
|
|
257
|
+
else:
|
|
258
|
+
py.line('yield Header()')
|
|
259
|
+
|
|
260
|
+
elif isinstance(node, FooterNode):
|
|
261
|
+
py.line('yield Footer()')
|
|
262
|
+
|
|
263
|
+
elif isinstance(node, TextNode_Terminal):
|
|
264
|
+
content = node.content or ''
|
|
265
|
+
id_part = f', id={py_string(node.text_id)}' if node.text_id else ''
|
|
266
|
+
py.line(f'yield Static({py_string(content)}{id_part})')
|
|
267
|
+
|
|
268
|
+
elif isinstance(node, ProgressNode):
|
|
269
|
+
parts = []
|
|
270
|
+
parts.append(f'total={node.total}')
|
|
271
|
+
if node.progress_id:
|
|
272
|
+
parts.append(f'id={py_string(node.progress_id)}')
|
|
273
|
+
py.line(f'yield ProgressBar({", ".join(parts)})')
|
|
274
|
+
|
|
275
|
+
elif isinstance(node, TerminalInputNode):
|
|
276
|
+
parts = []
|
|
277
|
+
if node.placeholder:
|
|
278
|
+
parts.append(f'placeholder={py_string(node.placeholder)}')
|
|
279
|
+
if node.password:
|
|
280
|
+
parts.append('password=True')
|
|
281
|
+
if node.input_id:
|
|
282
|
+
parts.append(f'id={py_string(node.input_id)}')
|
|
283
|
+
py.line(f'yield Input({", ".join(parts)})')
|
|
284
|
+
|
|
285
|
+
elif isinstance(node, ButtonNode):
|
|
286
|
+
parts = [py_string(node.label)]
|
|
287
|
+
if node.variant and node.variant != 'default':
|
|
288
|
+
parts.append(f'variant={py_string(node.variant)}')
|
|
289
|
+
if node.button_id:
|
|
290
|
+
parts.append(f'id={py_string(node.button_id)}')
|
|
291
|
+
py.line(f'yield Button({", ".join(parts)})')
|
|
292
|
+
|
|
293
|
+
elif isinstance(node, LogNode_Terminal):
|
|
294
|
+
parts = []
|
|
295
|
+
if not node.auto_scroll:
|
|
296
|
+
parts.append('auto_scroll=False')
|
|
297
|
+
if not node.markup:
|
|
298
|
+
parts.append('markup=False')
|
|
299
|
+
if node.max_lines:
|
|
300
|
+
parts.append(f'max_lines={node.max_lines}')
|
|
301
|
+
if node.log_id:
|
|
302
|
+
parts.append(f'id={py_string(node.log_id)}')
|
|
303
|
+
py.line(f'yield RichLog({", ".join(parts)})')
|
|
304
|
+
|
|
305
|
+
elif isinstance(node, TreeNode):
|
|
306
|
+
label = node.label or 'Root'
|
|
307
|
+
parts = [py_string(label)]
|
|
308
|
+
if node.tree_id:
|
|
309
|
+
parts.append(f'id={py_string(node.tree_id)}')
|
|
310
|
+
py.line(f'yield Tree({", ".join(parts)})')
|
|
311
|
+
|
|
312
|
+
elif isinstance(node, MenuNode):
|
|
313
|
+
parts = []
|
|
314
|
+
if node.menu_id:
|
|
315
|
+
parts.append(f'id={py_string(node.menu_id)}')
|
|
316
|
+
py.line(f'yield OptionList({", ".join(parts)})')
|
|
317
|
+
|
|
318
|
+
elif isinstance(node, StatusNode):
|
|
319
|
+
content = node.content or ''
|
|
320
|
+
id_part = f', id={py_string(node.status_id)}' if node.status_id else ''
|
|
321
|
+
py.line(f'yield Static({py_string(content)}{id_part})')
|
|
322
|
+
|
|
323
|
+
elif isinstance(node, TableNode):
|
|
324
|
+
self._emit_table_widget(py, node)
|
|
325
|
+
|
|
326
|
+
elif isinstance(node, PanelNode):
|
|
327
|
+
self._emit_container(py, node, 'Vertical')
|
|
328
|
+
|
|
329
|
+
elif isinstance(node, LayoutNode):
|
|
330
|
+
container = 'Horizontal' if node.direction == 'horizontal' else 'Vertical'
|
|
331
|
+
self._emit_container(py, node, container)
|
|
332
|
+
|
|
333
|
+
elif isinstance(node, TabsNode):
|
|
334
|
+
self._emit_tabs_widget(py, node)
|
|
335
|
+
|
|
336
|
+
# Skip non-widget nodes (SetNode, FunctionNode, etc.)
|
|
337
|
+
|
|
338
|
+
def _emit_container(self, py: PyBuilder, node, container_type: str):
|
|
339
|
+
"""Emit a container widget (Vertical/Horizontal) with children."""
|
|
340
|
+
node_id = getattr(node, 'panel_id', None) or getattr(node, 'layout_id', None)
|
|
341
|
+
id_part = f'id={py_string(node_id)}' if node_id else ''
|
|
342
|
+
py.line(f'with {container_type}({id_part}):')
|
|
343
|
+
py.indent()
|
|
344
|
+
|
|
345
|
+
has_children = False
|
|
346
|
+
for child in node.children:
|
|
347
|
+
if self._is_widget_node(child):
|
|
348
|
+
self._emit_widget(py, child)
|
|
349
|
+
has_children = True
|
|
350
|
+
|
|
351
|
+
if not has_children:
|
|
352
|
+
py.pass_stmt()
|
|
353
|
+
py.end_block()
|
|
354
|
+
|
|
355
|
+
def _emit_table_widget(self, py: PyBuilder, node: TableNode):
|
|
356
|
+
"""Emit DataTable setup."""
|
|
357
|
+
id_part = f'id={py_string(node.table_id)}' if node.table_id else ''
|
|
358
|
+
if node.zebra:
|
|
359
|
+
if id_part:
|
|
360
|
+
id_part += ', '
|
|
361
|
+
id_part += 'zebra_stripes=True'
|
|
362
|
+
py.line(f'yield DataTable({id_part})')
|
|
363
|
+
|
|
364
|
+
def _emit_tabs_widget(self, py: PyBuilder, node: TabsNode):
|
|
365
|
+
"""Emit TabbedContent with TabPanes."""
|
|
366
|
+
id_part = f'id={py_string(node.tabs_id)}' if node.tabs_id else ''
|
|
367
|
+
py.line(f'with TabbedContent({id_part}):')
|
|
368
|
+
py.indent()
|
|
369
|
+
|
|
370
|
+
for tab in node.tabs:
|
|
371
|
+
tab_id_part = f', id={py_string(tab.tab_id)}' if tab.tab_id else ''
|
|
372
|
+
py.line(f'with TabPane({py_string(tab.title)}{tab_id_part}):')
|
|
373
|
+
py.indent()
|
|
374
|
+
has_children = False
|
|
375
|
+
for child in tab.children:
|
|
376
|
+
if self._is_widget_node(child):
|
|
377
|
+
self._emit_widget(py, child)
|
|
378
|
+
has_children = True
|
|
379
|
+
if not has_children:
|
|
380
|
+
py.pass_stmt()
|
|
381
|
+
py.end_block()
|
|
382
|
+
|
|
383
|
+
if not node.tabs:
|
|
384
|
+
py.pass_stmt()
|
|
385
|
+
py.end_block()
|
|
386
|
+
|
|
387
|
+
def _is_widget_node(self, node: QuantumNode) -> bool:
|
|
388
|
+
"""Check if a node produces a widget."""
|
|
389
|
+
return isinstance(node, (
|
|
390
|
+
HeaderNode_Terminal, FooterNode, TextNode_Terminal, ProgressNode,
|
|
391
|
+
TerminalInputNode, ButtonNode, LogNode_Terminal, TreeNode,
|
|
392
|
+
MenuNode, StatusNode, TableNode, PanelNode, LayoutNode,
|
|
393
|
+
TabsNode,
|
|
394
|
+
))
|
|
395
|
+
|
|
396
|
+
# ------------------------------------------------------------------
|
|
397
|
+
# Event handlers
|
|
398
|
+
# ------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
def _emit_event_handlers(self, py: PyBuilder):
|
|
401
|
+
"""Emit Textual event handler methods (on_button_pressed, on_input_submitted)."""
|
|
402
|
+
# Collect button on_click handlers
|
|
403
|
+
button_handlers = self._collect_handlers_of_type(ButtonNode, 'on_click')
|
|
404
|
+
input_handlers = self._collect_handlers_of_type(TerminalInputNode, 'on_submit')
|
|
405
|
+
|
|
406
|
+
if button_handlers:
|
|
407
|
+
py.blank()
|
|
408
|
+
py.method_def('on_button_pressed', 'self, event: Button.Pressed')
|
|
409
|
+
py.docstring('Handle button press events.')
|
|
410
|
+
first = True
|
|
411
|
+
for btn_id, action in button_handlers:
|
|
412
|
+
if btn_id:
|
|
413
|
+
cond = f'event.button.id == {py_string(btn_id)}'
|
|
414
|
+
if first:
|
|
415
|
+
py.if_block(cond)
|
|
416
|
+
first = False
|
|
417
|
+
else:
|
|
418
|
+
py.elif_block(cond)
|
|
419
|
+
py.line(f'self.action_{py_id(split_call(action)[0])}()')
|
|
420
|
+
if not first:
|
|
421
|
+
py.end_block()
|
|
422
|
+
py.end_block()
|
|
423
|
+
|
|
424
|
+
if input_handlers:
|
|
425
|
+
py.blank()
|
|
426
|
+
py.method_def('on_input_submitted', 'self, event: Input.Submitted')
|
|
427
|
+
py.docstring('Handle input submit events.')
|
|
428
|
+
first = True
|
|
429
|
+
for input_id, action in input_handlers:
|
|
430
|
+
if input_id:
|
|
431
|
+
cond = f'event.input.id == {py_string(input_id)}'
|
|
432
|
+
if first:
|
|
433
|
+
py.if_block(cond)
|
|
434
|
+
first = False
|
|
435
|
+
else:
|
|
436
|
+
py.elif_block(cond)
|
|
437
|
+
py.line(f'self.action_{py_id(split_call(action)[0])}()')
|
|
438
|
+
if not first:
|
|
439
|
+
py.end_block()
|
|
440
|
+
py.end_block()
|
|
441
|
+
|
|
442
|
+
def _collect_handlers_of_type(self, node_type, attr_name: str) -> List[tuple]:
|
|
443
|
+
"""Walk all screens to collect (id, handler) pairs for a widget type."""
|
|
444
|
+
handlers = []
|
|
445
|
+
for screen in self._screens:
|
|
446
|
+
self._walk_for_handlers(screen.children, node_type, attr_name, handlers)
|
|
447
|
+
return handlers
|
|
448
|
+
|
|
449
|
+
def _walk_for_handlers(self, children, node_type, attr_name, handlers):
|
|
450
|
+
for child in children:
|
|
451
|
+
if isinstance(child, node_type):
|
|
452
|
+
handler = getattr(child, attr_name, None)
|
|
453
|
+
node_id = getattr(child, 'button_id', None) or getattr(child, 'input_id', None)
|
|
454
|
+
if handler:
|
|
455
|
+
handlers.append((node_id, handler))
|
|
456
|
+
if hasattr(child, 'children'):
|
|
457
|
+
self._walk_for_handlers(child.children, node_type, attr_name, handlers)
|
|
458
|
+
if isinstance(child, TabsNode):
|
|
459
|
+
for tab in child.tabs:
|
|
460
|
+
self._walk_for_handlers(tab.children, node_type, attr_name, handlers)
|
|
461
|
+
if isinstance(child, TableNode):
|
|
462
|
+
pass # No children to walk
|
|
463
|
+
|
|
464
|
+
# ------------------------------------------------------------------
|
|
465
|
+
# User functions
|
|
466
|
+
# ------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
def _emit_functions(self, py: PyBuilder):
|
|
469
|
+
"""Emit user-defined q:function nodes as app methods."""
|
|
470
|
+
for func in self._functions:
|
|
471
|
+
py.blank()
|
|
472
|
+
params = 'self'
|
|
473
|
+
if hasattr(func, 'params') and func.params:
|
|
474
|
+
param_names = ', '.join(p.name for p in func.params)
|
|
475
|
+
params = f'self, {param_names}'
|
|
476
|
+
|
|
477
|
+
py.method_def(py_id(func.name), params)
|
|
478
|
+
|
|
479
|
+
if hasattr(func, 'body') and func.body:
|
|
480
|
+
for stmt in func.body:
|
|
481
|
+
if isinstance(stmt, RawCodeNode_Terminal):
|
|
482
|
+
py.line(stmt.code)
|
|
483
|
+
elif isinstance(stmt, SetNode):
|
|
484
|
+
py.line(f'self.{py_id(stmt.name)} = {self._format_value(stmt.value, getattr(stmt, "type", "string"))}')
|
|
485
|
+
else:
|
|
486
|
+
py.comment(f'TODO: {type(stmt).__name__}')
|
|
487
|
+
else:
|
|
488
|
+
py.pass_stmt()
|
|
489
|
+
|
|
490
|
+
py.end_block()
|
|
491
|
+
|
|
492
|
+
# ------------------------------------------------------------------
|
|
493
|
+
# Query functions
|
|
494
|
+
# ------------------------------------------------------------------
|
|
495
|
+
|
|
496
|
+
def _emit_queries(self, py: PyBuilder):
|
|
497
|
+
"""Emit q:query nodes as database access methods."""
|
|
498
|
+
for query in self._queries:
|
|
499
|
+
py.blank()
|
|
500
|
+
py.method_def(f'query_{py_id(query["name"])}', 'self')
|
|
501
|
+
py.docstring(f'Execute query: {query["name"]}')
|
|
502
|
+
ds = query.get('datasource', 'app.db')
|
|
503
|
+
sql = query.get('sql', '').strip()
|
|
504
|
+
py.line(f'conn = sqlite3.connect({py_string(ds)})')
|
|
505
|
+
py.line('cursor = conn.cursor()')
|
|
506
|
+
py.line(f'cursor.execute({py_string(sql)})')
|
|
507
|
+
py.line('columns = [desc[0] for desc in cursor.description] if cursor.description else []')
|
|
508
|
+
py.line('rows = [dict(zip(columns, row)) for row in cursor.fetchall()]')
|
|
509
|
+
py.line('conn.close()')
|
|
510
|
+
py.return_stmt('rows')
|
|
511
|
+
py.end_block()
|
|
512
|
+
|
|
513
|
+
# ------------------------------------------------------------------
|
|
514
|
+
# Action methods for keybindings & timers
|
|
515
|
+
# ------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
def _emit_action_methods(self, py: PyBuilder):
|
|
518
|
+
"""Emit action_ methods for keybindings and timers."""
|
|
519
|
+
emitted = set()
|
|
520
|
+
|
|
521
|
+
# Built-in actions don't need methods
|
|
522
|
+
builtin_actions = {'quit', 'toggle_dark', 'bell', 'screenshot'}
|
|
523
|
+
|
|
524
|
+
all_actions = set()
|
|
525
|
+
for kb in self._keybindings:
|
|
526
|
+
all_actions.add(kb['action'])
|
|
527
|
+
for timer in self._timers:
|
|
528
|
+
all_actions.add(timer['action'])
|
|
529
|
+
|
|
530
|
+
# Collect actions from button/input handlers
|
|
531
|
+
button_handlers = self._collect_handlers_of_type(ButtonNode, 'on_click')
|
|
532
|
+
input_handlers = self._collect_handlers_of_type(TerminalInputNode, 'on_submit')
|
|
533
|
+
for _, action in button_handlers:
|
|
534
|
+
all_actions.add(action)
|
|
535
|
+
for _, action in input_handlers:
|
|
536
|
+
all_actions.add(action)
|
|
537
|
+
|
|
538
|
+
# Check which actions already have a user function
|
|
539
|
+
user_func_names = {f.name for f in self._functions}
|
|
540
|
+
|
|
541
|
+
for action in sorted(all_actions):
|
|
542
|
+
# `on-click="salvar()"` e a mesma acao que `on-click="salvar"`.
|
|
543
|
+
#
|
|
544
|
+
# Antes o texto CRU virava nome de metodo: `py_id("salvar()")`
|
|
545
|
+
# devolvia `salvar()` e o gerador escrevia
|
|
546
|
+
# `def action_salvar()(self):` — SyntaxError, o app inteiro
|
|
547
|
+
# deixava de importar por causa de um botao. E a comparacao
|
|
548
|
+
# `action in user_func_names` nunca casava com parenteses, entao
|
|
549
|
+
# nem a q:function certa era chamada.
|
|
550
|
+
nome, args = split_call(action)
|
|
551
|
+
if nome in builtin_actions:
|
|
552
|
+
continue
|
|
553
|
+
if nome in emitted:
|
|
554
|
+
continue
|
|
555
|
+
emitted.add(nome)
|
|
556
|
+
|
|
557
|
+
py.blank()
|
|
558
|
+
safe_action = py_id(nome)
|
|
559
|
+
|
|
560
|
+
# If user defined a function with this name, the action calls it
|
|
561
|
+
if nome in user_func_names:
|
|
562
|
+
py.method_def(f'action_{safe_action}', 'self')
|
|
563
|
+
py.docstring(f'Action: {action}')
|
|
564
|
+
py.line(f'self.{safe_action}({args or ""})')
|
|
565
|
+
py.end_block()
|
|
566
|
+
else:
|
|
567
|
+
py.method_def(f'action_{safe_action}', 'self')
|
|
568
|
+
py.docstring(f'Action: {action}')
|
|
569
|
+
py.line(f'self.notify({py_string(f"Action: {nome}")})')
|
|
570
|
+
py.end_block()
|
|
571
|
+
|
|
572
|
+
# ------------------------------------------------------------------
|
|
573
|
+
# Helpers
|
|
574
|
+
# ------------------------------------------------------------------
|
|
575
|
+
|
|
576
|
+
def _get_default_value(self, var: Dict) -> str:
|
|
577
|
+
"""Convert a q:set variable to its Python reactive default."""
|
|
578
|
+
val = var.get('value', '')
|
|
579
|
+
vtype = var.get('type', 'string')
|
|
580
|
+
return self._format_value(val, vtype)
|
|
581
|
+
|
|
582
|
+
def _format_value(self, val: str, vtype: str) -> str:
|
|
583
|
+
"""Format a value string according to its type for Python code."""
|
|
584
|
+
if vtype == 'integer':
|
|
585
|
+
try:
|
|
586
|
+
return str(int(val)) if val else '0'
|
|
587
|
+
except (ValueError, TypeError):
|
|
588
|
+
return '0'
|
|
589
|
+
elif vtype == 'number' or vtype == 'float':
|
|
590
|
+
try:
|
|
591
|
+
return str(float(val)) if val else '0.0'
|
|
592
|
+
except (ValueError, TypeError):
|
|
593
|
+
return '0.0'
|
|
594
|
+
elif vtype == 'boolean':
|
|
595
|
+
if isinstance(val, str):
|
|
596
|
+
return 'True' if val.lower() in ('true', '1', 'yes') else 'False'
|
|
597
|
+
return py_bool(bool(val))
|
|
598
|
+
elif vtype == 'array':
|
|
599
|
+
if val and val.strip().startswith('['):
|
|
600
|
+
return val
|
|
601
|
+
return '[]'
|
|
602
|
+
elif vtype == 'object':
|
|
603
|
+
if val and val.strip().startswith('{'):
|
|
604
|
+
return val
|
|
605
|
+
return '{}'
|
|
606
|
+
else:
|
|
607
|
+
return py_string(val) if val else py_string('')
|