pyshort-cli 1.1.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.
@@ -0,0 +1,8 @@
1
+ exclude myenv
2
+ recursive-exclude myenv *
3
+ recursive-exclude .pytest_cache *
4
+ recursive-exclude __pycache__ *
5
+ recursive-exclude *.egg-info *
6
+ recursive-exclude tests *
7
+ recursive-exclude dist *
8
+ recursive-exclude build *
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyshort-cli
3
+ Version: 1.1.0
4
+ Summary: Raccourcisseur d'URLs en ligne de commande
5
+ License: MIT
6
+ Keywords: url,shortener,cli
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: typer>=0.12.0
10
+ Requires-Dist: httpx>=0.27.0
11
+ Requires-Dist: pyperclip>=1.8.2
12
+ Requires-Dist: rich>=13.7.0
13
+
14
+ # pyshort — Raccourcisseur d'URLs en ligne de commande
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install pyshort
20
+ # ou en local depuis ce dossier :
21
+ pip install -e .
22
+ ```
23
+
24
+ ## Configuration (une seule fois)
25
+
26
+ ```bash
27
+ pyshort config --api-url https://mon-api.render.com
28
+ ```
29
+
30
+ ## Utilisation
31
+
32
+ ```bash
33
+ # Raccourcir une URL (résultat copié automatiquement)
34
+ pyshort shorten https://www.monsite.com/une/url/tres/longue
35
+ # ╭─ 🔗 URL raccourcie ─╮
36
+ # │ https://shr.ty/abc12x │
37
+ # │ ✅ Copié dans le presse-papier │
38
+ # ╰────────────────────────╯
39
+
40
+ # Sans copie automatique
41
+ pyshort shorten https://example.com --no-copy
42
+
43
+ # Voir les statistiques (code ou URL courte)
44
+ pyshort stats abc12x
45
+ pyshort stats https://shr.ty/abc12x
46
+
47
+ # Voir la config actuelle
48
+ pyshort config --show
49
+
50
+ # Version
51
+ pyshort version
52
+ ```
53
+
54
+ ## Lancer les tests
55
+
56
+ ```bash
57
+ pip install pytest
58
+ pytest tests/ -v
59
+ ```
@@ -0,0 +1,46 @@
1
+ # pyshort — Raccourcisseur d'URLs en ligne de commande
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ pip install pyshort
7
+ # ou en local depuis ce dossier :
8
+ pip install -e .
9
+ ```
10
+
11
+ ## Configuration (une seule fois)
12
+
13
+ ```bash
14
+ pyshort config --api-url https://mon-api.render.com
15
+ ```
16
+
17
+ ## Utilisation
18
+
19
+ ```bash
20
+ # Raccourcir une URL (résultat copié automatiquement)
21
+ pyshort shorten https://www.monsite.com/une/url/tres/longue
22
+ # ╭─ 🔗 URL raccourcie ─╮
23
+ # │ https://shr.ty/abc12x │
24
+ # │ ✅ Copié dans le presse-papier │
25
+ # ╰────────────────────────╯
26
+
27
+ # Sans copie automatique
28
+ pyshort shorten https://example.com --no-copy
29
+
30
+ # Voir les statistiques (code ou URL courte)
31
+ pyshort stats abc12x
32
+ pyshort stats https://shr.ty/abc12x
33
+
34
+ # Voir la config actuelle
35
+ pyshort config --show
36
+
37
+ # Version
38
+ pyshort version
39
+ ```
40
+
41
+ ## Lancer les tests
42
+
43
+ ```bash
44
+ pip install pytest
45
+ pytest tests/ -v
46
+ ```
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyshort-cli"
7
+ version = "1.1.0"
8
+ description = "Raccourcisseur d'URLs en ligne de commande"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ keywords = ["url", "shortener", "cli"]
13
+ dependencies = [
14
+ "typer>=0.12.0",
15
+ "httpx>=0.27.0",
16
+ "pyperclip>=1.8.2",
17
+ "rich>=13.7.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ pyshort = "pyshort.cli:app"
22
+ pyshort-gui = "pyshort.gui:launch_gui"
23
+
24
+ # On liste explicitement les packages à inclure — UNIQUEMENT pyshort/
25
+ [tool.setuptools.packages]
26
+ find = { include = ["pyshort", "pyshort.*"] }
@@ -0,0 +1 @@
1
+ __version__ = "1.1.0"
@@ -0,0 +1,4 @@
1
+ from pyshort.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,31 @@
1
+ import httpx
2
+
3
+ from pyshort.config import get_api_url
4
+
5
+
6
+ def shorten(url: str) -> dict:
7
+ """
8
+ Appelle POST /shorten et retourne le dict de réponse.
9
+ Lève une exception httpx en cas d'erreur réseau ou HTTP.
10
+ """
11
+ api_url = get_api_url()
12
+ response = httpx.post(
13
+ f"{api_url}/shorten",
14
+ json={"url": url},
15
+ timeout=10,
16
+ )
17
+ response.raise_for_status()
18
+ return response.json()
19
+
20
+
21
+ def stats(code: str) -> dict:
22
+ """
23
+ Appelle GET /stats/{code} et retourne le dict de réponse.
24
+ """
25
+ api_url = get_api_url()
26
+ response = httpx.get(
27
+ f"{api_url}/stats/{code}",
28
+ timeout=10,
29
+ )
30
+ response.raise_for_status()
31
+ return response.json()
@@ -0,0 +1,151 @@
1
+ from typing import Optional
2
+
3
+ import httpx
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+
9
+ from pyshort import __version__
10
+ from pyshort.api import shorten as api_shorten
11
+ from pyshort.api import stats as api_stats
12
+ from pyshort.config import get_api_url, save_api_url
13
+
14
+ app = typer.Typer(
15
+ name="pyshort",
16
+ help="🔗 Raccourcissez vos URLs directement depuis le terminal.",
17
+ add_completion=False,
18
+ )
19
+
20
+ console = Console()
21
+
22
+
23
+ def _try_copy(text: str) -> bool:
24
+ """Tente de copier le texte dans le presse-papier. Retourne True si succès."""
25
+ try:
26
+ import pyperclip
27
+ pyperclip.copy(text)
28
+ return True
29
+ except Exception:
30
+ return False
31
+
32
+
33
+ # ─────────────────────────────────────────────
34
+ # pyshort shorten <url>
35
+ # ─────────────────────────────────────────────
36
+ @app.command()
37
+ def shorten(
38
+ url: str = typer.Argument(..., help="L'URL longue à raccourcir."),
39
+ no_copy: bool = typer.Option(False, "--no-copy", help="Ne pas copier dans le presse-papier."),
40
+ ):
41
+ """Raccourcit une URL et copie le résultat dans le presse-papier."""
42
+ with console.status("[bold cyan]Raccourcissement en cours…[/]"):
43
+ try:
44
+ data = api_shorten(url)
45
+ except httpx.HTTPStatusError as e:
46
+ console.print(f"[bold red]Erreur HTTP {e.response.status_code}[/] : {e.response.text}")
47
+ raise typer.Exit(1)
48
+ except httpx.RequestError:
49
+ api = get_api_url()
50
+ console.print(f"[bold red]Impossible de joindre l'API[/] ({api})")
51
+ console.print("💡 Vérifiez que le serveur tourne ou configurez l'URL avec [bold]pyshort config[/]")
52
+ raise typer.Exit(1)
53
+
54
+ short_url = data["short"]
55
+
56
+ copied = False
57
+ if not no_copy:
58
+ copied = _try_copy(short_url)
59
+
60
+ panel_content = f"[bold green]{short_url}[/]"
61
+ if copied:
62
+ panel_content += "\n[dim]✅ Copié dans le presse-papier[/]"
63
+
64
+ console.print(Panel(panel_content, title="🔗 URL raccourcie", border_style="green"))
65
+
66
+
67
+ # ─────────────────────────────────────────────
68
+ # pyshort stats <code>
69
+ # ─────────────────────────────────────────────
70
+ @app.command()
71
+ def stats(
72
+ code: str = typer.Argument(..., help="Le code court (ex: abc12x) ou l'URL courte complète."),
73
+ ):
74
+ """Affiche les statistiques d'une URL raccourcie."""
75
+ # Accepte aussi bien "abc12x" que "https://shr.ty/abc12x"
76
+ if "/" in code:
77
+ code = code.rstrip("/").split("/")[-1]
78
+
79
+ with console.status("[bold cyan]Récupération des stats…[/]"):
80
+ try:
81
+ data = api_stats(code)
82
+ except httpx.HTTPStatusError as e:
83
+ if e.response.status_code == 404:
84
+ console.print(f"[bold red]Code introuvable :[/] '{code}'")
85
+ else:
86
+ console.print(f"[bold red]Erreur HTTP {e.response.status_code}[/]")
87
+ raise typer.Exit(1)
88
+ except httpx.RequestError:
89
+ api = get_api_url()
90
+ console.print(f"[bold red]Impossible de joindre l'API[/] ({api})")
91
+ raise typer.Exit(1)
92
+
93
+ table = Table(show_header=False, border_style="cyan", min_width=50)
94
+ table.add_column("Clé", style="bold cyan", width=14)
95
+ table.add_column("Valeur")
96
+
97
+ # Formater la date
98
+ created = data["created_at"].replace("T", " ").split(".")[0]
99
+
100
+ table.add_row("🔑 Code", data["code"])
101
+ table.add_row("🔗 URL courte", f"[green]{data['short']}[/]")
102
+ table.add_row("📄 Originale", data["original"])
103
+ table.add_row("👆 Clics", str(data["clicks"]))
104
+ table.add_row("📅 Créé le", created)
105
+
106
+ console.print(table)
107
+
108
+
109
+ # ─────────────────────────────────────────────
110
+ # pyshort config (commande cachée)
111
+ # ─────────────────────────────────────────────
112
+ @app.command(hidden=True)
113
+ def config(
114
+ api_url: Optional[str] = typer.Option(None, "--api-url", help="URL de l'API backend."),
115
+ show: bool = typer.Option(False, "--show", help="Affiche la configuration actuelle."),
116
+ ):
117
+ """Configure l'URL de l'API backend (sauvegardée dans ~/.pyshortrc)."""
118
+ if show or api_url is None:
119
+ current = get_api_url()
120
+ console.print(f"[bold]API URL :[/] {current}")
121
+ return
122
+
123
+ save_api_url(api_url)
124
+ console.print(f"[bold green]✅ API URL mise à jour :[/] {api_url}")
125
+
126
+
127
+ # ─────────────────────────────────────────────
128
+ # pyshort version
129
+ # ─────────────────────────────────────────────
130
+ @app.command()
131
+ def version():
132
+ """Affiche la version de pyshort."""
133
+ console.print(f"[bold]pyshort[/] v{__version__}")
134
+
135
+
136
+ if __name__ == "__main__":
137
+ app()
138
+
139
+ # ─────────────────────────────────────────────
140
+ # pyshort gui
141
+ # ─────────────────────────────────────────────
142
+ @app.command()
143
+ def gui():
144
+ """Lance l'interface graphique (pour les utilisateurs non-développeurs)."""
145
+ try:
146
+ from pyshort.gui import launch_gui
147
+ console.print("[bold cyan]Ouverture de l'interface graphique…[/]")
148
+ launch_gui()
149
+ except ImportError:
150
+ console.print("[bold red]tkinter n'est pas disponible sur ce système.[/]")
151
+ console.print("Sur Ubuntu/Debian : [bold]sudo apt install python3-tk[/]")
@@ -0,0 +1,26 @@
1
+ import configparser
2
+ from pathlib import Path
3
+
4
+ CONFIG_PATH = Path.home() / ".pyshortrc"
5
+ DEFAULT_API_URL = "http://localhost:8000"
6
+
7
+
8
+ def load_config() -> configparser.ConfigParser:
9
+ config = configparser.ConfigParser()
10
+ if CONFIG_PATH.exists():
11
+ config.read(CONFIG_PATH)
12
+ return config
13
+
14
+
15
+ def get_api_url() -> str:
16
+ config = load_config()
17
+ return config.get("default", "api_url", fallback=DEFAULT_API_URL)
18
+
19
+
20
+ def save_api_url(api_url: str) -> None:
21
+ config = load_config()
22
+ if "default" not in config:
23
+ config["default"] = {}
24
+ config["default"]["api_url"] = api_url
25
+ with open(CONFIG_PATH, "w") as f:
26
+ config.write(f)
@@ -0,0 +1,268 @@
1
+ import threading
2
+ import tkinter as tk
3
+ from tkinter import font as tkfont
4
+ from tkinter import messagebox
5
+
6
+ from pyshort.api import shorten as api_shorten
7
+ from pyshort.api import stats as api_stats
8
+ from pyshort.config import get_api_url, save_api_url
9
+
10
+ # ── Couleurs ──────────────────────────────────────────────────────────────────
11
+ BG = "#0f172a"
12
+ CARD = "#1e293b"
13
+ BORDER = "#334155"
14
+ BLUE = "#0ea5e9"
15
+ BLUE_H = "#38bdf8"
16
+ TEXT = "#e2e8f0"
17
+ MUTED = "#64748b"
18
+ GREEN = "#22c55e"
19
+ RED_BG = "#450a0a"
20
+ RED_FG = "#fca5a5"
21
+
22
+
23
+ class PyShortGUI:
24
+ def __init__(self):
25
+ self.root = tk.Tk()
26
+ self.root.title("PyShort — Raccourcisseur d'URLs")
27
+ self.root.configure(bg=BG)
28
+ self.root.resizable(False, False)
29
+
30
+ # Centrer la fenêtre
31
+ w, h = 420, 520
32
+ self.root.geometry(f"{w}x{h}")
33
+ self.root.eval("tk::PlaceWindow . center")
34
+
35
+ self._build_ui()
36
+
37
+ # ── Construction de l'interface ───────────────────────────────────────────
38
+ def _build_ui(self):
39
+ # ── Header ────────────────────────────────────────────────────────────
40
+ header = tk.Frame(self.root, bg=CARD, pady=12)
41
+ header.pack(fill="x")
42
+
43
+ tk.Label(header, text="🔗 PyShort", bg=CARD, fg=BLUE,
44
+ font=("Segoe UI", 16, "bold")).pack(side="left", padx=16)
45
+ tk.Label(header, text="Raccourcisseur d'URLs", bg=CARD, fg=MUTED,
46
+ font=("Segoe UI", 10)).pack(side="left")
47
+
48
+ # ── Corps ─────────────────────────────────────────────────────────────
49
+ body = tk.Frame(self.root, bg=BG, padx=20, pady=20)
50
+ body.pack(fill="both", expand=True)
51
+
52
+ # Label URL
53
+ tk.Label(body, text="Collez ou tapez votre URL :", bg=BG, fg=MUTED,
54
+ font=("Segoe UI", 9, "bold")).pack(anchor="w", pady=(0, 6))
55
+
56
+ # Champ URL
57
+ url_frame = tk.Frame(body, bg=BORDER, padx=1, pady=1)
58
+ url_frame.pack(fill="x", pady=(0, 8))
59
+
60
+ self.url_var = tk.StringVar()
61
+ self.url_entry = tk.Entry(
62
+ url_frame, textvariable=self.url_var,
63
+ bg=CARD, fg=TEXT, insertbackground=TEXT,
64
+ font=("Segoe UI", 11), relief="flat",
65
+ bd=0,
66
+ )
67
+ self.url_entry.pack(fill="x", padx=10, pady=10)
68
+ self.url_entry.bind("<Return>", lambda e: self._shorten())
69
+ self.url_entry.focus()
70
+
71
+ # Bouton coller
72
+ self._btn_paste = self._make_button(
73
+ body, "📋 Coller depuis le presse-papier",
74
+ self._paste, bg=CARD, fg=MUTED, hover_bg=BORDER
75
+ )
76
+ self._btn_paste.pack(fill="x", pady=(0, 10))
77
+
78
+ # Bouton raccourcir
79
+ self._btn_shorten = self._make_button(
80
+ body, "🔗 Raccourcir l'URL",
81
+ self._shorten, bg=BLUE, fg="white", hover_bg=BLUE_H,
82
+ font_size=12, bold=True
83
+ )
84
+ self._btn_shorten.pack(fill="x", pady=(0, 16))
85
+
86
+ # Séparateur
87
+ tk.Frame(body, bg=BORDER, height=1).pack(fill="x", pady=(0, 16))
88
+
89
+ # Zone résultat
90
+ tk.Label(body, text="URL raccourcie :", bg=BG, fg=MUTED,
91
+ font=("Segoe UI", 9, "bold")).pack(anchor="w", pady=(0, 6))
92
+
93
+ result_frame = tk.Frame(body, bg=BORDER, padx=1, pady=1)
94
+ result_frame.pack(fill="x", pady=(0, 8))
95
+
96
+ self.result_var = tk.StringVar(value="")
97
+ result_inner = tk.Frame(result_frame, bg=CARD)
98
+ result_inner.pack(fill="x")
99
+
100
+ self.result_entry = tk.Entry(
101
+ result_inner, textvariable=self.result_var,
102
+ bg=CARD, fg=BLUE, font=("Segoe UI", 11),
103
+ relief="flat", bd=0, state="readonly",
104
+ readonlybackground=CARD, cursor="hand2",
105
+ )
106
+ self.result_entry.pack(fill="x", padx=10, pady=10)
107
+
108
+ # Bouton copier
109
+ self._btn_copy = self._make_button(
110
+ body, "📋 Copier dans le presse-papier",
111
+ self._copy, bg=CARD, fg=MUTED, hover_bg=BORDER
112
+ )
113
+ self._btn_copy.pack(fill="x", pady=(0, 8))
114
+
115
+ # Stats
116
+ self.stats_var = tk.StringVar(value="")
117
+ tk.Label(body, textvariable=self.stats_var, bg=BG, fg=MUTED,
118
+ font=("Segoe UI", 9)).pack(anchor="w")
119
+
120
+ # Message status (erreur ou succès)
121
+ self.status_var = tk.StringVar(value="")
122
+ self.status_label = tk.Label(
123
+ body, textvariable=self.status_var,
124
+ bg=BG, fg=RED_FG, font=("Segoe UI", 9),
125
+ wraplength=380, justify="left"
126
+ )
127
+ self.status_label.pack(anchor="w", pady=(8, 0))
128
+
129
+ # ── Footer ────────────────────────────────────────────────────────────
130
+ footer = tk.Frame(self.root, bg=CARD, pady=8)
131
+ footer.pack(fill="x", side="bottom")
132
+
133
+
134
+
135
+ # ── Actions ───────────────────────────────────────────────────────────────
136
+ def _paste(self):
137
+ try:
138
+ text = self.root.clipboard_get()
139
+ self.url_var.set(text.strip())
140
+ self.url_entry.focus()
141
+ except tk.TclError:
142
+ self._set_status("Presse-papier vide ou inaccessible.", error=True)
143
+
144
+ def _shorten(self):
145
+ url = self.url_var.get().strip()
146
+ if not url:
147
+ self._set_status("Veuillez entrer une URL.", error=True)
148
+ return
149
+
150
+ # Ajoute https:// si manquant
151
+ if not url.startswith("http"):
152
+ url = "https://" + url
153
+ self.url_var.set(url)
154
+
155
+ self._btn_shorten.config(text="⏳ Raccourcissement…", state="disabled")
156
+ self._set_status("")
157
+ self.result_var.set("")
158
+ self.stats_var.set("")
159
+
160
+ # Appel API dans un thread séparé pour ne pas bloquer l'UI
161
+ threading.Thread(target=self._do_shorten, args=(url,), daemon=True).start()
162
+
163
+ def _do_shorten(self, url):
164
+ try:
165
+ import httpx
166
+ data = api_shorten(url)
167
+ self.root.after(0, self._on_shorten_success, data)
168
+ except httpx.RequestError:
169
+ api = get_api_url()
170
+ self.root.after(0, self._set_status,
171
+ f"Impossible de joindre l'API ({api}).\nVérifiez que le serveur est lancé.", True)
172
+ except httpx.HTTPStatusError as e:
173
+ self.root.after(0, self._set_status,
174
+ f"Erreur {e.response.status_code} : URL invalide.", True)
175
+ except Exception as e:
176
+ self.root.after(0, self._set_status, str(e), True)
177
+ finally:
178
+ self.root.after(0, self._btn_shorten.config,
179
+ {"text": "🔗 Raccourcir l'URL", "state": "normal"})
180
+
181
+ def _on_shorten_success(self, data):
182
+ self.result_var.set(data["short"])
183
+ self._set_status("✅ URL raccourcie avec succès !", error=False)
184
+ self.status_label.config(fg=GREEN)
185
+
186
+ # Charger les stats
187
+ threading.Thread(target=self._load_stats, args=(data["code"],), daemon=True).start()
188
+
189
+ def _load_stats(self, code):
190
+ try:
191
+ data = api_stats(code)
192
+ clicks = data["clicks"]
193
+ date = data["created_at"][:10]
194
+ self.root.after(0, self.stats_var.set,
195
+ f"👆 {clicks} clic{'s' if clicks != 1 else ''} • 📅 Créé le {date}")
196
+ except Exception:
197
+ pass
198
+
199
+ def _copy(self):
200
+ url = self.result_var.get()
201
+ if not url:
202
+ self._set_status("Aucune URL à copier.", error=True)
203
+ return
204
+ self.root.clipboard_clear()
205
+ self.root.clipboard_append(url)
206
+ self._btn_copy.config(text="✅ Copié !", fg=GREEN)
207
+ self.root.after(2000, lambda: self._btn_copy.config(
208
+ text="📋 Copier dans le presse-papier", fg=MUTED))
209
+
210
+ def _set_status(self, msg, error=False):
211
+ self.status_var.set(msg)
212
+ self.status_label.config(fg=RED_FG if error else GREEN)
213
+
214
+ def _open_settings(self):
215
+ win = tk.Toplevel(self.root)
216
+ win.title("Paramètres")
217
+ win.configure(bg=BG)
218
+ win.resizable(False, False)
219
+ win.geometry("360x180")
220
+ win.grab_set()
221
+
222
+ tk.Label(win, text="URL de l'API backend :", bg=BG, fg=MUTED,
223
+ font=("Segoe UI", 9, "bold")).pack(anchor="w", padx=20, pady=(20, 6))
224
+
225
+ api_var = tk.StringVar(value=get_api_url())
226
+ entry = tk.Entry(win, textvariable=api_var, bg=CARD, fg=TEXT,
227
+ insertbackground=TEXT, font=("Segoe UI", 11),
228
+ relief="flat", bd=0)
229
+ frame = tk.Frame(win, bg=BORDER, padx=1, pady=1)
230
+ frame.pack(fill="x", padx=20, pady=(0, 16))
231
+ entry.pack(fill="x", padx=10, pady=8, in_=frame)
232
+
233
+ def save():
234
+ url = api_var.get().strip().rstrip("/")
235
+ if url:
236
+ save_api_url(url)
237
+ self.api_label_var.set(f"API : {url}")
238
+ win.destroy()
239
+
240
+ self._make_button(win, "💾 Enregistrer", save,
241
+ bg=BLUE, fg="white", hover_bg=BLUE_H,
242
+ font_size=11, bold=True).pack(fill="x", padx=20)
243
+
244
+ # ── Helper bouton ─────────────────────────────────────────────────────────
245
+ def _make_button(self, parent, text, command,
246
+ bg=CARD, fg=TEXT, hover_bg=BORDER,
247
+ font_size=10, bold=False):
248
+ btn = tk.Button(
249
+ parent, text=text, command=command,
250
+ bg=bg, fg=fg, activebackground=hover_bg, activeforeground=fg,
251
+ font=("Segoe UI", font_size, "bold" if bold else "normal"),
252
+ relief="flat", bd=0, cursor="hand2", pady=10,
253
+ )
254
+ btn.bind("<Enter>", lambda e: btn.config(bg=hover_bg))
255
+ btn.bind("<Leave>", lambda e: btn.config(bg=bg))
256
+ return btn
257
+
258
+ def run(self):
259
+ self.root.mainloop()
260
+
261
+
262
+ def launch_gui():
263
+ app = PyShortGUI()
264
+ app.run()
265
+
266
+
267
+ if __name__ == "__main__":
268
+ launch_gui()
@@ -0,0 +1,9 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ pyshort/__init__.py
5
+ pyshort/__main__.py
6
+ pyshort/api.py
7
+ pyshort/cli.py
8
+ pyshort/config.py
9
+ pyshort/gui.py
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+