webbee 0.4.1__tar.gz → 0.4.2__tar.gz
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.
- {webbee-0.4.1 → webbee-0.4.2}/CHANGELOG.md +33 -0
- {webbee-0.4.1 → webbee-0.4.2}/PKG-INFO +6 -3
- {webbee-0.4.1 → webbee-0.4.2}/README.md +1 -1
- {webbee-0.4.1 → webbee-0.4.2}/pyproject.toml +5 -2
- webbee-0.4.2/src/webbee/__init__.py +1 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/clipboard.py +24 -4
- webbee-0.4.2/src/webbee/media.py +137 -0
- webbee-0.4.2/src/webbee/modals.py +504 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/render.py +18 -4
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/repl.py +22 -2
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/selection.py +3 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/session.py +12 -1
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tabs.py +64 -44
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tools.py +8 -1
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tui.py +472 -124
- webbee-0.4.2/tests/test_modals_e2e.py +188 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_render.py +9 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_repl.py +1 -1
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_tui.py +30 -3
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_version_badge_corner.py +21 -0
- webbee-0.4.1/src/webbee/__init__.py +0 -1
- {webbee-0.4.1 → webbee-0.4.2}/.github/workflows/publish.yml +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/.gitignore +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/LICENSE +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/install.sh +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/account.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/active_sessions.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/banner_art.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/boot.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/checkpoints.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/cli.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/clipboard_read.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/clipboard_session.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/coding_context.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/commands.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/config.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/consent.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/details.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/events.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/frames.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/home.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/home_view.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/http.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/instance_lock.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/contracts.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/embed.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/graph.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/models.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/query.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/service.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/store.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/watch.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/mode_store.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/native_files.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/native_read.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/output_pane.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/queue_panel.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/reflow.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/remote.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/repo.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/sessions.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/sizing.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/slots.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/steer.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/stream.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tab_store.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/thread.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tier_store.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/todo_panel.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tokens.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/update.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/urlopen.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/wallet.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/src/webbee/worktrees.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/__init__.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/conftest.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_account.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_active_sessions.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_audit_2026_07_25_findings.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_checkpoints.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_cli.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_clipboard.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_clipboard_read.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_coding_context.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_commands.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_config.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_details.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_events.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_freeze_fix.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_home.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_home_view.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_instance_lock.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_chunker.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_contracts.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_embed.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_graph.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_indexer.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_query.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_service.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_store.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_vectors.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_watch.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_marathon.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_mode_store.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_native_files.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_newtab_mode.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_packaging.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_queue_manage_0337.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_reflow.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_repo.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_session.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_sessions.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_sizing.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_slots.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_steer.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_stream.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_tab_restore_0337.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_tabs.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_terminal_ux_0336.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_thread.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_tier_store.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_todo_panel.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_tokens.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_tools.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_tools_vision_websearch.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_tui_hardening.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_update.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_update_badge_0340.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_urlopen.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_version.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_wallet.py +0 -0
- {webbee-0.4.1 → webbee-0.4.2}/tests/test_worktrees.py +0 -0
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
**Webbee v0.4.2: Universal `shell` Action Across Windows/macOS/Linux, Interactive TUI Modals, Cloud Voice STT & Complete Hover Engine.**
|
|
6
|
+
|
|
7
|
+
This release introduces the cross-platform `shell` tool (replacing the platform-specific `bash` terminology for complete Windows, macOS, and Linux clarity), brings responsive modal dialogs (`Attach File`, `Voice Note`, `Session Menu`) with fluid mouse hover highlights and keyboard navigation, integrates zero-dependency cloud speech-to-text (STT) via `/v1/voice/stt`, and guarantees zero mouse escape garbage in the prompt.
|
|
8
|
+
|
|
9
|
+
### Highlights & New Capabilities
|
|
10
|
+
|
|
11
|
+
* **Universal Cross-Platform `shell` Tool**:
|
|
12
|
+
- Renamed the core command execution tool from `bash` to `shell` to accurately reflect universal execution across Windows (`cmd`/`PowerShell`), macOS (`zsh`), and Linux (`bash`/`sh`).
|
|
13
|
+
- Retains transparent backward compatibility for existing scripts, tests, and model tool calls with aliased dispatch.
|
|
14
|
+
- Automatic UTF-8 environment forcing, path resolution for Homebrew (Apple Silicon + Intel) and Linuxbrew, and fail-soft non-interactive safeguards against password prompt deadlocks.
|
|
15
|
+
|
|
16
|
+
* **Interactive TUI Modals & Dialog System (`webbee.modals`)**:
|
|
17
|
+
- Responsive, centered modal dialogs for **File Attachment** (`📎`), **Voice Recording** (`🎤`), and **Session / Remote Routing** (`⋮`).
|
|
18
|
+
- Adaptive layout dynamically scaling to terminal dimensions (compact on 80-column terminals, comfortably expansive on ultra-wide screens).
|
|
19
|
+
- Native OS File Picker integration (macOS Finder, Linux Zenity/Kdialog, Windows PowerShell file dialog) with terminal list fallback.
|
|
20
|
+
|
|
21
|
+
* **Fluid Mouse Hover Engine & Keyboard Navigation**:
|
|
22
|
+
- Full support for `?1003h` any-event mouse tracking during active interactive sessions.
|
|
23
|
+
- Distinct 3-tier visual hierarchy: idle controls (`class:button`), crisp white hover highlight on pointer hover (`class:button.hover`), and signature Imperal yellow keyboard focus (`class:button.focused`).
|
|
24
|
+
- Interactive hover support enabled across tabs, tab close buttons (`✕`), new tab button (`+`), prompt action icons (`📎`, `🎤`, `⋮`), and footer mode/tier chips.
|
|
25
|
+
|
|
26
|
+
* **Cloud-Native Speech-To-Text (STT) Voice Notes**:
|
|
27
|
+
- In-terminal audio recording directly from default system microphones (Apple Silicon / Intel AVFoundation, Linux ALSA/PulseAudio, Windows DirectShow).
|
|
28
|
+
- Audio payloads sent to Imperal Core STT endpoint `/v1/voice/stt` and transcribed directly into clean conversational text in the input buffer before turn dispatch.
|
|
29
|
+
|
|
30
|
+
* **Zero-Garbage Prompt Protection (`scrub_mouse_residue`)**:
|
|
31
|
+
- Hardened input buffer against stray VT100 / DEC escape sequence leaks (`[<...` mouse coordinates or `[I` / `[O` focus reports).
|
|
32
|
+
- Reactive `on_text_changed` filter and modal-close buffer sanitizer instantly scrub residue before rendering.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
3
36
|
## 0.4.1
|
|
4
37
|
|
|
5
38
|
**Webbee v0.4.1: Cross-Platform Clipboard Resilience, Icon Gutter Alignment & Refined Terminal UX.**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: webbee
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Webbee 🐝 — the Imperal Cloud coding agent in your terminal
|
|
5
5
|
Project-URL: Homepage, https://imperal.io
|
|
6
6
|
Project-URL: Documentation, https://docs.imperal.io
|
|
@@ -16,12 +16,15 @@ Classifier: Environment :: Console
|
|
|
16
16
|
Classifier: Intended Audience :: Developers
|
|
17
17
|
Classifier: Operating System :: OS Independent
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
25
|
Classifier: Topic :: Software Development
|
|
23
26
|
Classifier: Topic :: Utilities
|
|
24
|
-
Requires-Python: >=3.
|
|
27
|
+
Requires-Python: >=3.9
|
|
25
28
|
Requires-Dist: httpx-sse>=0.4
|
|
26
29
|
Requires-Dist: httpx>=0.27
|
|
27
30
|
Requires-Dist: imperal-mcp>=0.5.0
|
|
@@ -120,7 +123,7 @@ Inside the interactive terminal:
|
|
|
120
123
|
| 📖 | `read_file` | Read text and source files byte-exact, with offset/limit paging |
|
|
121
124
|
| ✏️ | `write_file` | Create or overwrite files atomically with snapshot tracking |
|
|
122
125
|
| 🔧 | `edit_file` / `multi_edit` | Precise find-and-replace edits across one or multiple files |
|
|
123
|
-
| ⚡ | `
|
|
126
|
+
| ⚡ | `shell` | Execute shell/terminal commands across Windows, macOS and Linux with streaming |
|
|
124
127
|
| 🔎 | `grep` / `glob` | Fast regex code search and workspace file pattern matching |
|
|
125
128
|
| 🌐 | `web_search` / `read_url` | Search the web and read web documentation in clean Markdown |
|
|
126
129
|
| 🖼️ | `view_image` | Inspect images with native multimodal vision + OCR fallback |
|
|
@@ -73,7 +73,7 @@ Inside the interactive terminal:
|
|
|
73
73
|
| 📖 | `read_file` | Read text and source files byte-exact, with offset/limit paging |
|
|
74
74
|
| ✏️ | `write_file` | Create or overwrite files atomically with snapshot tracking |
|
|
75
75
|
| 🔧 | `edit_file` / `multi_edit` | Precise find-and-replace edits across one or multiple files |
|
|
76
|
-
| ⚡ | `
|
|
76
|
+
| ⚡ | `shell` | Execute shell/terminal commands across Windows, macOS and Linux with streaming |
|
|
77
77
|
| 🔎 | `grep` / `glob` | Fast regex code search and workspace file pattern matching |
|
|
78
78
|
| 🌐 | `web_search` / `read_url` | Search the web and read web documentation in clean Markdown |
|
|
79
79
|
| 🖼️ | `view_image` | Inspect images with native multimodal vision + OCR fallback |
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "webbee"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.2"
|
|
4
4
|
description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "GPL-3.0-or-later"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
8
|
-
requires-python = ">=3.
|
|
8
|
+
requires-python = ">=3.9"
|
|
9
9
|
authors = [{ name = "Imperal, Inc.", email = "hello@imperal.io" }]
|
|
10
10
|
dependencies = [
|
|
11
11
|
"imperal-mcp>=0.5.0",
|
|
@@ -41,9 +41,12 @@ classifiers = [
|
|
|
41
41
|
"Environment :: Console",
|
|
42
42
|
"Intended Audience :: Developers",
|
|
43
43
|
"Programming Language :: Python :: 3",
|
|
44
|
+
"Programming Language :: Python :: 3.9",
|
|
45
|
+
"Programming Language :: Python :: 3.10",
|
|
44
46
|
"Programming Language :: Python :: 3.11",
|
|
45
47
|
"Programming Language :: Python :: 3.12",
|
|
46
48
|
"Programming Language :: Python :: 3.13",
|
|
49
|
+
"Programming Language :: Python :: 3.14",
|
|
47
50
|
"Operating System :: OS Independent",
|
|
48
51
|
"Topic :: Software Development",
|
|
49
52
|
"Topic :: Utilities",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.2"
|
|
@@ -60,15 +60,35 @@ def _local_copy_cmd() -> list[str] | None:
|
|
|
60
60
|
|
|
61
61
|
def _try_local_copy(text: str) -> bool:
|
|
62
62
|
"""Feed `text` to the local clipboard tool via stdin. True only on a
|
|
63
|
-
clean (returncode 0) run — never raises.
|
|
63
|
+
clean (returncode 0) run — never raises.
|
|
64
|
+
On Linux desktop (Pop!_OS / GNOME / Cosmic), also mirrors to the primary
|
|
65
|
+
selection buffer so middle-click paste and Shift+Insert work seamlessly
|
|
66
|
+
alongside Ctrl+Shift+V."""
|
|
64
67
|
cmd = _local_copy_cmd()
|
|
65
68
|
if cmd is None:
|
|
66
69
|
return False
|
|
70
|
+
payload = text.encode("utf-8", "replace")
|
|
71
|
+
success = False
|
|
67
72
|
try:
|
|
68
|
-
proc = subprocess.run(cmd, input=
|
|
69
|
-
|
|
73
|
+
proc = subprocess.run(cmd, input=payload, timeout=2)
|
|
74
|
+
success = (proc.returncode == 0)
|
|
70
75
|
except Exception:
|
|
71
|
-
|
|
76
|
+
success = False
|
|
77
|
+
|
|
78
|
+
if success and sys.platform not in ("darwin", "win32"):
|
|
79
|
+
# Linux primary selection dual-write
|
|
80
|
+
try:
|
|
81
|
+
from webbee.clipboard_session import is_wayland_session
|
|
82
|
+
if is_wayland_session() and shutil.which("wl-copy"):
|
|
83
|
+
subprocess.run(["wl-copy", "--primary"], input=payload, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=1)
|
|
84
|
+
elif shutil.which("xclip"):
|
|
85
|
+
subprocess.run(["xclip", "-selection", "primary"], input=payload, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=1)
|
|
86
|
+
elif shutil.which("xsel"):
|
|
87
|
+
subprocess.run(["xsel", "--primary", "--input"], input=payload, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=1)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
return success
|
|
72
92
|
|
|
73
93
|
|
|
74
94
|
def _osc52_emit(text: str) -> bool:
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Enterprise Cross-Platform Audio Playback & Terminal Image Viewer for Webbee Code.
|
|
2
|
+
|
|
3
|
+
Provides robust, zero-crash audio playback and in-terminal image viewing across:
|
|
4
|
+
- macOS (afplay, AVFoundation, iTerm2/Kitty image protocol)
|
|
5
|
+
- Linux (paplay, aplay, pw-play, ffplay, Sixel, Kitty, ANSI half-blocks)
|
|
6
|
+
- Windows (PowerShell System.Media.SoundPlayer, Windows Media Player, WT inline)
|
|
7
|
+
- Universal ANSI/Half-block fallback for any terminal on any OS.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
from typing import Optional, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def play_audio_file(file_path: str, *, block: bool = False) -> Optional[subprocess.Popen]:
|
|
21
|
+
"""Play an audio file (.wav, .mp3, .m4a, .ogg) across macOS, Linux, and Windows.
|
|
22
|
+
|
|
23
|
+
Non-blocking by default (runs in background process).
|
|
24
|
+
"""
|
|
25
|
+
if not os.path.exists(file_path):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
cmd = []
|
|
29
|
+
if sys.platform == "darwin":
|
|
30
|
+
if shutil.which("afplay"):
|
|
31
|
+
cmd = ["afplay", file_path]
|
|
32
|
+
elif shutil.which("ffplay"):
|
|
33
|
+
cmd = ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", file_path]
|
|
34
|
+
elif sys.platform.startswith("linux"):
|
|
35
|
+
if shutil.which("paplay") and file_path.endswith(".wav"):
|
|
36
|
+
cmd = ["paplay", file_path]
|
|
37
|
+
elif shutil.which("pw-play"):
|
|
38
|
+
cmd = ["pw-play", file_path]
|
|
39
|
+
elif shutil.which("aplay") and file_path.endswith(".wav"):
|
|
40
|
+
cmd = ["aplay", "-q", file_path]
|
|
41
|
+
elif shutil.which("ffplay"):
|
|
42
|
+
cmd = ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", file_path]
|
|
43
|
+
elif shutil.which("mpv"):
|
|
44
|
+
cmd = ["mpv", "--no-video", "--really-quiet", file_path]
|
|
45
|
+
elif sys.platform in ("win32", "cygwin"):
|
|
46
|
+
ps = shutil.which("powershell") or shutil.which("pwsh")
|
|
47
|
+
if ps:
|
|
48
|
+
abs_p = os.path.abspath(file_path).replace("'", "''")
|
|
49
|
+
cmd = [ps, "-c", f"(New-Object System.Media.SoundPlayer '{abs_p}').PlaySync()"]
|
|
50
|
+
elif shutil.which("ffplay"):
|
|
51
|
+
cmd = ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", file_path]
|
|
52
|
+
|
|
53
|
+
if not cmd:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
if block:
|
|
58
|
+
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
59
|
+
return None
|
|
60
|
+
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
61
|
+
return proc
|
|
62
|
+
except Exception:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def render_image_in_terminal(image_path: str, max_cols: int = 60, max_rows: int = 30) -> str:
|
|
67
|
+
"""Render an image directly inside the terminal.
|
|
68
|
+
|
|
69
|
+
Supported rendering protocols:
|
|
70
|
+
1. iTerm2 / WezTerm / Ghostty inline image protocol (OSC 1337)
|
|
71
|
+
2. Kitty terminal graphics protocol (APC G ...)
|
|
72
|
+
3. High-quality ANSI half-block (▀ / ▄) TrueColor fallback using PIL if available
|
|
73
|
+
4. Text description fallback if binary / non-renderable
|
|
74
|
+
"""
|
|
75
|
+
if not os.path.exists(image_path):
|
|
76
|
+
return f"[Image not found: {image_path}]"
|
|
77
|
+
|
|
78
|
+
# Check terminal protocol support
|
|
79
|
+
term = os.environ.get("TERM", "").lower()
|
|
80
|
+
term_prog = os.environ.get("TERM_PROGRAM", "").lower()
|
|
81
|
+
lc_term = os.environ.get("LC_TERMINAL", "").lower()
|
|
82
|
+
|
|
83
|
+
# 1. iTerm2 / WezTerm / Ghostty OSC 1337 Inline Image Protocol
|
|
84
|
+
if "iterm" in term_prog or "wezterm" in term_prog or "ghostty" in term_prog or "iterm" in lc_term:
|
|
85
|
+
try:
|
|
86
|
+
with open(image_path, "rb") as f:
|
|
87
|
+
b64_data = base64.b64encode(f.read()).decode("ascii")
|
|
88
|
+
filename_b64 = base64.b64encode(os.path.basename(image_path).encode("utf-8")).decode("ascii")
|
|
89
|
+
# OSC 1337 ; File=name=...;inline=1;width=auto;height=auto : <base64> ^G
|
|
90
|
+
return f"\033]1337;File=name={filename_b64};inline=1;width=auto;height={max_rows}:{b64_data}\007"
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
# 2. Kitty Graphics Protocol
|
|
95
|
+
if "kitty" in term or "kitty" in term_prog:
|
|
96
|
+
try:
|
|
97
|
+
with open(image_path, "rb") as f:
|
|
98
|
+
b64_data = base64.b64encode(f.read()).decode("ascii")
|
|
99
|
+
# Kitty chunked graphics transmission
|
|
100
|
+
return f"\033_Ga=T,f=100,t=d;{b64_data}\033\\"
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
# 3. ANSI TrueColor Half-Block Fallback (Works in ANY 24-bit terminal on macOS, Linux, Windows)
|
|
105
|
+
try:
|
|
106
|
+
from PIL import Image
|
|
107
|
+
img = Image.open(image_path).convert("RGB")
|
|
108
|
+
# Scale to max_cols x (max_rows * 2) because each character cell has 2 vertical subpixels (▀)
|
|
109
|
+
w, h = img.size
|
|
110
|
+
aspect = w / max(1, h)
|
|
111
|
+
target_w = min(max_cols, w)
|
|
112
|
+
target_h = int(target_w / max(0.1, aspect * 2))
|
|
113
|
+
target_h = min(max_rows, max(1, target_h)) * 2
|
|
114
|
+
|
|
115
|
+
img = img.resize((target_w, target_h), Image.Resampling.BILINEAR)
|
|
116
|
+
pix = img.load()
|
|
117
|
+
|
|
118
|
+
lines = []
|
|
119
|
+
for y in range(0, target_h, 2):
|
|
120
|
+
line_parts = []
|
|
121
|
+
for x in range(target_w):
|
|
122
|
+
r_top, g_top, b_top = pix[x, y]
|
|
123
|
+
if y + 1 < target_h:
|
|
124
|
+
r_bot, g_bot, b_bot = pix[x, y + 1]
|
|
125
|
+
# ▀ with fg=top, bg=bot
|
|
126
|
+
line_parts.append(f"\033[38;2;{r_top};{g_top};{b_top}m\033[48;2;{r_bot};{g_bot};{b_bot}m▀")
|
|
127
|
+
else:
|
|
128
|
+
line_parts.append(f"\033[38;2;{r_top};{g_top};{b_top}m▀")
|
|
129
|
+
line_parts.append("\033[0m")
|
|
130
|
+
lines.append("".join(line_parts))
|
|
131
|
+
return "\n".join(lines)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
# 4. Graceful metadata banner
|
|
136
|
+
size_kb = os.path.getsize(image_path) / 1024.0
|
|
137
|
+
return f"🖼️ [Image: {os.path.basename(image_path)} ({size_kb:.1f} KB)]"
|