tacklebox-cli 0.3.0__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.3.0 → tacklebox_cli-0.4.0}/PKG-INFO +43 -22
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/pyproject.toml +2 -4
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/tacklebox/commands/__init__.py +2 -5
- tacklebox_cli-0.4.0/tacklebox/commands/find_desktop_entry.py +55 -0
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/tacklebox/commands/version.py +1 -5
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/tacklebox/entrypoint.py +2 -10
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/tacklebox/version.py +2 -2
- tacklebox_cli-0.3.0/tacklebox/commands/find_desktop_entry.py +0 -49
- tacklebox_cli-0.3.0/tacklebox/commands/spectacle.py +0 -334
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/.gitignore +0 -0
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/LICENSE.md +0 -0
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/tacklebox/__init__.py +0 -0
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/tacklebox/commands/clipboard.py +0 -0
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/tacklebox/commands/prepend_to_file.py +0 -0
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/tacklebox/sync.py +0 -0
- {tacklebox_cli-0.3.0 → tacklebox_cli-0.4.0}/tacklebox/utils.py +0 -0
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tacklebox-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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: Source Archive, https://c.csw.im/cswimr/tacklebox/archive/
|
|
7
|
+
Project-URL: Source Archive, https://c.csw.im/cswimr/tacklebox/archive/9286755479cd8bf8d5ad51736ce7b519c01e1479.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
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
-
Requires-Python: >=3.
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
Requires-Dist: platformdirs>=4.3.0
|
|
17
|
-
Requires-Dist: zipline-py[cli]>=0.27.0
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Requires-Dist: typer>=0.20.0
|
|
18
16
|
Description-Content-Type: text/markdown
|
|
19
17
|
|
|
20
18
|
# tacklebox-cli
|
|
@@ -22,40 +20,63 @@ Description-Content-Type: text/markdown
|
|
|
22
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)
|
|
23
21
|
[<img alt="PyPI - Version" src="https://img.shields.io/pypi/v/tacklebox-cli?style=plastic">](https://pypi.org/project/tacklebox-cli/)
|
|
24
22
|
[<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/tacklebox-cli?style=plastic">](https://pypi.org/project/tacklebox-cli/)
|
|
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.
|
|
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)
|
|
26
24
|
tacklebox-cli offers a suite of useful CLI tools.
|
|
27
25
|
|
|
28
26
|
## Usage
|
|
29
27
|
|
|
30
28
|
### tacklebox copy / paste
|
|
31
29
|
|
|
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.
|
|
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.
|
|
33
31
|
|
|
34
32
|
```bash
|
|
35
33
|
$ echo "a" | tacklebox copy --trim && tacklebox paste
|
|
36
34
|
a
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
### tacklebox
|
|
37
|
+
### tacklebox prepend-to-file
|
|
40
38
|
|
|
41
|
-
|
|
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.
|
|
42
40
|
|
|
43
41
|
```bash
|
|
44
|
-
$
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
$ tacklebox spectacle --mode region --record \
|
|
51
|
-
--server "https://zipline.example.com" \
|
|
52
|
-
--token "$(cat /file/containing/zipline/token)" \
|
|
53
|
-
| tacklebox copy --trim
|
|
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
|
|
54
48
|
```
|
|
55
49
|
|
|
56
|
-
### tacklebox
|
|
50
|
+
### tacklebox find-desktop-entry (Linux only)
|
|
57
51
|
|
|
58
|
-
|
|
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
|
+
```
|
|
59
80
|
|
|
60
81
|
## License - The MIT License
|
|
61
82
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tacklebox-cli"
|
|
3
3
|
description = "A small collection of CLI utilities."
|
|
4
|
-
requires-python = ">=3.
|
|
4
|
+
requires-python = ">=3.13"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
license-files = ["LICEN[CS]E*"]
|
|
7
7
|
authors = [{ name = "cswimr", email = "seaswimmerthefsh@gmail.com" }]
|
|
@@ -11,9 +11,7 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.14"
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
|
|
15
|
-
"platformdirs>=4.3.0",
|
|
16
|
-
"zipline.py[cli]>=0.27.0",
|
|
14
|
+
"typer>=0.20.0",
|
|
17
15
|
]
|
|
18
16
|
dynamic = ["readme", "urls", "version"]
|
|
19
17
|
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import platform
|
|
2
2
|
|
|
3
3
|
from typer import Typer
|
|
4
|
-
from zipline.cli.entrypoint import app as zipline
|
|
5
4
|
|
|
6
5
|
from .clipboard import app as clipboard
|
|
7
6
|
from .find_desktop_entry import app as find_desktop_entry
|
|
8
7
|
from .prepend_to_file import app as prepend_to_file
|
|
9
8
|
from .version import app as version
|
|
10
9
|
|
|
11
|
-
commands: list[Typer] = [clipboard,
|
|
10
|
+
commands: list[Typer] = [clipboard, prepend_to_file, version]
|
|
12
11
|
|
|
13
12
|
if platform.system() == "Linux":
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
commands.append(spectacle)
|
|
13
|
+
commands.extend([find_desktop_entry])
|
|
@@ -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)
|
|
@@ -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"),
|
|
@@ -11,19 +11,11 @@ if platform.system() == "Darwin":
|
|
|
11
11
|
|
|
12
12
|
asyncio.set_event_loop_policy(EventLoopPolicy()) # pyright: ignore[reportUnknownArgumentType, reportDeprecated]
|
|
13
13
|
|
|
14
|
-
app = Typer(name="tacklebox", pretty_exceptions_show_locals=False)
|
|
14
|
+
app = Typer(name="tacklebox", no_args_is_help=True, pretty_exceptions_show_locals=False)
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
for typer_instance in commands:
|
|
18
|
-
|
|
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)
|
|
18
|
+
app.add_typer(typer_instance)
|
|
27
19
|
|
|
28
20
|
if __name__ == "__main__":
|
|
29
21
|
app()
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,49 +0,0 @@
|
|
|
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)
|
|
@@ -1,334 +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.progress import Progress, SpinnerColumn, TextColumn
|
|
16
|
-
from typer import Exit, Option, Typer
|
|
17
|
-
from zipline.cli.commands._handling import handle_api_errors
|
|
18
|
-
from zipline.cli.commands.upload import _complete_format # pyright: ignore[reportPrivateUsage]
|
|
19
|
-
from zipline.client import Client, FileData, NameFormat
|
|
20
|
-
|
|
21
|
-
from tacklebox import sync
|
|
22
|
-
from tacklebox.utils import stderr, stdout
|
|
23
|
-
|
|
24
|
-
if not platform.system() == "Linux":
|
|
25
|
-
stderr.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 rectangular 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-cli - Spectacle")
|
|
250
|
-
|
|
251
|
-
with Progress(
|
|
252
|
-
SpinnerColumn(),
|
|
253
|
-
TextColumn("[progress.description]{task.description}"),
|
|
254
|
-
console=stderr,
|
|
255
|
-
transient=True,
|
|
256
|
-
) as progress:
|
|
257
|
-
task = progress.add_task(description="Setting up...", total=None)
|
|
258
|
-
|
|
259
|
-
if not which("spectacle"):
|
|
260
|
-
stderr.print("spectacle is not installed!")
|
|
261
|
-
raise Exit(code=1)
|
|
262
|
-
|
|
263
|
-
image_format, video_format = _read_spectacle_config()
|
|
264
|
-
if record:
|
|
265
|
-
ext = video_format.ext
|
|
266
|
-
else:
|
|
267
|
-
ext = "." + image_format
|
|
268
|
-
|
|
269
|
-
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
|
|
270
|
-
file_path = Path(temp_file.name)
|
|
271
|
-
temp_file.close()
|
|
272
|
-
|
|
273
|
-
command: list[str] = [
|
|
274
|
-
"spectacle",
|
|
275
|
-
"--nonotify",
|
|
276
|
-
"--background",
|
|
277
|
-
"--pointer",
|
|
278
|
-
mode.get_argument(record),
|
|
279
|
-
"--copy-image",
|
|
280
|
-
"--output",
|
|
281
|
-
str(file_path),
|
|
282
|
-
]
|
|
283
|
-
|
|
284
|
-
proc = await asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
285
|
-
_stdout, _stderr = await proc.communicate()
|
|
286
|
-
|
|
287
|
-
if proc.returncode != 0:
|
|
288
|
-
if file_path.exists():
|
|
289
|
-
file_path.unlink(missing_ok=True)
|
|
290
|
-
raise subprocess.CalledProcessError(proc.returncode or 1, command, output=_stdout, stderr=_stderr)
|
|
291
|
-
|
|
292
|
-
if not file_path.stat().st_size:
|
|
293
|
-
file_path.unlink(missing_ok=True)
|
|
294
|
-
raise FileNotFoundError("The file was not created properly.")
|
|
295
|
-
|
|
296
|
-
progress.update(task, description="Reading file...", total=None)
|
|
297
|
-
file_data = FileData(data=file_path)
|
|
298
|
-
|
|
299
|
-
progress.update(task, description="Uploading file...", total=None)
|
|
300
|
-
async with Client(server_url, token) as client:
|
|
301
|
-
try:
|
|
302
|
-
uploaded_file = await client.upload_file(
|
|
303
|
-
payload=file_data,
|
|
304
|
-
compression_percent=compression_percent,
|
|
305
|
-
expiry=expiry.astimezone(tz=timezone.utc) if expiry else None,
|
|
306
|
-
format=format,
|
|
307
|
-
password=password,
|
|
308
|
-
max_views=max_views,
|
|
309
|
-
override_name=override_name,
|
|
310
|
-
override_domain=override_domain,
|
|
311
|
-
folder=folder,
|
|
312
|
-
)
|
|
313
|
-
except Exception as exception:
|
|
314
|
-
handle_api_errors(exception, server_url, traceback=verbose)
|
|
315
|
-
|
|
316
|
-
if print_object:
|
|
317
|
-
stdout.print(uploaded_file)
|
|
318
|
-
else:
|
|
319
|
-
stdout.print(uploaded_file.files[0].url)
|
|
320
|
-
|
|
321
|
-
await notifier.send(
|
|
322
|
-
title="File Uploaded!",
|
|
323
|
-
message=f"File successfully uploaded to {uploaded_file.files[0].url}",
|
|
324
|
-
attachment=Attachment(path=file_path),
|
|
325
|
-
urgency=Urgency.Low,
|
|
326
|
-
timeout=5,
|
|
327
|
-
sound=DEFAULT_SOUND,
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
file_path.unlink(missing_ok=True)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if __name__ == "__main__":
|
|
334
|
-
app()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|