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.
- shortiepy-0.0.0/LICENSE +21 -0
- shortiepy-0.0.0/PKG-INFO +174 -0
- shortiepy-0.0.0/README.md +144 -0
- shortiepy-0.0.0/pyproject.toml +49 -0
- shortiepy-0.0.0/setup.cfg +4 -0
- shortiepy-0.0.0/shortiepy/__init__.py +0 -0
- shortiepy-0.0.0/shortiepy/__main__.py +422 -0
- shortiepy-0.0.0/shortiepy/app.py +109 -0
- shortiepy-0.0.0/shortiepy/completions/shortiepy.bash +29 -0
- shortiepy-0.0.0/shortiepy/completions/shortiepy.fish +18 -0
- shortiepy-0.0.0/shortiepy/completions/shortiepy.zsh +41 -0
- shortiepy-0.0.0/shortiepy.egg-info/PKG-INFO +174 -0
- shortiepy-0.0.0/shortiepy.egg-info/SOURCES.txt +15 -0
- shortiepy-0.0.0/shortiepy.egg-info/dependency_links.txt +1 -0
- shortiepy-0.0.0/shortiepy.egg-info/entry_points.txt +2 -0
- shortiepy-0.0.0/shortiepy.egg-info/requires.txt +6 -0
- shortiepy-0.0.0/shortiepy.egg-info/top_level.txt +1 -0
shortiepy-0.0.0/LICENSE
ADDED
|
@@ -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.
|
shortiepy-0.0.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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/*"]
|
|
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
|
+

|
|
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
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shortiepy
|