commit ee78eef527e3dbb5867de91fcadd6fbae411a81d
Author: Squibid
Date: Wed Sep 17 23:05:25 2025 -0400
initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c73b058
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+misctils.xpi
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..bef4f6f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,25 @@
+MOZ_NATIVE=$(HOME)/.mozilla/native-messaging-hosts
+
+release: xpi
+
+xpi:
+ zip -r misctils.xpi *.js manifest.json icons/ startpage/
+
+startpage:
+ ./genfs.sh
+
+install:
+ mkdir -p $(MOZ_NATIVE)
+ cp nativemsg/bg.py $(MOZ_NATIVE)
+
+ # make the home directory work for all users
+ sed "s@HOME@$(HOME)@" nativemsg/bg_app.json > $(MOZ_NATIVE)/bg_app.json
+ chmod +x $(MOZ_NATIVE)/bg.py
+
+uninstall:
+ rm $(MOZ_NATIVE)/bg_app.json $(MOZ_NATIVE)/bg.py
+
+clean:
+ rm misctils.xpi
+
+.PHONY: startpage
diff --git a/background.js b/background.js
new file mode 100644
index 0000000..bc352fe
--- /dev/null
+++ b/background.js
@@ -0,0 +1,96 @@
+/* add a context menu for setting the background */
+browser.menus.create({
+ id: "set-background",
+ title: "Set Background Image",
+ contexts: [ "image" ],
+ icons: {
+ "16": "icons/background.png",
+ "32": "icons/background.png",
+ "64": "icons/background.png",
+ "128": "icons/background.png",
+ "256": "icons/background.png"
+ }
+})
+
+/* add a context menu for accessing repo on github */
+browser.menus.create({
+ id: "search-github",
+ title: "Open in Github",
+ contexts: [ "selection" ]
+})
+
+browser.menus.onClicked.addListener(info => {
+ if (info.menuItemId == "set-background") {
+ browser.runtime.sendNativeMessage("bg_app", info.srcUrl);
+ } else if (info.menuItemId == "search-github" && info.selectionText) {
+ const trimmedText = info.selectionText.trim();
+ const githubUrl = `https://github.com/${trimmedText}`;
+ chrome.tabs.create({ url: githubUrl })
+ }
+});
+
+/* add functionality to commands defined in manifest.json */
+chrome.commands.onCommand.addListener(function(action) {
+ action.preventDefault();
+ switch (action) {
+ case "toggle-pin-tab": /* toggle current tab pinning */
+ chrome.tabs.query({ currentWindow: true, active: true }, function(found_tab) {
+ const currentTab = found_tab[0]
+ const toggledValue = !currentTab.pinned;
+
+ chrome.tabs.update(currentTab.id, { pinned: toggledValue });
+ });
+ break;
+
+ /* switch between tabs */
+ case "tab-up":
+ chrome.tabs.query({ currentWindow: true, active: true }, function(found_tab) {
+ switch_tab(found_tab[0].index - 1);
+ });
+ break;
+ case "tab-down":
+ chrome.tabs.query({ currentWindow: true, active: true }, function(found_tab) {
+ switch_tab(found_tab[0].index + 1);
+ });
+ break;
+
+ /* close tab */
+ case "tab-close":
+ chrome.tabs.query({ currentWindow: true, active: true }, function(found_tab) {
+ browser.tabs.remove(found_tab[0].id);
+ });
+ break;
+ case "scroll-half-down":
+ window.scrollBy(0, window.innerHeight / 2, "smooth");
+ console.log(window)
+ console.log("hello world")
+ break;
+ case "scroll-half-up":
+ window.scrollBy(0, -(window.innerHeight / 2), "smooth");
+ console.log(window)
+ console.log("hello world")
+ break;
+ }
+});
+
+function switch_tab(tab_number) {
+ /* I don't want it to cycle around */
+ if (tab_number < 0) {
+ return;
+ }
+
+ chrome.tabs.query({ currentWindow: true, active: true }, function(found_tabs) {
+ const currentTab = found_tabs[0];
+ if (currentTab.index == tab_number) {
+ /* we're already on the current tab */
+ return;
+ }
+
+ /* change the selected tab to active */
+ browser.tabs.query({ currentWindow: true }, function(window_tabs) {
+ if (typeof window_tabs.at(tab_number) != "undefined") {
+ chrome.tabs.update(window_tabs.at(tab_number).id, { active: true });
+ }
+ });
+ });
+}
diff --git a/genfs.sh b/genfs.sh
new file mode 100755
index 0000000..eba1faa
--- /dev/null
+++ b/genfs.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+# generate fs files because we can't do directory traversal from js within a
+# browser.
+
+for d in ./startpage/root/*; do
+ if [ ! -d "$d" ]; then
+ continue
+ fi
+
+ s=""
+ for f in "$d"/*; do
+ s=${s:+$s\\n}$(echo "$f" | cut -d '/' -f 5-)
+ done
+ echo "$s" > ./startpage/root/."$(echo "$d" | cut -d '/' -f 4-)"
+done
diff --git a/icons/background.png b/icons/background.png
new file mode 100644
index 0000000..2bdfeae
Binary files /dev/null and b/icons/background.png differ
diff --git a/icons/misc.png b/icons/misc.png
new file mode 100644
index 0000000..335529d
Binary files /dev/null and b/icons/misc.png differ
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..0a21fd0
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,59 @@
+{
+ "name": "misctils",
+ "version": "1.0",
+ "description": "Miscelanious Utils for Firefox",
+ "icons": {
+ "16": "icons/misc.png",
+ "32": "icons/misc.png",
+ "64": "icons/misc.png",
+ "128": "icons/misc.png",
+ "256": "icons/misc.png"
+ },
+ "manifest_version": 2,
+ "permissions": [ "menus", "activeTab", "tabs", "nativeMessaging", "storage" ],
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "misc@tils",
+ "strict_min_version": "58.0"
+ }
+ },
+
+ "background": {
+ "scripts": [ "background.js" ],
+ "persistent": false
+ },
+ "chrome_settings_overrides" : {
+ "homepage": "startpage/index.html"
+ },
+ "chrome_url_overrides" : {
+ "newtab": "startpage/index.html"
+ },
+
+ "commands": {
+ "toggle-pin-tab": {
+ "suggested_key": { "default": "Alt+P" },
+ "description": "Pin the currently active tab."
+ },
+ "tab-up": {
+ "suggested_key": { "default": "Alt+K" },
+ "description": "Switch tab focus to one up."
+ },
+ "tab-down": {
+ "suggested_key": { "default": "Alt+J" },
+ "description": "Switch tab focus to one down."
+ },
+ "tab-close": {
+ "suggested_key": { "default": "Alt+C" },
+ "description": "Close the current tab."
+ },
+ "scroll-half-down": {
+ "suggested_key": { "default": "Alt+D" },
+ "description": "Scroll down half a page."
+ },
+ "scroll-half-up": {
+ "suggested_key": { "default": "Alt+U" },
+ "description": "Scroll up half a page."
+ }
+ }
+}
diff --git a/nativemsg/bg.py b/nativemsg/bg.py
new file mode 100644
index 0000000..48242ec
--- /dev/null
+++ b/nativemsg/bg.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env -S python3
+import sys
+import struct
+import subprocess
+
+# Read a message from stdin and decode it.
+rawLength = sys.stdin.buffer.read(4)
+if len(rawLength) == 0:
+ sys.exit(0)
+
+# Read the length of the message
+messageLength: int = struct.unpack("@I", rawLength)[0]
+
+# Decode the message
+message = sys.stdin.buffer.read(messageLength).decode("utf-8")
+
+# Run the background setting with a link to the image
+_ = subprocess.run(["setbg", "-tw", message.replace('"', "")])
diff --git a/nativemsg/bg_app.json b/nativemsg/bg_app.json
new file mode 100644
index 0000000..7fe4e79
--- /dev/null
+++ b/nativemsg/bg_app.json
@@ -0,0 +1,7 @@
+{
+ "name": "bg_app",
+ "description": "set background via native script",
+ "path": "HOME/.mozilla/native-messaging-hosts/bg.py",
+ "type": "stdio",
+ "allowed_extensions": [ "misc@tils" ]
+}
diff --git a/startpage/index.html b/startpage/index.html
new file mode 100644
index 0000000..e3a7635
--- /dev/null
+++ b/startpage/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ Startpage
+
+
+
+
+
+
+
+
+
+
+
diff --git a/startpage/index.js b/startpage/index.js
new file mode 100644
index 0000000..c414ef1
--- /dev/null
+++ b/startpage/index.js
@@ -0,0 +1,134 @@
+let clock = document.getElementById("clock");
+let date = document.getElementById("date");
+let input = document.getElementById("search-bar");
+let result = document.getElementById("search-results");
+
+/* Allow toggling between military and 12 hr time */
+let military_time = false
+clock.onclick = function() {
+ military_time = !military_time;
+ time();
+}
+
+function time(timeout) {
+ let d = new Date();
+ let m = d.getMinutes();
+ let h = d.getHours();
+
+ /* if it hasn't been set then set it here */
+ if (military_time) {
+ clock.textContent = `${("0" + h).substr(-2)}:${("0" + m).substr(-2)}`;
+ } else {
+ let hours = h % 12;
+ let minutes = ("0" + m).substr(-2);
+ hours = hours ? hours : 12;
+
+ clock.textContent = `${hours}:${minutes} ${(h >= 12 ? "PM" : "AM")}`;
+ }
+
+ /* set the time every second */
+ timeout && setTimeout(() => time(true), (60 - d.getSeconds()) * 1000);
+}
+
+let last_date;
+/* Sun 31st, May */
+function cal(force) {
+ const days = [ "Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat" ];
+ const months = [ "January", "Febuary", "March", "April", "May", "June",
+ "July", "August", "September", "November", "December", ];
+
+ let d = new Date();
+ let day = days[d.getDay()];
+ let day_of_month = d.getDate();
+ let month = months[d.getMonth()];
+ let day_suffix
+
+ /* get a suffix for the day of the month */
+ if (day_of_month > 3 && day_of_month < 21) {
+ day_suffix = "th";
+ } else if ((day_of_month / 1) % 10 == 1) {
+ day_suffix = "st";
+ } else if ((day_of_month / 1) % 10 == 2) {
+ day_suffix = "nd";
+ } else if ((day_of_month / 1) % 10 == 3) {
+ day_suffix = "rd";
+ } else {
+ day_suffix = "th";
+ }
+
+ /* check if this date has already been set before */
+ if (!force && last_date == `${d.getDay()}${d.getMonth()}`) {
+ return;
+ }
+
+ date.textContent = `${day} ${day_of_month}${day_suffix}, ${month}`;
+
+ /* store the last date that was set */
+ last_date = `${d.getDay()}${d.getMonth()}`;
+}
+
+/* show the time on the page */
+time(true);
+
+/* just incase something dumb happens the time should update when the user
+ * focus's the page */
+window.addEventListener('focus', time);
+
+cal();
+setInterval(cal, 1000);
+
+function shell(event) {
+ var env = {
+ path: "/startpage/root/bin/",
+ };
+
+ /* convert a directory such as /startpage/root/bin/ to /startpage/root/.bin
+ * to read static fs data.
+ */
+ function convertPath(path) {
+ if (path.endsWith('/')) {
+ path = path.slice(0, -1);
+ }
+ const parts = path.split('/');
+ const hidden = '.' + parts.pop();
+ return parts.concat(hidden).join('/');
+ }
+
+ async function run(path) {
+ try {
+ const response = await fetch(convertPath(env.path));
+ if (!response.ok) {
+ throw new Error(`Failed to load file: ${response.status}`);
+ }
+
+ const text = await response.text();
+ const lines = text.split(/\r?\n/);
+ const matchedLine = lines.find(line => line.split(".")[0] == path);
+ if (!matchedLine) {
+ throw new Error(`${path} cannot be found in path.`);
+ }
+
+ const file = env.path + matchedLine;
+ const { cmd } = await import(file);
+ return await cmd(input.value, event.key);
+ } catch (err) {
+ throw err;
+ }
+ }
+
+ if (input.value == "") {
+ input.style.color = "white";
+ } else {
+ run(input.value.substring(0, input.value.indexOf(" ")) || input.value)
+ .then(res => {
+ result.innerHTML = res;
+ input.style.color = "var(--blue)";
+ })
+ .catch(err => {
+ input.style.color = "white";
+ });
+ }
+}
+
+input.addEventListener('keyup', shell);
+shell({ key: "" });
diff --git a/startpage/root/.bin b/startpage/root/.bin
new file mode 100644
index 0000000..6524928
--- /dev/null
+++ b/startpage/root/.bin
@@ -0,0 +1,4 @@
+clear.js
+fetch.js
+gpt.js
+man.js
diff --git a/startpage/root/bin/clear.js b/startpage/root/bin/clear.js
new file mode 100644
index 0000000..ba1a2d6
--- /dev/null
+++ b/startpage/root/bin/clear.js
@@ -0,0 +1,12 @@
+export async function cmd() {
+ return ``;
+}
+
+export let man = `
+
+CLEAR(1)
+
+NAME
+\tclear - clear the screen
+
+`
diff --git a/startpage/root/bin/fetch.js b/startpage/root/bin/fetch.js
new file mode 100644
index 0000000..723adbb
--- /dev/null
+++ b/startpage/root/bin/fetch.js
@@ -0,0 +1,29 @@
+export async function cmd(_) {
+return `
+
+ _____\t\t\tOS: ${navigator.platform}
+ .-"" ""-.\t\tLang: ${navigator.language}
+ /(/ _ }\`;\`.\t\tTimezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}
+ |\`---'/ \` \`;\\\t\tAgent: ${navigator.userAgent}
+ .' (\`._ ; \`:
+: \--' '|\`;,;
+| .---' ; | |
+: ' \`.__.-. / ; ;
+ : .----' .' / :
+ \`. \`----' .' /
+ \`.\`--. __ .- .'
+ \`-._ _.-' fsc
+ \`""""
+
+`;
+}
+
+export let man = `
+
+FETCH(1)
+
+NAME
+\tfetch – Get information about the browser. This is specific to firefox because
+\tthis addon is specific to firefox.
+
+`
diff --git a/startpage/root/bin/gpt.js b/startpage/root/bin/gpt.js
new file mode 100644
index 0000000..d1882cf
--- /dev/null
+++ b/startpage/root/bin/gpt.js
@@ -0,0 +1,26 @@
+export async function cmd(inp, k) {
+ var search = ""
+ if (inp.indexOf(" ") > 0) {
+ search = inp.substring(inp.indexOf(" ") + 1)
+ }
+
+ if (k == 'Enter') {
+ window.open(`https://chatgpt.com/?q=${search}`, '_self');
+ }
+
+ return `
+
+press Enter to search chat.openai.com for: '${search}'
+
+`;
+}
+
+export let man = `
+
+GPT(1)
+
+NAME
+\tgpt - search chat.openai.com for something. You'll probably only get slop
+\tbut it may have a chance of helping.
+
+`
diff --git a/startpage/root/bin/man.js b/startpage/root/bin/man.js
new file mode 100644
index 0000000..f3f8f44
--- /dev/null
+++ b/startpage/root/bin/man.js
@@ -0,0 +1,53 @@
+var getmanval;
+async function getman(path) {
+ try {
+ const module = await import("/startpage/" + path);
+ return module.man;
+ } catch (error) {
+ return null;
+ }
+}
+
+/* TODO: update to use the path in the env this can be done once we first
+ * make the env available to the programs running in the shell
+ */
+export async function cmd(inp) {
+ var search = ""
+ if (inp.indexOf(" ") > 0) {
+ search = inp.substring(inp.indexOf(" ") + 1)
+ }
+ switch (search) {
+ case "fetch": return getman('/root/bin/fetch.js');
+ case "gpt": return getman('/root/bin/gpt.js');
+ case "clear": return getman('/root/bin/clear.js');
+ case "shell": return manshell;
+ case "man": default: return man;
+ }
+}
+
+export let man = `
+
+MAN(1)
+
+NAME
+\tman – display manual pages
+
+SEE ALSO
+\tfetch(1), gpt(1), shell(1), clear(1)
+
+`;
+
+export let manshell = `
+
+SHELL(1)
+
+NAME
+\tshell – command interpreter
+
+DESCRIPTION
+\tshell is a shell for my startpage. It's not POSIX nor will it ever be. It's
+\tsole job it to interpret text and run a command if a valid one is found.
+\tWhile this is quite unintuitive in a standard UNIX shell in my opinion this
+\tmakes sense here because of such a limited number of commands.
+
+`;
diff --git a/startpage/style.css b/startpage/style.css
new file mode 100644
index 0000000..d803252
--- /dev/null
+++ b/startpage/style.css
@@ -0,0 +1,103 @@
+:root {
+ --bg: #161617;
+ --fg: #c9c7cd;
+ --bg-dark: #131314;
+
+ /* Normal */
+ --black: #27272a;
+ --red: #f5a191;
+ --green: #90b99f;
+ --yellow: #e6b99d;
+ --blue: #aca1cf;
+ --magenta: #e29eca;
+ --cyan: #ea83a5;
+ --white: #c1c0d4;
+ /* Bright */
+ --bright-black: #353539;
+ --bright-red: #ffae9f;
+ --bright-green: #9dc6ac;
+ --bright-yellow: #f0c5a9;
+ --bright-blue: #b9aeda;
+ --bright-magenta: #ecaad6;
+ --bright-cyan: #f591b2;
+ --bright-white: #cac9dd;
+ /* Grays */
+ --gray01: #1b1b1d;
+ --gray02: #2a2a2d;
+ --gray03: #3e3e43;
+ --gray04: #57575f;
+ --gray05: #757581;
+ --gray06: #9998a8;
+ --gray07: var(--white);
+}
+
+html, body {
+ background-color: var(--bg-dark);
+ max-width: 80ch;
+ max-height: 100%;
+ margin: auto;
+}
+
+@media (orientation: portrait) {
+ html, body {
+ max-width: 100% !important;
+ padding: 5px !important;
+ }
+}
+
+#font, p, ul, ol, h1, h2, h3, h4, h5, table {
+ font-family: sans-serif;
+ color: white;
+}
+pre {
+ color: white;
+}
+h1 {
+ font-size: 3em;
+}
+a {
+ font-family: sans-serif;
+ text-decoration: none;
+ color: var(--cyan);
+}
+a:hover, a:active {
+ text-decoration: underline;
+}
+
+#search {
+ display: flex;
+ align-items: center;
+
+ #search-bar, #search-btn {
+ height: 2.25em;
+ background-color: var(--bg);
+ color: white;
+ padding: 0 15px;
+ border: none;
+ }
+ #search-bar {
+ width: 100%;
+ }
+ #search-bar:focus {
+ outline: none;
+ background-color: var(--gray01);
+ }
+ #search-btn {
+ background-color: var(--yellow);
+ color: var(--bg);
+ font-weight: bold;
+ cursor: pointer;
+ }
+ #search-btn:focus {
+ outline: none;
+ background-color: var(--bright-yellow);
+ }
+}
+#search-results {
+ height: 15lh;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ padding: 15px;
+ overflow: scroll;
+ background-color: #111112;
+}