tacklebox-cli 0.2.6__tar.gz → 0.3.0__tar.gz

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