osc52 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.
osc52-0.1.0/.gitignore ADDED
@@ -0,0 +1,39 @@
1
+ # Byte-compiled / optimized
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ wheels/
12
+ .eggs/
13
+
14
+ # Virtual environments
15
+ .venv/
16
+ venv/
17
+ env/
18
+ ENV/
19
+
20
+ # Test / coverage
21
+ .pytest_cache/
22
+ .tox/
23
+ .nox/
24
+ .coverage
25
+ .coverage.*
26
+ htmlcov/
27
+ .cache
28
+
29
+ # Type checkers / linters
30
+ .mypy_cache/
31
+ .ruff_cache/
32
+ .dmypy.json
33
+
34
+ # Editors / OS
35
+ .idea/
36
+ .vscode/
37
+ *.swp
38
+ .DS_Store
39
+ Thumbs.db
osc52-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yeyi0003
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.
osc52-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: osc52
3
+ Version: 0.1.0
4
+ Summary: Copy stdin or arguments to the system clipboard via the OSC 52 terminal escape sequence — works over SSH + tmux.
5
+ Project-URL: Homepage, https://github.com/yeyi0003/osc52
6
+ Project-URL: Repository, https://github.com/yeyi0003/osc52
7
+ Author: yeyi0003
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: cli,clipboard,copy,osc52,ssh,terminal,tmux
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Terminals
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+
23
+ # osc52
24
+
25
+ Copy stdin (or arguments) to the system clipboard via the **OSC 52** terminal
26
+ escape sequence. Designed to work over **SSH + tmux**: the escape is written to
27
+ stderr, streamed back to your *local* terminal, and routed to your *local*
28
+ clipboard — so `ssh server "nvidia-smi | osc52"` copies the remote output to
29
+ your local machine, no remote clipboard tool required.
30
+
31
+ This is the Python port of the Go tool of the same name, distributed on PyPI so
32
+ `pip install osc52` gives you the `osc52` command directly.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install osc52
38
+ ```
39
+
40
+ This installs an `osc52` executable onto your `PATH` (an `osc52.exe` shim on
41
+ Windows).
42
+
43
+ ## Usage
44
+
45
+ ```bash
46
+ # Default: echo input to stdout AND copy to clipboard.
47
+ nvidia-smi | osc52
48
+
49
+ # Copy only, no echo.
50
+ nvidia-smi | osc52 -q
51
+
52
+ # Copy a file.
53
+ osc52 < config.yaml
54
+
55
+ # Copy a literal string.
56
+ osc52 "https://example.com/token"
57
+ ```
58
+
59
+ ## How it works
60
+
61
+ ```
62
+ stdin/argv ──▶ echo to stdout (visible) ─────────────────▶ terminal / pipe
63
+ └─▶ base64 ─▶ ESC ] 52 ; c ; <b64> BEL ─▶ stderr ─▶ terminal
64
+ (parses OSC 52
65
+ → local clipboard)
66
+ ```
67
+
68
+ - **Echo** goes to **stdout** so downstream pipes stay clean of escape sequences.
69
+ - **OSC 52** goes to **stderr** so the terminal still parses it, but it never
70
+ pollutes piped output.
71
+ - Large payloads are split into ≤32KB base64 chunks; terminals accumulate
72
+ same-selection sequences into the clipboard.
73
+
74
+ ## Options
75
+
76
+ | Flag | Meaning |
77
+ |------|---------|
78
+ | `-q, --quiet` | Do not echo input to stdout |
79
+ | `--version` | Print version |
80
+ | `-h, --help` | Help |
81
+
82
+ ## Terminal support
83
+
84
+ OSC 52 is supported by Windows Terminal, iTerm2, kitty, Alacritty, WezTerm,
85
+ foot, and recent GNOME Terminal. In tmux, ensure `set -g set-clipboard on`.
86
+
87
+ ## License
88
+
89
+ MIT
osc52-0.1.0/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # osc52
2
+
3
+ Copy stdin (or arguments) to the system clipboard via the **OSC 52** terminal
4
+ escape sequence. Designed to work over **SSH + tmux**: the escape is written to
5
+ stderr, streamed back to your *local* terminal, and routed to your *local*
6
+ clipboard — so `ssh server "nvidia-smi | osc52"` copies the remote output to
7
+ your local machine, no remote clipboard tool required.
8
+
9
+ This is the Python port of the Go tool of the same name, distributed on PyPI so
10
+ `pip install osc52` gives you the `osc52` command directly.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install osc52
16
+ ```
17
+
18
+ This installs an `osc52` executable onto your `PATH` (an `osc52.exe` shim on
19
+ Windows).
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ # Default: echo input to stdout AND copy to clipboard.
25
+ nvidia-smi | osc52
26
+
27
+ # Copy only, no echo.
28
+ nvidia-smi | osc52 -q
29
+
30
+ # Copy a file.
31
+ osc52 < config.yaml
32
+
33
+ # Copy a literal string.
34
+ osc52 "https://example.com/token"
35
+ ```
36
+
37
+ ## How it works
38
+
39
+ ```
40
+ stdin/argv ──▶ echo to stdout (visible) ─────────────────▶ terminal / pipe
41
+ └─▶ base64 ─▶ ESC ] 52 ; c ; <b64> BEL ─▶ stderr ─▶ terminal
42
+ (parses OSC 52
43
+ → local clipboard)
44
+ ```
45
+
46
+ - **Echo** goes to **stdout** so downstream pipes stay clean of escape sequences.
47
+ - **OSC 52** goes to **stderr** so the terminal still parses it, but it never
48
+ pollutes piped output.
49
+ - Large payloads are split into ≤32KB base64 chunks; terminals accumulate
50
+ same-selection sequences into the clipboard.
51
+
52
+ ## Options
53
+
54
+ | Flag | Meaning |
55
+ |------|---------|
56
+ | `-q, --quiet` | Do not echo input to stdout |
57
+ | `--version` | Print version |
58
+ | `-h, --help` | Help |
59
+
60
+ ## Terminal support
61
+
62
+ OSC 52 is supported by Windows Terminal, iTerm2, kitty, Alacritty, WezTerm,
63
+ foot, and recent GNOME Terminal. In tmux, ensure `set -g set-clipboard on`.
64
+
65
+ ## License
66
+
67
+ MIT
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "osc52"
7
+ version = "0.1.0"
8
+ description = "Copy stdin or arguments to the system clipboard via the OSC 52 terminal escape sequence — works over SSH + tmux."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "yeyi0003" }]
13
+ keywords = ["clipboard", "osc52", "terminal", "ssh", "tmux", "copy", "cli"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Topic :: Terminals",
23
+ "Topic :: Utilities",
24
+ ]
25
+ dependencies = []
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/yeyi0003/osc52"
29
+ Repository = "https://github.com/yeyi0003/osc52"
30
+
31
+ [project.scripts]
32
+ osc52 = "osc52.cli:main"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/osc52"]
36
+
37
+ [tool.hatch.build.targets.sdist]
38
+ include = ["src/osc52", "README.md", "LICENSE", "pyproject.toml", "tests"]
@@ -0,0 +1,3 @@
1
+ """osc52 — copy to the system clipboard via the OSC 52 terminal escape."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,113 @@
1
+ """osc52 — copy stdin (or arguments) to the system clipboard via OSC 52.
2
+
3
+ Designed to work over SSH + tmux: the escape sequence is written to stderr so
4
+ the controlling terminal parses it and routes the payload to the *local*
5
+ clipboard, while the original data is echoed to stdout (kept clean for any
6
+ downstream pipe).
7
+
8
+ Behavior:
9
+ * Default: echo input to stdout AND copy via OSC 52 to stderr.
10
+ * -q/--quiet: copy only, do not echo.
11
+
12
+ This is a Python port of the Go tool github.com/yeyi0003/osc52.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import base64
19
+ import sys
20
+
21
+ from . import __version__
22
+
23
+ # Largest base64 chunk emitted in a single OSC 52 sequence. Some terminals cap a
24
+ # single sequence; 32KB is a safe midpoint that keeps big copies working by
25
+ # sending multiple same-selection sequences, which terminals accumulate.
26
+ MAX_PAYLOAD = 32 * 1024
27
+
28
+ OSC_START = b"\x1b]52;c;"
29
+ OSC_END = b"\x07"
30
+
31
+ PROG = "osc52"
32
+
33
+
34
+ def _eprint(msg: str) -> None:
35
+ """Print a diagnostic line to stderr."""
36
+ print(f"{PROG}: {msg}", file=sys.stderr)
37
+
38
+
39
+ def write_osc52(buf, data: bytes) -> None:
40
+ """Emit one or more OSC 52 sequences to a binary writer.
41
+
42
+ Large payloads are split into MAX_PAYLOAD-sized base64 chunks; terminals
43
+ accumulate same-selection sequences into the clipboard.
44
+ """
45
+ b64 = base64.standard_b64encode(data).decode("ascii") if data else ""
46
+ chunks = 0
47
+ for i in range(0, len(b64), MAX_PAYLOAD):
48
+ chunk = b64[i : i + MAX_PAYLOAD].encode("ascii")
49
+ buf.write(OSC_START + chunk + OSC_END)
50
+ chunks += 1
51
+ buf.flush()
52
+ if chunks > 1:
53
+ _eprint(f"sent {chunks} chunks")
54
+
55
+
56
+ def _build_parser() -> argparse.ArgumentParser:
57
+ p = argparse.ArgumentParser(
58
+ prog=PROG,
59
+ description="copy stdin/args to the clipboard via OSC 52",
60
+ formatter_class=argparse.RawDescriptionHelpFormatter,
61
+ epilog=(
62
+ "examples:\n"
63
+ " nvidia-smi | osc52 # echo + copy\n"
64
+ " nvidia-smi | osc52 -q # copy only, no echo\n"
65
+ " osc52 < file # copy file contents\n"
66
+ ' osc52 "some text" # copy a literal string\n'
67
+ ),
68
+ )
69
+ p.add_argument("-q", "--quiet", action="store_true",
70
+ help="do not echo input to stdout")
71
+ p.add_argument("--version", action="version",
72
+ version=f"{PROG} {__version__}")
73
+ p.add_argument("args", nargs="*", help=argparse.SUPPRESS)
74
+ return p
75
+
76
+
77
+ def main(argv: list[str] | None = None) -> int:
78
+ parser = _build_parser()
79
+ ns = parser.parse_args(argv)
80
+
81
+ # Gather the data to copy.
82
+ if ns.args:
83
+ # Arguments: join with spaces. Warn if stdin is also piped.
84
+ if not sys.stdin.isatty():
85
+ _eprint("warning: arguments given, ignoring piped stdin")
86
+ data = " ".join(ns.args).encode("utf-8", "surrogateescape") + b"\n"
87
+ else:
88
+ # Read all of stdin as raw bytes.
89
+ data = sys.stdin.buffer.read()
90
+
91
+ # Echo to stdout unless quiet. Done before copying so the user sees output
92
+ # even if the copy path fails downstream.
93
+ if not ns.quiet:
94
+ try:
95
+ sys.stdout.buffer.write(data)
96
+ sys.stdout.buffer.flush()
97
+ except BrokenPipeError:
98
+ pass
99
+ except OSError as exc:
100
+ _eprint(f"write stdout: {exc}")
101
+ return 1
102
+
103
+ # Copy. OSC 52 goes to stderr so it does not pollute echoed stdout.
104
+ try:
105
+ write_osc52(sys.stderr.buffer, data)
106
+ except Exception as exc: # noqa: BLE001 - single top-level error path
107
+ _eprint(str(exc))
108
+ return 1
109
+ return 0
110
+
111
+
112
+ if __name__ == "__main__":
113
+ raise SystemExit(main())
@@ -0,0 +1,42 @@
1
+ """Tests for osc52 core logic."""
2
+
3
+ import base64
4
+ import io
5
+
6
+ from osc52.cli import MAX_PAYLOAD, OSC_END, OSC_START, write_osc52
7
+
8
+
9
+ def test_basic_sequence():
10
+ buf = io.BytesIO()
11
+ write_osc52(buf, b"hello")
12
+ expected = OSC_START + base64.standard_b64encode(b"hello") + OSC_END
13
+ assert buf.getvalue() == expected
14
+
15
+
16
+ def test_chunking():
17
+ # Choose data whose base64 length exceeds MAX_PAYLOAD to force >1 chunk.
18
+ data = b"a" * MAX_PAYLOAD # base64 grows ~4/3, guaranteeing multiple chunks
19
+ buf = io.BytesIO()
20
+ write_osc52(buf, data)
21
+ out = buf.getvalue()
22
+ assert out.count(OSC_START) >= 2
23
+ # Reassemble the base64 payloads and verify they decode back to data.
24
+ parts = out.split(OSC_START)[1:]
25
+ b64 = b"".join(p[: -len(OSC_END)] for p in parts)
26
+ assert base64.standard_b64decode(b64) == data
27
+
28
+
29
+ def test_roundtrip_decode():
30
+ payload = b"the quick brown fox\n\x00\xff binary too"
31
+ buf = io.BytesIO()
32
+ write_osc52(buf, payload)
33
+ out = buf.getvalue()
34
+ b64 = out[len(OSC_START) : -len(OSC_END)]
35
+ assert base64.standard_b64decode(b64) == payload
36
+
37
+
38
+ def test_empty_payload():
39
+ buf = io.BytesIO()
40
+ write_osc52(buf, b"")
41
+ # Empty input produces no sequence.
42
+ assert buf.getvalue() == b""