tacklebox-cli 0.2.6__tar.gz → 0.3.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.
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/LICENSE.md +1 -2
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/PKG-INFO +31 -8
- tacklebox_cli-0.3.0/pyproject.toml +74 -0
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/tacklebox/commands/__init__.py +3 -1
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/tacklebox/commands/clipboard.py +19 -23
- tacklebox_cli-0.3.0/tacklebox/commands/find_desktop_entry.py +49 -0
- tacklebox_cli-0.3.0/tacklebox/commands/prepend_to_file.py +84 -0
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/tacklebox/commands/spectacle.py +10 -9
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/tacklebox/commands/version.py +2 -2
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/tacklebox/entrypoint.py +2 -2
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/tacklebox/utils.py +5 -0
- tacklebox_cli-0.3.0/tacklebox/version.py +24 -0
- tacklebox_cli-0.2.6/README.md +0 -39
- tacklebox_cli-0.2.6/pyproject.toml +0 -60
- tacklebox_cli-0.2.6/tacklebox/version.py +0 -21
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/.gitignore +0 -0
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/tacklebox/__init__.py +0 -0
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.3.0}/tacklebox/sync.py +0 -0
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tacklebox-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A small collection of CLI utilities.
|
|
5
5
|
Project-URL: Homepage, https://c.csw.im/cswimr/tacklebox
|
|
6
6
|
Project-URL: Issues, https://c.csw.im/cswimr/tacklebox/issues
|
|
7
|
-
Project-URL:
|
|
7
|
+
Project-URL: Source Archive, https://c.csw.im/cswimr/tacklebox/archive/951186f80430bac4501fe5167aabef121dfbb457.tar.gz
|
|
8
8
|
Author-email: cswimr <seaswimmerthefsh@gmail.com>
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
License-File: LICENSE.md
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Programming Language :: Python
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.
|
|
14
|
-
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Requires-Python: >=3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Python: >=3.14
|
|
17
15
|
Requires-Dist: desktop-notifier>=6.1.0
|
|
18
16
|
Requires-Dist: platformdirs>=4.3.0
|
|
19
17
|
Requires-Dist: zipline-py[cli]>=0.27.0
|
|
@@ -24,14 +22,14 @@ Description-Content-Type: text/markdown
|
|
|
24
22
|
[<img alt="Actions Status" src="https://c.csw.im/cswimr/tacklebox/badges/workflows/actions.yml/badge.svg?style=plastic">](https://c.csw.im/cswimr/tacklebox/actions?workflow=actions.yml)
|
|
25
23
|
[<img alt="PyPI - Version" src="https://img.shields.io/pypi/v/tacklebox-cli?style=plastic">](https://pypi.org/project/tacklebox-cli/)
|
|
26
24
|
[<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/tacklebox-cli?style=plastic">](https://pypi.org/project/tacklebox-cli/)
|
|
27
|
-
[<img alt="PyPI - License" src="https://img.shields.io/pypi/l/tacklebox-cli?style=plastic">](https://c.csw.im/cswimr/tacklebox/src/
|
|
25
|
+
[<img alt="PyPI - License" src="https://img.shields.io/pypi/l/tacklebox-cli?style=plastic">](https://c.csw.im/cswimr/tacklebox/src/tag/0.3.0/LICENSE.md)
|
|
28
26
|
tacklebox-cli offers a suite of useful CLI tools.
|
|
29
27
|
|
|
30
28
|
## Usage
|
|
31
29
|
|
|
32
30
|
### tacklebox copy / paste
|
|
33
31
|
|
|
34
|
-
Cross-platform clipboard management tool. Uses system tools such as `wl-copy` on Linux Wayland or `clip.exe` on Windows, and [OSC 52](https://www.reddit.com/r/vim/comments/k1ydpn/a_guide_on_how_to_copy_text_from_anywhere/) escape codes when **copying** over SSH or when no other tools are available. See [`copy_with_tooling()`](https://c.csw.im/cswimr/tacklebox/src/
|
|
32
|
+
Cross-platform clipboard management tool. Uses system tools such as `wl-copy` on Linux Wayland or `clip.exe` on Windows, and [OSC 52](https://www.reddit.com/r/vim/comments/k1ydpn/a_guide_on_how_to_copy_text_from_anywhere/) escape codes when **copying** over SSH or when no other tools are available. See [`copy_with_tooling()`](https://c.csw.im/cswimr/tacklebox/src/tag/0.3.0/tacklebox/commands/clipboard.py) for all supported tools.
|
|
35
33
|
|
|
36
34
|
```bash
|
|
37
35
|
$ echo "a" | tacklebox copy --trim && tacklebox paste
|
|
@@ -58,3 +56,28 @@ $ tacklebox spectacle --mode region --record \
|
|
|
58
56
|
### tacklebox zipline
|
|
59
57
|
|
|
60
58
|
Wraps the [zipline.py](https://pypi.org/project/zipline-py/) CLI. See the [zipline.py CLI documentation](https://ziplinepy.readthedocs.io/en/latest/cli.html) for more information.
|
|
59
|
+
|
|
60
|
+
## License - The MIT License
|
|
61
|
+
|
|
62
|
+
Copyright © 2025 cswimr
|
|
63
|
+
|
|
64
|
+
Permission is hereby granted, free of charge, to any person
|
|
65
|
+
obtaining a copy of this software and associated documentation
|
|
66
|
+
files (the “Software”), to deal in the Software without
|
|
67
|
+
restriction, including without limitation the rights to use,
|
|
68
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
69
|
+
copies of the Software, and to permit persons to whom the
|
|
70
|
+
Software is furnished to do so, subject to the following
|
|
71
|
+
conditions:
|
|
72
|
+
|
|
73
|
+
The above copyright notice and this permission notice shall be
|
|
74
|
+
included in all copies or substantial portions of the Software.
|
|
75
|
+
|
|
76
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
|
77
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
78
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
79
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
80
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
81
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
82
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
83
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tacklebox-cli"
|
|
3
|
+
description = "A small collection of CLI utilities."
|
|
4
|
+
requires-python = ">=3.14"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
license-files = ["LICEN[CS]E*"]
|
|
7
|
+
authors = [{ name = "cswimr", email = "seaswimmerthefsh@gmail.com" }]
|
|
8
|
+
classifiers = [
|
|
9
|
+
"License :: OSI Approved :: MIT License",
|
|
10
|
+
"Programming Language :: Python",
|
|
11
|
+
"Programming Language :: Python :: 3.14"
|
|
12
|
+
]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"desktop-notifier>=6.1.0",
|
|
15
|
+
"platformdirs>=4.3.0",
|
|
16
|
+
"zipline.py[cli]>=0.27.0",
|
|
17
|
+
]
|
|
18
|
+
dynamic = ["readme", "urls", "version"]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
tacklebox = "tacklebox.entrypoint:app"
|
|
22
|
+
tacklebox-cli = "tacklebox.entrypoint:app" # for uvx / pipx, prefer tacklebox when possible
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"basedpyright~=1.38.2",
|
|
27
|
+
"pyinstrument~=5.1.2",
|
|
28
|
+
"ruff~=0.15.4",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["hatch-fancy-pypi-readme", "hatch-vcs", "hatchling"]
|
|
33
|
+
build-backend = "hatchling.build"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.version]
|
|
36
|
+
source = "vcs"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build]
|
|
39
|
+
include = ["/tacklebox", "/LICENSE.md", "/pyproject.toml"]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.hooks.vcs]
|
|
42
|
+
version-file = "tacklebox/version.py"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.metadata.hooks.vcs.urls]
|
|
45
|
+
Homepage = "https://c.csw.im/cswimr/tacklebox"
|
|
46
|
+
Issues = "https://c.csw.im/cswimr/tacklebox/issues"
|
|
47
|
+
"Source Archive" = "https://c.csw.im/cswimr/tacklebox/archive/{commit_hash}.tar.gz"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.metadata.hooks.fancy-pypi-readme]
|
|
50
|
+
content-type = "text/markdown"
|
|
51
|
+
|
|
52
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
|
53
|
+
path = "README.md"
|
|
54
|
+
|
|
55
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
|
56
|
+
text = "\n## License - The MIT License"
|
|
57
|
+
|
|
58
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
|
59
|
+
path = "LICENSE.md"
|
|
60
|
+
start-after = "# The MIT License (MIT)"
|
|
61
|
+
|
|
62
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
|
|
63
|
+
pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)'
|
|
64
|
+
replacement = '[\1](https://c.csw.im/cswimr/tacklebox/src/tag/$HFPR_VERSION\g<2>)'
|
|
65
|
+
|
|
66
|
+
[tool.pyright]
|
|
67
|
+
typeCheckingMode = "strict"
|
|
68
|
+
reportMissingTypeStubs = false
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
line-length = 145
|
|
72
|
+
|
|
73
|
+
[tool.typos]
|
|
74
|
+
files.extend-exclude = [".direnv/**", ".venv/**"]
|
|
@@ -4,9 +4,11 @@ from typer import Typer
|
|
|
4
4
|
from zipline.cli.entrypoint import app as zipline
|
|
5
5
|
|
|
6
6
|
from .clipboard import app as clipboard
|
|
7
|
+
from .find_desktop_entry import app as find_desktop_entry
|
|
8
|
+
from .prepend_to_file import app as prepend_to_file
|
|
7
9
|
from .version import app as version
|
|
8
10
|
|
|
9
|
-
commands: list[Typer] = [clipboard, version, zipline]
|
|
11
|
+
commands: list[Typer] = [clipboard, find_desktop_entry, prepend_to_file, version, zipline]
|
|
10
12
|
|
|
11
13
|
if platform.system() == "Linux":
|
|
12
14
|
from .spectacle import app as spectacle
|
|
@@ -9,7 +9,7 @@ from typing import Annotated, Literal
|
|
|
9
9
|
|
|
10
10
|
from typer import Argument, Exit, Option, Typer, echo
|
|
11
11
|
|
|
12
|
-
from tacklebox.utils import get_environment
|
|
12
|
+
from tacklebox.utils import get_environment, stderr, stdout
|
|
13
13
|
|
|
14
14
|
app = Typer()
|
|
15
15
|
|
|
@@ -140,14 +140,14 @@ def use_tooling(
|
|
|
140
140
|
|
|
141
141
|
if in_wsl:
|
|
142
142
|
if verbose:
|
|
143
|
-
|
|
143
|
+
stderr.print("Detected Windows Subsystem for Linux.")
|
|
144
144
|
commands.extend(tools[mode]["windows"])
|
|
145
145
|
|
|
146
146
|
protocol: Literal["wayland", "x11"] | None = (
|
|
147
147
|
"wayland" if "WAYLAND_DISPLAY" in get_environment() else ("x11" if "DISPLAY" in get_environment() else None)
|
|
148
148
|
)
|
|
149
149
|
if verbose:
|
|
150
|
-
|
|
150
|
+
stderr.print(f"Detected display protocol: {protocol}")
|
|
151
151
|
|
|
152
152
|
match protocol:
|
|
153
153
|
case "wayland":
|
|
@@ -157,10 +157,7 @@ def use_tooling(
|
|
|
157
157
|
case _:
|
|
158
158
|
if not in_wsl:
|
|
159
159
|
if verbose:
|
|
160
|
-
|
|
161
|
-
"Unknown display protocol: neither WAYLAND_DISPLAY nor DISPLAY set.",
|
|
162
|
-
err=True,
|
|
163
|
-
)
|
|
160
|
+
stderr.print("Unknown display protocol: neither WAYLAND_DISPLAY nor DISPLAY set.")
|
|
164
161
|
return False, None
|
|
165
162
|
|
|
166
163
|
case "darwin":
|
|
@@ -169,11 +166,11 @@ def use_tooling(
|
|
|
169
166
|
commands.extend(tools[mode]["windows"])
|
|
170
167
|
case _:
|
|
171
168
|
if verbose:
|
|
172
|
-
|
|
169
|
+
stderr.print(f"No suitable clipboard tool known for platform '{system}'")
|
|
173
170
|
return False, None
|
|
174
171
|
|
|
175
172
|
if verbose:
|
|
176
|
-
|
|
173
|
+
stderr.print("\nAttempting commands:\n" + "\n".join(" ".join(cmd) for cmd in commands) + "\n")
|
|
177
174
|
for cmd in commands:
|
|
178
175
|
if (success := _try_command(mode, cmd, verbose, data))[0]:
|
|
179
176
|
return success
|
|
@@ -195,28 +192,27 @@ def encode_osc52(data: str, verbose: bool = False) -> str:
|
|
|
195
192
|
|
|
196
193
|
if "TMUX" in os.environ:
|
|
197
194
|
if verbose:
|
|
198
|
-
|
|
195
|
+
stderr.print("Wrapping OSC 52 for tmux.")
|
|
199
196
|
return f"\x1bPtmux;\x1b{osc_seq}\x1b\\"
|
|
200
197
|
elif os.environ.get("TERM", "").startswith("screen"):
|
|
201
198
|
if verbose:
|
|
202
|
-
|
|
199
|
+
stderr.print("Wrapping OSC 52 for screen.")
|
|
203
200
|
return f"\x1bP{osc_seq}\x1b\\"
|
|
204
201
|
else:
|
|
205
202
|
if verbose:
|
|
206
|
-
|
|
203
|
+
stderr.print("Using plain OSC 52.")
|
|
207
204
|
return osc_seq
|
|
208
205
|
|
|
209
206
|
|
|
210
207
|
def _maybe_print_environment_information(verbose: bool) -> None:
|
|
211
208
|
if verbose:
|
|
212
|
-
|
|
209
|
+
stderr.print(
|
|
213
210
|
(
|
|
214
211
|
f"Platform: {platform.system()}\n"
|
|
215
212
|
f"TERM: {os.environ.get('TERM')}\n"
|
|
216
213
|
f"TMUX: {'present' if 'TMUX' in os.environ else 'absent'}\n"
|
|
217
214
|
f"SCREEN: {'present' if os.environ.get('TERM', '').startswith('screen') else 'absent'}"
|
|
218
|
-
)
|
|
219
|
-
err=True,
|
|
215
|
+
)
|
|
220
216
|
)
|
|
221
217
|
|
|
222
218
|
|
|
@@ -250,8 +246,8 @@ def copy(
|
|
|
250
246
|
data = sys.stdin.read()
|
|
251
247
|
|
|
252
248
|
if not data:
|
|
253
|
-
|
|
254
|
-
raise Exit(
|
|
249
|
+
stderr.print("No input received from stdin.")
|
|
250
|
+
raise Exit(1)
|
|
255
251
|
|
|
256
252
|
if trim:
|
|
257
253
|
data = data.rstrip("\r\n")
|
|
@@ -266,16 +262,16 @@ def copy(
|
|
|
266
262
|
return
|
|
267
263
|
|
|
268
264
|
if verbose:
|
|
269
|
-
|
|
265
|
+
stderr.print("Clipboard tools failed; trying OSC 52...")
|
|
270
266
|
|
|
271
267
|
osc = encode_osc52(data, verbose)
|
|
272
268
|
try:
|
|
273
269
|
with open("/dev/tty", "w") as tty:
|
|
274
270
|
tty.write(osc)
|
|
275
271
|
if verbose:
|
|
276
|
-
|
|
272
|
+
stderr.print("Copied using OSC 52.")
|
|
277
273
|
except Exception as e:
|
|
278
|
-
|
|
274
|
+
stderr.print(f"OSC 52 failed: {e}")
|
|
279
275
|
raise Exit(code=1)
|
|
280
276
|
|
|
281
277
|
|
|
@@ -307,11 +303,11 @@ def paste(
|
|
|
307
303
|
if trim and string is not None:
|
|
308
304
|
string = string.rstrip("\r\n")
|
|
309
305
|
|
|
310
|
-
|
|
306
|
+
stdout.out(string)
|
|
311
307
|
return
|
|
312
308
|
|
|
313
|
-
|
|
314
|
-
|
|
309
|
+
stderr.print(f"Paste command failed!\n{output}")
|
|
310
|
+
raise Exit(1)
|
|
315
311
|
|
|
316
312
|
|
|
317
313
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from shutil import which
|
|
5
|
+
|
|
6
|
+
from typer import Exit, Typer
|
|
7
|
+
|
|
8
|
+
from tacklebox.utils import stderr, stdout
|
|
9
|
+
|
|
10
|
+
app = Typer()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def find_desktop_entry(desktop_entry: str, show_all_paths: bool = False) -> None:
|
|
15
|
+
if not desktop_entry:
|
|
16
|
+
raise ValueError("Please provide the full filename of the desktop entry.")
|
|
17
|
+
|
|
18
|
+
if not desktop_entry.endswith(".desktop"):
|
|
19
|
+
desktop_entry += ".desktop"
|
|
20
|
+
|
|
21
|
+
entry_paths = []
|
|
22
|
+
|
|
23
|
+
if which("qtpaths"):
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
["qtpaths", "--paths", "ApplicationsLocation"],
|
|
26
|
+
stdout=subprocess.PIPE,
|
|
27
|
+
text=True,
|
|
28
|
+
)
|
|
29
|
+
entry_paths = result.stdout.strip().split(":")
|
|
30
|
+
else:
|
|
31
|
+
stderr.print("qtpaths is not installed, falling back to XDG_DATA_DIRS.")
|
|
32
|
+
xdg_data_dirs = os.getenv("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split(":")
|
|
33
|
+
entry_paths = [os.path.join(path, "applications") for path in xdg_data_dirs]
|
|
34
|
+
entry_paths.append(os.path.expanduser("~/.local/share/applications"))
|
|
35
|
+
|
|
36
|
+
if show_all_paths:
|
|
37
|
+
stderr.print(f"Checking the following paths for {desktop_entry}:\n{entry_paths}\n{'-' * 20}")
|
|
38
|
+
|
|
39
|
+
for entry_path in entry_paths:
|
|
40
|
+
entry_file = Path(entry_path) / f"{desktop_entry}"
|
|
41
|
+
if show_all_paths:
|
|
42
|
+
stderr.print(f"Checking for {entry_file}")
|
|
43
|
+
if entry_file.is_file():
|
|
44
|
+
stderr.print(f"{desktop_entry} found in {entry_path}")
|
|
45
|
+
stdout.out(entry_file)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
stderr.print(f"Desktop entry {desktop_entry} could not be found.")
|
|
49
|
+
raise Exit(1)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from tacklebox.utils import stderr
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_file(file: Path, /, *, write: bool = True) -> None:
|
|
17
|
+
if not file.exists():
|
|
18
|
+
raise FileNotFoundError(f"{file} does not exist!")
|
|
19
|
+
if not file.is_file():
|
|
20
|
+
raise FileNotFoundError(f"{file} is not a file!")
|
|
21
|
+
if not os.access(file, os.R_OK):
|
|
22
|
+
raise PermissionError(f"Cannot read file {file}!")
|
|
23
|
+
|
|
24
|
+
if write:
|
|
25
|
+
if not os.access(file, os.W_OK):
|
|
26
|
+
raise PermissionError(f"Cannot write to file {file}!")
|
|
27
|
+
if not os.access(file.parent, os.W_OK | os.X_OK):
|
|
28
|
+
raise PermissionError(f"Cannot write to directory {file.parent}!")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command()
|
|
32
|
+
def prepend_to_file(
|
|
33
|
+
source_file: Annotated[Path, typer.Argument(help="The file to read content from.")],
|
|
34
|
+
dest_file: Annotated[Path, typer.Argument(help="The target file to modify.")],
|
|
35
|
+
add_newline: Annotated[
|
|
36
|
+
bool,
|
|
37
|
+
typer.Option(
|
|
38
|
+
"--add-newline/--no-add-newline",
|
|
39
|
+
"-a/-n",
|
|
40
|
+
help="Automatically strip newlines and append a newline to the provided content.",
|
|
41
|
+
),
|
|
42
|
+
] = True,
|
|
43
|
+
check_for_existence: Annotated[
|
|
44
|
+
bool,
|
|
45
|
+
typer.Option(
|
|
46
|
+
"--check-existence/--no-check-existence",
|
|
47
|
+
"-c/-C",
|
|
48
|
+
help="Check if the file already starts with the provided text.",
|
|
49
|
+
),
|
|
50
|
+
] = True,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Prepend text to the top of a file."""
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
check_file(source_file, write=False)
|
|
56
|
+
check_file(dest_file, write=True)
|
|
57
|
+
except (PermissionError, FileNotFoundError) as err:
|
|
58
|
+
stderr.print(f"[red]{err}[reset]")
|
|
59
|
+
|
|
60
|
+
source_content = source_file.read_text()
|
|
61
|
+
dest_content = dest_file.read_text()
|
|
62
|
+
|
|
63
|
+
tmp_path: Path | None = None
|
|
64
|
+
|
|
65
|
+
stripped = source_content.rstrip("\r\n")
|
|
66
|
+
if add_newline:
|
|
67
|
+
source_content = source_content + "\n"
|
|
68
|
+
|
|
69
|
+
if check_for_existence and dest_content.startswith(stripped):
|
|
70
|
+
stderr.print(
|
|
71
|
+
f"[red]File [purple]{dest_file}[red] already begins with the given content; skipping[reset]",
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
with tempfile.NamedTemporaryFile("w", delete=False, dir=dest_file.parent) as tmp:
|
|
77
|
+
tmp.write(source_content)
|
|
78
|
+
tmp.write(dest_content)
|
|
79
|
+
tmp_path = Path(tmp.name)
|
|
80
|
+
|
|
81
|
+
shutil.move(tmp.name, dest_file)
|
|
82
|
+
finally:
|
|
83
|
+
if tmp_path and tmp_path.exists():
|
|
84
|
+
os.remove(tmp_path)
|
|
@@ -12,7 +12,6 @@ from typing import Annotated
|
|
|
12
12
|
|
|
13
13
|
from desktop_notifier import DEFAULT_SOUND, Attachment, DesktopNotifier, Urgency
|
|
14
14
|
from platformdirs import user_config_dir
|
|
15
|
-
from rich import print
|
|
16
15
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
17
16
|
from typer import Exit, Option, Typer
|
|
18
17
|
from zipline.cli.commands._handling import handle_api_errors
|
|
@@ -20,9 +19,10 @@ from zipline.cli.commands.upload import _complete_format # pyright: ignore[repo
|
|
|
20
19
|
from zipline.client import Client, FileData, NameFormat
|
|
21
20
|
|
|
22
21
|
from tacklebox import sync
|
|
22
|
+
from tacklebox.utils import stderr, stdout
|
|
23
23
|
|
|
24
24
|
if not platform.system() == "Linux":
|
|
25
|
-
print("Spectacle is only supported on Linux.")
|
|
25
|
+
stderr.print("Spectacle is only supported on Linux.")
|
|
26
26
|
raise Exit(code=1)
|
|
27
27
|
|
|
28
28
|
app = Typer()
|
|
@@ -83,7 +83,7 @@ class SpectacleMode(StrEnum):
|
|
|
83
83
|
record_argument = self.get_argument(True)
|
|
84
84
|
|
|
85
85
|
completion_strings: dict[SpectacleMode, str] = {
|
|
86
|
-
SpectacleMode.REGION: f"Capture a
|
|
86
|
+
SpectacleMode.REGION: f"Capture a rectangular region of the desktop. ({argument} | {record_argument})",
|
|
87
87
|
SpectacleMode.DESKTOP: f"Capture the entire desktop (default). When recording, this will capture only the current monitor. ({argument} | {record_argument})",
|
|
88
88
|
SpectacleMode.MONITOR: f"Capture the current monitor. ({argument} | {record_argument})",
|
|
89
89
|
SpectacleMode.WINDOW: f"Capture the window currently under the cursor, including parents of pop-up menus. ({argument} | {record_argument})",
|
|
@@ -246,17 +246,18 @@ async def spectacle(
|
|
|
246
246
|
] = False,
|
|
247
247
|
) -> None:
|
|
248
248
|
"""Take a screenshot or record a video using Spectacle, then upload it to a remote Zipline instance using zipline.py."""
|
|
249
|
-
notifier = DesktopNotifier(app_name="
|
|
249
|
+
notifier = DesktopNotifier(app_name="tacklebox-cli - Spectacle")
|
|
250
250
|
|
|
251
251
|
with Progress(
|
|
252
252
|
SpinnerColumn(),
|
|
253
253
|
TextColumn("[progress.description]{task.description}"),
|
|
254
|
+
console=stderr,
|
|
254
255
|
transient=True,
|
|
255
256
|
) as progress:
|
|
256
257
|
task = progress.add_task(description="Setting up...", total=None)
|
|
257
258
|
|
|
258
259
|
if not which("spectacle"):
|
|
259
|
-
print("spectacle is not installed!")
|
|
260
|
+
stderr.print("spectacle is not installed!")
|
|
260
261
|
raise Exit(code=1)
|
|
261
262
|
|
|
262
263
|
image_format, video_format = _read_spectacle_config()
|
|
@@ -281,12 +282,12 @@ async def spectacle(
|
|
|
281
282
|
]
|
|
282
283
|
|
|
283
284
|
proc = await asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
284
|
-
|
|
285
|
+
_stdout, _stderr = await proc.communicate()
|
|
285
286
|
|
|
286
287
|
if proc.returncode != 0:
|
|
287
288
|
if file_path.exists():
|
|
288
289
|
file_path.unlink(missing_ok=True)
|
|
289
|
-
raise subprocess.CalledProcessError(proc.returncode or 1, command, output=
|
|
290
|
+
raise subprocess.CalledProcessError(proc.returncode or 1, command, output=_stdout, stderr=_stderr)
|
|
290
291
|
|
|
291
292
|
if not file_path.stat().st_size:
|
|
292
293
|
file_path.unlink(missing_ok=True)
|
|
@@ -313,9 +314,9 @@ async def spectacle(
|
|
|
313
314
|
handle_api_errors(exception, server_url, traceback=verbose)
|
|
314
315
|
|
|
315
316
|
if print_object:
|
|
316
|
-
print(uploaded_file)
|
|
317
|
+
stdout.print(uploaded_file)
|
|
317
318
|
else:
|
|
318
|
-
print(uploaded_file.files[0].url)
|
|
319
|
+
stdout.print(uploaded_file.files[0].url)
|
|
319
320
|
|
|
320
321
|
await notifier.send(
|
|
321
322
|
title="File Uploaded!",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from importlib.metadata import version
|
|
2
2
|
|
|
3
|
-
from rich import print
|
|
4
3
|
from typer import Typer
|
|
5
4
|
|
|
6
5
|
from tacklebox import __version__
|
|
6
|
+
from tacklebox.utils import stdout
|
|
7
7
|
|
|
8
8
|
app = Typer()
|
|
9
9
|
|
|
@@ -35,7 +35,7 @@ def show_versions() -> None:
|
|
|
35
35
|
for name, version in versions.items()
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
-
print(output)
|
|
38
|
+
stdout.print(output)
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
if __name__ == "__main__":
|
|
@@ -9,9 +9,9 @@ from tacklebox.commands import commands
|
|
|
9
9
|
if platform.system() == "Darwin":
|
|
10
10
|
from rubicon.objc.eventloop import EventLoopPolicy # pyright: ignore[reportMissingImports, reportUnknownVariableType]
|
|
11
11
|
|
|
12
|
-
asyncio.set_event_loop_policy(EventLoopPolicy()) # pyright: ignore[reportUnknownArgumentType]
|
|
12
|
+
asyncio.set_event_loop_policy(EventLoopPolicy()) # pyright: ignore[reportUnknownArgumentType, reportDeprecated]
|
|
13
13
|
|
|
14
|
-
app = Typer(name="
|
|
14
|
+
app = Typer(name="tacklebox", pretty_exceptions_show_locals=False)
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
for typer_instance in commands:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
tacklebox_cli-0.2.6/README.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# tacklebox-cli
|
|
2
|
-
|
|
3
|
-
[<img alt="Actions Status" src="https://c.csw.im/cswimr/tacklebox/badges/workflows/actions.yml/badge.svg?style=plastic">](https://c.csw.im/cswimr/tacklebox/actions?workflow=actions.yml)
|
|
4
|
-
[<img alt="PyPI - Version" src="https://img.shields.io/pypi/v/tacklebox-cli?style=plastic">](https://pypi.org/project/tacklebox-cli/)
|
|
5
|
-
[<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/tacklebox-cli?style=plastic">](https://pypi.org/project/tacklebox-cli/)
|
|
6
|
-
[<img alt="PyPI - License" src="https://img.shields.io/pypi/l/tacklebox-cli?style=plastic">](https://c.csw.im/cswimr/tacklebox/src/branch/main/LICENSE/)
|
|
7
|
-
tacklebox-cli offers a suite of useful CLI tools.
|
|
8
|
-
|
|
9
|
-
## Usage
|
|
10
|
-
|
|
11
|
-
### tacklebox copy / paste
|
|
12
|
-
|
|
13
|
-
Cross-platform clipboard management tool. Uses system tools such as `wl-copy` on Linux Wayland or `clip.exe` on Windows, and [OSC 52](https://www.reddit.com/r/vim/comments/k1ydpn/a_guide_on_how_to_copy_text_from_anywhere/) escape codes when **copying** over SSH or when no other tools are available. See [`copy_with_tooling()`](https://c.csw.im/cswimr/tacklebox/src/branch/main/tacklebox/commands/clipboard.py) for all supported tools.
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
$ echo "a" | tacklebox copy --trim && tacklebox paste
|
|
17
|
-
a
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### tacklebox spectacle (Linux only)
|
|
21
|
-
|
|
22
|
-
Uses the [zipline.py](https://pypi.org/project/zipline-py/) library alongside KDE's [Spectacle](https://invent.kde.org/plasma/spectacle) application to take a screenshot or screen recording and automatically upload it to a [Zipline](https://github.com/diced/zipline) instance. This automatically reads Spectacle's configuration files to determine file formats.
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
$ tacklebox spectacle --mode region \
|
|
26
|
-
--server "https://zipline.example.com" \
|
|
27
|
-
--token "$(cat /file/containing/zipline/token)" \
|
|
28
|
-
| tacklebox copy --trim
|
|
29
|
-
|
|
30
|
-
# or to record a video
|
|
31
|
-
$ tacklebox spectacle --mode region --record \
|
|
32
|
-
--server "https://zipline.example.com" \
|
|
33
|
-
--token "$(cat /file/containing/zipline/token)" \
|
|
34
|
-
| tacklebox copy --trim
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### tacklebox zipline
|
|
38
|
-
|
|
39
|
-
Wraps the [zipline.py](https://pypi.org/project/zipline-py/) CLI. See the [zipline.py CLI documentation](https://ziplinepy.readthedocs.io/en/latest/cli.html) for more information.
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "tacklebox-cli"
|
|
3
|
-
description = "A small collection of CLI utilities."
|
|
4
|
-
authors = [{ name = "cswimr", email = "seaswimmerthefsh@gmail.com" }]
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.11"
|
|
7
|
-
license = "MIT"
|
|
8
|
-
license-files = ["LICEN[CS]E*"]
|
|
9
|
-
classifiers = [
|
|
10
|
-
"License :: OSI Approved :: MIT License",
|
|
11
|
-
"Programming Language :: Python",
|
|
12
|
-
"Programming Language :: Python :: 3.11",
|
|
13
|
-
"Programming Language :: Python :: 3.12",
|
|
14
|
-
"Programming Language :: Python :: 3.13",
|
|
15
|
-
]
|
|
16
|
-
dependencies = [
|
|
17
|
-
"desktop-notifier>=6.1.0",
|
|
18
|
-
"platformdirs>=4.3.0",
|
|
19
|
-
"zipline.py[cli]>=0.27.0",
|
|
20
|
-
]
|
|
21
|
-
dynamic = ["version", "urls"]
|
|
22
|
-
|
|
23
|
-
[dependency-groups]
|
|
24
|
-
dev = [
|
|
25
|
-
"basedpyright==1.29.2",
|
|
26
|
-
"pyinstrument==5.0.2",
|
|
27
|
-
"ruff==0.11.12",
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
[project.scripts]
|
|
31
|
-
tacklebox = "tacklebox.entrypoint:app"
|
|
32
|
-
tacklebox-cli = "tacklebox.entrypoint:app" # for uvx / pipx, prefer tacklebox when possible
|
|
33
|
-
|
|
34
|
-
[tool.ruff]
|
|
35
|
-
line-length = 145
|
|
36
|
-
|
|
37
|
-
[tool.pyright]
|
|
38
|
-
typeCheckingMode = "strict"
|
|
39
|
-
reportMissingTypeStubs = false
|
|
40
|
-
|
|
41
|
-
[tool.typos]
|
|
42
|
-
files.extend-exclude = [".direnv/**", ".venv/**"]
|
|
43
|
-
|
|
44
|
-
[tool.hatch.version]
|
|
45
|
-
source = "vcs"
|
|
46
|
-
|
|
47
|
-
[tool.hatch.build]
|
|
48
|
-
include = ["/tacklebox", "/README.md", "/LICENSE.md", "/pyproject.toml"]
|
|
49
|
-
|
|
50
|
-
[tool.hatch.build.hooks.vcs]
|
|
51
|
-
version-file = "tacklebox/version.py"
|
|
52
|
-
|
|
53
|
-
[tool.hatch.metadata.hooks.vcs.urls]
|
|
54
|
-
Homepage = "https://c.csw.im/cswimr/tacklebox"
|
|
55
|
-
Issues = "https://c.csw.im/cswimr/tacklebox/issues"
|
|
56
|
-
source_archive = "https://c.csw.im/cswimr/tacklebox/archive/{commit_hash}.tar.gz"
|
|
57
|
-
|
|
58
|
-
[build-system]
|
|
59
|
-
requires = ["hatchling", "hatch-vcs"]
|
|
60
|
-
build-backend = "hatchling.build"
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
-
|
|
6
|
-
TYPE_CHECKING = False
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from typing import Tuple
|
|
9
|
-
from typing import Union
|
|
10
|
-
|
|
11
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
-
else:
|
|
13
|
-
VERSION_TUPLE = object
|
|
14
|
-
|
|
15
|
-
version: str
|
|
16
|
-
__version__: str
|
|
17
|
-
__version_tuple__: VERSION_TUPLE
|
|
18
|
-
version_tuple: VERSION_TUPLE
|
|
19
|
-
|
|
20
|
-
__version__ = version = '0.2.6'
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 2, 6)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|