feat: init 1d5f918d
Steve · 2026-02-18 19:37 21 file(s) · +512 −0
.gitignore (added) +2 −0
1 +
/target
2 +
*.sqlite
Cargo.toml (added) +14 −0
1 +
[package]
2 +
name = "sipp-rust"
3 +
version = "0.1.0"
4 +
edition = "2024"
5 +
6 +
[dependencies]
7 +
axum = "0.8.8"
8 +
tokio = { version = "1", features = ["full"] }
9 +
askama = "0.15.4"
10 +
askama_web = { version = "0.15.1", features = ["axum-0.8"] }
11 +
rusqlite = { version = "0.38", features = ["bundled"] }
12 +
serde = { version = "1", features = ["derive"] }
13 +
tower-http = { version = "0.6.8", features = ["fs"] }
14 +
rand = "0.10"
assets/android-chrome-192x192.png (added) +0 −0

Binary file — no preview.

assets/android-chrome-512x512.png (added) +0 −0

Binary file — no preview.

assets/apple-touch-icon.png (added) +0 −0

Binary file — no preview.

assets/favicon-16x16.png (added) +0 −0

Binary file — no preview.

assets/favicon-32x32.png (added) +0 −0

Binary file — no preview.

assets/favicon.ico (added) +0 −0

Binary file — no preview.

assets/fonts/CommitMono-400-Italic.otf (added) +0 −0

Binary file — no preview.

assets/fonts/CommitMono-400-Regular.otf (added) +0 −0

Binary file — no preview.

assets/fonts/CommitMono-700-Italic.otf (added) +0 −0

Binary file — no preview.

assets/fonts/CommitMono-700-Regular.otf (added) +0 −0

Binary file — no preview.

assets/icon.png (added) +0 −0

Binary file — no preview.

assets/og.png (added) +0 −0

Binary file — no preview.

