tacklebox-cli 0.3.0__tar.gz → 0.4.1__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.
@@ -1,20 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tacklebox-cli
3
- Version: 0.3.0
3
+ Version: 0.4.1
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/951186f80430bac4501fe5167aabef121dfbb457.tar.gz
7
+ Project-URL: Source Archive, https://c.csw.im/cswimr/tacklebox/archive/eb1820071f1c4336448054757a2da3fc115b2e56.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.14
15
- Requires-Dist: desktop-notifier>=6.1.0
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.3.0/LICENSE.md)
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.1/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.3.0/tacklebox/commands/clipboard.py) for all supported tools.
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.1/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 spectacle (Linux only)
37
+ ### tacklebox prepend-to-file
40
38
 
41
- 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.
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
- $ tacklebox spectacle --mode region \
45
- --server "https://zipline.example.com" \
46
- --token "$(cat /file/containing/zipline/token)" \
47
- | tacklebox copy --trim
48
-
49
- # or to record a video
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 zipline
50
+ ### tacklebox find-desktop-entry (Linux only)
57
51
 
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.
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.14"
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
- "desktop-notifier>=6.1.0",
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, find_desktop_entry, prepend_to_file, version, zipline]
10
+ commands: list[Typer] = [clipboard, prepend_to_file, version]
12
11
 
13
12
  if platform.system() == "Linux":
14
- from .spectacle import app as spectacle
15
-
16
- commands.append(spectacle)
13
+ commands.extend([find_desktop_entry])
@@ -45,7 +45,7 @@ def _try_command(mode: ClipboardMode, cmd: list[str], verbose: bool = False, dat
45
45
  result = subprocess.run(
46
46
  cmd,
47
47
  input=data.encode(encoder) if data else None,
48
- stderr=None if verbose else subprocess.DEVNULL,
48
+ capture_output=True,
49
49
  env=get_environment(),
50
50
  timeout=30,
51
51
  )
@@ -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, zipline-py, desktop-notifier, platformdirs, aiohttp, typer, click, and rich packages."""
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
- 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)
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.3.0'
22
- __version_tuple__ = version_tuple = (0, 3, 0)
21
+ __version__ = version = '0.4.1'
22
+ __version_tuple__ = version_tuple = (0, 4, 1)
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