tacklebox-cli 0.2.6__tar.gz → 0.4.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.4.0}/LICENSE.md +1 -2
- tacklebox_cli-0.4.0/PKG-INFO +104 -0
- tacklebox_cli-0.4.0/pyproject.toml +72 -0
- tacklebox_cli-0.4.0/tacklebox/commands/__init__.py +13 -0
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.4.0}/tacklebox/commands/clipboard.py +19 -23
- tacklebox_cli-0.4.0/tacklebox/commands/find_desktop_entry.py +55 -0
- tacklebox_cli-0.4.0/tacklebox/commands/prepend_to_file.py +84 -0
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.4.0}/tacklebox/commands/version.py +3 -7
- tacklebox_cli-0.4.0/tacklebox/entrypoint.py +21 -0
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.4.0}/tacklebox/utils.py +5 -0
- tacklebox_cli-0.4.0/tacklebox/version.py +24 -0
- tacklebox_cli-0.2.6/PKG-INFO +0 -60
- tacklebox_cli-0.2.6/README.md +0 -39
- tacklebox_cli-0.2.6/pyproject.toml +0 -60
- tacklebox_cli-0.2.6/tacklebox/commands/__init__.py +0 -14
- tacklebox_cli-0.2.6/tacklebox/commands/spectacle.py +0 -333
- tacklebox_cli-0.2.6/tacklebox/entrypoint.py +0 -29
- tacklebox_cli-0.2.6/tacklebox/version.py +0 -21
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.4.0}/.gitignore +0 -0
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.4.0}/tacklebox/__init__.py +0 -0
- {tacklebox_cli-0.2.6 → tacklebox_cli-0.4.0}/tacklebox/sync.py +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tacklebox-cli
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A small collection of CLI utilities.
|
|
5
|
+
Project-URL: Homepage, https://c.csw.im/cswimr/tacklebox
|
|
6
|
+
Project-URL: Issues, https://c.csw.im/cswimr/tacklebox/issues
|
|
7
|
+
Project-URL: Source Archive, https://c.csw.im/cswimr/tacklebox/archive/9286755479cd8bf8d5ad51736ce7b519c01e1479.tar.gz
|
|
8
|
+
Author-email: cswimr <seaswimmerthefsh@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE.md
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Requires-Dist: typer>=0.20.0
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# tacklebox-cli
|
|
19
|
+
|
|
20
|
+
[<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)
|
|
21
|
+
[<img alt="PyPI - Version" src="https://img.shields.io/pypi/v/tacklebox-cli?style=plastic">](https://pypi.org/project/tacklebox-cli/)
|
|
22
|
+
[<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/tacklebox-cli?style=plastic">](https://pypi.org/project/tacklebox-cli/)
|
|
23
|
+
[<img alt="PyPI - License" src="https://img.shields.io/pypi/l/tacklebox-cli?style=plastic">](https://c.csw.im/cswimr/tacklebox/src/tag/0.4.0/LICENSE.md)
|
|
24
|
+
tacklebox-cli offers a suite of useful CLI tools.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### tacklebox copy / paste
|
|
29
|
+
|
|
30
|
+
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.4.0/tacklebox/commands/clipboard.py) for all supported tools.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
$ echo "a" | tacklebox copy --trim && tacklebox paste
|
|
34
|
+
a
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### tacklebox prepend-to-file
|
|
38
|
+
|
|
39
|
+
Prepend the contents of one file to another, with support for adding a newline automatically and checking if the operation has already been performed on the destination file.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
$ echo "template-example" > template
|
|
43
|
+
$ echo "hello world" > hello-world.txt
|
|
44
|
+
$ tacklebox prepend-to-file template hello-world.txt -n
|
|
45
|
+
$ cat hello-world.txt
|
|
46
|
+
template-example
|
|
47
|
+
hello world
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### tacklebox find-desktop-entry (Linux only)
|
|
51
|
+
|
|
52
|
+
Checks all of the paths under `$XDG_DATA_DIRS` for an application desktop file with the given name.
|
|
53
|
+
Prints the full path of the desktop file to stdout if found.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
$ tacklebox find-desktop-entry kitty
|
|
57
|
+
kitty.desktop found in /etc/profiles/per-user/cswimr/share/applications
|
|
58
|
+
/etc/profiles/per-user/cswimr/share/applications/kitty.desktop
|
|
59
|
+
|
|
60
|
+
$ tacklebox find-desktop-entry --read kitty
|
|
61
|
+
kitty.desktop found in /etc/profiles/per-user/cswimr/share/applications
|
|
62
|
+
# /etc/profiles/per-user/cswimr/share/applications/kitty.desktop
|
|
63
|
+
[Desktop Entry]
|
|
64
|
+
Version=1.0
|
|
65
|
+
Type=Application
|
|
66
|
+
Name=kitty
|
|
67
|
+
GenericName=Terminal emulator
|
|
68
|
+
Comment=Fast, feature-rich, GPU based terminal
|
|
69
|
+
TryExec=kitty
|
|
70
|
+
StartupNotify=true
|
|
71
|
+
Exec=kitty
|
|
72
|
+
Icon=kitty
|
|
73
|
+
Categories=System;TerminalEmulator;
|
|
74
|
+
X-TerminalArgExec=--
|
|
75
|
+
X-TerminalArgTitle=--title
|
|
76
|
+
X-TerminalArgAppId=--class
|
|
77
|
+
X-TerminalArgDir=--working-directory
|
|
78
|
+
X-TerminalArgHold=--hold
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License - The MIT License
|
|
82
|
+
|
|
83
|
+
Copyright © 2025 cswimr
|
|
84
|
+
|
|
85
|
+
Permission is hereby granted, free of charge, to any person
|
|
86
|
+
obtaining a copy of this software and associated documentation
|
|
87
|
+
files (the “Software”), to deal in the Software without
|
|
88
|
+
restriction, including without limitation the rights to use,
|
|
89
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
90
|
+
copies of the Software, and to permit persons to whom the
|
|
91
|
+
Software is furnished to do so, subject to the following
|
|
92
|
+
conditions:
|
|
93
|
+
|
|
94
|
+
The above copyright notice and this permission notice shall be
|
|
95
|
+
included in all copies or substantial portions of the Software.
|
|
96
|
+
|
|
97
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
|
98
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
99
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
100
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
101
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
102
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
103
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
104
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tacklebox-cli"
|
|
3
|
+
description = "A small collection of CLI utilities."
|
|
4
|
+
requires-python = ">=3.13"
|
|
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
|
+
"typer>=0.20.0",
|
|
15
|
+
]
|
|
16
|
+
dynamic = ["readme", "urls", "version"]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
tacklebox = "tacklebox.entrypoint:app"
|
|
20
|
+
tacklebox-cli = "tacklebox.entrypoint:app" # for uvx / pipx, prefer tacklebox when possible
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"basedpyright~=1.38.2",
|
|
25
|
+
"pyinstrument~=5.1.2",
|
|
26
|
+
"ruff~=0.15.4",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatch-fancy-pypi-readme", "hatch-vcs", "hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.version]
|
|
34
|
+
source = "vcs"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build]
|
|
37
|
+
include = ["/tacklebox", "/LICENSE.md", "/pyproject.toml"]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.hooks.vcs]
|
|
40
|
+
version-file = "tacklebox/version.py"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.metadata.hooks.vcs.urls]
|
|
43
|
+
Homepage = "https://c.csw.im/cswimr/tacklebox"
|
|
44
|
+
Issues = "https://c.csw.im/cswimr/tacklebox/issues"
|
|
45
|
+
"Source Archive" = "https://c.csw.im/cswimr/tacklebox/archive/{commit_hash}.tar.gz"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.metadata.hooks.fancy-pypi-readme]
|
|
48
|
+
content-type = "text/markdown"
|
|
49
|
+
|
|
50
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
|
51
|
+
path = "README.md"
|
|
52
|
+
|
|
53
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
|
54
|
+
text = "\n## License - The MIT License"
|
|
55
|
+
|
|
56
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
|
57
|
+
path = "LICENSE.md"
|
|
58
|
+
start-after = "# The MIT License (MIT)"
|
|
59
|
+
|
|
60
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
|
|
61
|
+
pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)'
|
|
62
|
+
replacement = '[\1](https://c.csw.im/cswimr/tacklebox/src/tag/$HFPR_VERSION\g<2>)'
|
|
63
|
+
|
|
64
|
+
[tool.pyright]
|
|
65
|
+
typeCheckingMode = "strict"
|
|
66
|
+
reportMissingTypeStubs = false
|
|
67
|
+
|
|
68
|
+
[tool.ruff]
|
|
69
|
+
line-length = 145
|
|
70
|
+
|
|
71
|
+
[tool.typos]
|
|
72
|
+
files.extend-exclude = [".direnv/**", ".venv/**"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
|
|
3
|
+
from typer import Typer
|
|
4
|
+
|
|
5
|
+
from .clipboard import app as clipboard
|
|
6
|
+
from .find_desktop_entry import app as find_desktop_entry
|
|
7
|
+
from .prepend_to_file import app as prepend_to_file
|
|
8
|
+
from .version import app as version
|
|
9
|
+
|
|
10
|
+
commands: list[Typer] = [clipboard, prepend_to_file, version]
|
|
11
|
+
|
|
12
|
+
if platform.system() == "Linux":
|
|
13
|
+
commands.extend([find_desktop_entry])
|
|
@@ -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,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from typer import Argument, Exit, Option, Typer
|
|
6
|
+
|
|
7
|
+
from tacklebox.utils import stderr, stdout
|
|
8
|
+
|
|
9
|
+
app = Typer()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def find_desktop_entry(
|
|
14
|
+
desktop_entry: Annotated[str, Argument(help="The name (application ID) of the desktop file to search for.")],
|
|
15
|
+
*,
|
|
16
|
+
read: Annotated[
|
|
17
|
+
bool,
|
|
18
|
+
Option(..., "--read", "-r", help="Print the contents of the desktop file, if found."),
|
|
19
|
+
] = False,
|
|
20
|
+
print_paths: Annotated[bool, Option(..., "--print-paths", help="Print the paths that the command will check for desktop files.")] = False,
|
|
21
|
+
include_user_data_dir: Annotated[
|
|
22
|
+
bool,
|
|
23
|
+
Option(..., "--user-data-dir", "-u", help="Include the user data directory in the search, if not already included in `XDG_DATA_DIRS`."),
|
|
24
|
+
] = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Locate a desktop entry according to the value of your `XDG_DATA_DIRS` environment variable."""
|
|
27
|
+
if not desktop_entry.endswith(".desktop"):
|
|
28
|
+
desktop_entry += ".desktop"
|
|
29
|
+
|
|
30
|
+
entry_paths = []
|
|
31
|
+
|
|
32
|
+
xdg_data_dirs = [Path(path) for path in os.getenv("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split(":")]
|
|
33
|
+
entry_paths = [path / "applications" for path in xdg_data_dirs]
|
|
34
|
+
if include_user_data_dir:
|
|
35
|
+
data_home = Path(os.getenv("XDG_DATA_HOME", "~/.local/share")).expanduser()
|
|
36
|
+
application_path = data_home / "applications"
|
|
37
|
+
if application_path not in entry_paths:
|
|
38
|
+
entry_paths.append(application_path)
|
|
39
|
+
|
|
40
|
+
if print_paths:
|
|
41
|
+
stderr.print(f"Checking the following paths for {desktop_entry}:\n{[str(path) for path in entry_paths]}\n{'-' * 20}")
|
|
42
|
+
|
|
43
|
+
for entry_path in entry_paths:
|
|
44
|
+
entry_file = entry_path / f"{desktop_entry}"
|
|
45
|
+
if entry_file.is_file():
|
|
46
|
+
stderr.print(f"{desktop_entry} found in {entry_path}")
|
|
47
|
+
if read:
|
|
48
|
+
content = entry_file.read_text("utf-8")
|
|
49
|
+
stdout.out(f"# {entry_file}\n{content}")
|
|
50
|
+
else:
|
|
51
|
+
stdout.out(entry_file)
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
stderr.print(f"Desktop entry {desktop_entry} could not be found.")
|
|
55
|
+
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)
|
|
@@ -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
|
|
|
@@ -18,13 +18,9 @@ def _get_package_version(package_name: str) -> str | None:
|
|
|
18
18
|
|
|
19
19
|
@app.command(name="version")
|
|
20
20
|
def show_versions() -> None:
|
|
21
|
-
"""Shows versions of the tacklebox,
|
|
21
|
+
"""Shows versions of the tacklebox, typer, click, and rich packages."""
|
|
22
22
|
versions: dict[str, str | None] = {
|
|
23
23
|
"tacklebox": __version__,
|
|
24
|
-
"zipline-py": _get_package_version("zipline-py"),
|
|
25
|
-
"desktop-notifier": _get_package_version("desktop-notifier"),
|
|
26
|
-
"platformdirs": _get_package_version("platformdirs"),
|
|
27
|
-
"aiohttp": _get_package_version("aiohttp"),
|
|
28
24
|
"typer": _get_package_version("typer"),
|
|
29
25
|
"click": _get_package_version("click"),
|
|
30
26
|
"rich": _get_package_version("rich"),
|
|
@@ -35,7 +31,7 @@ def show_versions() -> None:
|
|
|
35
31
|
for name, version in versions.items()
|
|
36
32
|
)
|
|
37
33
|
|
|
38
|
-
print(output)
|
|
34
|
+
stdout.print(output)
|
|
39
35
|
|
|
40
36
|
|
|
41
37
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import platform
|
|
3
|
+
|
|
4
|
+
from typer import Typer
|
|
5
|
+
|
|
6
|
+
from tacklebox.commands import commands
|
|
7
|
+
|
|
8
|
+
# fmt: off
|
|
9
|
+
if platform.system() == "Darwin":
|
|
10
|
+
from rubicon.objc.eventloop import EventLoopPolicy # pyright: ignore[reportMissingImports, reportUnknownVariableType]
|
|
11
|
+
|
|
12
|
+
asyncio.set_event_loop_policy(EventLoopPolicy()) # pyright: ignore[reportUnknownArgumentType, reportDeprecated]
|
|
13
|
+
|
|
14
|
+
app = Typer(name="tacklebox", no_args_is_help=True, pretty_exceptions_show_locals=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
for typer_instance in commands:
|
|
18
|
+
app.add_typer(typer_instance)
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
app()
|
|
@@ -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.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
tacklebox_cli-0.2.6/PKG-INFO
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: tacklebox-cli
|
|
3
|
-
Version: 0.2.6
|
|
4
|
-
Summary: A small collection of CLI utilities.
|
|
5
|
-
Project-URL: Homepage, https://c.csw.im/cswimr/tacklebox
|
|
6
|
-
Project-URL: Issues, https://c.csw.im/cswimr/tacklebox/issues
|
|
7
|
-
Project-URL: source_archive, https://c.csw.im/cswimr/tacklebox/archive/7d8c5beaa8fce3cf47c233f13720ed44548464bd.tar.gz
|
|
8
|
-
Author-email: cswimr <seaswimmerthefsh@gmail.com>
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE.md
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Requires-Python: >=3.11
|
|
17
|
-
Requires-Dist: desktop-notifier>=6.1.0
|
|
18
|
-
Requires-Dist: platformdirs>=4.3.0
|
|
19
|
-
Requires-Dist: zipline-py[cli]>=0.27.0
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
|
|
22
|
-
# tacklebox-cli
|
|
23
|
-
|
|
24
|
-
[<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
|
-
[<img alt="PyPI - Version" src="https://img.shields.io/pypi/v/tacklebox-cli?style=plastic">](https://pypi.org/project/tacklebox-cli/)
|
|
26
|
-
[<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/branch/main/LICENSE/)
|
|
28
|
-
tacklebox-cli offers a suite of useful CLI tools.
|
|
29
|
-
|
|
30
|
-
## Usage
|
|
31
|
-
|
|
32
|
-
### tacklebox copy / paste
|
|
33
|
-
|
|
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/branch/main/tacklebox/commands/clipboard.py) for all supported tools.
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
$ echo "a" | tacklebox copy --trim && tacklebox paste
|
|
38
|
-
a
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### tacklebox spectacle (Linux only)
|
|
42
|
-
|
|
43
|
-
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.
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
$ tacklebox spectacle --mode region \
|
|
47
|
-
--server "https://zipline.example.com" \
|
|
48
|
-
--token "$(cat /file/containing/zipline/token)" \
|
|
49
|
-
| tacklebox copy --trim
|
|
50
|
-
|
|
51
|
-
# or to record a video
|
|
52
|
-
$ tacklebox spectacle --mode region --record \
|
|
53
|
-
--server "https://zipline.example.com" \
|
|
54
|
-
--token "$(cat /file/containing/zipline/token)" \
|
|
55
|
-
| tacklebox copy --trim
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### tacklebox zipline
|
|
59
|
-
|
|
60
|
-
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.
|
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,14 +0,0 @@
|
|
|
1
|
-
import platform
|
|
2
|
-
|
|
3
|
-
from typer import Typer
|
|
4
|
-
from zipline.cli.entrypoint import app as zipline
|
|
5
|
-
|
|
6
|
-
from .clipboard import app as clipboard
|
|
7
|
-
from .version import app as version
|
|
8
|
-
|
|
9
|
-
commands: list[Typer] = [clipboard, version, zipline]
|
|
10
|
-
|
|
11
|
-
if platform.system() == "Linux":
|
|
12
|
-
from .spectacle import app as spectacle
|
|
13
|
-
|
|
14
|
-
commands.append(spectacle)
|
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import configparser
|
|
3
|
-
import platform
|
|
4
|
-
import subprocess
|
|
5
|
-
import sys
|
|
6
|
-
import tempfile
|
|
7
|
-
from datetime import datetime, timezone
|
|
8
|
-
from enum import IntEnum, StrEnum
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from shutil import which
|
|
11
|
-
from typing import Annotated
|
|
12
|
-
|
|
13
|
-
from desktop_notifier import DEFAULT_SOUND, Attachment, DesktopNotifier, Urgency
|
|
14
|
-
from platformdirs import user_config_dir
|
|
15
|
-
from rich import print
|
|
16
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
17
|
-
from typer import Exit, Option, Typer
|
|
18
|
-
from zipline.cli.commands._handling import handle_api_errors
|
|
19
|
-
from zipline.cli.commands.upload import _complete_format # pyright: ignore[reportPrivateUsage]
|
|
20
|
-
from zipline.client import Client, FileData, NameFormat
|
|
21
|
-
|
|
22
|
-
from tacklebox import sync
|
|
23
|
-
|
|
24
|
-
if not platform.system() == "Linux":
|
|
25
|
-
print("Spectacle is only supported on Linux.")
|
|
26
|
-
raise Exit(code=1)
|
|
27
|
-
|
|
28
|
-
app = Typer()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class VideoFormat(IntEnum):
|
|
32
|
-
"""Because Spectacle is weird and doesn't just use file extensions for storing video formats 😭"""
|
|
33
|
-
|
|
34
|
-
WEBM = 0
|
|
35
|
-
MP4 = 2
|
|
36
|
-
WEBP = 4
|
|
37
|
-
GIF = 8
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def ext(self) -> str:
|
|
41
|
-
return "." + self.__str__()
|
|
42
|
-
|
|
43
|
-
def __str__(self) -> str:
|
|
44
|
-
return self.name.lower()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _read_spectacle_config() -> tuple[str, VideoFormat]:
|
|
48
|
-
"""Read the Spectacle config file and return the configured file formats."""
|
|
49
|
-
path = Path(user_config_dir("spectaclerc"))
|
|
50
|
-
if not path.exists():
|
|
51
|
-
return "png", VideoFormat.WEBM
|
|
52
|
-
|
|
53
|
-
config = configparser.ConfigParser(strict=False)
|
|
54
|
-
config.read(path)
|
|
55
|
-
|
|
56
|
-
preferred_image_format = config.get("ImageSave", "preferredImageFormat", fallback="png").lower()
|
|
57
|
-
|
|
58
|
-
preferred_video_format = VideoFormat(config.getint("VideoSave", "preferredVideoFormat", fallback=0))
|
|
59
|
-
|
|
60
|
-
return preferred_image_format, preferred_video_format
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class SpectacleMode(StrEnum):
|
|
64
|
-
REGION = "region"
|
|
65
|
-
DESKTOP = "desktop"
|
|
66
|
-
MONITOR = "monitor"
|
|
67
|
-
WINDOW = "window"
|
|
68
|
-
ACTIVEWINDOW = "activewindow"
|
|
69
|
-
TRANSIENT = "transient"
|
|
70
|
-
|
|
71
|
-
@classmethod
|
|
72
|
-
def complete_format(cls, incomplete: str) -> list[tuple[str, str] | str]:
|
|
73
|
-
"""Generate autocompletion strings for Typer."""
|
|
74
|
-
completions: list[tuple[str, str] | str] = []
|
|
75
|
-
for mode in SpectacleMode:
|
|
76
|
-
if mode.startswith(incomplete):
|
|
77
|
-
completions.append((mode, mode.completion_string) if mode.completion_string else mode)
|
|
78
|
-
return completions
|
|
79
|
-
|
|
80
|
-
@property
|
|
81
|
-
def completion_string(self) -> str | None:
|
|
82
|
-
argument = self.get_argument()
|
|
83
|
-
record_argument = self.get_argument(True)
|
|
84
|
-
|
|
85
|
-
completion_strings: dict[SpectacleMode, str] = {
|
|
86
|
-
SpectacleMode.REGION: f"Capture a retangular region of the desktop. ({argument} | {record_argument})",
|
|
87
|
-
SpectacleMode.DESKTOP: f"Capture the entire desktop (default). When recording, this will capture only the current monitor. ({argument} | {record_argument})",
|
|
88
|
-
SpectacleMode.MONITOR: f"Capture the current monitor. ({argument} | {record_argument})",
|
|
89
|
-
SpectacleMode.WINDOW: f"Capture the window currently under the cursor, including parents of pop-up menus. ({argument} | {record_argument})",
|
|
90
|
-
SpectacleMode.ACTIVEWINDOW: f"Capture the currently selected window. ({argument} | {record_argument})",
|
|
91
|
-
SpectacleMode.TRANSIENT: f"Capture the window currently under the cursor, excluding parents of pop-up menus. ({argument} | {record_argument})",
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return completion_strings.get(self, None)
|
|
95
|
-
|
|
96
|
-
def get_argument(self, record: bool = False) -> str:
|
|
97
|
-
match self:
|
|
98
|
-
case SpectacleMode.REGION:
|
|
99
|
-
return "--record=region" if record else "--region"
|
|
100
|
-
case SpectacleMode.DESKTOP:
|
|
101
|
-
return "--record=screen" if record else "--fullscreen"
|
|
102
|
-
case SpectacleMode.MONITOR:
|
|
103
|
-
return "--record=screen" if record else "--current"
|
|
104
|
-
case SpectacleMode.WINDOW:
|
|
105
|
-
return "--record=window" if record else "--windowundercursor"
|
|
106
|
-
case SpectacleMode.ACTIVEWINDOW:
|
|
107
|
-
return "--record=window" if record else "--activewindow"
|
|
108
|
-
case SpectacleMode.TRANSIENT:
|
|
109
|
-
return "--record=window" if record else "--transientonly"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@app.command(name="spectacle")
|
|
113
|
-
@sync
|
|
114
|
-
async def spectacle(
|
|
115
|
-
server_url: Annotated[
|
|
116
|
-
str,
|
|
117
|
-
Option(
|
|
118
|
-
...,
|
|
119
|
-
"--server",
|
|
120
|
-
"-s",
|
|
121
|
-
help="Specify the URL to your Zipline instance.",
|
|
122
|
-
envvar="ZIPLINE_SERVER",
|
|
123
|
-
prompt=True,
|
|
124
|
-
),
|
|
125
|
-
],
|
|
126
|
-
token: Annotated[
|
|
127
|
-
str,
|
|
128
|
-
Option(
|
|
129
|
-
...,
|
|
130
|
-
"--token",
|
|
131
|
-
"-t",
|
|
132
|
-
help="Specify a token used for authentication against your chosen Zipline instance.",
|
|
133
|
-
envvar="ZIPLINE_TOKEN",
|
|
134
|
-
prompt=True,
|
|
135
|
-
hide_input=True,
|
|
136
|
-
),
|
|
137
|
-
],
|
|
138
|
-
print_object: Annotated[
|
|
139
|
-
bool,
|
|
140
|
-
Option(
|
|
141
|
-
...,
|
|
142
|
-
"--object/--text",
|
|
143
|
-
"-o/-O",
|
|
144
|
-
default_factory=lambda: bool(sys.stdout.isatty()),
|
|
145
|
-
help=(
|
|
146
|
-
"Specify how to format the output. If --text (or piped), "
|
|
147
|
-
"you'll get a link to the uploaded file; if --object (or on a TTY), "
|
|
148
|
-
"you'll get the raw Python object."
|
|
149
|
-
),
|
|
150
|
-
envvar="ZIPLINE_PRINT_OBJECT",
|
|
151
|
-
),
|
|
152
|
-
],
|
|
153
|
-
mode: Annotated[
|
|
154
|
-
SpectacleMode,
|
|
155
|
-
Option(..., "--mode", "-M", help="Specify what mode Spectacle should be launched in.", autocompletion=SpectacleMode.complete_format),
|
|
156
|
-
] = SpectacleMode.DESKTOP,
|
|
157
|
-
record: Annotated[
|
|
158
|
-
bool, Option(..., "--record", "-r", help="Specify whether or not to record a video instead of taking a screenshot.")
|
|
159
|
-
] = False,
|
|
160
|
-
format: Annotated[
|
|
161
|
-
NameFormat | None,
|
|
162
|
-
Option(
|
|
163
|
-
...,
|
|
164
|
-
"--format",
|
|
165
|
-
"-f",
|
|
166
|
-
help="Specify what format Zipline should use to generate a link for this file.",
|
|
167
|
-
autocompletion=_complete_format,
|
|
168
|
-
),
|
|
169
|
-
] = None,
|
|
170
|
-
compression_percent: Annotated[
|
|
171
|
-
int,
|
|
172
|
-
Option(
|
|
173
|
-
...,
|
|
174
|
-
"--compression-percent",
|
|
175
|
-
"-c",
|
|
176
|
-
help="Specify how much this file should be compressed.",
|
|
177
|
-
),
|
|
178
|
-
] = 0,
|
|
179
|
-
expiry: Annotated[
|
|
180
|
-
datetime | None,
|
|
181
|
-
Option(
|
|
182
|
-
...,
|
|
183
|
-
"--expiry",
|
|
184
|
-
"-e",
|
|
185
|
-
help=(
|
|
186
|
-
"Specify when this file should expire.\n"
|
|
187
|
-
"When this time expires, the file will be deleted from the Zipline instance.\n"
|
|
188
|
-
"This argument uses your system's local timezone, not UTC dates."
|
|
189
|
-
),
|
|
190
|
-
),
|
|
191
|
-
] = None,
|
|
192
|
-
password: Annotated[
|
|
193
|
-
str | None,
|
|
194
|
-
Option(
|
|
195
|
-
...,
|
|
196
|
-
"--password",
|
|
197
|
-
"-p",
|
|
198
|
-
help="Specify a password for this file. Viewing this file from Zipline will then require having this password.",
|
|
199
|
-
),
|
|
200
|
-
] = None,
|
|
201
|
-
max_views: Annotated[
|
|
202
|
-
int | None,
|
|
203
|
-
Option(
|
|
204
|
-
...,
|
|
205
|
-
"--max-views",
|
|
206
|
-
"-m",
|
|
207
|
-
help="Specify how many times this file can be viewed before it will be automatically deleted.",
|
|
208
|
-
),
|
|
209
|
-
] = None,
|
|
210
|
-
override_name: Annotated[
|
|
211
|
-
str | None,
|
|
212
|
-
Option(
|
|
213
|
-
...,
|
|
214
|
-
"--name",
|
|
215
|
-
"-n",
|
|
216
|
-
help="Specify the name to give this file in Zipline. This overrides the --format option, if provided.",
|
|
217
|
-
),
|
|
218
|
-
] = None,
|
|
219
|
-
folder: Annotated[
|
|
220
|
-
str | None,
|
|
221
|
-
Option(
|
|
222
|
-
...,
|
|
223
|
-
"--folder",
|
|
224
|
-
"-F",
|
|
225
|
-
help="Specify what folder the file should be added to after it is uploaded.",
|
|
226
|
-
),
|
|
227
|
-
] = None,
|
|
228
|
-
override_domain: Annotated[
|
|
229
|
-
str | None,
|
|
230
|
-
Option(
|
|
231
|
-
...,
|
|
232
|
-
"--domain",
|
|
233
|
-
"-d",
|
|
234
|
-
help="Specify what domain should be used instead of the Zipline instance's core domain.",
|
|
235
|
-
),
|
|
236
|
-
] = None,
|
|
237
|
-
verbose: Annotated[
|
|
238
|
-
bool,
|
|
239
|
-
Option(
|
|
240
|
-
...,
|
|
241
|
-
"--verbose",
|
|
242
|
-
"-v",
|
|
243
|
-
help="Specify whether or not the application should print tracebacks from exceptions to the console. If the application encounters an exception it doesn't expect, it will always be printed to the console regardless of this option.",
|
|
244
|
-
envvar="ZIPLINE_VERBOSE",
|
|
245
|
-
),
|
|
246
|
-
] = False,
|
|
247
|
-
) -> None:
|
|
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="Tacklebox - Spectacle")
|
|
250
|
-
|
|
251
|
-
with Progress(
|
|
252
|
-
SpinnerColumn(),
|
|
253
|
-
TextColumn("[progress.description]{task.description}"),
|
|
254
|
-
transient=True,
|
|
255
|
-
) as progress:
|
|
256
|
-
task = progress.add_task(description="Setting up...", total=None)
|
|
257
|
-
|
|
258
|
-
if not which("spectacle"):
|
|
259
|
-
print("spectacle is not installed!")
|
|
260
|
-
raise Exit(code=1)
|
|
261
|
-
|
|
262
|
-
image_format, video_format = _read_spectacle_config()
|
|
263
|
-
if record:
|
|
264
|
-
ext = video_format.ext
|
|
265
|
-
else:
|
|
266
|
-
ext = "." + image_format
|
|
267
|
-
|
|
268
|
-
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
|
|
269
|
-
file_path = Path(temp_file.name)
|
|
270
|
-
temp_file.close()
|
|
271
|
-
|
|
272
|
-
command: list[str] = [
|
|
273
|
-
"spectacle",
|
|
274
|
-
"--nonotify",
|
|
275
|
-
"--background",
|
|
276
|
-
"--pointer",
|
|
277
|
-
mode.get_argument(record),
|
|
278
|
-
"--copy-image",
|
|
279
|
-
"--output",
|
|
280
|
-
str(file_path),
|
|
281
|
-
]
|
|
282
|
-
|
|
283
|
-
proc = await asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
284
|
-
stdout, stderr = await proc.communicate()
|
|
285
|
-
|
|
286
|
-
if proc.returncode != 0:
|
|
287
|
-
if file_path.exists():
|
|
288
|
-
file_path.unlink(missing_ok=True)
|
|
289
|
-
raise subprocess.CalledProcessError(proc.returncode or 1, command, output=stdout, stderr=stderr)
|
|
290
|
-
|
|
291
|
-
if not file_path.stat().st_size:
|
|
292
|
-
file_path.unlink(missing_ok=True)
|
|
293
|
-
raise FileNotFoundError("The file was not created properly.")
|
|
294
|
-
|
|
295
|
-
progress.update(task, description="Reading file...", total=None)
|
|
296
|
-
file_data = FileData(data=file_path)
|
|
297
|
-
|
|
298
|
-
progress.update(task, description="Uploading file...", total=None)
|
|
299
|
-
async with Client(server_url, token) as client:
|
|
300
|
-
try:
|
|
301
|
-
uploaded_file = await client.upload_file(
|
|
302
|
-
payload=file_data,
|
|
303
|
-
compression_percent=compression_percent,
|
|
304
|
-
expiry=expiry.astimezone(tz=timezone.utc) if expiry else None,
|
|
305
|
-
format=format,
|
|
306
|
-
password=password,
|
|
307
|
-
max_views=max_views,
|
|
308
|
-
override_name=override_name,
|
|
309
|
-
override_domain=override_domain,
|
|
310
|
-
folder=folder,
|
|
311
|
-
)
|
|
312
|
-
except Exception as exception:
|
|
313
|
-
handle_api_errors(exception, server_url, traceback=verbose)
|
|
314
|
-
|
|
315
|
-
if print_object:
|
|
316
|
-
print(uploaded_file)
|
|
317
|
-
else:
|
|
318
|
-
print(uploaded_file.files[0].url)
|
|
319
|
-
|
|
320
|
-
await notifier.send(
|
|
321
|
-
title="File Uploaded!",
|
|
322
|
-
message=f"File successfully uploaded to {uploaded_file.files[0].url}",
|
|
323
|
-
attachment=Attachment(path=file_path),
|
|
324
|
-
urgency=Urgency.Low,
|
|
325
|
-
timeout=5,
|
|
326
|
-
sound=DEFAULT_SOUND,
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
file_path.unlink(missing_ok=True)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if __name__ == "__main__":
|
|
333
|
-
app()
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import platform
|
|
3
|
-
|
|
4
|
-
from typer import Typer
|
|
5
|
-
|
|
6
|
-
from tacklebox.commands import commands
|
|
7
|
-
|
|
8
|
-
# fmt: off
|
|
9
|
-
if platform.system() == "Darwin":
|
|
10
|
-
from rubicon.objc.eventloop import EventLoopPolicy # pyright: ignore[reportMissingImports, reportUnknownVariableType]
|
|
11
|
-
|
|
12
|
-
asyncio.set_event_loop_policy(EventLoopPolicy()) # pyright: ignore[reportUnknownArgumentType]
|
|
13
|
-
|
|
14
|
-
app = Typer(name="Tacklebox", pretty_exceptions_show_locals=False)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
for typer_instance in commands:
|
|
18
|
-
match typer_instance.info.name:
|
|
19
|
-
case "zipline.py":
|
|
20
|
-
app.add_typer(
|
|
21
|
-
typer_instance,
|
|
22
|
-
name="zipline",
|
|
23
|
-
help="Interact with a remote Zipline instance using the zipline.py library's CLI.",
|
|
24
|
-
)
|
|
25
|
-
case _:
|
|
26
|
-
app.add_typer(typer_instance)
|
|
27
|
-
|
|
28
|
-
if __name__ == "__main__":
|
|
29
|
-
app()
|
|
@@ -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
|