assets/site.webmanifest (added) +1 −0
1 +
{"name":"","short_name":"","icons":[{"src":"/assets/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/assets/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
src/db.rs (added) +71 −0
1 +
use rand::RngExt;
2 +
use rusqlite::{Connection, params};
3 +
use std::sync::{Arc, Mutex};
4 +
5 +
pub type Db = Arc<Mutex<Connection>>;
6 +
7 +
pub struct Snippet {
8 +
    #[allow(dead_code)]
9 +
    pub id: i64,
10 +
    pub short_id: String,
11 +
    pub content: String,
12 +
    pub name: String,
13 +
}
14 +
15 +
const ALPHABET: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
16 +
17 +
fn generate_short_id() -> String {
18 +
    let mut rng = rand::rng();
19 +
    (0..10)
20 +
        .map(|_| ALPHABET[rng.random_range(0..ALPHABET.len())] as char)
21 +
        .collect()
22 +
}
23 +
24 +
pub fn init_db() -> Db {
25 +
    let conn = Connection::open("sipp.sqlite").expect("Failed to open database");
26 +
    conn.execute(
27 +
        "CREATE TABLE IF NOT EXISTS snippets (
28 +
            id INTEGER PRIMARY KEY AUTOINCREMENT,
29 +
            short_id TEXT NOT NULL UNIQUE,
30 +
            content TEXT NOT NULL,
31 +
            name TEXT NOT NULL
32 +
        )",
33 +
        [],
34 +
    )
35 +
    .expect("Failed to create table");
36 +
    Arc::new(Mutex::new(conn))
37 +
}
38 +
39 +
pub fn create_snippet(db: &Db, name: &str, content: &str) -> Snippet {
40 +
    let conn = db.lock().unwrap();
41 +
    let short_id = generate_short_id();
42 +
    conn.execute(
43 +
        "INSERT INTO snippets (short_id, content, name) VALUES (?1, ?2, ?3)",
44 +
        params![short_id, content, name],
45 +
    )
46 +
    .expect("Failed to insert snippet");
47 +
    let id = conn.last_insert_rowid();
48 +
    Snippet {
49 +
        id,
50 +
        short_id,
51 +
        content: content.to_string(),
52 +
        name: name.to_string(),
53 +
    }
54 +
}
55 +
56 +
pub fn get_snippet_by_short_id(db: &Db, short_id: &str) -> Option<Snippet> {
57 +
    let conn = db.lock().unwrap();
58 +
    conn.query_row(
59 +
        "SELECT id, short_id, content, name FROM snippets WHERE short_id = ?1",
60 +
        params![short_id],
61 +
        |row| {
62 +
            Ok(Snippet {
63 +
                id: row.get(0)?,
64 +
                short_id: row.get(1)?,
65 +
                content: row.get(2)?,
66 +
                name: row.get(3)?,
67 +
            })
68 +
        },
69 +
    )
70 +
    .ok()
71 +
}
src/main.rs (added) +92 −0
1 +
mod db;
2 +
3 +
use askama::Template;
4 +
use askama_web::WebTemplate;
5 +
use axum::{
6 +
    Router,
7 +
    extract::{Form, Path, State},
8 +
    http::StatusCode,
9 +
    response::{Html, IntoResponse, Redirect},
10 +
    routing::{get, post},
11 +
};
12 +
use serde::Deserialize;
13 +
use tower_http::services::ServeDir;
14 +
15 +
use db::Db;
16 +
17 +
#[derive(Template)]
18 +
#[template(path = "index.html")]
19 +
struct IndexTemplate;
20 +
21 +
#[derive(Template)]
22 +
#[template(path = "snippet.html")]
23 +
struct SnippetTemplate {
24 +
    name: String,
25 +
    content: String,
26 +
}
27 +
28 +
#[derive(Template)]
29 +
#[template(path = "about.html")]
30 +
struct AboutTemplate;
31 +
32 +
#[derive(Deserialize)]
33 +
struct CreateSnippetForm {
34 +
    name: String,
35 +
    content: String,
36 +
}
37 +
38 +
async fn index() -> WebTemplate<IndexTemplate> {
39 +
    WebTemplate(IndexTemplate)
40 +
}
41 +
42 +
async fn about() -> WebTemplate<AboutTemplate> {
43 +
    WebTemplate(AboutTemplate)
44 +
}
45 +
46 +
async fn view_snippet(
47 +
    State(db): State<Db>,
48 +
    Path(short_id): Path<String>,
49 +
) -> Result<WebTemplate<SnippetTemplate>, (StatusCode, Html<String>)> {
50 +
    match db::get_snippet_by_short_id(&db, &short_id) {
51 +
        Some(snippet) => Ok(WebTemplate(SnippetTemplate {
52 +
            name: snippet.name,
53 +
            content: snippet.content,
54 +
        })),
55 +
        None => Err((
56 +
            StatusCode::NOT_FOUND,
57 +
            Html("<h1>Snippet not found</h1>".to_string()),
58 +
        )),
59 +
    }
60 +
}
61 +
62 +
async fn create_snippet(
63 +
    State(db): State<Db>,
64 +
    Form(form): Form<CreateSnippetForm>,
65 +
) -> impl IntoResponse {
66 +
    let snippet = db::create_snippet(&db, &form.name, &form.content);
67 +
    Redirect::to(&format!("/s/{}", snippet.short_id))
68 +
}
69 +
70 +
#[tokio::main]
71 +
async fn main() {
72 +
    let db = db::init_db();
73 +
74 +
    let app = Router::new()
75 +
        .route("/", get(index))
76 +
        .route("/about", get(about))
77 +
        .route("/s/{short_id}", get(view_snippet))
78 +
        .route("/snippets", post(create_snippet))
79 +
        .nest_service("/assets", ServeDir::new("assets"))
80 +
        .nest_service("/static", ServeDir::new("static"))
81 +
        .with_state(db);
82 +
83 +
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
84 +
        .await
85 +
        .expect("Failed to bind to port 3000");
86 +
87 +
    println!("Server running at http://localhost:3000");
88 +
89 +
    axum::serve(listener, app)
90 +
        .await
91 +
        .expect("Failed to start server");
92 +
}
static/styles.css (added) +108 −0
1 +
* {
2 +
	padding: 0;
3 +
	margin: 0;
4 +
	box-sizing: border-box;
5 +
	font-family: "Commit Mono", monospace, sans-serif;
6 +
	scrollbar-width: none;
7 +
	-ms-overflow-style: none;
8 +
}
9 +
10 +
html {
11 +
	background: #121113;
12 +
	color: #ffffff;
13 +
}
14 +
15 +
html::-webkit-scrollbar {
16 +
	display: none;
17 +
}
18 +
19 +
body {
20 +
	display: flex;
21 +
	flex-direction: column;
22 +
	justify-content: start;
23 +
	align-items: start;
24 +
	gap: 1.5rem;
25 +
	min-height: 100vh;
26 +
	max-width: 700px;
27 +
	margin: auto;
28 +
}
29 +
30 +
.header {
31 +
	display: flex;
32 +
	flex-direction: column;
33 +
	gap: 0.5rem;
34 +
	text-decoration: none;
35 +
	margin-top: 2rem;
36 +
}
37 +
38 +
#snippetForm {
39 +
	display: flex;
40 +
	flex-direction: column;
41 +
	gap: 1rem;
42 +
	width: 100%;
43 +
}
44 +
45 +
#snippetForm input {
46 +
	background: #121113;
47 +
	color: #ffffff;
48 +
	border: 1px solid white;
49 +
	padding: 4px;
50 +
}
51 +
52 +
.links {
53 +
	display: flex;
54 +
	align-items: center;
55 +
	gap: 1.5rem;
56 +
	font-size: 12px;
57 +
}
58 +
59 +
.about {
60 +
	display: flex;
61 +
	flex-direction: column;
62 +
	gap: 0.5rem;
63 +
	font-size: 14px;
64 +
}
65 +
66 +
textarea {
67 +
	background: #121113;
68 +
	color: #ffffff;
69 +
	width: 100%;
70 +
	min-height: 400px;
71 +
	padding: 6px;
72 +
	border: 1px solid white;
73 +
}
74 +
75 +
button {
76 +
	background: #121113;
77 +
	color: #ffffff;
78 +
	padding: 6px;
79 +
	border: 1px solid white;
80 +
	cursor: pointer;
81 +
	width: fit-content;
82 +
}
83 +
84 +
a {
85 +
	background: #121113;
86 +
	color: #ffffff;
87 +
}
88 +
89 +
@media (max-width: 480px) {
90 +
	body {
91 +
		padding: 1rem;
92 +
		gap: 1rem;
93 +
	}
94 +
}
95 +
96 +
@font-face {
97 +
	font-family: "Commit Mono";
98 +
	src: url("/assets/fonts/CommitMono-400-Regular.otf") format("opentype");
99 +
	font-weight: 400;
100 +
	font-style: normal;
101 +
}
102 +
103 +
@font-face {
104 +
	font-family: "Commit Mono";
105 +
	src: url("/assets/fonts/CommitMono-700-Regular.otf") format("opentype");
106 +
	font-weight: 700;
107 +
	font-style: normal;
108 +
}
templates/about.html (added) +48 −0
1 +
<!doctype html>
2 +
<html lang="en">
3 +
  <head>
