shortiepy 0.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ポテト
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: shortiepy
3
+ Version: 0.0.0
4
+ Summary: A local-only URL shortener (˶˘ ³˘)♡
5
+ Author: ポテト ^. .^₎ฅ
6
+ Author-email: "ポテト ^. .^₎ฅ" <nya@cheapnightbot.me>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/CheapNightbot/shortiepy
9
+ Project-URL: Repository, https://github.com/CheapNightbot/shortiepy.git
10
+ Project-URL: Issues, https://github.com/CheapNightbot/shortiepy/issues
11
+ Keywords: cli,shortie,shortiepy,URL shortener
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: click>=8.3.1
24
+ Requires-Dist: colorama>=0.4.6
25
+ Requires-Dist: Flask>=3.1.2
26
+ Requires-Dist: pyperclip>=1.11.0
27
+ Requires-Dist: tabulate>=0.9.0
28
+ Requires-Dist: waitress>=3.0.2
29
+ Dynamic: license-file
30
+
31
+ # shortiepy 🌸
32
+
33
+ Your local URL shortener (˶˘ ³˘)♡
34
+
35
+ - 🔒 100% offline - no data leaves your machine
36
+ - 🌈 Cross-platform (Linux/macOS/Windows)
37
+ - 📋 Auto-copies short URLs to clipboard
38
+ - 🎀 Pastel colors & kaomojis everywhere!
39
+
40
+ ## Installation
41
+
42
+ - **Using `pipx`** [Recommended]
43
+
44
+ Follow instructions to install `pipx` here: [pipx.pypa.io/stable/installation](https://pipx.pypa.io/stable/installation/) and after installing `pipx` in your system, install `shortiepy`:
45
+
46
+ ```bash
47
+ pipx install shortiepy
48
+ ```
49
+
50
+ - **Using `pip`**
51
+
52
+ ```bash
53
+ pip install shortiepy
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ - **Add a URL**
59
+
60
+ ```bash
61
+ shortiepy add https://example.com
62
+ ```
63
+
64
+ - **Start server**
65
+
66
+ ```bash
67
+ shortiepy serve # will run in forground
68
+ # OR
69
+ shortiepy start # will run in background
70
+ ```
71
+
72
+ - **View docs**
73
+
74
+ ```bash
75
+ shortiepy docs
76
+ ```
77
+
78
+ ### Create Links from browser
79
+
80
+ shortiepy supports creating short URLs from within your browser (without having to use CLI `add` command).
81
+
82
+ You can visit `/new` route and provide `code` (this will be used to generate short url) and `url` (the URL you want to create short link for) query paramters:
83
+
84
+ - `http://localhost:9876/new?code=meow&url=https://example.com`
85
+
86
+ - It will create short link for `https://example.com` using provided short code: `http://localhost:9876/meow`
87
+
88
+ - The `code` parameter is optional and can be omitted.
89
+
90
+ However, vising that URL and route manully is tedious! But if your browser allows it, like I have [Brave](https://brave.com/) browser, you can add a shortcut for it:
91
+
92
+ - In your browser go to settings and find "search engines". In Brave, it's `brave://settings/searchEngines` and add the following in "Site search":
93
+
94
+ - Name: `shortiepy` (or anything you like)
95
+ - Shortcut: `:sh` (or anything you like)
96
+ - URL: `http://localhost:9876/new?url=%s`
97
+
98
+ <details>
99
+ <summary>Click to See Screen Recording of above steps!</summary>
100
+
101
+ ![Add Shortcut in Browser](https://github.com/user-attachments/assets/2f03b437-0452-4f22-9225-e12e10a2b15c)
102
+
103
+ </details>
104
+
105
+ Now when you want to create a short link, just type `:sh` in url bar and press `spacebar`, then you can paste the URL you want to create short link for and press enter!
106
+
107
+ <details>
108
+ <summary>Click to See Screen Recording of above steps!</summary>
109
+
110
+ ![Use Shortcut to create short link](https://github.com/user-attachments/assets/bf04838a-09f8-4d30-888c-f6c043328ae4)
111
+
112
+ </details>
113
+
114
+ ## Shell Completion
115
+
116
+ Get tab-completion with **one command**:
117
+
118
+ ```bash
119
+ shortiepy completion
120
+ ```
121
+
122
+ > Restart your shell or reload config (`source ~/.bashrc` for bash OR `source ~/.zshrc` for zsh).
123
+ > Fish users: no restart needed!
124
+
125
+ That's it! Works for bash, zsh, and fish.
126
+
127
+ ## Why
128
+
129
+ For some reason, when I’m working on things or trying to learn something new, my browser ends up filled with tons of tabs—which makes my laptop-chan angry ~ ₍^. ̫.^₎
130
+
131
+ I don’t want to close them or bookmark them. I tried manually copying URLs into a `.txt` file, but then I wished there was a simple way to turn long links into short ones I could use later.
132
+
133
+ I didn’t want to send anything online, and existing self-hosted URL shorteners felt like overkill for such a small need.
134
+
135
+ So I made this: a minimal, local-only URL shortener. It started as a single script file and isn’t perfect—but it just works! ~ ദ്ദി/ᐠ。‸。ᐟ\
136
+
137
+
138
+ ## For Developers
139
+
140
+ Want to tinker with `shortiepy` or contribute? Here's how to set it up locally:
141
+
142
+ - Clone the repository locally and change the directory into it:
143
+
144
+ ```bash
145
+ git clone https://github.com/CheapNightbot/shortiepy.git && cd shortiepy
146
+ ```
147
+
148
+ - Install `shortiepy`:
149
+
150
+ ```bash
151
+ # Create a virtual environment (keeps things clean!)
152
+ python -m venv .venv
153
+
154
+ # Activate it
155
+ source .venv/bin/activate # Linux/macOS
156
+ # OR
157
+ .venv\Scripts\activate # Windows
158
+
159
+ # Install in editable mode (changes reflect instantly!)
160
+ pip install -e .
161
+ ```
162
+
163
+ Now you can run `shortiepy` from anywhere in your terminal!
164
+ Made a change? It’ll work immediately—no reinstall needed!
165
+
166
+ ### Updating Shell Completions
167
+
168
+ If you modify CLI commands or options, regenerate completions:
169
+
170
+ ```bash
171
+ ./scripts/generate-completions.sh
172
+ ```
173
+
174
+ > This updates the files in `shortiepy/completions/` directory.
@@ -0,0 +1,144 @@
1
+ # shortiepy 🌸
2
+
3
+ Your local URL shortener (˶˘ ³˘)♡
4
+
5
+ - 🔒 100% offline - no data leaves your machine
6
+ - 🌈 Cross-platform (Linux/macOS/Windows)
7
+ - 📋 Auto-copies short URLs to clipboard
8
+ - 🎀 Pastel colors & kaomojis everywhere!
9
+
10
+ ## Installation
11
+
12
+ - **Using `pipx`** [Recommended]
13
+
14
+ Follow instructions to install `pipx` here: [pipx.pypa.io/stable/installation](https://pipx.pypa.io/stable/installation/) and after installing `pipx` in your system, install `shortiepy`:
15
+
16
+ ```bash
17
+ pipx install shortiepy
18
+ ```
19
+
20
+ - **Using `pip`**
21
+
22
+ ```bash
23
+ pip install shortiepy
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ - **Add a URL**
29
+
30
+ ```bash
31
+ shortiepy add https://example.com
32
+ ```
33
+
34
+ - **Start server**
35
+
36
+ ```bash
37
+ shortiepy serve # will run in forground
38
+ # OR
39
+ shortiepy start # will run in background
40
+ ```
41
+
42
+ - **View docs**
43
+
44
+ ```bash
45
+ shortiepy docs
46
+ ```
47
+
48
+ ### Create Links from browser
49
+
50
+ shortiepy supports creating short URLs from within your browser (without having to use CLI `add` command).
51
+
52
+ You can visit `/new` route and provide `code` (this will be used to generate short url) and `url` (the URL you want to create short link for) query paramters:
53
+
54
+ - `http://localhost:9876/new?code=meow&url=https://example.com`
55
+
56
+ - It will create short link for `https://example.com` using provided short code: `http://localhost:9876/meow`
57
+
58
+ - The `code` parameter is optional and can be omitted.
59
+
60
+ However, vising that URL and route manully is tedious! But if your browser allows it, like I have [Brave](https://brave.com/) browser, you can add a shortcut for it:
61
+
62
+ - In your browser go to settings and find "search engines". In Brave, it's `brave://settings/searchEngines` and add the following in "Site search":
63
+
64
+ - Name: `shortiepy` (or anything you like)
65
+ - Shortcut: `:sh` (or anything you like)
66
+ - URL: `http://localhost:9876/new?url=%s`
67
+
68
+ <details>
69
+ <summary>Click to See Screen Recording of above steps!</summary>
70
+
71
+ ![Add Shortcut in Browser](https://github.com/user-attachments/assets/2f03b437-0452-4f22-9225-e12e10a2b15c)
72
+
73
+ </details>
74
+
75
+ Now when you want to create a short link, just type `:sh` in url bar and press `spacebar`, then you can paste the URL you want to create short link for and press enter!
76
+
77
+ <details>
78
+ <summary>Click to See Screen Recording of above steps!</summary>
79
+
80
+ ![Use Shortcut to create short link](https://github.com/user-attachments/assets/bf04838a-09f8-4d30-888c-f6c043328ae4)
81
+
82
+ </details>
83
+
84
+ ## Shell Completion
85
+
86
+ Get tab-completion with **one command**:
87
+
88
+ ```bash
89
+ shortiepy completion
90
+ ```
91
+
92
+ > Restart your shell or reload config (`source ~/.bashrc` for bash OR `source ~/.zshrc` for zsh).
93
+ > Fish users: no restart needed!
94
+
95
+ That's it! Works for bash, zsh, and fish.
96
+
97
+ ## Why
98
+
99
+ For some reason, when I’m working on things or trying to learn something new, my browser ends up filled with tons of tabs—which makes my laptop-chan angry ~ ₍^. ̫.^₎
100
+
101
+ I don’t want to close them or bookmark them. I tried manually copying URLs into a `.txt` file, but then I wished there was a simple way to turn long links into short ones I could use later.
102
+
103
+ I didn’t want to send anything online, and existing self-hosted URL shorteners felt like overkill for such a small need.
104
+
105
+ So I made this: a minimal, local-only URL shortener. It started as a single script file and isn’t perfect—but it just works! ~ ദ്ദി/ᐠ。‸。ᐟ\
106
+
107
+
108
+ ## For Developers
109
+
110
+ Want to tinker with `shortiepy` or contribute? Here's how to set it up locally:
111
+
112
+ - Clone the repository locally and change the directory into it:
113
+
114
+ ```bash
115
+ git clone https://github.com/CheapNightbot/shortiepy.git && cd shortiepy
116
+ ```
117
+
118
+ - Install `shortiepy`:
119
+
120
+ ```bash
121
+ # Create a virtual environment (keeps things clean!)
122
+ python -m venv .venv
123
+
124
+ # Activate it
125
+ source .venv/bin/activate # Linux/macOS
126
+ # OR
127
+ .venv\Scripts\activate # Windows
128
+
129
+ # Install in editable mode (changes reflect instantly!)
130
+ pip install -e .
131
+ ```
132
+
133
+ Now you can run `shortiepy` from anywhere in your terminal!
134
+ Made a change? It’ll work immediately—no reinstall needed!
135
+
136
+ ### Updating Shell Completions
137
+
138
+ If you modify CLI commands or options, regenerate completions:
139
+
140
+ ```bash
141
+ ./scripts/generate-completions.sh
142
+ ```
143
+
144
+ > This updates the files in `shortiepy/completions/` directory.
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "shortiepy"
7
+ description = "A local-only URL shortener (˶˘ ³˘)♡"
8
+ keywords = ["cli", "shortie", "shortiepy", "URL shortener"]
9
+ dynamic = ["version"]
10
+ readme = "README.md"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "ポテト ^. .^₎ฅ"},
14
+ {name = "ポテト ^. .^₎ฅ", email = "nya@cheapnightbot.me"},
15
+ ]
16
+ requires-python = ">=3.10"
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Console",
20
+ "Intended Audience :: End Users/Desktop",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Topic :: Utilities",
26
+ ]
27
+ dependencies = [
28
+ "click>=8.3.1",
29
+ "colorama>=0.4.6",
30
+ "Flask>=3.1.2",
31
+ "pyperclip>=1.11.0",
32
+ "tabulate>=0.9.0",
33
+ "waitress>=3.0.2",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/CheapNightbot/shortiepy"
38
+ Repository = "https://github.com/CheapNightbot/shortiepy.git"
39
+ Issues = "https://github.com/CheapNightbot/shortiepy/issues"
40
+
41
+ [project.scripts]
42
+ shortiepy = "shortiepy.__main__:cli"
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["."]
46
+ include = ["shortiepy*"]
47
+
48
+ [tool.setuptools.package-data]
49
+ shortiepy = ["completions/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("shortiepy")
7
+ except PackageNotFoundError:
8
+ __version__ = "unknown"
9
+
10
+ import json
11
+ import os
12
+ import platform
13
+ import secrets
14
+ import shutil
15
+ import sqlite3
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+ import webbrowser
20
+ from pathlib import Path
21
+
22
+ import click
23
+ import pyperclip
24
+ from colorama import init as colorama_init
25
+ from tabulate import tabulate
26
+ from waitress import serve
27
+
28
+ # --- Kaomoji & Color Helpers ---
29
+ colorama_init() # Required for Windows
30
+
31
+
32
+ def cute_echo(text, fg="bright_magenta"):
33
+ """Echo with pastel colors and sparkles"""
34
+ click.echo(click.style(text, fg=fg))
35
+
36
+
37
+ def success(text):
38
+ return click.style(f"🌸 {text}", fg="bright_magenta")
39
+
40
+
41
+ def error(text):
42
+ return click.style(f"❌ {text}", fg="bright_red")
43
+
44
+
45
+ def info(text):
46
+ return click.style(f"ℹ️ {text}", fg="bright_blue")
47
+
48
+
49
+ def warn(text):
50
+ return click.style(f"⚠️ {text}", fg="bright_yellow")
51
+
52
+
53
+ class Config:
54
+ def __init__(self, config_path: Path, default_port):
55
+ self.config_path = config_path
56
+ self.default_port = default_port
57
+ self._port = None # Lazy-loaded
58
+
59
+ @property
60
+ def port(self):
61
+ if self._port is None:
62
+ self._port = self._load()
63
+ return self._port
64
+
65
+ def _load(self):
66
+ if self.config_path.exists():
67
+ try:
68
+ with open(self.config_path) as f:
69
+ return json.load(f).get("port", self.default_port)
70
+ except (json.JSONDecodeError, KeyError):
71
+ pass
72
+ return self.default_port
73
+
74
+ def save(self, port):
75
+ """Save new port and update cache"""
76
+ with open(self.config_path, "w") as f:
77
+ json.dump({"port": port}, f)
78
+ self._port = port # Update cache
79
+
80
+
81
+ # Determine OS-specific data directory
82
+ def get_data_dir():
83
+ home = Path.home()
84
+ system = platform.system()
85
+ if system == "Windows":
86
+ return home / "AppData" / "Roaming" / "shortiepy"
87
+ elif system == "Darwin": # macOS
88
+ return home / "Library" / "Application Support" / "shortiepy"
89
+ else: # Linux and others
90
+ return home / ".local" / "share" / "shortiepy"
91
+
92
+
93
+ # Paths
94
+ DATA_DIR = get_data_dir()
95
+ DATA_DIR.mkdir(parents=True, exist_ok=True) # Create if missing
96
+ DB_PATH = DATA_DIR / "shortiepy.db"
97
+ LOCK_FILE = Path(tempfile.gettempdir()) / "shortiepy.lock"
98
+ LOG_FILE = Path(tempfile.gettempdir()) / "shortiepy.log"
99
+
100
+ # Config
101
+ CONFIG_PATH = DATA_DIR / "config.json"
102
+ DEFAULT_PORT = 9876
103
+ config = Config(CONFIG_PATH, DEFAULT_PORT)
104
+
105
+
106
+ # --- Helper Functions ---
107
+ def generate_code(length=5):
108
+ return secrets.token_urlsafe(length)[:length]
109
+
110
+
111
+ def init_db():
112
+ conn = sqlite3.connect(DB_PATH)
113
+ conn.execute(
114
+ """
115
+ CREATE TABLE IF NOT EXISTS urls (
116
+ code TEXT PRIMARY KEY,
117
+ url TEXT NOT NULL,
118
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
119
+ )
120
+ """
121
+ )
122
+ conn.close()
123
+
124
+
125
+ def db_execute(query, params=(), fetch=False, fetchone=False):
126
+ """Execute DB query safely with automatic connection handling"""
127
+ try:
128
+ with sqlite3.connect(DB_PATH) as conn:
129
+ cur = conn.cursor()
130
+ cur.execute(query, params)
131
+ if fetch:
132
+ return cur.fetchall()
133
+ if fetchone:
134
+ return cur.fetchone()
135
+ return cur.rowcount
136
+ except sqlite3.OperationalError as e:
137
+ if "database is locked" in str(e):
138
+ raise RuntimeError("Database busy! Try again later. (;′⌒`)")
139
+ except sqlite3.IntegrityError as e:
140
+ raise RuntimeError(f"Database error: {str(e)}")
141
+
142
+
143
+ # --- Flask App (for server) ---
144
+ def create_flask_app():
145
+ from .app import create_app
146
+
147
+ return create_app(config.port)
148
+
149
+
150
+ # --- CLI Commands ---
151
+ @click.group()
152
+ @click.version_option(version=__version__, prog_name="shortiepy")
153
+ def cli():
154
+ """shortiepy: your local URL shortner ( ˶˘ ³˘)♡"""
155
+ pass
156
+
157
+
158
+ @cli.command()
159
+ @click.argument("action", required=False, default="install")
160
+ def completion(action):
161
+ """Manage shell completions"""
162
+ if action != "install":
163
+ cute_echo(warn("Only 'install' is supported"))
164
+ return
165
+
166
+ # Detect shell
167
+ shell = os.environ.get("SHELL", "").split("/")[-1]
168
+ home = Path.home()
169
+
170
+ if shell == "bash":
171
+ dest_dir = home / ".local" / "share" / "bash-completion" / "completions"
172
+ dest_file = dest_dir / "shortiepy"
173
+ src_file = Path(__file__).parent / "completions" / "shortiepy.bash"
174
+
175
+ elif shell == "zsh":
176
+ dest_dir = home / ".zsh" / "completions"
177
+ dest_file = dest_dir / "_shortiepy"
178
+ src_file = Path(__file__).parent / "completions" / "shortiepy.zsh"
179
+
180
+ elif shell == "fish":
181
+ dest_dir = home / ".config" / "fish" / "completions"
182
+ dest_file = dest_dir / "shortiepy.fish"
183
+ src_file = Path(__file__).parent / "completions" / "shortiepy.fish"
184
+
185
+ else:
186
+ cute_echo(error(f"Unsupported shell: {shell}"))
187
+ cute_echo(info("Supported: bash, zsh, fish"))
188
+ return
189
+
190
+ # Create directory
191
+ dest_dir.mkdir(parents=True, exist_ok=True)
192
+
193
+ # Copy file
194
+ try:
195
+ shutil.copy(src_file, dest_file)
196
+ cute_echo(success(f"Installed completion for {shell}!"))
197
+ if shell == "bash":
198
+ cute_echo(
199
+ info(
200
+ """Restart your shell or run:
201
+ source ~/.bashrc"""
202
+ )
203
+ )
204
+ elif shell == "zsh":
205
+ cute_echo(
206
+ info(
207
+ """Restart your shell or run:
208
+ source ~/.zshrc"""
209
+ )
210
+ )
211
+ # Fish loads automatically /ᐠ •⩊•マ
212
+
213
+ except Exception as e:
214
+ cute_echo(error(f"Failed to install: {e}"))
215
+
216
+
217
+ @cli.command()
218
+ @click.argument("url")
219
+ def add(url):
220
+ """Add a new URL and copy short link to clipboard"""
221
+ init_db()
222
+ code = generate_code()
223
+
224
+ try:
225
+ db_execute("INSERT INTO urls (code, url) VALUES (?, ?)", (code, url))
226
+ short_url = f"http://localhost:{config.port}/{code}"
227
+ pyperclip.copy(short_url)
228
+ cute_echo(success(f"Copied to clipboard: {short_url}"))
229
+ except sqlite3.IntegrityError:
230
+ # Very rare, but handle duplicate codes
231
+ cute_echo(warn("Oops! Code collision (unlikely!) ~ (ᵕ—ᴗ—)"))
232
+ return add(url) # retry
233
+ except RuntimeError as e:
234
+ cute_echo(error(str(e)))
235
+
236
+
237
+ @cli.command()
238
+ @click.argument("code")
239
+ def delete(code):
240
+ """Delete a short URL by code"""
241
+ try:
242
+ deleted = db_execute("DELETE FROM urls WHERE code = ?", (code,))
243
+ except RuntimeError as e:
244
+ cute_echo(error(str(e)))
245
+
246
+ if deleted:
247
+ cute_echo(success(f"Deleted: http://localhost:{config.port}/{code}"))
248
+ else:
249
+ cute_echo(error(f"Code '{code}' not found! (;′⌒`)"))
250
+
251
+
252
+ @cli.command()
253
+ def docs():
254
+ """Open documentation in browser"""
255
+ # Check if server is running
256
+ if not LOCK_FILE.exists():
257
+ cute_echo(error("Server not running! (;′⌒`)"))
258
+ cute_echo(
259
+ info(
260
+ """Please start the server first:
261
+ shortiepy serve → Foreground server
262
+ shortiepy start → Background server"""
263
+ )
264
+ )
265
+ return
266
+
267
+ url = f"http://localhost:{config.port}"
268
+ try:
269
+ cute_echo(info(f"Opening docs: {url}"))
270
+ webbrowser.open(url)
271
+ except Exception as e:
272
+ cute_echo(error(f"Failed to open browser: {str(e)}"))
273
+ cute_echo(info(f"Visit manually: {url}"))
274
+
275
+
276
+ @cli.command()
277
+ def list():
278
+ """List all shortened URLs"""
279
+ init_db()
280
+
281
+ try:
282
+ rows = db_execute(
283
+ "SELECT code, url, created_at FROM urls ORDER BY created_at DESC",
284
+ fetch=True,
285
+ )
286
+ except RuntimeError as e:
287
+ cute_echo(error(str(e)))
288
+
289
+ if not rows:
290
+ cute_echo(warn("(;´༎ຶД༎ຶ`) No links yet! Add one with `shortiepy add <URL>`"))
291
+ return
292
+
293
+ # Prepare data
294
+ table_data = []
295
+ for code, url, created in rows:
296
+ short_url = f"http://localhost:{config.port}/{code}"
297
+ # Truncate long URLs for readability
298
+ display_url = (url[:40] + "...") if len(url) > 40 else url
299
+ table_data.append((code, short_url, display_url, created))
300
+
301
+ headers = ["Code", "Short URL", "Original URL", "Created At"]
302
+ output = tabulate(table_data, headers=headers, tablefmt="rounded_grid")
303
+ cute_echo(info("Your shortiepy links:"))
304
+ click.echo(output)
305
+
306
+
307
+ @cli.command(name="serve")
308
+ @click.option("--port", default=DEFAULT_PORT, help="Port to run shortiepy on")
309
+ def run(port):
310
+ """Start the local redirect server"""
311
+ config.save(port)
312
+ cute_echo(info(f"Running shortiepy server on http://localhost:{config.port}"))
313
+ cute_echo(warn("Press CTRL + C to stop the server. (๑•̀ㅂ•́)و✧"))
314
+ app = create_flask_app()
315
+ serve(app=app, host="localhost", port=config.port)
316
+
317
+
318
+ @cli.command(name="config")
319
+ def show_config():
320
+ """Show shortiepy configurations"""
321
+ config_data = [
322
+ ("Version", __version__),
323
+ ("Port", str(config.port)),
324
+ ("Host", "localhost"),
325
+ ("Data Directory", str(DATA_DIR)),
326
+ ("Database", str(DB_PATH)),
327
+ ("Config File", str(CONFIG_PATH)),
328
+ ("Log File", str(LOG_FILE)),
329
+ ("Lock File", str(LOCK_FILE)),
330
+ ]
331
+
332
+ click.echo(tabulate(config_data, tablefmt="rounded_grid"))
333
+
334
+
335
+ @cli.command()
336
+ @click.option("--port", default=DEFAULT_PORT, help="Port to run shortiepy on")
337
+ def start(port):
338
+ """Start shortiepy server in the background"""
339
+ config.save(port)
340
+
341
+ if LOCK_FILE.exists():
342
+ with open(LOCK_FILE) as f:
343
+ pid = int(f.read().strip())
344
+ try:
345
+ os.kill(pid, 0)
346
+ cute_echo(info(f"Server already running (PID: {pid})"))
347
+ cute_echo(info(f"URL: http://localhost:{config.port}"))
348
+ return
349
+ except OSError:
350
+ LOCK_FILE.unlink()
351
+
352
+ package_dir = Path(__file__).parent.resolve()
353
+ if not (package_dir / "__main__.py").exists():
354
+ raise RuntimeError("Cannot find shortiepy package")
355
+
356
+ # Create a minimal script to start the server
357
+ server_script = f"""
358
+ import sys
359
+ sys.path.insert(0, {repr(str(package_dir))})
360
+ from shortiepy.__main__ import create_flask_app, DB_PATH
361
+ from waitress import serve
362
+
363
+ app = create_flask_app()
364
+ serve(app=app, host="localhost", port={port})
365
+ """
366
+
367
+ # Start in background
368
+ proc = subprocess.Popen(
369
+ [sys.executable, "-c", server_script],
370
+ stdout=open(LOG_FILE, "w"),
371
+ stderr=subprocess.STDOUT,
372
+ start_new_session=True,
373
+ )
374
+
375
+ with open(LOCK_FILE, "w") as f:
376
+ f.write(str(proc.pid))
377
+
378
+ cute_echo(success(f"Started server (PID: {proc.pid})"))
379
+ cute_echo(info(f"Logs: {LOG_FILE}"))
380
+ cute_echo(info(f"URL: http://localhost:{config.port}"))
381
+
382
+
383
+ @cli.command()
384
+ def stop():
385
+ """Stop the background server"""
386
+ if not os.path.exists(LOCK_FILE):
387
+ cute_echo(warn("No background server running („• ֊ •„)"))
388
+ return
389
+
390
+ with open(LOCK_FILE) as f:
391
+ pid = int(f.read().strip())
392
+
393
+ try:
394
+ os.kill(pid, 15) # SIGTERM
395
+ os.remove(LOCK_FILE)
396
+ cute_echo(success(f"Stopped server (PID: {pid}) ദ്ദി◝ ⩊ ◜.ᐟ"))
397
+ except ProcessLookupError:
398
+ cute_echo(error("Server not found. Cleaning up lock file."))
399
+ os.remove(LOCK_FILE)
400
+
401
+
402
+ @cli.command()
403
+ def status():
404
+ """Show server status and stats"""
405
+ if LOCK_FILE.exists():
406
+ with open(LOCK_FILE) as f:
407
+ try:
408
+ pid = int(f.read().strip())
409
+ os.kill(pid, 0) # Check if running
410
+ cute_echo(success(f"Server: Running (PID: {pid}) (˶˃ ᵕ ˂˶) .ᐟ.ᐟ"))
411
+ except (OSError, ValueError):
412
+ cute_echo(warn("Server: Stopped (stale lock)"))
413
+ LOCK_FILE.unlink()
414
+ else:
415
+ cute_echo(warn("Server: Stopped (•˕ •マ.ᐟ"))
416
+
417
+ # Show DB stats
418
+ init_db()
419
+ conn = sqlite3.connect(DB_PATH)
420
+ count = conn.execute("SELECT COUNT(*) FROM urls").fetchone()[0]
421
+ conn.close()
422
+ cute_echo(info(f"Total URLs: {count}"))
@@ -0,0 +1,109 @@
1
+ import sqlite3
2
+ from pathlib import Path
3
+
4
+ from flask import Flask, abort, redirect, render_template, request
5
+
6
+ from .__main__ import __version__, db_execute, generate_code
7
+
8
+
9
+ def create_app(config_port):
10
+ app = Flask(
11
+ __name__,
12
+ template_folder=Path(__file__).parent / "templates",
13
+ static_folder=Path(__file__).parent / "static",
14
+ )
15
+
16
+ app.config["PORT"] = config_port
17
+
18
+ @app.context_processor
19
+ def inject_version():
20
+ return {"version": __version__}
21
+
22
+ @app.route("/")
23
+ def index():
24
+ try:
25
+ count = db_execute("SELECT COUNT(*) FROM urls", fetchone=True)[0]
26
+ except RuntimeError:
27
+ count = -1
28
+ return render_template("index.html", total_urls=count, port=app.config["PORT"])
29
+
30
+ @app.route("/<code>")
31
+ def redirect_url(code):
32
+ try:
33
+ row = db_execute(
34
+ "SELECT url FROM urls WHERE code = ?", (code,), fetchone=True
35
+ )
36
+ except RuntimeError:
37
+ abort(404)
38
+ return redirect(row[0])
39
+
40
+ @app.route("/new")
41
+ def create_short_url():
42
+ code = request.args.get("code") or generate_code()
43
+ url = request.args.get("url")
44
+ templete_file = "message.html"
45
+
46
+ if not url:
47
+ return (
48
+ render_template(
49
+ templete_file,
50
+ title="❌ Missing Parameters",
51
+ message="Use: <code>/new?code=your_code&url=https://example.com</code>",
52
+ link="/",
53
+ ),
54
+ 400,
55
+ )
56
+
57
+ try:
58
+ db_execute("INSERT INTO urls (code, url) VALUES (?, ?)", (code, url))
59
+ short_url = f"http://localhost:{app.config['PORT']}/{code}"
60
+ return render_template(
61
+ templete_file,
62
+ title="✨ Success!",
63
+ message=f"Created short URL: <a href='{short_url}' target='_blank' rel='nofollow noopener'>{short_url}</a>",
64
+ link="/",
65
+ )
66
+ except RuntimeError:
67
+ return (
68
+ render_template(
69
+ templete_file,
70
+ title="⚠️ Code Exists",
71
+ message=f"Code '{code}' is already taken!",
72
+ link="/",
73
+ ),
74
+ 409,
75
+ )
76
+
77
+ @app.route("/list")
78
+ def list_urls():
79
+ try:
80
+ rows = db_execute(
81
+ "SELECT code, url, created_at FROM urls ORDER BY created_at DESC",
82
+ fetch=True,
83
+ )
84
+ except RuntimeError:
85
+ rows = []
86
+
87
+ urls = []
88
+ for code, url, created in rows:
89
+ short_url = f"http://localhost:{app.config['PORT']}/{code}"
90
+ display_url = (url[:50] + "...") if len(url) > 50 else url
91
+ urls.append(
92
+ {
93
+ "code": code,
94
+ "short_url": short_url,
95
+ "display_url": display_url,
96
+ "created": created,
97
+ }
98
+ )
99
+ return render_template("list.html", urls=urls, port=app.config["PORT"])
100
+
101
+ @app.route("/delete/<code>", methods=["POST"])
102
+ def delete_url(code):
103
+ try:
104
+ db_execute("DELETE FROM urls WHERE code = ?", (code,))
105
+ except RuntimeError:
106
+ return {"success": False, "message": "Failed to delete URL."}, 500
107
+ return {"success": True}
108
+
109
+ return app
@@ -0,0 +1,29 @@
1
+ _shortiepy_completion() {
2
+ local IFS=$'\n'
3
+ local response
4
+
5
+ response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _SHORTIEPY_COMPLETE=bash_complete $1)
6
+
7
+ for completion in $response; do
8
+ IFS=',' read type value <<< "$completion"
9
+
10
+ if [[ $type == 'dir' ]]; then
11
+ COMPREPLY=()
12
+ compopt -o dirnames
13
+ elif [[ $type == 'file' ]]; then
14
+ COMPREPLY=()
15
+ compopt -o default
16
+ elif [[ $type == 'plain' ]]; then
17
+ COMPREPLY+=($value)
18
+ fi
19
+ done
20
+
21
+ return 0
22
+ }
23
+
24
+ _shortiepy_completion_setup() {
25
+ complete -o nosort -F _shortiepy_completion shortiepy
26
+ }
27
+
28
+ _shortiepy_completion_setup;
29
+
@@ -0,0 +1,18 @@
1
+ function _shortiepy_completion;
2
+ set -l response (env _SHORTIEPY_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) shortiepy);
3
+
4
+ for completion in $response;
5
+ set -l metadata (string split "," $completion);
6
+
7
+ if test $metadata[1] = "dir";
8
+ __fish_complete_directories $metadata[2];
9
+ else if test $metadata[1] = "file";
10
+ __fish_complete_path $metadata[2];
11
+ else if test $metadata[1] = "plain";
12
+ echo $metadata[2];
13
+ end;
14
+ end;
15
+ end;
16
+
17
+ complete --no-files --command shortiepy --arguments "(_shortiepy_completion)";
18
+
@@ -0,0 +1,41 @@
1
+ #compdef shortiepy
2
+
3
+ _shortiepy_completion() {
4
+ local -a completions
5
+ local -a completions_with_descriptions
6
+ local -a response
7
+ (( ! $+commands[shortiepy] )) && return 1
8
+
9
+ response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _SHORTIEPY_COMPLETE=zsh_complete shortiepy)}")
10
+
11
+ for type key descr in ${response}; do
12
+ if [[ "$type" == "plain" ]]; then
13
+ if [[ "$descr" == "_" ]]; then
14
+ completions+=("$key")
15
+ else
16
+ completions_with_descriptions+=("$key":"$descr")
17
+ fi
18
+ elif [[ "$type" == "dir" ]]; then
19
+ _path_files -/
20
+ elif [[ "$type" == "file" ]]; then
21
+ _path_files -f
22
+ fi
23
+ done
24
+
25
+ if [ -n "$completions_with_descriptions" ]; then
26
+ _describe -V unsorted completions_with_descriptions -U
27
+ fi
28
+
29
+ if [ -n "$completions" ]; then
30
+ compadd -U -V unsorted -a completions
31
+ fi
32
+ }
33
+
34
+ if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
35
+ # autoload from fpath, call function directly
36
+ _shortiepy_completion "$@"
37
+ else
38
+ # eval/source/. command, register function for later
39
+ compdef _shortiepy_completion shortiepy
40
+ fi
41
+
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: shortiepy
3
+ Version: 0.0.0
4
+ Summary: A local-only URL shortener (˶˘ ³˘)♡
5
+ Author: ポテト ^. .^₎ฅ
6
+ Author-email: "ポテト ^. .^₎ฅ" <nya@cheapnightbot.me>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/CheapNightbot/shortiepy
9
+ Project-URL: Repository, https://github.com/CheapNightbot/shortiepy.git
10
+ Project-URL: Issues, https://github.com/CheapNightbot/shortiepy/issues
11
+ Keywords: cli,shortie,shortiepy,URL shortener
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: click>=8.3.1
24
+ Requires-Dist: colorama>=0.4.6
25
+ Requires-Dist: Flask>=3.1.2
26
+ Requires-Dist: pyperclip>=1.11.0
27
+ Requires-Dist: tabulate>=0.9.0
28
+ Requires-Dist: waitress>=3.0.2
29
+ Dynamic: license-file
30
+
31
+ # shortiepy 🌸
32
+
33
+ Your local URL shortener (˶˘ ³˘)♡
34
+
35
+ - 🔒 100% offline - no data leaves your machine
36
+ - 🌈 Cross-platform (Linux/macOS/Windows)
37
+ - 📋 Auto-copies short URLs to clipboard
38
+ - 🎀 Pastel colors & kaomojis everywhere!
39
+
40
+ ## Installation
41
+
42
+ - **Using `pipx`** [Recommended]
43
+
44
+ Follow instructions to install `pipx` here: [pipx.pypa.io/stable/installation](https://pipx.pypa.io/stable/installation/) and after installing `pipx` in your system, install `shortiepy`:
45
+
46
+ ```bash
47
+ pipx install shortiepy
48
+ ```
49
+
50
+ - **Using `pip`**
51
+
52
+ ```bash
53
+ pip install shortiepy
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ - **Add a URL**
59
+
60
+ ```bash
61
+ shortiepy add https://example.com
62
+ ```
63
+
64
+ - **Start server**
65
+
66
+ ```bash
67
+ shortiepy serve # will run in forground
68
+ # OR
69
+ shortiepy start # will run in background
70
+ ```
71
+
72
+ - **View docs**
73
+
74
+ ```bash
75
+ shortiepy docs
76
+ ```
77
+
78
+ ### Create Links from browser
79
+
80
+ shortiepy supports creating short URLs from within your browser (without having to use CLI `add` command).
81
+
82
+ You can visit `/new` route and provide `code` (this will be used to generate short url) and `url` (the URL you want to create short link for) query paramters:
83
+
84
+ - `http://localhost:9876/new?code=meow&url=https://example.com`
85
+
86
+ - It will create short link for `https://example.com` using provided short code: `http://localhost:9876/meow`
87
+
88
+ - The `code` parameter is optional and can be omitted.
89
+
90
+ However, vising that URL and route manully is tedious! But if your browser allows it, like I have [Brave](https://brave.com/) browser, you can add a shortcut for it:
91
+
92
+ - In your browser go to settings and find "search engines". In Brave, it's `brave://settings/searchEngines` and add the following in "Site search":
93
+
94
+ - Name: `shortiepy` (or anything you like)
95
+ - Shortcut: `:sh` (or anything you like)
96
+ - URL: `http://localhost:9876/new?url=%s`
97
+
98
+ <details>
99
+ <summary>Click to See Screen Recording of above steps!</summary>
100
+
101
+ ![Add Shortcut in Browser](https://github.com/user-attachments/assets/2f03b437-0452-4f22-9225-e12e10a2b15c)
102
+
103
+ </details>
104
+
105
+ Now when you want to create a short link, just type `:sh` in url bar and press `spacebar`, then you can paste the URL you want to create short link for and press enter!
106
+
107
+ <details>
108
+ <summary>Click to See Screen Recording of above steps!</summary>
109
+
110
+ ![Use Shortcut to create short link](https://github.com/user-attachments/assets/bf04838a-09f8-4d30-888c-f6c043328ae4)
111
+
112
+ </details>
113
+
114
+ ## Shell Completion
115
+
116
+ Get tab-completion with **one command**:
117
+
118
+ ```bash
119
+ shortiepy completion
120
+ ```
121
+
122
+ > Restart your shell or reload config (`source ~/.bashrc` for bash OR `source ~/.zshrc` for zsh).
123
+ > Fish users: no restart needed!
124
+
125
+ That's it! Works for bash, zsh, and fish.
126
+
127
+ ## Why
128
+
129
+ For some reason, when I’m working on things or trying to learn something new, my browser ends up filled with tons of tabs—which makes my laptop-chan angry ~ ₍^. ̫.^₎
130
+
131
+ I don’t want to close them or bookmark them. I tried manually copying URLs into a `.txt` file, but then I wished there was a simple way to turn long links into short ones I could use later.
132
+
133
+ I didn’t want to send anything online, and existing self-hosted URL shorteners felt like overkill for such a small need.
134
+
135
+ So I made this: a minimal, local-only URL shortener. It started as a single script file and isn’t perfect—but it just works! ~ ദ്ദി/ᐠ。‸。ᐟ\
136
+
137
+
138
+ ## For Developers
139
+
140
+ Want to tinker with `shortiepy` or contribute? Here's how to set it up locally:
141
+
142
+ - Clone the repository locally and change the directory into it:
143
+
144
+ ```bash
145
+ git clone https://github.com/CheapNightbot/shortiepy.git && cd shortiepy
146
+ ```
147
+
148
+ - Install `shortiepy`:
149
+
150
+ ```bash
151
+ # Create a virtual environment (keeps things clean!)
152
+ python -m venv .venv
153
+
154
+ # Activate it
155
+ source .venv/bin/activate # Linux/macOS
156
+ # OR
157
+ .venv\Scripts\activate # Windows
158
+
159
+ # Install in editable mode (changes reflect instantly!)
160
+ pip install -e .
161
+ ```
162
+
163
+ Now you can run `shortiepy` from anywhere in your terminal!
164
+ Made a change? It’ll work immediately—no reinstall needed!
165
+
166
+ ### Updating Shell Completions
167
+
168
+ If you modify CLI commands or options, regenerate completions:
169
+
170
+ ```bash
171
+ ./scripts/generate-completions.sh
172
+ ```
173
+
174
+ > This updates the files in `shortiepy/completions/` directory.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ shortiepy/__init__.py
5
+ shortiepy/__main__.py
6
+ shortiepy/app.py
7
+ shortiepy.egg-info/PKG-INFO
8
+ shortiepy.egg-info/SOURCES.txt
9
+ shortiepy.egg-info/dependency_links.txt
10
+ shortiepy.egg-info/entry_points.txt
11
+ shortiepy.egg-info/requires.txt
12
+ shortiepy.egg-info/top_level.txt
13
+ shortiepy/completions/shortiepy.bash
14
+ shortiepy/completions/shortiepy.fish
15
+ shortiepy/completions/shortiepy.zsh
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ shortiepy = shortiepy.__main__:cli
@@ -0,0 +1,6 @@
1
+ click>=8.3.1
2
+ colorama>=0.4.6
3
+ Flask>=3.1.2
4
+ pyperclip>=1.11.0
5
+ tabulate>=0.9.0
6
+ waitress>=3.0.2
@@ -0,0 +1 @@
1
+ shortiepy