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 +39 -0
- osc52-0.1.0/LICENSE +21 -0
- osc52-0.1.0/PKG-INFO +89 -0
- osc52-0.1.0/README.md +67 -0
- osc52-0.1.0/pyproject.toml +38 -0
- osc52-0.1.0/src/osc52/__init__.py +3 -0
- osc52-0.1.0/src/osc52/cli.py +113 -0
- osc52-0.1.0/tests/test_osc52.py +42 -0
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,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""
|