termview 0.1.0__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.
@@ -0,0 +1,28 @@
1
+ # Build artifacts
2
+ build/
3
+ dist/
4
+ *.egg-info/
5
+ *.egg
6
+ wheels/
7
+ .eggs/
8
+
9
+ # Python
10
+ __pycache__/
11
+ *.py[cod]
12
+ *$py.class
13
+ *.so
14
+ .Python
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+
19
+ # Virtualenvs
20
+ .venv/
21
+ venv/
22
+ env/
23
+
24
+ # OS / editor
25
+ .DS_Store
26
+ .idea/
27
+ .vscode/
28
+ *.swp
termview-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zain ul Wahaj
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,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: termview
3
+ Version: 0.1.0
4
+ Summary: View images, animated GIFs, and videos in the terminal — with audio and keyboard controls
5
+ Project-URL: Homepage, https://github.com/yourusername/termview
6
+ Project-URL: Repository, https://github.com/yourusername/termview
7
+ Project-URL: Issues, https://github.com/yourusername/termview/issues
8
+ Author: Zain ul Wahaj
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ansi,image,iterm2,kitty,sixel,terminal,video,viewer
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Multimedia :: Graphics :: Viewers
23
+ Classifier: Topic :: Multimedia :: Video :: Display
24
+ Classifier: Topic :: Terminals
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: numpy>=1.24
27
+ Requires-Dist: pillow>=10.0
28
+ Provides-Extra: video
29
+ Requires-Dist: opencv-python-headless>=4.8; extra == 'video'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # termview
33
+
34
+ View images, animated GIFs, and videos in your terminal. Audio + keyboard controls included.
35
+
36
+ ```bash
37
+ tv photo.jpg
38
+ tv animation.gif
39
+ tv movie.mp4
40
+ ```
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install -e ".[video]"
46
+ ```
47
+
48
+ Video playback also wants `ffmpeg` for audio. Without it, video plays silently:
49
+
50
+ ```bash
51
+ brew install ffmpeg # macOS
52
+ apt install ffmpeg # Debian/Ubuntu
53
+ ```
54
+
55
+ ## How it picks a renderer
56
+
57
+ `tv` auto-detects the best graphics protocol your terminal supports and falls
58
+ back gracefully. The four paths, in quality order:
59
+
60
+ | Renderer | Used when | Quality |
61
+ |---|---|---|
62
+ | **kitty** | Kitty, WezTerm, Ghostty (sets `$KITTY_WINDOW_ID` or `$TERM_PROGRAM`) | pixel-perfect |
63
+ | **iterm2** | iTerm2, Warp (sets `$TERM_PROGRAM=iTerm.app`) | pixel-perfect |
64
+ | **sixel** | xterm, foot, Windows Terminal, mlterm (queried via DA1) | pixel-perfect |
65
+ | **block** | everywhere else (universal fallback) | ANSI background fills, one image pixel per cell |
66
+
67
+ The **block** renderer auto-switches between truecolor (`\033[48;2;R;G;Bm`) and
68
+ xterm 256-color with Floyd-Steinberg dithering depending on what your terminal
69
+ actually supports — macOS Terminal.app gets dithered output, everything else
70
+ gets full 24-bit.
71
+
72
+ Force a renderer:
73
+
74
+ ```bash
75
+ tv photo.jpg --renderer kitty
76
+ tv photo.jpg --renderer block --depth 256
77
+ ```
78
+
79
+ ## Video playback
80
+
81
+ ```bash
82
+ tv movie.mp4 # plays with audio (if ffmpeg installed)
83
+ tv movie.mp4 --no-audio # silent
84
+ tv movie.mp4 --fps 8 # throttle frame rate
85
+ ```
86
+
87
+ ### Keyboard controls
88
+
89
+ | Key | Action |
90
+ |---|---|
91
+ | `space` | play / pause |
92
+ | `←` `→` | seek -5s / +5s |
93
+ | `↓` `↑` | seek -30s / +30s |
94
+ | `,` `.` | previous / next frame (while paused) |
95
+ | `m` | mute / unmute |
96
+ | `+` `-` | volume up / down |
97
+ | `0` | restart from beginning |
98
+ | `q` / `esc` | quit |
99
+
100
+ Add `--no-controls` to disable for scripting / asciinema recording.
101
+
102
+ ## Cross-terminal notes
103
+
104
+ | Environment | Behavior |
105
+ |---|---|
106
+ | **tmux** | Forces the block renderer. Pixel-protocol passthrough is fragile across tmux versions; `--renderer kitty` etc. can still be forced if you've enabled `allow-passthrough on` (tmux 3.4+). |
107
+ | **SSH** | Forces the block renderer. Inline-image protocols don't survive most SSH chains. |
108
+ | **macOS Terminal.app** | Auto-detected as 256-color. Floyd-Steinberg dithering kicks in for stills; video uses no-dither for stability and an automatic 12fps cap. |
109
+ | **Windows Terminal** | Auto-detected via `$WT_SESSION`, uses sixel. |
110
+ | **non-TTY stdout** (`tv x.png > out`) | Video playback refuses. Images write a renderable stream that's only meaningful when re-played to a terminal. |
111
+
112
+ ## CLI reference
113
+
114
+ ```text
115
+ usage: tv [-h] [--renderer NAME] [--depth DEPTH] [--width COLS] [--no-crop]
116
+ [--fps N] [--no-audio] [--no-controls] [--loop] [-v]
117
+ file
118
+
119
+ rendering:
120
+ --renderer NAME kitty | iterm2 | sixel | block (default: auto)
121
+ --depth DEPTH truecolor | 256 (default: auto)
122
+ --width COLS override terminal width
123
+ --no-crop disable automatic border cropping
124
+
125
+ video / animation:
126
+ --fps N limit playback frame rate
127
+ --no-audio disable audio
128
+ --no-controls disable keyboard controls
129
+ --loop loop animated images (default: on)
130
+
131
+ -v, --verbose print detection diagnostics
132
+ ```
133
+
134
+ ## Library use
135
+
136
+ ```python
137
+ from termview import load_image, fit_image, get_renderer, detect_renderer, terminal_size
138
+
139
+ img = load_image("photo.jpg")
140
+ cols, rows = terminal_size()
141
+ renderer_type = detect_renderer()
142
+ fitted = fit_image(img, cols, rows, renderer_type)
143
+ get_renderer(renderer_type).display(fitted)
144
+ ```
145
+
146
+ Video and animation playback have higher-level entry points
147
+ (`stream_video`, `stream_animation`) that bundle the playback loop, audio
148
+ process management, keyboard input, and terminal state restoration.
@@ -0,0 +1,117 @@
1
+ # termview
2
+
3
+ View images, animated GIFs, and videos in your terminal. Audio + keyboard controls included.
4
+
5
+ ```bash
6
+ tv photo.jpg
7
+ tv animation.gif
8
+ tv movie.mp4
9
+ ```
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install -e ".[video]"
15
+ ```
16
+
17
+ Video playback also wants `ffmpeg` for audio. Without it, video plays silently:
18
+
19
+ ```bash
20
+ brew install ffmpeg # macOS
21
+ apt install ffmpeg # Debian/Ubuntu
22
+ ```
23
+
24
+ ## How it picks a renderer
25
+
26
+ `tv` auto-detects the best graphics protocol your terminal supports and falls
27
+ back gracefully. The four paths, in quality order:
28
+
29
+ | Renderer | Used when | Quality |
30
+ |---|---|---|
31
+ | **kitty** | Kitty, WezTerm, Ghostty (sets `$KITTY_WINDOW_ID` or `$TERM_PROGRAM`) | pixel-perfect |
32
+ | **iterm2** | iTerm2, Warp (sets `$TERM_PROGRAM=iTerm.app`) | pixel-perfect |
33
+ | **sixel** | xterm, foot, Windows Terminal, mlterm (queried via DA1) | pixel-perfect |
34
+ | **block** | everywhere else (universal fallback) | ANSI background fills, one image pixel per cell |
35
+
36
+ The **block** renderer auto-switches between truecolor (`\033[48;2;R;G;Bm`) and
37
+ xterm 256-color with Floyd-Steinberg dithering depending on what your terminal
38
+ actually supports — macOS Terminal.app gets dithered output, everything else
39
+ gets full 24-bit.
40
+
41
+ Force a renderer:
42
+
43
+ ```bash
44
+ tv photo.jpg --renderer kitty
45
+ tv photo.jpg --renderer block --depth 256
46
+ ```
47
+
48
+ ## Video playback
49
+
50
+ ```bash
51
+ tv movie.mp4 # plays with audio (if ffmpeg installed)
52
+ tv movie.mp4 --no-audio # silent
53
+ tv movie.mp4 --fps 8 # throttle frame rate
54
+ ```
55
+
56
+ ### Keyboard controls
57
+
58
+ | Key | Action |
59
+ |---|---|
60
+ | `space` | play / pause |
61
+ | `←` `→` | seek -5s / +5s |
62
+ | `↓` `↑` | seek -30s / +30s |
63
+ | `,` `.` | previous / next frame (while paused) |
64
+ | `m` | mute / unmute |
65
+ | `+` `-` | volume up / down |
66
+ | `0` | restart from beginning |
67
+ | `q` / `esc` | quit |
68
+
69
+ Add `--no-controls` to disable for scripting / asciinema recording.
70
+
71
+ ## Cross-terminal notes
72
+
73
+ | Environment | Behavior |
74
+ |---|---|
75
+ | **tmux** | Forces the block renderer. Pixel-protocol passthrough is fragile across tmux versions; `--renderer kitty` etc. can still be forced if you've enabled `allow-passthrough on` (tmux 3.4+). |
76
+ | **SSH** | Forces the block renderer. Inline-image protocols don't survive most SSH chains. |
77
+ | **macOS Terminal.app** | Auto-detected as 256-color. Floyd-Steinberg dithering kicks in for stills; video uses no-dither for stability and an automatic 12fps cap. |
78
+ | **Windows Terminal** | Auto-detected via `$WT_SESSION`, uses sixel. |
79
+ | **non-TTY stdout** (`tv x.png > out`) | Video playback refuses. Images write a renderable stream that's only meaningful when re-played to a terminal. |
80
+
81
+ ## CLI reference
82
+
83
+ ```text
84
+ usage: tv [-h] [--renderer NAME] [--depth DEPTH] [--width COLS] [--no-crop]
85
+ [--fps N] [--no-audio] [--no-controls] [--loop] [-v]
86
+ file
87
+
88
+ rendering:
89
+ --renderer NAME kitty | iterm2 | sixel | block (default: auto)
90
+ --depth DEPTH truecolor | 256 (default: auto)
91
+ --width COLS override terminal width
92
+ --no-crop disable automatic border cropping
93
+
94
+ video / animation:
95
+ --fps N limit playback frame rate
96
+ --no-audio disable audio
97
+ --no-controls disable keyboard controls
98
+ --loop loop animated images (default: on)
99
+
100
+ -v, --verbose print detection diagnostics
101
+ ```
102
+
103
+ ## Library use
104
+
105
+ ```python
106
+ from termview import load_image, fit_image, get_renderer, detect_renderer, terminal_size
107
+
108
+ img = load_image("photo.jpg")
109
+ cols, rows = terminal_size()
110
+ renderer_type = detect_renderer()
111
+ fitted = fit_image(img, cols, rows, renderer_type)
112
+ get_renderer(renderer_type).display(fitted)
113
+ ```
114
+
115
+ Video and animation playback have higher-level entry points
116
+ (`stream_video`, `stream_animation`) that bundle the playback loop, audio
117
+ process management, keyboard input, and terminal state restoration.
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "termview"
7
+ version = "0.1.0"
8
+ description = "View images, animated GIFs, and videos in the terminal — with audio and keyboard controls"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Zain ul Wahaj" },
15
+ ]
16
+ keywords = ["terminal", "image", "video", "viewer", "ansi", "sixel", "kitty", "iterm2"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Console",
20
+ "Intended Audience :: End Users/Desktop",
21
+ "Intended Audience :: Developers",
22
+ "Operating System :: MacOS",
23
+ "Operating System :: POSIX :: Linux",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Topic :: Multimedia :: Graphics :: Viewers",
29
+ "Topic :: Multimedia :: Video :: Display",
30
+ "Topic :: Terminals",
31
+ ]
32
+ dependencies = [
33
+ "pillow>=10.0",
34
+ "numpy>=1.24",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ video = ["opencv-python-headless>=4.8"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/yourusername/termview"
42
+ Repository = "https://github.com/yourusername/termview"
43
+ Issues = "https://github.com/yourusername/termview/issues"
44
+
45
+ [project.scripts]
46
+ tv = "termview.cli:main"
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["termview"]
50
+
51
+ [tool.hatch.build.targets.sdist]
52
+ include = [
53
+ "termview",
54
+ "README.md",
55
+ "LICENSE",
56
+ "pyproject.toml",
57
+ ]
@@ -0,0 +1,55 @@
1
+ """
2
+ termview — view images, animations, and videos in the terminal.
3
+
4
+ Public API:
5
+
6
+ from termview import detect_renderer, get_renderer, fit_image, load_image
7
+ from termview import stream_video, stream_animation
8
+
9
+ Most users want the `tv` CLI command rather than the library API.
10
+ """
11
+
12
+ from .audio import AudioPlayer, is_available as audio_available
13
+ from .controls import Key, read_key
14
+ from .detect import (
15
+ ColorDepth,
16
+ RendererType,
17
+ detect_color_depth,
18
+ detect_environment,
19
+ detect_renderer,
20
+ terminal_size,
21
+ )
22
+ from .loader import (
23
+ is_animated,
24
+ is_image,
25
+ is_video,
26
+ iter_frames,
27
+ load_image,
28
+ )
29
+ from .renderers import get_renderer
30
+ from .resize import fit_image
31
+ from .stream import stream_animation, stream_video
32
+ from .terminal import TerminalState
33
+
34
+ __all__ = [
35
+ "AudioPlayer",
36
+ "ColorDepth",
37
+ "Key",
38
+ "RendererType",
39
+ "TerminalState",
40
+ "audio_available",
41
+ "detect_color_depth",
42
+ "detect_environment",
43
+ "detect_renderer",
44
+ "fit_image",
45
+ "get_renderer",
46
+ "is_animated",
47
+ "is_image",
48
+ "is_video",
49
+ "iter_frames",
50
+ "load_image",
51
+ "read_key",
52
+ "stream_animation",
53
+ "stream_video",
54
+ "terminal_size",
55
+ ]
@@ -0,0 +1,141 @@
1
+ """
2
+ Audio playback for video files via an external subprocess.
3
+
4
+ We do *not* decode or mix audio in-process — that would require either PyAV
5
+ (50 MB+ C extension) or PyAudio + manual A/V sync, both of which are vastly
6
+ more complex than the actual problem. Instead we shell out to the first
7
+ available audio player on the system:
8
+
9
+ ffplay — cross-platform, ships with ffmpeg, handles every format
10
+ afplay — built into macOS, handles mp3/m4a/wav but not raw video
11
+ paplay — Linux PulseAudio, handles wav only
12
+
13
+ ffplay is the only one that decodes audio out of arbitrary video containers,
14
+ so it's the one we actively support. The others are documented fallbacks for
15
+ when the user has audio files (.mp3 etc.) rather than video.
16
+
17
+ Sync model
18
+ ----------
19
+ We don't try to read the audio process's clock. Instead:
20
+ - audio subprocess is started at wall-clock T₀ and paces itself.
21
+ - the video render loop also derives its target frame time from T₀.
22
+ - both run off the same origin → drift is bounded by the audio player's
23
+ internal A/V skew correction (ffplay's is good for hours of playback).
24
+
25
+ Pause / seek invalidate the audio subprocess and we respawn at the new
26
+ position with `-ss <seconds>`. Spawn latency is ~250ms, accepted as the
27
+ cost of audio sync.
28
+ """
29
+
30
+ import os
31
+ import shutil
32
+ import signal
33
+ import subprocess
34
+ import sys
35
+ from pathlib import Path
36
+
37
+
38
+ def _find_player() -> str | None:
39
+ """Return the path to ffplay if installed, else None."""
40
+ return shutil.which("ffplay")
41
+
42
+
43
+ def is_available() -> bool:
44
+ return _find_player() is not None
45
+
46
+
47
+ def install_hint() -> str:
48
+ if sys.platform == "darwin":
49
+ return "Install with: brew install ffmpeg"
50
+ if sys.platform.startswith("linux"):
51
+ return "Install with: apt install ffmpeg (or your distro's equivalent)"
52
+ return "Install ffmpeg from https://ffmpeg.org/download.html"
53
+
54
+
55
+ class AudioPlayer:
56
+ """
57
+ Wraps an ffplay subprocess. Each method is best-effort — audio is a
58
+ nice-to-have; failures must never crash video playback.
59
+ """
60
+
61
+ def __init__(self, path: Path, volume: int = 100) -> None:
62
+ self.path = path
63
+ self.volume = max(0, min(100, volume))
64
+ self.muted = False
65
+ self._proc: subprocess.Popen | None = None
66
+ self._start_offset: float = 0.0 # seconds into the file
67
+ self._player = _find_player()
68
+
69
+ @property
70
+ def available(self) -> bool:
71
+ return self._player is not None
72
+
73
+ # ------------------------------------------------------------------ control
74
+
75
+ def start(self, position_sec: float = 0.0) -> None:
76
+ """Start (or restart) audio at the given position."""
77
+ if not self.available:
78
+ return
79
+ self.stop()
80
+ self._start_offset = max(0.0, position_sec)
81
+
82
+ vol = 0 if self.muted else self.volume
83
+ cmd = [
84
+ self._player,
85
+ "-nodisp", # no video window
86
+ "-autoexit", # die when file ends
87
+ "-loglevel", "quiet",
88
+ "-volume", str(vol),
89
+ "-ss", f"{self._start_offset:.3f}",
90
+ str(self.path),
91
+ ]
92
+ try:
93
+ self._proc = subprocess.Popen(
94
+ cmd,
95
+ stdin=subprocess.DEVNULL,
96
+ stdout=subprocess.DEVNULL,
97
+ stderr=subprocess.DEVNULL,
98
+ # New process group so SIGINT to our terminal doesn't reach it
99
+ # before we get a chance to cleanly SIGTERM.
100
+ start_new_session=True,
101
+ )
102
+ except (OSError, FileNotFoundError):
103
+ self._proc = None
104
+
105
+ def stop(self) -> None:
106
+ """Terminate the audio subprocess if running. Idempotent."""
107
+ if self._proc is None:
108
+ return
109
+ try:
110
+ if self._proc.poll() is None:
111
+ # SIGTERM gives ffplay a chance to close its audio device cleanly,
112
+ # which avoids the "audio drop-out into next thing you play"
113
+ # CoreAudio bug on macOS.
114
+ os.killpg(os.getpgid(self._proc.pid), signal.SIGTERM)
115
+ try:
116
+ self._proc.wait(timeout=0.5)
117
+ except subprocess.TimeoutExpired:
118
+ os.killpg(os.getpgid(self._proc.pid), signal.SIGKILL)
119
+ except (ProcessLookupError, PermissionError, OSError):
120
+ pass
121
+ finally:
122
+ self._proc = None
123
+
124
+ def toggle_mute(self) -> bool:
125
+ """Toggle mute. Returns new muted state. Requires respawn."""
126
+ self.muted = not self.muted
127
+ return self.muted
128
+
129
+ def set_volume(self, vol: int) -> None:
130
+ self.volume = max(0, min(100, vol))
131
+
132
+ def is_running(self) -> bool:
133
+ return self._proc is not None and self._proc.poll() is None
134
+
135
+ # ------------------------------------------------------------------ context
136
+
137
+ def __enter__(self) -> "AudioPlayer":
138
+ return self
139
+
140
+ def __exit__(self, *exc) -> None:
141
+ self.stop()