4 +
    <meta charset="UTF-8" />
5 +
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 +
    <link rel="stylesheet" href="/static/styles.css" />
7 +
    <meta name="theme-color" content="#121113" />
8 +
    <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
9 +
    <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
10 +
    <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
11 +
    <link rel="manifest" href="/assets/site.webmanifest">
12 +
13 +
    <title>Sipp | About</title>
14 +
    <meta name="description" content="Minimal Code Sharing">
15 +
16 +
    <meta property="og:url" content="https://sipp.so">
17 +
    <meta property="og:type" content="website">
18 +
    <meta property="og:title" content="Sipp | About">
19 +
    <meta property="og:description" content="Minimal Code Sharing">
20 +
    <meta property="og:image" content="https://sipp.so/assets/og.png">
21 +
22 +
    <meta name="twitter:card" content="summary_large_image">
23 +
    <meta property="twitter:domain" content="sipp.so">
24 +
    <meta property="twitter:url" content="https://sipp.so">
25 +
    <meta name="twitter:title" content="Sipp | About">
26 +
    <meta name="twitter:description" content="Minimal Code Sharing">
27 +
    <meta name="twitter:image" content="https://sipp.so/assets/og.png">
28 +
29 +
  </head>
30 +
  <body>
31 +
    <a href="/" class="header">
32 +
      <h1>SIPP</h1>
33 +
    </a>
34 +
    <div class="links">
35 +
      <a href="/">Go Back</a>
36 +
      <a target="_blank" href="https://github.com/stevedylandev/sipp">
37 +
        GitHub
38 +
      </a>
