Add stuff so we can figure out how to be able to add this in native firefox

This commit is contained in:
Gabriel Brown 2025-05-09 09:06:15 -05:00
parent 3781ae935d
commit f6b6639905
3 changed files with 394 additions and 12 deletions

View File

@ -1,24 +1,21 @@
networks:
default:
name: ${NETWORK}
external: true
services:
bang-web-server:
env_file:
- .env
build:
context: ../
dockerfile: docker/Dockerfile
container_name: bang
domainname: ${DOMAIN}
hostname: bang.gib
domainname: bang.gbrown.org
networks:
- default
hostname: bang
ports:
- ${PORT}:${PORT}
- nginx-bridge
#ports:
#- 5000:5000
tty: true
restart: unless-stopped
volumes:
- ../:/app
command: serve -s /app/dist -l 5000
networks:
nginx-bridge:
external: true

280
output.md Normal file
View File

@ -0,0 +1,280 @@
docker/.env
```
PORT=5000
DOMAIN=bang.gbrown.org
NETWORK=node_apps
```
docker/Dockerfile
```
# Stage 1: Build the project
FROM node:18 AS builder
WORKDIR /app
# Copy package.json and pnpm-lock.yaml to the working directory
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN npm install -g pnpm
RUN pnpm install
# Copy project files into the docker image
COPY . .
# Build the project
RUN pnpm run build
# Stage 2: Serve the app using the same version of Node
FROM node:18-alpine
WORKDIR /app
# Install a simple http server
RUN npm install -g serve
# Copy built assets from the builder stage
COPY --from=builder /app/dist ./
# Expose port 5000 for the server
EXPOSE 5000
# Start the server using the `serve` package
CMD ["serve", "-s", ".", "-l", "5000"]
```
docker/docker-compose.yml
```yml
services:
bang-web-server:
build:
context: ../
dockerfile: docker/Dockerfile
container_name: bang
hostname: bang.gib
domainname: bang.gbrown.org
networks:
- nginx-bridge
#ports:
#- 5000:5000
tty: true
restart: unless-stopped
volumes:
- ../:/app
command: serve -s /app/dist -l 5000
networks:
nginx-bridge:
external: true
```
src/main.ts
```ts
import { bangs } from "./bang";
import "./global.css";
function noSearchDefaultPageRender() {
const app = document.querySelector<HTMLDivElement>("#app")!;
app.innerHTML = `
<main class="main-container">
<div class="content-container">
<h1 class="bang-title">💣 Bang!</h1>
<p>
Add the following URL as a custom search engine to your browser in order to enable
<a href="https://duckduckgo.com/bang.html" target="_blank">
all of DuckDuckGo's bangs
</a>
right from your browser's search bar!
</p>
<div class="url-container">
<input
type="text"
class="url-input"
value="${import.meta.env.VITE_BANG_URL}?q=%s"
readonly
/>
<button class="copy-button">
<img src="/clipboard.svg" alt="Copy" />
</button>
</div>
<h3 class="settings-title">How to add Bang! Search Engine</h3>
<p style="margin-bottom: 8px; font-size: 14px;">
Below are links to the search engine settings in your browser.
<br />
Copy the URL for the browser engine you are using & paste it into a new tab.
<br />
From there you should see an option to add a search engine. Copy the link above
& fill out the form as follows.
</p>
<table class="form-table">
<tr>
<td><b>Name:</b></td>
<td>Bang!</td>
</tr>
<tr>
<td><b>Engine URL:</b></td>
<td>${import.meta.env.VITE_BANG_URL}?q=%s</td>
</tr>
<tr>
<td><b>Alias:</b></td>
<td>@bang</td>
</tr>
</table>
<div class="settings-links-container">
<div class="browser-link-container">
<img src="/firefox.svg" alt="Firefox" width="24" />
<input
type="text"
readonly value="about:preferences#search"
class="firefox-textbox"
title="Click to copy Firefox settings URL"
/>
<button class="copy-firefox">
<img src="/clipboard.svg" alt="Copy" />
</button>
</div>
<div class="browser-link-container">
<img src="/chrome.svg" alt="Chrome" width="24" />
<input
type="text"
readonly value="chrome://settings/searchEngines"
class="chrome-textbox"
title="Click to copy Chrome settings URL"
/>
<button class="copy-chrome">
<img src="/clipboard.svg" alt="Copy" />
</button>
</div>
</div>
</div>
</main>
`;
const copyButton = app.querySelector<HTMLButtonElement>(".copy-button")!;
const copyIcon = copyButton.querySelector("img")!;
const urlInput = app.querySelector<HTMLInputElement>(".url-input")!;
const copyFirefox = app.querySelector<HTMLInputElement>(".copy-firefox")!;
const copyFirefoxIcon = copyFirefox.querySelector("img")!;
const firefoxInput = app.querySelector<HTMLInputElement>(".firefox-textbox")!;
const copyChrome = app.querySelector<HTMLInputElement>(".copy-chrome")!;
const copyChromeIcon = copyChrome.querySelector("img")!;
const chromeInput = app.querySelector<HTMLInputElement>(".chrome-textbox")!;
copyButton.addEventListener("click", async () => {
await navigator.clipboard.writeText(urlInput.value);
copyIcon.src = "/clipboard-check.svg";
setTimeout(() => {
copyIcon.src = "/clipboard.svg";
}, 2000);
});
copyFirefox.addEventListener("click", async () => {
await navigator.clipboard.writeText(firefoxInput.value);
copyFirefoxIcon.src = "/clipboard-check.svg";
setTimeout(() => {
copyFirefoxIcon.src = "/clipboard.svg";
}, 2000);
});
copyChrome.addEventListener("click", async () => {
await navigator.clipboard.writeText(chromeInput.value);
copyChromeIcon.src = "/clipboard-check.svg";
setTimeout(() => {
copyChromeIcon.src = "/clipboard.svg";
}, 2000);
});
}
const envDefaultBang = import.meta.env.VITE_DEFAULT_BANG ?? "g";
const LS_DEFAULT_BANG = localStorage.getItem("default-bang") ?? envDefaultBang;
const defaultBang = bangs.find((b) => b.t === LS_DEFAULT_BANG);
function getBangredirectUrl() {
const url = new URL(window.location.href);
const query = url.searchParams.get("q")?.trim() ?? "";
if (!query) {
noSearchDefaultPageRender();
return null;
}
const match = query.match(/!(\S+)/i);
const bangCandidate = match?.[1]?.toLowerCase();
const selectedBang = bangs.find((b) => b.t === bangCandidate) ?? defaultBang;
// Remove the first bang from the query
const cleanQuery = query.replace(/!\S+\s*/i, "").trim();
// Format of the url is:
// https://www.google.com/search?q={{{s}}}
const searchUrl = selectedBang?.u.replace(
"{{{s}}}",
// Replace %2F with / to fix formats like "!ghr+t3dotgg/unduck"
encodeURIComponent(cleanQuery).replace(/%2F/g, "/")
);
if (!searchUrl) return null;
return searchUrl;
}
function doRedirect() {
const searchUrl = getBangredirectUrl();
if (!searchUrl) return;
window.location.replace(searchUrl);
}
doRedirect();
```
src/vite-env.d.ts
```ts
/// <reference types="vite/client" />
```
tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
```
vite.config.ts
```ts
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({
plugins: [
VitePWA({
registerType: "autoUpdate",
}),
],
});
```

105
scripts/files_to_clipboard Executable file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
import os
import sys
import argparse
from pathlib import Path
import pyperclip
import questionary
# List of directories to exclude
EXCLUDED_DIRS = {'node_modules', '.next', '.venv', '.git', '__pycache__', '.idea', '.vscode', 'ui'}
def collect_files(project_path):
"""
Collects files from the project directory, excluding specified directories and filtering by extensions.
Returns a list of file paths relative to the project directory.
"""
collected_files = []
for root, dirs, files in os.walk(project_path):
# Exclude specified directories
dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS]
for file in files:
file_path = Path(root) / file
relative_path = file_path.relative_to(project_path)
collected_files.append(relative_path)
return collected_files
def main():
# Parse command-line arguments
parser = argparse.ArgumentParser(description='Generate Markdown from selected files.')
parser.add_argument('path', nargs='?', default='.', help='Path to the project directory')
args = parser.parse_args()
project_path = Path(args.path).resolve()
if not project_path.is_dir():
print(f"Error: '{project_path}' is not a directory.")
sys.exit(1)
# Collect files from the project directory
file_list = collect_files(project_path)
if not file_list:
print("No files found in the project directory with the specified extensions.")
sys.exit(1)
# Sort file_list for better organization
file_list.sort()
# Interactive file selection using questionary
print("\nSelect the files you want to include:")
selected_files = questionary.checkbox(
"Press space to select files, and Enter when you're done:",
choices=[str(f) for f in file_list]
).ask()
if not selected_files:
print("No files selected.")
sys.exit(1)
# Generate markdown
markdown_lines = []
markdown_lines.append('')
for selected_file in selected_files:
file_path = project_path / selected_file
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Determine the language for code block from file extension
language = file_path.suffix.lstrip('.')
markdown_lines.append(f'{selected_file}')
markdown_lines.append(f'```{language}')
markdown_lines.append(content)
markdown_lines.append('```')
markdown_lines.append('')
except Exception as e:
print(f"Error reading file {selected_file}: {e}")
markdown_text = '\n'.join(markdown_lines)
# Write markdown to file
output_file = 'output.md'
with open(output_file, 'w', encoding='utf-8') as f:
f.write(markdown_text)
print(f"\nMarkdown file '{output_file}' has been generated.")
# Copy markdown content to clipboard
pyperclip.copy(markdown_text)
print("Markdown content has been copied to the clipboard.")
if __name__ == "__main__":
# Check if required libraries are installed
try:
import questionary
import pyperclip
except ImportError as e:
missing_module = e.name
print(f"Error: Missing required module '{missing_module}'.")
print(f"Please install it by running: pip install {missing_module}")
sys.exit(1)
main()