deft-console 0.11.1__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.
- deft_console/__init__.py +38 -0
- deft_console/__main__.py +4 -0
- deft_console/_built.py +3 -0
- deft_console/_loop.py +86 -0
- deft_console/_path_fuzzy_search.py +48 -0
- deft_console/_path_match.py +141 -0
- deft_console/_vendor/VENDORED.md +9 -0
- deft_console/_vendor/__init__.py +1 -0
- deft_console/_vendor/pixcell/LICENSE +21 -0
- deft_console/_vendor/pixcell/__init__.py +12 -0
- deft_console/_vendor/pixcell/__main__.py +229 -0
- deft_console/_vendor/pixcell/_diacritics.py +43 -0
- deft_console/_vendor/pixcell/frames.py +176 -0
- deft_console/_vendor/pixcell/halfblock.py +34 -0
- deft_console/_vendor/pixcell/iterm2.py +18 -0
- deft_console/_vendor/pixcell/kitty.py +215 -0
- deft_console/_vendor/pixcell/live.py +238 -0
- deft_console/_vendor/pixcell/probe.py +396 -0
- deft_console/_vendor/pixcell/sixel.py +68 -0
- deft_console/_vendor/pixcell/terminal_facts.py +308 -0
- deft_console/_vendor/pixcell/terminal_facts.source +1 -0
- deft_console/_vendor/pixcell/terminals.toml +159 -0
- deft_console/_vendor/pixcell/textual.py +787 -0
- deft_console/_vendor/termaid/LICENSE +21 -0
- deft_console/_vendor/termaid/__init__.py +375 -0
- deft_console/_vendor/termaid/__main__.py +6 -0
- deft_console/_vendor/termaid/claude_hook.py +265 -0
- deft_console/_vendor/termaid/cli.py +670 -0
- deft_console/_vendor/termaid/diagnostics.py +41 -0
- deft_console/_vendor/termaid/graph/__init__.py +4 -0
- deft_console/_vendor/termaid/graph/model.py +203 -0
- deft_console/_vendor/termaid/graph/shapes.py +23 -0
- deft_console/_vendor/termaid/ingest.py +226 -0
- deft_console/_vendor/termaid/layout/__init__.py +3 -0
- deft_console/_vendor/termaid/layout/coordinates.py +51 -0
- deft_console/_vendor/termaid/layout/grid.py +270 -0
- deft_console/_vendor/termaid/layout/layers.py +508 -0
- deft_console/_vendor/termaid/layout/placement.py +411 -0
- deft_console/_vendor/termaid/layout/subgraphs.py +184 -0
- deft_console/_vendor/termaid/markdown.py +103 -0
- deft_console/_vendor/termaid/model/__init__.py +0 -0
- deft_console/_vendor/termaid/model/blockdiagram.py +30 -0
- deft_console/_vendor/termaid/model/classdiagram.py +47 -0
- deft_console/_vendor/termaid/model/erdiagram.py +41 -0
- deft_console/_vendor/termaid/model/gantt.py +101 -0
- deft_console/_vendor/termaid/model/gitgraph.py +31 -0
- deft_console/_vendor/termaid/model/journey.py +24 -0
- deft_console/_vendor/termaid/model/kanban.py +23 -0
- deft_console/_vendor/termaid/model/mindmap.py +23 -0
- deft_console/_vendor/termaid/model/packet.py +22 -0
- deft_console/_vendor/termaid/model/piechart.py +18 -0
- deft_console/_vendor/termaid/model/quadrant.py +24 -0
- deft_console/_vendor/termaid/model/sequence.py +66 -0
- deft_console/_vendor/termaid/model/timeline.py +23 -0
- deft_console/_vendor/termaid/model/treemap.py +28 -0
- deft_console/_vendor/termaid/model/xychart.py +24 -0
- deft_console/_vendor/termaid/output/__init__.py +0 -0
- deft_console/_vendor/termaid/output/rich.py +275 -0
- deft_console/_vendor/termaid/output/styled.py +66 -0
- deft_console/_vendor/termaid/output/widget.py +46 -0
- deft_console/_vendor/termaid/parser/__init__.py +4 -0
- deft_console/_vendor/termaid/parser/architecture.py +332 -0
- deft_console/_vendor/termaid/parser/blockdiagram.py +316 -0
- deft_console/_vendor/termaid/parser/classdiagram.py +255 -0
- deft_console/_vendor/termaid/parser/erdiagram.py +247 -0
- deft_console/_vendor/termaid/parser/flowchart.py +871 -0
- deft_console/_vendor/termaid/parser/gantt.py +250 -0
- deft_console/_vendor/termaid/parser/gitgraph.py +340 -0
- deft_console/_vendor/termaid/parser/journey.py +74 -0
- deft_console/_vendor/termaid/parser/kanban.py +80 -0
- deft_console/_vendor/termaid/parser/mindmap.py +81 -0
- deft_console/_vendor/termaid/parser/packet.py +70 -0
- deft_console/_vendor/termaid/parser/piechart.py +67 -0
- deft_console/_vendor/termaid/parser/quadrant.py +67 -0
- deft_console/_vendor/termaid/parser/sequence.py +250 -0
- deft_console/_vendor/termaid/parser/statediagram.py +245 -0
- deft_console/_vendor/termaid/parser/timeline.py +68 -0
- deft_console/_vendor/termaid/parser/tokens.py +53 -0
- deft_console/_vendor/termaid/parser/treemap.py +67 -0
- deft_console/_vendor/termaid/parser/xychart.py +143 -0
- deft_console/_vendor/termaid/partial.py +150 -0
- deft_console/_vendor/termaid/py.typed +0 -0
- deft_console/_vendor/termaid/renderer/__init__.py +3 -0
- deft_console/_vendor/termaid/renderer/blockdiagram.py +476 -0
- deft_console/_vendor/termaid/renderer/canvas.py +366 -0
- deft_console/_vendor/termaid/renderer/charset.py +193 -0
- deft_console/_vendor/termaid/renderer/classdiagram.py +854 -0
- deft_console/_vendor/termaid/renderer/draw.py +829 -0
- deft_console/_vendor/termaid/renderer/erdiagram.py +600 -0
- deft_console/_vendor/termaid/renderer/gantt.py +315 -0
- deft_console/_vendor/termaid/renderer/gitgraph.py +425 -0
- deft_console/_vendor/termaid/renderer/journey.py +185 -0
- deft_console/_vendor/termaid/renderer/kanban.py +139 -0
- deft_console/_vendor/termaid/renderer/mindmap.py +295 -0
- deft_console/_vendor/termaid/renderer/packet.py +200 -0
- deft_console/_vendor/termaid/renderer/piechart.py +122 -0
- deft_console/_vendor/termaid/renderer/quadrant.py +147 -0
- deft_console/_vendor/termaid/renderer/relation_tracks.py +31 -0
- deft_console/_vendor/termaid/renderer/sequence.py +986 -0
- deft_console/_vendor/termaid/renderer/shapes/__init__.py +461 -0
- deft_console/_vendor/termaid/renderer/themes.py +179 -0
- deft_console/_vendor/termaid/renderer/timeline.py +67 -0
- deft_console/_vendor/termaid/renderer/treemap.py +245 -0
- deft_console/_vendor/termaid/renderer/xychart.py +381 -0
- deft_console/_vendor/termaid/routing/__init__.py +3 -0
- deft_console/_vendor/termaid/routing/pathfinder.py +197 -0
- deft_console/_vendor/termaid/routing/router.py +1206 -0
- deft_console/_vendor/termaid/utils.py +246 -0
- deft_console/about.py +93 -0
- deft_console/acp/agent.py +1802 -0
- deft_console/acp/api.py +123 -0
- deft_console/acp/display.py +147 -0
- deft_console/acp/encode_tool_call_id.py +12 -0
- deft_console/acp/extensions/__init__.py +48 -0
- deft_console/acp/extensions/deft.py +107 -0
- deft_console/acp/extensions/xai.py +377 -0
- deft_console/acp/messages.py +400 -0
- deft_console/acp/prompt.py +82 -0
- deft_console/acp/protocol.py +575 -0
- deft_console/activity.py +211 -0
- deft_console/activity_history.py +112 -0
- deft_console/agent.py +130 -0
- deft_console/agent_actions.py +68 -0
- deft_console/agent_process.py +121 -0
- deft_console/agent_schema.py +80 -0
- deft_console/agents.py +120 -0
- deft_console/ansi/__init__.py +1 -0
- deft_console/ansi/_ansi.py +1876 -0
- deft_console/ansi/_ansi_colors.py +264 -0
- deft_console/ansi/_control_codes.py +37 -0
- deft_console/ansi/_keys.py +251 -0
- deft_console/ansi/_sgr_styles.py +64 -0
- deft_console/ansi/_stream_parser.py +348 -0
- deft_console/answer.py +24 -0
- deft_console/app.py +1094 -0
- deft_console/atomic.py +37 -0
- deft_console/branding.py +29 -0
- deft_console/browser/__init__.py +0 -0
- deft_console/browser/cdp.py +389 -0
- deft_console/browser/content.py +96 -0
- deft_console/browser/install.py +187 -0
- deft_console/browser/keys.py +50 -0
- deft_console/browser/policy.py +78 -0
- deft_console/browser/view.py +991 -0
- deft_console/caret.py +57 -0
- deft_console/cli.py +378 -0
- deft_console/code_analyze.py +27 -0
- deft_console/complete.py +38 -0
- deft_console/constants.py +58 -0
- deft_console/conversation_markdown.py +358 -0
- deft_console/danger.py +372 -0
- deft_console/data/agents/ampcode.com.toml +55 -0
- deft_console/data/agents/augmentcode.com.toml +44 -0
- deft_console/data/agents/claude-bridge.deftco.toml +40 -0
- deft_console/data/agents/claude.com.toml +44 -0
- deft_console/data/agents/claude.deftco.toml +52 -0
- deft_console/data/agents/copilot.github.com.toml +29 -0
- deft_console/data/agents/crowai.dev.toml +36 -0
- deft_console/data/agents/cursor.com.toml +39 -0
- deft_console/data/agents/deepagents.langchain.com.toml +32 -0
- deft_console/data/agents/demo.deftco.toml +42 -0
- deft_console/data/agents/docker.com.toml +60 -0
- deft_console/data/agents/geminicli.com.toml +28 -0
- deft_console/data/agents/goose.ai.toml +56 -0
- deft_console/data/agents/grok.x.ai.toml +37 -0
- deft_console/data/agents/inference.huggingface.co.toml +41 -0
- deft_console/data/agents/kimi.com.toml +44 -0
- deft_console/data/agents/kiro.dev.toml +43 -0
- deft_console/data/agents/openai.com.toml +55 -0
- deft_console/data/agents/openclaw.ai.toml +37 -0
- deft_console/data/agents/opencode.ai.toml +39 -0
- deft_console/data/agents/openhands.dev.toml +49 -0
- deft_console/data/agents/pi.dev.toml +50 -0
- deft_console/data/agents/prime.deftco.toml +51 -0
- deft_console/data/agents/sabal.deftco.toml +49 -0
- deft_console/data/agents/stakpak.dev.toml +62 -0
- deft_console/data/agents/vibe.mistral.ai.toml +35 -0
- deft_console/data/agents/vtcode.dev.toml +67 -0
- deft_console/data/images/frog.png +0 -0
- deft_console/data/sounds/question.wav +0 -0
- deft_console/data/sounds/turn-over.wav +0 -0
- deft_console/db.py +236 -0
- deft_console/dec.py +332 -0
- deft_console/deft_console.tcss +384 -0
- deft_console/demo_agent/__init__.py +29 -0
- deft_console/demo_agent/__main__.py +6 -0
- deft_console/demo_agent/agent.py +387 -0
- deft_console/demo_agent/connection.py +97 -0
- deft_console/demo_agent/content.py +455 -0
- deft_console/demo_agent/context.py +248 -0
- deft_console/demo_agent/demos.py +748 -0
- deft_console/demo_agent/mcp_client.py +66 -0
- deft_console/demo_agent/registry.py +123 -0
- deft_console/demo_agent/tutorial.py +301 -0
- deft_console/directory.py +234 -0
- deft_console/directory_suggester.py +93 -0
- deft_console/directory_watcher.py +214 -0
- deft_console/display_mcp.py +349 -0
- deft_console/doing.py +90 -0
- deft_console/format_path.py +25 -0
- deft_console/fuzzy.py +140 -0
- deft_console/fuzzy_index.py +275 -0
- deft_console/gist.py +33 -0
- deft_console/history.py +138 -0
- deft_console/images.py +534 -0
- deft_console/jsonrpc.py +587 -0
- deft_console/markdown_append.py +32 -0
- deft_console/menus.py +14 -0
- deft_console/mermaid.py +132 -0
- deft_console/mermaid_image.py +306 -0
- deft_console/messages.py +166 -0
- deft_console/node_runtime.py +215 -0
- deft_console/option_content.py +51 -0
- deft_console/os.py +0 -0
- deft_console/page_preview.py +154 -0
- deft_console/page_shot.py +168 -0
- deft_console/pane_cli.py +238 -0
- deft_console/panes/__init__.py +81 -0
- deft_console/panes/actions.py +208 -0
- deft_console/panes/api/__init__.py +5 -0
- deft_console/panes/api/client.py +115 -0
- deft_console/panes/api/running.py +95 -0
- deft_console/panes/api/server.py +228 -0
- deft_console/panes/content.py +134 -0
- deft_console/panes/events.py +145 -0
- deft_console/panes/manager.py +543 -0
- deft_console/panes/model.py +337 -0
- deft_console/panes/textual/__init__.py +503 -0
- deft_console/panes/textual/commands.py +95 -0
- deft_console/path_complete.py +217 -0
- deft_console/path_filter.py +157 -0
- deft_console/paths.py +100 -0
- deft_console/picture_content.py +95 -0
- deft_console/pill.py +23 -0
- deft_console/portability/__init__.py +133 -0
- deft_console/portability/_posix.py +579 -0
- deft_console/portability/_windows.py +652 -0
- deft_console/program_status.py +524 -0
- deft_console/prompt/attachments.py +188 -0
- deft_console/prompt/extract.py +20 -0
- deft_console/prompt/resource.py +68 -0
- deft_console/protocol.py +28 -0
- deft_console/screens/action_modal.py +107 -0
- deft_console/screens/agent_modal.py +172 -0
- deft_console/screens/agent_sessions.py +148 -0
- deft_console/screens/ask_user_question.py +658 -0
- deft_console/screens/command_edit_modal.py +74 -0
- deft_console/screens/elicitation.py +535 -0
- deft_console/screens/main.py +558 -0
- deft_console/screens/main.tcss +688 -0
- deft_console/screens/new_session.py +75 -0
- deft_console/screens/permissions.py +411 -0
- deft_console/screens/permissions.tcss +73 -0
- deft_console/screens/session_options.py +175 -0
- deft_console/screens/session_resume_modal.py +172 -0
- deft_console/screens/session_resume_modal.tcss +35 -0
- deft_console/screens/sessions.py +177 -0
- deft_console/screens/sessions.tcss +153 -0
- deft_console/screens/settings.py +254 -0
- deft_console/screens/settings.tcss +101 -0
- deft_console/screens/store.py +682 -0
- deft_console/screens/store.tcss +272 -0
- deft_console/screens/withdrawable.py +49 -0
- deft_console/session_messages.py +101 -0
- deft_console/session_tracker.py +156 -0
- deft_console/settings.py +353 -0
- deft_console/settings_schema.py +590 -0
- deft_console/shell.py +291 -0
- deft_console/shell_read.py +42 -0
- deft_console/slash_command.py +34 -0
- deft_console/terminal_content.py +144 -0
- deft_console/tool_summary.py +101 -0
- deft_console/twitter.py +38 -0
- deft_console/vendor/pele.LICENSE +40 -0
- deft_console/vendor/pele.VERSION +1 -0
- deft_console/vendor/pele.global.js +150 -0
- deft_console/version.py +81 -0
- deft_console/visuals/columns.py +292 -0
- deft_console/widgets/acp_content.py +50 -0
- deft_console/widgets/activity_top.py +480 -0
- deft_console/widgets/activity_view.py +292 -0
- deft_console/widgets/agent_response.py +77 -0
- deft_console/widgets/agent_thought.py +51 -0
- deft_console/widgets/blackhole.py +322 -0
- deft_console/widgets/check_list.py +67 -0
- deft_console/widgets/comets.py +335 -0
- deft_console/widgets/command_pane.py +174 -0
- deft_console/widgets/condensed_path.py +117 -0
- deft_console/widgets/conversation.py +3923 -0
- deft_console/widgets/danger_warning.py +65 -0
- deft_console/widgets/diff_view.py +59 -0
- deft_console/widgets/directory_input.py +8 -0
- deft_console/widgets/display_terminal.py +159 -0
- deft_console/widgets/fence_cursor.py +59 -0
- deft_console/widgets/flash.py +95 -0
- deft_console/widgets/future_text.py +126 -0
- deft_console/widgets/galaxy.py +293 -0
- deft_console/widgets/grid_select.py +234 -0
- deft_console/widgets/help_panel.py +13 -0
- deft_console/widgets/highlighted_textarea.py +180 -0
- deft_console/widgets/inline_question.py +190 -0
- deft_console/widgets/mandelbrot.py +294 -0
- deft_console/widgets/markdown_note.py +13 -0
- deft_console/widgets/menu.py +147 -0
- deft_console/widgets/moon.py +330 -0
- deft_console/widgets/non_selectable_label.py +5 -0
- deft_console/widgets/note.py +18 -0
- deft_console/widgets/path_search.py +555 -0
- deft_console/widgets/picture_pane.py +121 -0
- deft_console/widgets/plan.py +226 -0
- deft_console/widgets/project_directory_tree.py +90 -0
- deft_console/widgets/prompt.py +987 -0
- deft_console/widgets/prompt_queue.py +110 -0
- deft_console/widgets/pulsar.py +338 -0
- deft_console/widgets/question.py +375 -0
- deft_console/widgets/rocket.py +477 -0
- deft_console/widgets/session_grid_select.py +71 -0
- deft_console/widgets/session_list.py +360 -0
- deft_console/widgets/session_message.py +86 -0
- deft_console/widgets/session_summary.py +130 -0
- deft_console/widgets/session_tabs.py +184 -0
- deft_console/widgets/shell_result.py +35 -0
- deft_console/widgets/shell_terminal.py +50 -0
- deft_console/widgets/side_bar.py +89 -0
- deft_console/widgets/slash_complete.py +245 -0
- deft_console/widgets/solarsystem.py +300 -0
- deft_console/widgets/spark.py +106 -0
- deft_console/widgets/starfield.py +295 -0
- deft_console/widgets/strike_text.py +70 -0
- deft_console/widgets/subagent_view.py +412 -0
- deft_console/widgets/terminal.py +756 -0
- deft_console/widgets/terminal_pane.py +153 -0
- deft_console/widgets/terminal_tool.py +522 -0
- deft_console/widgets/throbber.py +100 -0
- deft_console/widgets/tool_call.py +347 -0
- deft_console/widgets/user_input.py +24 -0
- deft_console/widgets/version.py +5 -0
- deft_console/widgets/welcome.py +31 -0
- deft_console-0.11.1.dist-info/METADATA +255 -0
- deft_console-0.11.1.dist-info/RECORD +343 -0
- deft_console-0.11.1.dist-info/WHEEL +4 -0
- deft_console-0.11.1.dist-info/entry_points.txt +2 -0
- deft_console-0.11.1.dist-info/licenses/LICENSE +11 -0
deft_console/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Literal, Mapping
|
|
2
|
+
|
|
3
|
+
NAME = "toad"
|
|
4
|
+
TITLE = "Deft Console"
|
|
5
|
+
|
|
6
|
+
type OS = Literal["linux", "macos", "windows", "*"]
|
|
7
|
+
|
|
8
|
+
from deft_console.portability import SYSTEM as os # noqa: E402
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_os_matrix(matrix: Mapping[OS, str]) -> str | None:
|
|
12
|
+
"""Get a value from a mapping where the key is an OS, falling back to a wildcard ("*").
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
matrix: A mapping where an OS literal is the key.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The value, if one is found, or `None`.
|
|
19
|
+
"""
|
|
20
|
+
if (result := matrix.get(os)) is None:
|
|
21
|
+
result = matrix.get("*")
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_version() -> str:
|
|
26
|
+
"""Get the current version of Deft Console.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
str: Version string, e.g "1.2.3"
|
|
30
|
+
"""
|
|
31
|
+
from importlib.metadata import version
|
|
32
|
+
|
|
33
|
+
for distribution in ("deft-console", "batrachian-toad"): # the second: older checkouts
|
|
34
|
+
try:
|
|
35
|
+
return version(distribution)
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
return "0.0.0unknown"
|
deft_console/__main__.py
ADDED
deft_console/_built.py
ADDED
deft_console/_loop.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Iterable, Literal, Sequence, TypeVar
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
|
|
9
|
+
"""Iterate and generate a tuple with a flag for first value."""
|
|
10
|
+
iter_values = iter(values)
|
|
11
|
+
try:
|
|
12
|
+
value = next(iter_values)
|
|
13
|
+
except StopIteration:
|
|
14
|
+
return
|
|
15
|
+
yield True, value
|
|
16
|
+
for value in iter_values:
|
|
17
|
+
yield False, value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def loop_last(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
|
|
21
|
+
"""Iterate and generate a tuple with a flag for last value."""
|
|
22
|
+
iter_values = iter(values)
|
|
23
|
+
try:
|
|
24
|
+
previous_value = next(iter_values)
|
|
25
|
+
except StopIteration:
|
|
26
|
+
return
|
|
27
|
+
for value in iter_values:
|
|
28
|
+
yield False, previous_value
|
|
29
|
+
previous_value = value
|
|
30
|
+
yield True, previous_value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
|
|
34
|
+
"""Iterate and generate a tuple with a flag for first and last value."""
|
|
35
|
+
iter_values = iter(values)
|
|
36
|
+
try:
|
|
37
|
+
previous_value = next(iter_values)
|
|
38
|
+
except StopIteration:
|
|
39
|
+
return
|
|
40
|
+
first = True
|
|
41
|
+
for value in iter_values:
|
|
42
|
+
yield first, False, previous_value
|
|
43
|
+
first = False
|
|
44
|
+
previous_value = value
|
|
45
|
+
yield first, True, previous_value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def loop_from_index(
|
|
49
|
+
values: Sequence[T],
|
|
50
|
+
index: int,
|
|
51
|
+
direction: Literal[-1, +1] = +1,
|
|
52
|
+
wrap: bool = True,
|
|
53
|
+
) -> Iterable[tuple[int, T]]:
|
|
54
|
+
"""Iterate over values in a sequence from a given starting index, potentially wrapping the index
|
|
55
|
+
if it would go out of bounds.
|
|
56
|
+
|
|
57
|
+
Note that the first value to be yielded is a step from `index`, and `index` will be yielded *last*.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
values: A sequence of values.
|
|
62
|
+
index: Starting index.
|
|
63
|
+
direction: Direction to move index (+1 for forward, -1 for backward).
|
|
64
|
+
bool: Should the index wrap when out of bounds?
|
|
65
|
+
|
|
66
|
+
Yields:
|
|
67
|
+
A tuple of index and value from the sequence.
|
|
68
|
+
"""
|
|
69
|
+
# Sanity check for devs who miss the typing errors
|
|
70
|
+
assert direction in (-1, +1), "direction must be -1 or +1"
|
|
71
|
+
count = len(values)
|
|
72
|
+
if wrap:
|
|
73
|
+
for _ in range(count):
|
|
74
|
+
index = (index + direction) % count
|
|
75
|
+
yield (index, values[index])
|
|
76
|
+
else:
|
|
77
|
+
if direction == +1:
|
|
78
|
+
for _ in range(count):
|
|
79
|
+
if (index := index + 1) >= count:
|
|
80
|
+
break
|
|
81
|
+
yield (index, values[index])
|
|
82
|
+
else:
|
|
83
|
+
for _ in range(count):
|
|
84
|
+
if (index := index - 1) < 0:
|
|
85
|
+
break
|
|
86
|
+
yield (index, values[index])
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from typing import Sequence
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from deft_console.fuzzy import FuzzySearch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PathFuzzySearch(FuzzySearch):
|
|
10
|
+
@classmethod
|
|
11
|
+
@lru_cache(maxsize=1024)
|
|
12
|
+
def get_first_letters(cls, candidate: str) -> frozenset[int]:
|
|
13
|
+
return frozenset(
|
|
14
|
+
{
|
|
15
|
+
0,
|
|
16
|
+
*[match.start() + 1 for match in re.finditer(r"/", candidate)],
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def score(self, candidate: str, positions: Sequence[int]) -> float:
|
|
21
|
+
"""Score a search.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
search: Search object.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Score.
|
|
28
|
+
"""
|
|
29
|
+
first_letters = self.get_first_letters(candidate)
|
|
30
|
+
# This is a heuristic, and can be tweaked for better results
|
|
31
|
+
# Boost first letter matches
|
|
32
|
+
offset_count = len(positions)
|
|
33
|
+
score: float = offset_count + len(first_letters.intersection(positions))
|
|
34
|
+
|
|
35
|
+
groups = 1
|
|
36
|
+
last_offset, *offsets = positions
|
|
37
|
+
for offset in offsets:
|
|
38
|
+
if offset != last_offset + 1:
|
|
39
|
+
groups += 1
|
|
40
|
+
last_offset = offset
|
|
41
|
+
|
|
42
|
+
# Boost to favor less groups
|
|
43
|
+
normalized_groups = (offset_count - (groups - 1)) / offset_count
|
|
44
|
+
score *= 1 + (normalized_groups * normalized_groups)
|
|
45
|
+
|
|
46
|
+
if positions[0] > candidate.rfind("/"):
|
|
47
|
+
score *= 2
|
|
48
|
+
return score
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from operator import itemgetter
|
|
3
|
+
import re
|
|
4
|
+
from typing import Iterable, Sequence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PathFuzzySearch:
|
|
8
|
+
"""Performs a fuzzy search.
|
|
9
|
+
|
|
10
|
+
Unlike a regex solution, this will finds all possible matches.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self, case_sensitive: bool = False, *, cache_size: int = 1024 * 4
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Initialize fuzzy search.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
case_sensitive: Is the match case sensitive?
|
|
20
|
+
cache_size: Number of queries to cache.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
self.case_sensitive = case_sensitive
|
|
24
|
+
|
|
25
|
+
def match(self, query: str, candidate: str) -> tuple[float, Sequence[int]]:
|
|
26
|
+
"""Match against a query.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
query: The fuzzy query.
|
|
30
|
+
candidate: A candidate to check,.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A pair of (score, tuple of offsets). `(0, ())` for no result.
|
|
34
|
+
"""
|
|
35
|
+
default: tuple[float, Sequence[int]] = (0.0, [])
|
|
36
|
+
result = max(self._match(query, candidate), key=itemgetter(0), default=default)
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
@lru_cache(maxsize=1024)
|
|
41
|
+
def get_first_letters(cls, candidate: str) -> frozenset[int]:
|
|
42
|
+
return frozenset(
|
|
43
|
+
{
|
|
44
|
+
0,
|
|
45
|
+
*[match.start() + 1 for match in re.finditer(r"/", candidate)],
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def score(self, candidate: str, positions: Sequence[int]) -> float:
|
|
50
|
+
"""Score a search.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
search: Search object.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Score.
|
|
57
|
+
"""
|
|
58
|
+
first_letters = self.get_first_letters(candidate)
|
|
59
|
+
# This is a heuristic, and can be tweaked for better results
|
|
60
|
+
# Boost first letter matches
|
|
61
|
+
offset_count = len(positions)
|
|
62
|
+
score: float = offset_count + len(first_letters.intersection(positions))
|
|
63
|
+
|
|
64
|
+
groups = 1
|
|
65
|
+
last_offset, *offsets = positions
|
|
66
|
+
for offset in offsets:
|
|
67
|
+
if offset != last_offset + 1:
|
|
68
|
+
groups += 1
|
|
69
|
+
last_offset = offset
|
|
70
|
+
|
|
71
|
+
# Boost to favor less groups
|
|
72
|
+
normalized_groups = (offset_count - (groups - 1)) / offset_count
|
|
73
|
+
score *= 1 + (normalized_groups * normalized_groups)
|
|
74
|
+
|
|
75
|
+
if positions[0] > candidate.rfind("/"):
|
|
76
|
+
score *= 2
|
|
77
|
+
return score
|
|
78
|
+
|
|
79
|
+
def _match(
|
|
80
|
+
self, query: str, candidate: str
|
|
81
|
+
) -> Iterable[tuple[float, Sequence[int]]]:
|
|
82
|
+
letter_positions: list[list[int]] = []
|
|
83
|
+
position = 0
|
|
84
|
+
|
|
85
|
+
if not self.case_sensitive:
|
|
86
|
+
candidate = candidate.casefold()
|
|
87
|
+
query = query.casefold()
|
|
88
|
+
|
|
89
|
+
score = self.score
|
|
90
|
+
|
|
91
|
+
for offset, letter in enumerate(query):
|
|
92
|
+
last_index = len(candidate) - offset
|
|
93
|
+
positions: list[int] = []
|
|
94
|
+
letter_positions.append(positions)
|
|
95
|
+
index = position
|
|
96
|
+
while (location := candidate.find(letter, index)) != -1:
|
|
97
|
+
positions.append(location)
|
|
98
|
+
index = location + 1
|
|
99
|
+
if index >= last_index:
|
|
100
|
+
break
|
|
101
|
+
if not positions:
|
|
102
|
+
yield (0.0, ())
|
|
103
|
+
return
|
|
104
|
+
position = positions[0] + 1
|
|
105
|
+
|
|
106
|
+
possible_offsets: list[list[int]] = []
|
|
107
|
+
query_length = len(query)
|
|
108
|
+
|
|
109
|
+
def get_offsets(offsets: list[int], positions_index: int) -> None:
|
|
110
|
+
"""Recursively match offsets.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
offsets: A list of offsets.
|
|
114
|
+
positions_index: Index of query letter.
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
for offset in letter_positions[positions_index]:
|
|
118
|
+
if not offsets or offset > offsets[-1]:
|
|
119
|
+
new_offsets = [*offsets, offset]
|
|
120
|
+
if len(new_offsets) == query_length:
|
|
121
|
+
possible_offsets.append(new_offsets)
|
|
122
|
+
else:
|
|
123
|
+
get_offsets(new_offsets, positions_index + 1)
|
|
124
|
+
|
|
125
|
+
get_offsets([], 0)
|
|
126
|
+
for offsets in possible_offsets:
|
|
127
|
+
yield score(candidate, offsets), offsets
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
_fuzzy_search = PathFuzzySearch(case_sensitive=False)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def match_path(query_path: tuple[str, str]) -> tuple[float, Sequence[int], str]:
|
|
134
|
+
global _fuzzy_search
|
|
135
|
+
query, path = query_path
|
|
136
|
+
score, indices = _fuzzy_search.match(query, path)
|
|
137
|
+
return (
|
|
138
|
+
score,
|
|
139
|
+
tuple(indices),
|
|
140
|
+
path,
|
|
141
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Vendored libraries
|
|
2
|
+
|
|
3
|
+
Copied by `scripts/vendor.py`; don't edit them here. Change the ref in the script and run it again.
|
|
4
|
+
Each keeps its own license (`<name>/LICENSE`).
|
|
5
|
+
|
|
6
|
+
| Library | Repository | Ref | Commit |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| `termaid` | https://github.com/deftai/termaid | `0f2fb37f5cb65a75a778b298480d2fc2b306f634` | `0f2fb37f5cb6` |
|
|
9
|
+
| `pixcell` | https://github.com/visionik/pixcell | `v0.4.1` | `a0c5952cdd48` |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Libraries Console bundles: see VENDORED.md."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pixcell contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""pixcell: images in terminal cells.
|
|
2
|
+
|
|
3
|
+
- :mod:`pixcell.kitty` — Kitty graphics protocol escape codes and Unicode placeholders.
|
|
4
|
+
- :mod:`pixcell.iterm2`, :mod:`pixcell.sixel` — iTerm2 inline images and Sixel.
|
|
5
|
+
- :mod:`pixcell.halfblock` — two pixels per cell for any 24-bit-colour terminal.
|
|
6
|
+
- :mod:`pixcell.frames` — the frames of animated pictures (GIF, WebP, APNG).
|
|
7
|
+
- :mod:`pixcell.probe` — find out which the terminal supports, and its cell size.
|
|
8
|
+
- :mod:`pixcell.textual` — an ``Image`` widget for Textual (``pip install pixcell[textual]``).
|
|
9
|
+
- :mod:`pixcell.live` — ``LiveImage``, a Textual widget you keep pushing frames into.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.4.1"
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""``python -m pixcell [image ...]``: show images in a scrolling Textual view.
|
|
2
|
+
|
|
3
|
+
Each image is drawn twice: with the protocol the probe found, and with half blocks.
|
|
4
|
+
Without arguments it draws a test gradient.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _gradient():
|
|
13
|
+
from PIL import Image, ImageDraw
|
|
14
|
+
|
|
15
|
+
image = Image.new("RGB", (640, 360))
|
|
16
|
+
draw = ImageDraw.Draw(image)
|
|
17
|
+
for x in range(640):
|
|
18
|
+
draw.line([(x, 0), (x, 359)], fill=(x * 255 // 639, 120, 255 - x * 255 // 639))
|
|
19
|
+
draw.ellipse((220, 80, 420, 280), fill=(250, 250, 250), outline=(0, 0, 0), width=6)
|
|
20
|
+
draw.text((20, 20), "pixcell", fill=(255, 255, 255))
|
|
21
|
+
return image
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _bouncing(frames: int = 16):
|
|
25
|
+
"""An animated GIF: a ball bouncing across a gradient, with a frame counter. Small
|
|
26
|
+
enough (320x120, 16 frames: ~2.4 MB decoded) to play natively within Rex's budget."""
|
|
27
|
+
import io
|
|
28
|
+
|
|
29
|
+
from PIL import Image, ImageDraw
|
|
30
|
+
|
|
31
|
+
pictures = []
|
|
32
|
+
for number in range(frames):
|
|
33
|
+
image = Image.new("RGB", (320, 120))
|
|
34
|
+
draw = ImageDraw.Draw(image)
|
|
35
|
+
for x in range(320):
|
|
36
|
+
draw.line([(x, 0), (x, 119)], fill=(40, 60 + x * 120 // 319, 140))
|
|
37
|
+
position = abs((number * 2 * 260 // frames) % 520 - 260)
|
|
38
|
+
draw.ellipse((position + 10, 40, position + 50, 80), fill=(255, 200, 40))
|
|
39
|
+
draw.text((10, 8), f"frame {number + 1}/{frames}", fill=(255, 255, 255))
|
|
40
|
+
pictures.append(image)
|
|
41
|
+
buffer = io.BytesIO()
|
|
42
|
+
pictures[0].save(buffer, format="GIF", save_all=True, append_images=pictures[1:], duration=80, loop=0)
|
|
43
|
+
buffer.seek(0)
|
|
44
|
+
return Image.open(buffer)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _tall_page(width: int):
|
|
48
|
+
"""A long, detailed page (headings, text, code, boxes) to scroll: like a web page."""
|
|
49
|
+
import random
|
|
50
|
+
|
|
51
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
52
|
+
|
|
53
|
+
def font(size):
|
|
54
|
+
try:
|
|
55
|
+
return ImageFont.load_default(size=size)
|
|
56
|
+
except TypeError:
|
|
57
|
+
return ImageFont.load_default()
|
|
58
|
+
|
|
59
|
+
random.seed(7)
|
|
60
|
+
words = ("terminal pixels frame image kitty pixcell render scroll update browser page text code "
|
|
61
|
+
"placeholder diff zlib encode decode widget textual console agent stream click").split()
|
|
62
|
+
page = Image.new("RGB", (width, 3600), (250, 250, 247))
|
|
63
|
+
draw = ImageDraw.Draw(page)
|
|
64
|
+
y = 20
|
|
65
|
+
while y < page.height - 120:
|
|
66
|
+
kind = random.random()
|
|
67
|
+
if kind < 0.15:
|
|
68
|
+
draw.text((24, y), " ".join(random.choices(words, k=4)).title(), fill=(20, 30, 60), font=font(28))
|
|
69
|
+
y += 46
|
|
70
|
+
elif kind < 0.3:
|
|
71
|
+
draw.rectangle((24, y, width - 24, y + 90), fill=(30, 34, 44))
|
|
72
|
+
for line in range(5):
|
|
73
|
+
draw.text((36, y + 8 + line * 16), " " * random.randint(0, 3) + " ".join(random.choices(words, k=6)),
|
|
74
|
+
fill=random.choice([(152, 195, 121), (229, 192, 123), (97, 175, 239), (224, 108, 117)]), font=font(13))
|
|
75
|
+
y += 104
|
|
76
|
+
elif kind < 0.4:
|
|
77
|
+
colour = random.choice([(219, 234, 254), (220, 252, 231), (254, 243, 199), (252, 231, 243)])
|
|
78
|
+
draw.rounded_rectangle((24, y, width - 24, y + 60), radius=10, fill=colour)
|
|
79
|
+
draw.text((40, y + 20), " ".join(random.choices(words, k=9)), fill=(40, 40, 40), font=font(16))
|
|
80
|
+
y += 76
|
|
81
|
+
else:
|
|
82
|
+
for _ in range(random.randint(3, 6)):
|
|
83
|
+
draw.text((24, y), " ".join(random.choices(words, k=11)), fill=(55, 55, 55), font=font(15))
|
|
84
|
+
y += 21
|
|
85
|
+
y += 12
|
|
86
|
+
return page
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def live_demo() -> None:
|
|
90
|
+
"""``python -m pixcell live``: a generated 640x400 scene at 15 fps. Click to drop a
|
|
91
|
+
marker at that pixel; scroll to change the orbit's speed."""
|
|
92
|
+
import math
|
|
93
|
+
import os
|
|
94
|
+
import threading
|
|
95
|
+
import time
|
|
96
|
+
|
|
97
|
+
from PIL import Image, ImageDraw
|
|
98
|
+
from textual.app import App, ComposeResult
|
|
99
|
+
from textual.widgets import Footer, Static
|
|
100
|
+
|
|
101
|
+
from deft_console._vendor.pixcell import textual as ct
|
|
102
|
+
from deft_console._vendor.pixcell.live import LiveImage
|
|
103
|
+
|
|
104
|
+
found = ct.configure()
|
|
105
|
+
width, height = 640, 400
|
|
106
|
+
arguments = sys.argv[2:]
|
|
107
|
+
fps = float(arguments[0]) if arguments and arguments[0].replace(".", "").isdigit() else 15.0
|
|
108
|
+
full = "full" in arguments # every pixel changes (a flat colour: compresses well)
|
|
109
|
+
scrolling = "scroll" in arguments # real worst case: a detailed page scrolling
|
|
110
|
+
page = _tall_page(width) if scrolling else None
|
|
111
|
+
marks: list[tuple[int, int]] = []
|
|
112
|
+
speed = [1.0]
|
|
113
|
+
|
|
114
|
+
def scene(t: float) -> Image.Image:
|
|
115
|
+
if page is not None:
|
|
116
|
+
top = int(t * 60 * 6) % (page.height - height) # 6 pixels a frame at 60 fps
|
|
117
|
+
return page.crop((0, top, width, top + height))
|
|
118
|
+
return _scene(t)
|
|
119
|
+
|
|
120
|
+
def _scene(t: float) -> Image.Image:
|
|
121
|
+
shade = int(t * 40) % 40 if full else 0
|
|
122
|
+
picture = Image.new("RGB", (width, height), (18 + shade, 24, 38))
|
|
123
|
+
draw = ImageDraw.Draw(picture)
|
|
124
|
+
for x in range(0, width, 40):
|
|
125
|
+
draw.line([(x, 0), (x, height)], fill=(30, 40, 60))
|
|
126
|
+
for y in range(0, height, 40):
|
|
127
|
+
draw.line([(0, y), (width, y)], fill=(30, 40, 60))
|
|
128
|
+
angle = t * speed[0]
|
|
129
|
+
cx, cy = width / 2 + 180 * math.cos(angle), height / 2 + 120 * math.sin(angle)
|
|
130
|
+
draw.ellipse((cx - 22, cy - 22, cx + 22, cy + 22), fill=(80, 200, 255))
|
|
131
|
+
bar = int((math.sin(t * 2) + 1) / 2 * (width - 120))
|
|
132
|
+
draw.rectangle((bar, height - 30, bar + 120, height - 14), fill=(255, 170, 40))
|
|
133
|
+
draw.text((14, 12), time.strftime("%H:%M:%S") + f" t={t:6.2f}s", fill=(230, 230, 230))
|
|
134
|
+
for mx, my in marks[-5:]:
|
|
135
|
+
draw.line([(mx - 8, my), (mx + 8, my)], fill=(255, 60, 60), width=2)
|
|
136
|
+
draw.line([(mx, my - 8), (mx, my + 8)], fill=(255, 60, 60), width=2)
|
|
137
|
+
draw.text((mx + 10, my + 4), f"{mx},{my}", fill=(255, 120, 120))
|
|
138
|
+
return picture
|
|
139
|
+
|
|
140
|
+
class Live(App):
|
|
141
|
+
CSS = "LiveImage { width: 80; height: auto; } Static { color: $text-muted; }"
|
|
142
|
+
BINDINGS = [("q", "quit", "Quit")]
|
|
143
|
+
|
|
144
|
+
def compose(self) -> ComposeResult:
|
|
145
|
+
yield Static(f"{found.protocol} ({found.terminal} {found.version}); format {found.frame_format}; "
|
|
146
|
+
f"{'edits' if found.edits else 'whole frames'} — click to mark, scroll for speed")
|
|
147
|
+
yield LiveImage((width, height), fps=fps, id="live")
|
|
148
|
+
yield Static("", id="stats")
|
|
149
|
+
yield Footer()
|
|
150
|
+
|
|
151
|
+
def on_mount(self) -> None:
|
|
152
|
+
live = self.query_one(LiveImage)
|
|
153
|
+
started = time.monotonic()
|
|
154
|
+
|
|
155
|
+
def produce() -> None:
|
|
156
|
+
while self.is_running:
|
|
157
|
+
live.push(scene(time.monotonic() - started))
|
|
158
|
+
time.sleep(1 / (fps * 2)) # faster than shown: the newest frame wins
|
|
159
|
+
|
|
160
|
+
threading.Thread(target=produce, daemon=True).start()
|
|
161
|
+
self.started = time.monotonic()
|
|
162
|
+
self.set_interval(1, self.stats)
|
|
163
|
+
|
|
164
|
+
def stats(self) -> None:
|
|
165
|
+
live = self.query_one(LiveImage)
|
|
166
|
+
frames = max(1, live.frames_shown)
|
|
167
|
+
elapsed = time.monotonic() - self.started
|
|
168
|
+
line = (
|
|
169
|
+
f"target {fps:g} fps{' (whole frame changes)' if full else ''}{' (page scrolling)' if scrolling else ''}: {live.frames_shown / elapsed:.1f} fps shown, "
|
|
170
|
+
f"{live.bytes_sent / frames / 1024:.1f} KB/frame, {live.busy_seconds / frames * 1000:.1f} ms/frame main thread"
|
|
171
|
+
)
|
|
172
|
+
self.query_one("#stats", Static).update(line)
|
|
173
|
+
if path := os.environ.get("PIXCELL_LIVE_STATS"): # for terminals we can't read back
|
|
174
|
+
with open(path, "w") as out:
|
|
175
|
+
out.write(f"{found.protocol} {found.terminal} {found.version} {found.frame_format} "
|
|
176
|
+
f"{'edits' if found.edits else 'swap'}: {line}\n")
|
|
177
|
+
|
|
178
|
+
def on_live_image_clicked(self, message: LiveImage.Clicked) -> None:
|
|
179
|
+
marks.append((message.x, message.y))
|
|
180
|
+
|
|
181
|
+
def on_live_image_scrolled(self, message: LiveImage.Scrolled) -> None:
|
|
182
|
+
speed[0] = max(0.2, min(5.0, speed[0] * (0.8 if message.delta > 0 else 1.25)))
|
|
183
|
+
|
|
184
|
+
Live().run()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def main() -> None:
|
|
188
|
+
from PIL import Image as PILImage
|
|
189
|
+
from textual.app import App, ComposeResult
|
|
190
|
+
from textual.containers import VerticalScroll
|
|
191
|
+
from textual.widgets import Footer, Static
|
|
192
|
+
|
|
193
|
+
from deft_console._vendor.pixcell import textual as ct
|
|
194
|
+
|
|
195
|
+
found = ct.configure()
|
|
196
|
+
images = [(path, PILImage.open(path)) for path in sys.argv[1:]] or [
|
|
197
|
+
("animated GIF", _bouncing()),
|
|
198
|
+
("gradient", _gradient()),
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
class Demo(App):
|
|
202
|
+
CSS = "Image { width: 40; height: auto; margin: 0 0 1 0; } Static { color: $text-muted; }"
|
|
203
|
+
BINDINGS = [("q", "quit", "Quit")]
|
|
204
|
+
|
|
205
|
+
def compose(self) -> ComposeResult:
|
|
206
|
+
with VerticalScroll():
|
|
207
|
+
yield Static(
|
|
208
|
+
f"{found.protocol}: {found.reason}; cell {found.cell_size}; terminal {found.terminal or '?'}; "
|
|
209
|
+
f"animation {'plays' if found.animation else 'stays still'}"
|
|
210
|
+
)
|
|
211
|
+
for name, image in images:
|
|
212
|
+
picture = ct.Image(image)
|
|
213
|
+
moving = f", {picture._animation} animation" if picture.animated else (
|
|
214
|
+
", still" if getattr(image, "is_animated", False) else ""
|
|
215
|
+
)
|
|
216
|
+
yield Static(f"{name} ({picture.protocol}{moving})")
|
|
217
|
+
yield picture
|
|
218
|
+
yield Static(f"{name} (halfblock)")
|
|
219
|
+
yield ct.Image(image, protocol="halfblock")
|
|
220
|
+
yield Footer()
|
|
221
|
+
|
|
222
|
+
Demo().run()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
if sys.argv[1:2] == ["live"]:
|
|
227
|
+
live_demo()
|
|
228
|
+
else:
|
|
229
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Row/column diacritics for Kitty Unicode placeholders (generated by
|
|
2
|
+
tools/gen_diacritics.py from the Unicode 6.0.0 Character Database). Index = number."""
|
|
3
|
+
|
|
4
|
+
DIACRITICS: tuple[int, ...] = (
|
|
5
|
+
0x0305, 0x030D, 0x030E, 0x0310, 0x0312, 0x033D, 0x033E, 0x033F,
|
|
6
|
+
0x0346, 0x034A, 0x034B, 0x034C, 0x0350, 0x0351, 0x0352, 0x0357,
|
|
7
|
+
0x035B, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, 0x0368, 0x0369,
|
|
8
|
+
0x036A, 0x036B, 0x036C, 0x036D, 0x036E, 0x036F, 0x0483, 0x0484,
|
|
9
|
+
0x0485, 0x0486, 0x0487, 0x0592, 0x0593, 0x0594, 0x0595, 0x0597,
|
|
10
|
+
0x0598, 0x0599, 0x059C, 0x059D, 0x059E, 0x059F, 0x05A0, 0x05A1,
|
|
11
|
+
0x05A8, 0x05A9, 0x05AB, 0x05AC, 0x05AF, 0x05C4, 0x0610, 0x0611,
|
|
12
|
+
0x0612, 0x0613, 0x0614, 0x0615, 0x0616, 0x0617, 0x0657, 0x0658,
|
|
13
|
+
0x0659, 0x065A, 0x065B, 0x065D, 0x065E, 0x06D6, 0x06D7, 0x06D8,
|
|
14
|
+
0x06D9, 0x06DA, 0x06DB, 0x06DC, 0x06DF, 0x06E0, 0x06E1, 0x06E2,
|
|
15
|
+
0x06E4, 0x06E7, 0x06E8, 0x06EB, 0x06EC, 0x0730, 0x0732, 0x0733,
|
|
16
|
+
0x0735, 0x0736, 0x073A, 0x073D, 0x073F, 0x0740, 0x0741, 0x0743,
|
|
17
|
+
0x0745, 0x0747, 0x0749, 0x074A, 0x07EB, 0x07EC, 0x07ED, 0x07EE,
|
|
18
|
+
0x07EF, 0x07F0, 0x07F1, 0x07F3, 0x0816, 0x0817, 0x0818, 0x0819,
|
|
19
|
+
0x081B, 0x081C, 0x081D, 0x081E, 0x081F, 0x0820, 0x0821, 0x0822,
|
|
20
|
+
0x0823, 0x0825, 0x0826, 0x0827, 0x0829, 0x082A, 0x082B, 0x082C,
|
|
21
|
+
0x082D, 0x0951, 0x0953, 0x0954, 0x0F82, 0x0F83, 0x0F86, 0x0F87,
|
|
22
|
+
0x135D, 0x135E, 0x135F, 0x17DD, 0x193A, 0x1A17, 0x1A75, 0x1A76,
|
|
23
|
+
0x1A77, 0x1A78, 0x1A79, 0x1A7A, 0x1A7B, 0x1A7C, 0x1B6B, 0x1B6D,
|
|
24
|
+
0x1B6E, 0x1B6F, 0x1B70, 0x1B71, 0x1B72, 0x1B73, 0x1CD0, 0x1CD1,
|
|
25
|
+
0x1CD2, 0x1CDA, 0x1CDB, 0x1CE0, 0x1DC0, 0x1DC1, 0x1DC3, 0x1DC4,
|
|
26
|
+
0x1DC5, 0x1DC6, 0x1DC7, 0x1DC8, 0x1DC9, 0x1DCB, 0x1DCC, 0x1DD1,
|
|
27
|
+
0x1DD2, 0x1DD3, 0x1DD4, 0x1DD5, 0x1DD6, 0x1DD7, 0x1DD8, 0x1DD9,
|
|
28
|
+
0x1DDA, 0x1DDB, 0x1DDC, 0x1DDD, 0x1DDE, 0x1DDF, 0x1DE0, 0x1DE1,
|
|
29
|
+
0x1DE2, 0x1DE3, 0x1DE4, 0x1DE5, 0x1DE6, 0x1DFE, 0x20D0, 0x20D1,
|
|
30
|
+
0x20D4, 0x20D5, 0x20D6, 0x20D7, 0x20DB, 0x20DC, 0x20E1, 0x20E7,
|
|
31
|
+
0x20E9, 0x20F0, 0x2CEF, 0x2CF0, 0x2CF1, 0x2DE0, 0x2DE1, 0x2DE2,
|
|
32
|
+
0x2DE3, 0x2DE4, 0x2DE5, 0x2DE6, 0x2DE7, 0x2DE8, 0x2DE9, 0x2DEA,
|
|
33
|
+
0x2DEB, 0x2DEC, 0x2DED, 0x2DEE, 0x2DEF, 0x2DF0, 0x2DF1, 0x2DF2,
|
|
34
|
+
0x2DF3, 0x2DF4, 0x2DF5, 0x2DF6, 0x2DF7, 0x2DF8, 0x2DF9, 0x2DFA,
|
|
35
|
+
0x2DFB, 0x2DFC, 0x2DFD, 0x2DFE, 0x2DFF, 0xA66F, 0xA67C, 0xA67D,
|
|
36
|
+
0xA6F0, 0xA6F1, 0xA8E0, 0xA8E1, 0xA8E2, 0xA8E3, 0xA8E4, 0xA8E5,
|
|
37
|
+
0xA8E6, 0xA8E7, 0xA8E8, 0xA8E9, 0xA8EA, 0xA8EB, 0xA8EC, 0xA8ED,
|
|
38
|
+
0xA8EE, 0xA8EF, 0xA8F0, 0xA8F1, 0xAAB0, 0xAAB2, 0xAAB3, 0xAAB7,
|
|
39
|
+
0xAAB8, 0xAABE, 0xAABF, 0xAAC1, 0xFE20, 0xFE21, 0xFE22, 0xFE23,
|
|
40
|
+
0xFE24, 0xFE25, 0xFE26, 0x10A0F, 0x10A38, 0x1D185, 0x1D186, 0x1D187,
|
|
41
|
+
0x1D188, 0x1D189, 0x1D1AA, 0x1D1AB, 0x1D1AC, 0x1D1AD, 0x1D242, 0x1D243,
|
|
42
|
+
0x1D244,
|
|
43
|
+
)
|