39 +
      <p>by <a target="_blank" href="https://stevedylan.dev">Steve</a></p>
40 +
    </div>
41 +
42 +
    <div class="about">
43 +
      <p>A while back I released an app called Snippets. While it had lots of polish and the stuff influencers say you need to have, it was also bloated, slow, and had too much vendor lock-in. This app is the antitode that takes a different approach. There is no Next.js, shadcn/ui components, or even syntax highlighting. It's just text sharing, all powered by Bun and no other dependencies. From the ground up it's designed to be self hosted and used on your own hardware or VPS so you have control of the data. I've also designed this with longevity in mind by using just html css and js. You can edit any part you like, update it, and know that you can always come back and fix something.</p>
44 +
45 +
      <p>The web needs more simplicity, and the ability to build our own tools and solutions that can be picked up and used 5 or 10 years from now. Put down the framework, build simply, and make it open source.</p>
46 +
    </div>
47 +
  </body>
48 +
</html>
templates/index.html (added) +63 −0
1 +
<!doctype html>
2 +
<html lang="en">
3 +
  <head>
4 +
    <meta charset="UTF-8" />
5 +
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 +
    <meta name="theme-color" content="#121113" />
7 +
    <link rel="stylesheet" href="/static/styles.css" />
8 +
    <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
9 +
    <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
10 +
    <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
11 +
    <link rel="manifest" href="/assets/site.webmanifest">
12 +
13 +
    <title>Sipp</title>
14 +
    <meta name="description" content="Minimal Code Sharing">
15 +
16 +
    <meta property="og:url" content="https://sipp.so">
17 +
    <meta property="og:type" content="website">
18 +
    <meta property="og:title" content="Sipps">
19 +
    <meta property="og:description" content="Minimal Code Sharing">
20 +
    <meta property="og:image" content="https://sipp.so/assets/og.png">
21 +
22 +
    <meta name="twitter:card" content="summary_large_image">
23 +
    <meta property="twitter:domain" content="sipp.so">
24 +
    <meta property="twitter:url" content="https://sipp.so">
25 +
    <meta name="twitter:title" content="Sipps">
26 +
    <meta name="twitter:description" content="Minimal Code Sharing">
27 +
    <meta name="twitter:image" content="https://sipp.so/assets/og.png">
28 +
  </head>
29 +
  <body>
30 +
    <a href="/" class="header">
31 +
      <h1>SIPP</h1>
32 +
    </a>
33 +
34 +
    <div class="links">
35 +
      <a target="_blank" href="https://github.com/stevedylandev/sipp">
36 +
        GitHub
37 +
      </a>
38 +
      <a href="/about">About</a>
39 +
      <p>by <a target="_blank" href="https://stevedylan.dev">Steve</a></p>
40 +
    </div>
41 +
42 +
    <form id="snippetForm" method="POST" action="/snippets">
43 +
      <div>
44 +
        <input placeholder="index.ts" type="text" id="name" name="name" required>
45 +
      </div>
46 +
47 +
      <div>
48 +
        <textarea placeholder="// paste your code here" id="content" name="content" required></textarea>
49 +
      </div>
50 +
51 +
      <button type="submit">Create Snippet</button>
52 +
    </form>
53 +
54 +
    <script>
55 +
      document.getElementById('content').addEventListener('keydown', (e) => {
56 +
        if (e.metaKey && e.key === 'Enter' || e.ctrlKey && e.key === 'Enter') {
57 +
          e.preventDefault();
58 +
          document.getElementById('snippetForm').requestSubmit();
59 +
        }
60 +
      });
61 +
    </script>
62 +
  </body>
63 +
</html>
templates/snippet.html (added) +113 −0
1 +
<!doctype html>
2 +
<html lang="en">
3 +
  <head>
4 +
    <meta charset="UTF-8" />
5 +
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 +
    <link rel="stylesheet" href="/static/styles.css" />
7 +
    <meta name="theme-color" content="#121113" />
8 +
    <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
9 +
    <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
10 +
    <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
11 +
    <link rel="manifest" href="/assets/site.webmanifest">
12 +
13 +
    <title>Sipp | {{ name }}</title>
14 +
    <meta name="description" content="Minimal Code Sharing">
15 +
16 +
    <meta property="og:url" content="https://sipp.so">
