aria-code 4.1.3__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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- workspace/verify.py +113 -0
ui/completer.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""prompt_toolkit completer and base style for the Aria REPL.
|
|
2
|
+
|
|
3
|
+
Improvements over the original:
|
|
4
|
+
- Instant popup: triggers as soon as "/" is typed (no extra keypress needed)
|
|
5
|
+
- Fuzzy match: "/ch" matches /chart, /check, /watch; "/ta" matches /stat-arb
|
|
6
|
+
- Matched chars highlighted in amber so they're visible in all rows
|
|
7
|
+
- Results ranked: exact-prefix > fuzzy-command > description-fuzzy
|
|
8
|
+
- Category tag shown in display_meta (市场 / 分析 / 量化 / 数据源 …)
|
|
9
|
+
|
|
10
|
+
from ui.completer import AriaPTCompleter, ARIA_PT_STYLE
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Iterator, List, Tuple
|
|
16
|
+
|
|
17
|
+
from ui.console import HAS_PT
|
|
18
|
+
|
|
19
|
+
# ── Category map ────────────────────────────────────────────────────────────
|
|
20
|
+
# Keyed on command name fragments; first match wins.
|
|
21
|
+
_CATS: List[Tuple[Tuple[str, ...], str]] = [
|
|
22
|
+
(("/quote", "/market", "/macro", "/watch", "/alert", "/hot", "/indices",
|
|
23
|
+
"/cn", "/hk", "/crypto", "/forex", "/commodity", "/funding", "/feargreed",
|
|
24
|
+
"/edgar", "/datasource"), "市场"),
|
|
25
|
+
(("/team", "/analyze", "/options", "/factor", "/ta", "/ichimoku",
|
|
26
|
+
"/peer", "/quality", "/risk", "/signal", "/predict", "/earnings",
|
|
27
|
+
"/insights", "/deep", "/morning", "/trade-idea", "/research"), "分析"),
|
|
28
|
+
(("/backtest", "/wf", "/compare", "/execution", "/stat-arb",
|
|
29
|
+
"/ptbt", "/corr", "/optimize", "/stress", "/auto-strategy",
|
|
30
|
+
"/portfolio", "/journal"), "量化"),
|
|
31
|
+
(("/chart", "/report", "/shortterm", "/longterm", "/cloudbt"), "图表"),
|
|
32
|
+
(("/project", "/file", "/run", "/code", "/scaffold", "/init",
|
|
33
|
+
"/review", "/vision", "/browser", "/web"), "工具"),
|
|
34
|
+
(("/config", "/model", "/apikey", "/setup", "/local", "/mcp",
|
|
35
|
+
"/memory", "/cost", "/version"), "设置"),
|
|
36
|
+
(("/help", "/clear", "/btw", "/recap", "/exit", "/quit", "/history", "/session",
|
|
37
|
+
"/bug", "/feedback", "/privacy"), "系统"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
_CAT_BADGE: dict[str, str] = {
|
|
41
|
+
"市场": "mkt",
|
|
42
|
+
"分析": "ana",
|
|
43
|
+
"量化": "qnt",
|
|
44
|
+
"图表": "viz",
|
|
45
|
+
"工具": "dev",
|
|
46
|
+
"设置": "cfg",
|
|
47
|
+
"系统": "sys",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_cat(name: str) -> str:
|
|
52
|
+
for prefixes, cat in _CATS:
|
|
53
|
+
for p in prefixes:
|
|
54
|
+
if name.startswith(p):
|
|
55
|
+
return cat
|
|
56
|
+
return ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── Fuzzy matching ───────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def _fuzzy(pattern: str, text: str) -> Tuple[bool, List[int]]:
|
|
62
|
+
"""
|
|
63
|
+
Sequential fuzzy match — pattern chars must appear in order in text.
|
|
64
|
+
Returns (matched, indices_of_matched_chars_in_text).
|
|
65
|
+
Consecutive matches score higher because they produce a compact index list.
|
|
66
|
+
"""
|
|
67
|
+
if not pattern:
|
|
68
|
+
return True, []
|
|
69
|
+
pi = 0
|
|
70
|
+
indices: List[int] = []
|
|
71
|
+
for i, ch in enumerate(text):
|
|
72
|
+
if ch.lower() == pattern[pi].lower():
|
|
73
|
+
indices.append(i)
|
|
74
|
+
pi += 1
|
|
75
|
+
if pi == len(pattern):
|
|
76
|
+
return True, indices
|
|
77
|
+
return False, []
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _score(name: str, pattern: str, indices: List[int]) -> int:
|
|
81
|
+
"""
|
|
82
|
+
Lower score = better.
|
|
83
|
+
0 exact match
|
|
84
|
+
1 exact prefix (/ch → /chart)
|
|
85
|
+
5 word-segment exact match (/arb → /stat-arb because '-arb' segment)
|
|
86
|
+
10 consecutive run from position 0
|
|
87
|
+
12+ consecutive run from a word boundary
|
|
88
|
+
15+ consecutive run from elsewhere
|
|
89
|
+
20+ non-consecutive from a word boundary
|
|
90
|
+
30+ scattered fuzzy match
|
|
91
|
+
"""
|
|
92
|
+
if name == pattern:
|
|
93
|
+
return 0
|
|
94
|
+
if name.startswith(pattern):
|
|
95
|
+
return 1
|
|
96
|
+
if not indices:
|
|
97
|
+
return 99
|
|
98
|
+
|
|
99
|
+
# Word-segment exact match: pattern matches a whole segment after "-" or "_"
|
|
100
|
+
bare = name.lstrip("/")
|
|
101
|
+
pat_bare = pattern.lstrip("/")
|
|
102
|
+
for sep in ("-", "_"):
|
|
103
|
+
for seg in bare.split(sep)[1:]: # skip first segment (already caught by prefix)
|
|
104
|
+
if seg == pat_bare or seg.startswith(pat_bare):
|
|
105
|
+
return 5 + len(sep) # score 6 for "-", 6 for "_"
|
|
106
|
+
|
|
107
|
+
start = indices[0]
|
|
108
|
+
consecutive = (indices == list(range(start, start + len(indices))))
|
|
109
|
+
at_boundary = start == 0 or (start > 0 and bare[start - 1] in "-_")
|
|
110
|
+
|
|
111
|
+
if consecutive:
|
|
112
|
+
if start == 0:
|
|
113
|
+
return 10
|
|
114
|
+
if at_boundary:
|
|
115
|
+
return 12 + start
|
|
116
|
+
return 15 + start
|
|
117
|
+
if at_boundary:
|
|
118
|
+
return 20 + start
|
|
119
|
+
return 30 + start
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── FormattedText display builder ────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
def _highlighted(name: str, matched_indices: List[int]) -> List[Tuple[str, str]]:
|
|
125
|
+
"""
|
|
126
|
+
Build a FormattedText list: matched chars get 'class:fz-hi', rest are plain.
|
|
127
|
+
"""
|
|
128
|
+
idx_set = set(matched_indices)
|
|
129
|
+
parts: List[Tuple[str, str]] = []
|
|
130
|
+
for i, ch in enumerate(name):
|
|
131
|
+
if i in idx_set:
|
|
132
|
+
parts.append(("class:fz-hi", ch))
|
|
133
|
+
else:
|
|
134
|
+
parts.append(("", ch))
|
|
135
|
+
return parts
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if HAS_PT:
|
|
139
|
+
import os as _os
|
|
140
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
141
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
142
|
+
from prompt_toolkit.styles import Style as PTStyle
|
|
143
|
+
|
|
144
|
+
class AriaPTCompleter(Completer):
|
|
145
|
+
"""
|
|
146
|
+
Slash-command completer with instant popup + fuzzy search.
|
|
147
|
+
|
|
148
|
+
Activates the moment the user types "/" (complete_while_typing=True
|
|
149
|
+
is already set in PromptSession, so no extra keypress is needed).
|
|
150
|
+
|
|
151
|
+
Triggers:
|
|
152
|
+
/ → show ALL slash commands (fuzzy matched)
|
|
153
|
+
@ → file/directory path autocomplete (@ anywhere in input)
|
|
154
|
+
! → shell history autocomplete (first word after !)
|
|
155
|
+
|
|
156
|
+
Matching:
|
|
157
|
+
/ → show ALL commands sorted by category order
|
|
158
|
+
/ch → fuzzy-match "ch" against command names, highlight hits
|
|
159
|
+
/stat → matches /stat-arb even though it's not a prefix
|
|
160
|
+
/team AAPL → only complete the command part (stop after first space)
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(self, commands_dict: dict, skills: list, watchlist: list):
|
|
164
|
+
self.commands = commands_dict
|
|
165
|
+
self.skills = skills
|
|
166
|
+
self._shell_history: list[str] = [] # populated by REPL after ! commands
|
|
167
|
+
self.symbols = sorted(set([
|
|
168
|
+
"AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "NVDA", "META",
|
|
169
|
+
"NFLX", "AMD", "INTC", "SPY", "QQQ", "DIA", "IWM",
|
|
170
|
+
"BTC-USD", "ETH-USD", "SOL-USD",
|
|
171
|
+
"JPM", "BAC", "GS", "V", "MA", "UNH", "JNJ", "XOM",
|
|
172
|
+
"GLD", "SLV", "USO", "TLT", "HYG",
|
|
173
|
+
] + list(watchlist)))
|
|
174
|
+
|
|
175
|
+
# Pre-compute category for each command
|
|
176
|
+
self._cmd_cat: dict[str, str] = {}
|
|
177
|
+
for name in self.commands:
|
|
178
|
+
self._cmd_cat[name] = _get_cat(name)
|
|
179
|
+
for s in self.skills:
|
|
180
|
+
self._cmd_cat[s["command"]] = _get_cat(s["command"])
|
|
181
|
+
|
|
182
|
+
def get_completions(self, document, complete_event) -> Iterator[Completion]:
|
|
183
|
+
text = document.text_before_cursor
|
|
184
|
+
ltext = text.lstrip()
|
|
185
|
+
|
|
186
|
+
# ── @ file path autocomplete ────────────────────────────────────
|
|
187
|
+
# Triggered any time text contains "@" — complete path after it.
|
|
188
|
+
at_idx = text.rfind("@")
|
|
189
|
+
if at_idx != -1:
|
|
190
|
+
path_frag = text[at_idx + 1:]
|
|
191
|
+
# Only complete if fragment has no spaces (i.e. contiguous word)
|
|
192
|
+
if " " not in path_frag:
|
|
193
|
+
yield from self._file_completions(path_frag, at_idx + 1)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# ── ! shell command autocomplete ────────────────────────────────
|
|
197
|
+
if ltext.startswith("!"):
|
|
198
|
+
shell_frag = ltext[1:].lstrip()
|
|
199
|
+
if shell_frag:
|
|
200
|
+
for hist_cmd in reversed(self._shell_history):
|
|
201
|
+
if hist_cmd.startswith(shell_frag) and hist_cmd != shell_frag:
|
|
202
|
+
yield Completion(
|
|
203
|
+
hist_cmd,
|
|
204
|
+
start_position=-(len(ltext) - 1),
|
|
205
|
+
display=FormattedText([("class:fz-hi", hist_cmd)]),
|
|
206
|
+
display_meta="shell history",
|
|
207
|
+
)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Only activate for slash commands
|
|
211
|
+
if not ltext.startswith("/"):
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# Don't complete after first space — user is typing arguments
|
|
215
|
+
if " " in ltext:
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
# The typed prefix after "/"
|
|
219
|
+
pattern = ltext # includes leading "/"
|
|
220
|
+
|
|
221
|
+
# --- Build candidate list with scores ---
|
|
222
|
+
candidates: list[tuple[int, str, list[int], str, str]] = []
|
|
223
|
+
# (score, name, matched_indices, desc, category)
|
|
224
|
+
|
|
225
|
+
all_cmds = list(self.commands.items())
|
|
226
|
+
for name, (_, desc) in all_cmds:
|
|
227
|
+
cmd_part = name # e.g. "/chart"
|
|
228
|
+
matched, indices = _fuzzy(pattern.lstrip("/"), cmd_part.lstrip("/"))
|
|
229
|
+
if not matched and pattern != "/":
|
|
230
|
+
# Also try matching with the slash included
|
|
231
|
+
matched2, indices2 = _fuzzy(pattern, cmd_part)
|
|
232
|
+
if not matched2:
|
|
233
|
+
continue
|
|
234
|
+
indices = indices2
|
|
235
|
+
score = _score(cmd_part, pattern, indices)
|
|
236
|
+
cat = self._cmd_cat.get(name, "")
|
|
237
|
+
candidates.append((score, name, indices, desc, cat))
|
|
238
|
+
|
|
239
|
+
for s in self.skills:
|
|
240
|
+
cmd = s["command"]
|
|
241
|
+
desc = s.get("description", "")
|
|
242
|
+
matched, indices = _fuzzy(pattern.lstrip("/"), cmd.lstrip("/"))
|
|
243
|
+
if not matched and pattern != "/":
|
|
244
|
+
continue
|
|
245
|
+
score = _score(cmd, pattern, indices)
|
|
246
|
+
cat = self._cmd_cat.get(cmd, "")
|
|
247
|
+
candidates.append((score, cmd, indices, desc, cat))
|
|
248
|
+
|
|
249
|
+
# Sort: primary = score, secondary = name
|
|
250
|
+
candidates.sort(key=lambda x: (x[0], x[1]))
|
|
251
|
+
|
|
252
|
+
# Emit Completions
|
|
253
|
+
for score, name, indices, desc, cat in candidates:
|
|
254
|
+
# Build highlighted display (amber on matched chars)
|
|
255
|
+
display_parts = _highlighted(name, indices if pattern != "/" else [])
|
|
256
|
+
|
|
257
|
+
# Category badge as short suffix in display
|
|
258
|
+
badge = _CAT_BADGE.get(cat, "")
|
|
259
|
+
if badge:
|
|
260
|
+
display_parts += [("class:fz-cat", f" {badge}")]
|
|
261
|
+
|
|
262
|
+
# Truncate description to ~45 chars for meta column
|
|
263
|
+
meta_str = desc[:45] + ("…" if len(desc) > 45 else "")
|
|
264
|
+
|
|
265
|
+
# start_position: replace the entire typed prefix
|
|
266
|
+
start = -len(pattern)
|
|
267
|
+
|
|
268
|
+
yield Completion(
|
|
269
|
+
text = name,
|
|
270
|
+
start_position = start,
|
|
271
|
+
display = FormattedText(display_parts),
|
|
272
|
+
display_meta = meta_str,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def _file_completions(self, frag: str, cursor_offset: int) -> Iterator[Completion]:
|
|
276
|
+
"""Yield file/directory completions for @<frag>."""
|
|
277
|
+
try:
|
|
278
|
+
if frag.startswith("~"):
|
|
279
|
+
frag = _os.path.expanduser(frag)
|
|
280
|
+
base_dir = _os.path.dirname(frag) or "."
|
|
281
|
+
prefix = _os.path.basename(frag)
|
|
282
|
+
if not _os.path.isdir(base_dir):
|
|
283
|
+
return
|
|
284
|
+
for entry in sorted(_os.listdir(base_dir))[:40]:
|
|
285
|
+
if entry.startswith("."):
|
|
286
|
+
continue
|
|
287
|
+
if not entry.lower().startswith(prefix.lower()):
|
|
288
|
+
continue
|
|
289
|
+
full = _os.path.join(base_dir, entry) if base_dir != "." else entry
|
|
290
|
+
is_dir = _os.path.isdir(_os.path.join(base_dir, entry))
|
|
291
|
+
display_parts = [("class:fz-hi", prefix), ("", entry[len(prefix):])]
|
|
292
|
+
if is_dir:
|
|
293
|
+
display_parts.append(("class:fz-cat", "/"))
|
|
294
|
+
yield Completion(
|
|
295
|
+
full + ("/" if is_dir else ""),
|
|
296
|
+
start_position=-len(frag),
|
|
297
|
+
display=FormattedText(display_parts),
|
|
298
|
+
display_meta="dir" if is_dir else "file",
|
|
299
|
+
)
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
|
|
303
|
+
def add_shell_history(self, cmd: str) -> None:
|
|
304
|
+
"""Called by REPL after each ! command to update shell autocomplete."""
|
|
305
|
+
cmd = cmd.strip()
|
|
306
|
+
if cmd and cmd not in self._shell_history:
|
|
307
|
+
self._shell_history.append(cmd)
|
|
308
|
+
if len(self._shell_history) > 200:
|
|
309
|
+
self._shell_history = self._shell_history[-200:]
|
|
310
|
+
|
|
311
|
+
# ── Style ────────────────────────────────────────────────────────────────
|
|
312
|
+
# Theme-aware completion menu in the 5-color palette.
|
|
313
|
+
# Selection + fuzzy highlight use copper (brand); the menu surface
|
|
314
|
+
# matches the terminal theme so it never floats as a dark popup on a
|
|
315
|
+
# light terminal (or vice-versa).
|
|
316
|
+
|
|
317
|
+
# Copper-palette menu colors per theme.
|
|
318
|
+
# bg = menu surface (sits clearly above terminal bg)
|
|
319
|
+
# fg = row text
|
|
320
|
+
# sel_bg = selected row (copper tint)
|
|
321
|
+
# sel_fg = selected row text (copper, bold-applied at use site)
|
|
322
|
+
# meta = description column (dim)
|
|
323
|
+
# hi = fuzzy-matched chars (copper)
|
|
324
|
+
_MENU_THEMES = {
|
|
325
|
+
"dark": dict(
|
|
326
|
+
bg="#161b22", fg="#c9d1d9",
|
|
327
|
+
sel_bg="#3a2e20", sel_fg="#e8c9a6",
|
|
328
|
+
meta="#6e7681", meta_cur="#c0a585",
|
|
329
|
+
scroll_bg="#161b22", scroll_btn="#C08050", # copper position handle
|
|
330
|
+
hi="#C08050", cat="#6e7681",
|
|
331
|
+
base_bg="#0d1117", prompt="#8b949e", ph="#484f58",
|
|
332
|
+
tb_fg="#8b949e", tb_bg="#161b22",
|
|
333
|
+
),
|
|
334
|
+
"light": dict(
|
|
335
|
+
bg="#f2eee4", fg="#24292f", # warm surface, high contrast
|
|
336
|
+
sel_bg="#e7e1d3", sel_fg="#8a5a00", # stronger copper selection
|
|
337
|
+
meta="#6e7781", meta_cur="#8a5a00",
|
|
338
|
+
scroll_bg="#e7e1d3", scroll_btn="#9a6700", # copper position handle
|
|
339
|
+
hi="#9a6700", cat="#6e7781",
|
|
340
|
+
base_bg="default", prompt="#57606a", ph="#6e7781",
|
|
341
|
+
tb_fg="#57606a", tb_bg="#e7e1d3",
|
|
342
|
+
),
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
def _detect_theme() -> str:
|
|
346
|
+
try:
|
|
347
|
+
from ui.input_box import detect_terminal_theme
|
|
348
|
+
return detect_terminal_theme()
|
|
349
|
+
except Exception:
|
|
350
|
+
return "dark"
|
|
351
|
+
|
|
352
|
+
def build_aria_pt_style(theme: str = "auto") -> "PTStyle":
|
|
353
|
+
"""Build a theme-aware PromptSession style in the copper palette."""
|
|
354
|
+
if theme == "auto":
|
|
355
|
+
theme = _detect_theme()
|
|
356
|
+
c = _MENU_THEMES.get(theme, _MENU_THEMES["dark"])
|
|
357
|
+
base = f"{c['fg']} bg:{c['base_bg']}" if c["base_bg"] != "default" else c["fg"]
|
|
358
|
+
return PTStyle.from_dict({
|
|
359
|
+
"": base,
|
|
360
|
+
"prompt": c["prompt"],
|
|
361
|
+
"placeholder": c["ph"],
|
|
362
|
+
"input-bg": base,
|
|
363
|
+
"bottom-toolbar": f"noreverse {c['tb_fg']} bg:{c['tb_bg']}",
|
|
364
|
+
"bottom-toolbar.text":f"noreverse {c['tb_fg']} bg:{c['tb_bg']}",
|
|
365
|
+
|
|
366
|
+
# Completion menu — theme-aware surface, copper selection
|
|
367
|
+
"completion-menu": f"bg:{c['bg']} {c['fg']}",
|
|
368
|
+
"completion-menu.completion": f"bg:{c['bg']} {c['fg']}",
|
|
369
|
+
"completion-menu.completion.current": f"bg:{c['sel_bg']} {c['sel_fg']} bold",
|
|
370
|
+
"completion-menu.meta.completion": f"bg:{c['bg']} {c['meta']}",
|
|
371
|
+
"completion-menu.meta.completion.current": f"bg:{c['sel_bg']} {c['meta_cur']}",
|
|
372
|
+
"completion-menu.multi-column-meta": f"bg:{c['bg']} {c['meta']}",
|
|
373
|
+
"scrollbar.background": f"bg:{c['scroll_bg']}",
|
|
374
|
+
"scrollbar.button": f"bg:{c['scroll_btn']}",
|
|
375
|
+
|
|
376
|
+
# Fuzzy highlight classes — copper
|
|
377
|
+
"fz-hi": f"bold {c['hi']}",
|
|
378
|
+
"fz-cat": c["cat"],
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
# Back-compat default (dark). Prefer build_aria_pt_style(theme) at call sites.
|
|
382
|
+
ARIA_PT_STYLE = build_aria_pt_style("dark")
|
|
383
|
+
|
|
384
|
+
else:
|
|
385
|
+
def build_aria_pt_style(theme: str = "auto"): # type: ignore
|
|
386
|
+
return None
|
|
387
|
+
class AriaPTCompleter: # type: ignore
|
|
388
|
+
def __init__(self, *a, **kw): pass
|
|
389
|
+
def get_completions(self, *a, **kw): return iter([])
|
|
390
|
+
|
|
391
|
+
ARIA_PT_STYLE = None
|
ui/console.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Shared Rich console, availability flags, and ESC-key watcher.
|
|
2
|
+
|
|
3
|
+
Import this instead of repeating the try/except blocks everywhere:
|
|
4
|
+
|
|
5
|
+
from ui.console import console, HAS_RICH, HAS_PT, _SYNTAX_THEME
|
|
6
|
+
from ui.console import _EscWatcher, _esc_watcher
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
# ── Rich ───────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.markdown import Markdown
|
|
24
|
+
from rich.live import Live
|
|
25
|
+
from rich.text import Text
|
|
26
|
+
from rich.status import Status
|
|
27
|
+
from rich.syntax import Syntax
|
|
28
|
+
from rich.panel import Panel
|
|
29
|
+
from rich.rule import Rule
|
|
30
|
+
from rich import box as rich_box
|
|
31
|
+
from rich.theme import Theme
|
|
32
|
+
HAS_RICH = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
HAS_RICH = False
|
|
35
|
+
|
|
36
|
+
# ── prompt_toolkit ─────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from prompt_toolkit import PromptSession
|
|
40
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
41
|
+
from prompt_toolkit.formatted_text import HTML
|
|
42
|
+
from prompt_toolkit.history import FileHistory
|
|
43
|
+
from prompt_toolkit.styles import Style as PTStyle
|
|
44
|
+
HAS_PT = True
|
|
45
|
+
except ImportError:
|
|
46
|
+
HAS_PT = False
|
|
47
|
+
|
|
48
|
+
# ── Console singleton ──────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
_SYNTAX_THEME: str = "monokai"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _detect_terminal_theme() -> str:
|
|
54
|
+
"""Return a best-effort terminal theme: ``dark`` or ``light``."""
|
|
55
|
+
explicit = os.getenv("ARIA_RICH_THEME", os.getenv("ARIA_INPUT_THEME", "")).strip().lower()
|
|
56
|
+
if explicit in {"dark", "light"}:
|
|
57
|
+
return explicit
|
|
58
|
+
colorfgbg = os.getenv("COLORFGBG", "")
|
|
59
|
+
if colorfgbg:
|
|
60
|
+
try:
|
|
61
|
+
return "dark" if int(colorfgbg.split(";")[-1]) < 8 else "light"
|
|
62
|
+
except ValueError:
|
|
63
|
+
pass
|
|
64
|
+
if os.uname().sysname == "Darwin":
|
|
65
|
+
try:
|
|
66
|
+
r = subprocess.run(
|
|
67
|
+
["defaults", "read", "-g", "AppleInterfaceStyle"],
|
|
68
|
+
capture_output=True, text=True, timeout=0.2, check=False,
|
|
69
|
+
)
|
|
70
|
+
return "dark" if (r.returncode == 0 and "dark" in r.stdout.lower()) else "light"
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
return "dark"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_rich_theme(theme: str) -> "Theme":
|
|
77
|
+
"""Build a Markdown palette with enough contrast for the terminal theme."""
|
|
78
|
+
if theme == "light":
|
|
79
|
+
return Theme({
|
|
80
|
+
"markdown.h1": "bold #24292f",
|
|
81
|
+
"markdown.h2": "bold #24292f",
|
|
82
|
+
"markdown.h3": "bold #8a5a00",
|
|
83
|
+
"markdown.h4": "bold #8a5a00",
|
|
84
|
+
"markdown.h5": "bold #8a5a00",
|
|
85
|
+
"markdown.h6": "bold #8a5a00",
|
|
86
|
+
"markdown.heading": "bold #24292f",
|
|
87
|
+
"markdown.code": "bold #8a5a00",
|
|
88
|
+
"markdown.code_inline": "bold #8a5a00",
|
|
89
|
+
"markdown.link": "underline #0969da",
|
|
90
|
+
"markdown.link_url": "underline #57606a",
|
|
91
|
+
"markdown.item.bullet": "bold #8a5a00",
|
|
92
|
+
"markdown.item.number": "bold #8a5a00",
|
|
93
|
+
"markdown.table.header": "bold #8a5a00",
|
|
94
|
+
"markdown.table.border": "#8c959f",
|
|
95
|
+
"markdown.hr": "#8c959f",
|
|
96
|
+
"markdown.strong": "bold #1f2328",
|
|
97
|
+
"markdown.em": "italic #57606a",
|
|
98
|
+
"markdown.block_quote": "#6e7781",
|
|
99
|
+
})
|
|
100
|
+
return Theme({
|
|
101
|
+
# Keep Markdown close to the terminal palette: neutral text, copper
|
|
102
|
+
# accents, no blue/purple headings, and no black inline-code blocks.
|
|
103
|
+
"markdown.h1": "bold #e8e0d4",
|
|
104
|
+
"markdown.h2": "bold #e8e0d4",
|
|
105
|
+
"markdown.h3": "bold #d6ba8e",
|
|
106
|
+
"markdown.h4": "bold #d6ba8e",
|
|
107
|
+
"markdown.h5": "bold #d6ba8e",
|
|
108
|
+
"markdown.h6": "bold #d6ba8e",
|
|
109
|
+
"markdown.heading": "bold #e8e0d4",
|
|
110
|
+
"markdown.code": "bold #c08050",
|
|
111
|
+
"markdown.code_inline": "bold #c08050",
|
|
112
|
+
"markdown.link": "underline #c08050",
|
|
113
|
+
"markdown.link_url": "underline #8f867a",
|
|
114
|
+
"markdown.item.bullet": "bold #d6ba8e",
|
|
115
|
+
"markdown.item.number": "bold #d6ba8e",
|
|
116
|
+
"markdown.table.header": "bold #d6ba8e",
|
|
117
|
+
"markdown.table.border": "#6f675d",
|
|
118
|
+
"markdown.hr": "#6f675d",
|
|
119
|
+
"markdown.strong": "bold #e8e0d4",
|
|
120
|
+
"markdown.em": "italic #c7beb2",
|
|
121
|
+
"markdown.block_quote": "#a8a096",
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if HAS_RICH:
|
|
126
|
+
ARIA_RICH_THEME_NAME = _detect_terminal_theme()
|
|
127
|
+
ARIA_RICH_THEME = _build_rich_theme(ARIA_RICH_THEME_NAME)
|
|
128
|
+
console = Console(highlight=False, theme=ARIA_RICH_THEME)
|
|
129
|
+
|
|
130
|
+
def make_markdown(markup: str) -> Markdown:
|
|
131
|
+
"""Create Markdown with Aria's low-saturation terminal theme."""
|
|
132
|
+
return Markdown(
|
|
133
|
+
markup,
|
|
134
|
+
code_theme="bw",
|
|
135
|
+
inline_code_theme="bw",
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
class _FallbackConsole:
|
|
139
|
+
def print(self, *a, **kw):
|
|
140
|
+
print(*[str(x) for x in a])
|
|
141
|
+
|
|
142
|
+
def input(self, prompt: str = "") -> str:
|
|
143
|
+
return input(prompt)
|
|
144
|
+
|
|
145
|
+
def status(self, msg: str):
|
|
146
|
+
class _Ctx:
|
|
147
|
+
def __enter__(self):
|
|
148
|
+
print(msg)
|
|
149
|
+
return self
|
|
150
|
+
def __exit__(self, *a):
|
|
151
|
+
pass
|
|
152
|
+
def update(self, msg: str):
|
|
153
|
+
print(msg)
|
|
154
|
+
return _Ctx()
|
|
155
|
+
|
|
156
|
+
console = _FallbackConsole()
|
|
157
|
+
|
|
158
|
+
def make_markdown(markup: str) -> str:
|
|
159
|
+
return markup
|
|
160
|
+
|
|
161
|
+
# ── termios / raw-mode availability ───────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
import termios
|
|
165
|
+
import tty
|
|
166
|
+
import select as _select
|
|
167
|
+
_HAS_TERMIOS = True
|
|
168
|
+
except ImportError:
|
|
169
|
+
_HAS_TERMIOS = False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ── ESC-key watcher ───────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
class _EscWatcher:
|
|
175
|
+
"""Background thread that watches for ESC key press to cancel streaming."""
|
|
176
|
+
|
|
177
|
+
def __init__(self):
|
|
178
|
+
self._active = False
|
|
179
|
+
self._paused = False
|
|
180
|
+
self._thread: Optional[threading.Thread] = None
|
|
181
|
+
self._old_settings = None
|
|
182
|
+
self._cancel_event: Optional[asyncio.Event] = None
|
|
183
|
+
self._fd: Optional[int] = None
|
|
184
|
+
|
|
185
|
+
def start(self, cancel_event: asyncio.Event):
|
|
186
|
+
if not _HAS_TERMIOS or not sys.stdin.isatty():
|
|
187
|
+
return
|
|
188
|
+
self._cancel_event = cancel_event
|
|
189
|
+
self._fd = sys.stdin.fileno()
|
|
190
|
+
try:
|
|
191
|
+
self._old_settings = termios.tcgetattr(self._fd)
|
|
192
|
+
tty.setcbreak(self._fd)
|
|
193
|
+
except Exception:
|
|
194
|
+
self._old_settings = None
|
|
195
|
+
return
|
|
196
|
+
self._active = True
|
|
197
|
+
self._paused = False
|
|
198
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
199
|
+
self._thread.start()
|
|
200
|
+
|
|
201
|
+
def pause(self):
|
|
202
|
+
self._paused = True
|
|
203
|
+
if self._old_settings and self._fd is not None:
|
|
204
|
+
try:
|
|
205
|
+
termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_settings)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
def resume(self):
|
|
210
|
+
if not self._active or not _HAS_TERMIOS or self._fd is None:
|
|
211
|
+
return
|
|
212
|
+
if self._cancel_event and self._cancel_event.is_set():
|
|
213
|
+
return
|
|
214
|
+
try:
|
|
215
|
+
termios.tcflush(self._fd, termios.TCIFLUSH)
|
|
216
|
+
self._old_settings = termios.tcgetattr(self._fd)
|
|
217
|
+
tty.setcbreak(self._fd)
|
|
218
|
+
except Exception:
|
|
219
|
+
return
|
|
220
|
+
self._paused = False
|
|
221
|
+
|
|
222
|
+
def stop(self):
|
|
223
|
+
self._active = False
|
|
224
|
+
self._paused = False
|
|
225
|
+
if self._old_settings and self._fd is not None:
|
|
226
|
+
try:
|
|
227
|
+
termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_settings)
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
self._old_settings = None
|
|
231
|
+
if self._thread:
|
|
232
|
+
self._thread.join(timeout=0.3)
|
|
233
|
+
self._thread = None
|
|
234
|
+
|
|
235
|
+
def _run(self):
|
|
236
|
+
fd = self._fd
|
|
237
|
+
try:
|
|
238
|
+
while self._active:
|
|
239
|
+
if self._paused:
|
|
240
|
+
time.sleep(0.1)
|
|
241
|
+
continue
|
|
242
|
+
try:
|
|
243
|
+
ready, _, _ = _select.select([fd], [], [], 0.15)
|
|
244
|
+
except (ValueError, OSError):
|
|
245
|
+
break
|
|
246
|
+
if not self._active or self._paused:
|
|
247
|
+
continue
|
|
248
|
+
if ready:
|
|
249
|
+
try:
|
|
250
|
+
ch = os.read(fd, 1)
|
|
251
|
+
except OSError:
|
|
252
|
+
break
|
|
253
|
+
if ch == b'\x1b':
|
|
254
|
+
try:
|
|
255
|
+
r2, _, _ = _select.select([fd], [], [], 0.05)
|
|
256
|
+
except (ValueError, OSError):
|
|
257
|
+
break
|
|
258
|
+
if r2:
|
|
259
|
+
try:
|
|
260
|
+
os.read(fd, 16)
|
|
261
|
+
except OSError:
|
|
262
|
+
pass
|
|
263
|
+
else:
|
|
264
|
+
if self._cancel_event:
|
|
265
|
+
self._cancel_event.set()
|
|
266
|
+
self._active = False
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
_esc_watcher = _EscWatcher()
|