17 +
    <meta property="og:type" content="website">
18 +
    <meta property="og:title" content="Sipp | {{ name }}">
19 +
    <meta property="og:description" content="Minimal Code Sharing">
20 +
    <meta property="og:image" content="https://sipp.so/assets/og.png">
21 +
22 +
    <meta name="twitter:card" content="summary_large_image">
23 +
    <meta property="twitter:domain" content="sipp.so">
24 +
    <meta property="twitter:url" content="https://sipp.so">
25 +
    <meta name="twitter:title" content="Sipp | {{ name }}">
26 +
    <meta name="twitter:description" content="Minimal Code Sharing">
27 +
    <meta name="twitter:image" content="https://sipp.so/assets/og.png">
28 +
29 +
  </head>
30 +
  <body>
31 +
    <a href="/" class="header">
32 +
      <h1>SIPP</h1>
33 +
    </a>
34 +
    <div class="links">
35 +
      <a target="_blank" href="https://github.com/stevedylandev/sipp">
36 +
        GitHub
37 +
      </a>
38 +
      <a href="/about">About</a>
39 +
      <p>by <a target="_blank" href="https://stevedylan.dev">Steve</a></p>
40 +
    </div>
41 +
42 +
    <form id="snippetForm">
43 +
        <label id="snippetName">{{ name }}</label>
44 +
        <textarea id="content" name="content" rows="10" cols="50" readonly>{{ content }}</textarea>
45 +
      <div class="button-group">
46 +
        <button type="button" id="copyLinkBtn" data-original-text="Copy Link">Copy Link</button>
47 +
        <button type="button" id="copyContentBtn" data-original-text="Copy Content">Copy Content</button>
48 +
        <button type="button" id="createNewBtn">Create New Snippet</button>
49 +
      </div>
50 +
    </form>
51 +
52 +
    <script>
53 +
      async function copyToClipboard(text, button) {
54 +
        try {
55 +
          await navigator.clipboard.writeText(text);
56 +
          showButtonFeedback(button, '\u2714 Copied', 'success');
57 +
        } catch (error) {
58 +
          console.error('Copy failed:', error);
59 +
          showButtonFeedback(button, '\u2718 Failed', 'error');
60 +
61 +
          try {
62 +
            const textArea = document.createElement('textarea');
63 +
            textArea.value = text;
64 +
            textArea.style.position = 'fixed';
65 +
            textArea.style.opacity = '0';
66 +
            document.body.appendChild(textArea);
67 +
            textArea.select();
68 +
            document.execCommand('copy');
69 +
            document.body.removeChild(textArea);
70 +
            showButtonFeedback(button, '\u2714 Copied', 'success');
71 +
          } catch (fallbackError) {
72 +
            showButtonFeedback(button, '\u2718 Failed', 'error');
73 +
          }
74 +
        }
75 +
      }
76 +
77 +
      function showButtonFeedback(button, message, type = 'success') {
78 +
        const originalText = button.dataset.originalText || button.textContent;
79 +
        const originalDisabled = button.disabled;
80 +
81 +
        if (!button.dataset.originalText) {
82 +
          button.dataset.originalText = originalText;
83 +
        }
84 +
85 +
        button.textContent = message;
86 +
        button.disabled = true;
87 +
        button.classList.add(`copy-${type}`);
88 +
89 +
        setTimeout(() => {
90 +
          button.textContent = originalText;
91 +
          button.disabled = originalDisabled;
92 +
          button.classList.remove(`copy-${type}`);
93 +
        }, 1000);
94 +
      }
95 +
96 +
      document.getElementById('copyContentBtn').addEventListener('click', async () => {
97 +
        const content = document.getElementById('content').value;
98 +
        const button = document.getElementById('copyContentBtn');
99 +
        await copyToClipboard(content, button);
100 +
      });
101 +
102 +
      document.getElementById('copyLinkBtn').addEventListener('click', async () => {
103 +
        const currentUrl = window.location.href;
104 +
        const button = document.getElementById('copyLinkBtn');
105 +
        await copyToClipboard(currentUrl, button);
106 +
      });
107 +
108 +
      document.getElementById('createNewBtn').addEventListener('click', () => {
109 +
        window.location.href = '/';
110 +
      });
111 +
    </script>
112 +
  </body>
113 +
</html>