This commit is contained in:
GW_MC
2026-05-25 03:43:34 +00:00
commit e5cac44ce5
59 changed files with 14461 additions and 0 deletions

78
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,78 @@
FROM mcr.microsoft.com/devcontainers/typescript-node:24-bookworm
# Arguments for the non-root user (provided by VS Code remote extensions)
ARG USERNAME=node
# Install system packages required by Tauri on Linux and packages for VNC/noVNC
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
ca-certificates \
pkg-config \
libgtk-3-dev \
libgdk-pixbuf2.0-dev \
libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
libgdk-pixbuf2.0-dev \
librsvg2-dev \
xvfb \
xauth \
dbus-x11 \
xfce4 \
xfce4-goodies \
x11vnc \
websockify \
python3 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
# Install rustup and the stable toolchain into the non-root user's home
ENV RUSTUP_HOME=/home/${USERNAME}/.rustup \
CARGO_HOME=/home/${USERNAME}/.cargo \
PATH=/home/${USERNAME}/.cargo/bin:/usr/local/cargo/bin:$PATH
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup-init.sh \
&& chmod +x /tmp/rustup-init.sh \
&& su - ${USERNAME} -c "/tmp/rustup-init.sh -y" \
&& rm /tmp/rustup-init.sh
# Enable Corepack (for pnpm) and install latest pnpm shim
RUN corepack enable \
&& corepack prepare pnpm@latest --activate
# Install the JS Tauri CLI globally for convenience
RUN npm install -g @tauri-apps/cli
# Switch to the non-root user for user-local installs
USER ${USERNAME}
# Install `just` (task runner) into the user's cargo bin
RUN cargo install --locked just || true
WORKDIR /workspace
# Temporarily switch back to root to install noVNC into /opt (root-owned)
USER root
# clone noVNC so we can proxy VNC over WebSockets (browser access)
RUN git clone https://github.com/novnc/noVNC.git /opt/noVNC \
&& git clone https://github.com/novnc/websockify.git /opt/noVNC/utils/websockify \
&& chown -R ${USERNAME}:${USERNAME} /opt/noVNC
# Ensure novnc scripts are executable
RUN chmod +x /opt/noVNC/utils/novnc_proxy || true
# Return to the non-root user for the rest of the build
USER ${USERNAME}
# copy the VNC start script (added in devcontainer) and make it executable
COPY --chown=${USERNAME}:${USERNAME} start-vnc.sh /workspace/.devcontainer/start-vnc.sh
RUN chmod +x /workspace/.devcontainer/start-vnc.sh
EXPOSE 5900 6080

32
.devcontainer/README.md Normal file
View File

@@ -0,0 +1,32 @@
This devcontainer provides a development environment for the Otter Tauri app.
What it includes
- Node 24 (from base image)
- pnpm (via Corepack)
- Rust (installed with rustup, stable toolchain)
- @tauri-apps/cli (global npm install)
- `just` (installed via `cargo install just`)
- Linux libraries required by Tauri: libwebkit2gtk, libssl, GTK, appindicator, etc.
How to use
1. Open this repository in VS Code.
2. When prompted, reopen in container. If not prompted: Command Palette → Remote-Containers: Reopen in Container.
3. After the container builds, the `postCreateCommand` will run: it enables Corepack, activates `pnpm`, runs `pnpm install` and `cargo fetch`.
Notes
- If you need additional system libraries for your distribution, edit `Dockerfile`.
- Forwarded ports:
- `5173` (Vite dev server)
- `5900` (VNC server - direct VNC connection)
- `6080` (noVNC web interface - access via browser)
- `just` is available in the container; run `just` to execute repository tasks.
VNC / noVNC (Remote Desktop)
- This devcontainer includes a headless X11 environment with XFCE4 desktop.
- To start VNC: Run `just start-vnc` in the terminal
- To stop VNC: Run `just stop-vnc`
- Access the desktop via:
1. **Browser (recommended)**: http://localhost:6080/vnc.html
2. **VNC client**: Connect to `localhost:5900` with password `devpass`
- Default VNC password: `devpass` (set via `VNC_PASSWORD` environment variable)
- If you see "Port 6080 already in use", the noVNC service is already running.

View File

@@ -0,0 +1,41 @@
{
"name": "Otter — Tauri Devcontainer",
"build": {
"dockerfile": "Dockerfile"
},
"features": {
// Add Docker-in-Docker to allow act to run containers
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
// Add act
"ghcr.io/devcontainers-extra/features/act:1": {}
},
"customizations": {
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
"extensions": ["rust-lang.rust-analyzer", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "nefrob.vscode-just-syntax"]
},
"postCreateCommand": "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && cd src-tauri && cargo fetch && cd -",
"forwardPorts": [5173, 5900, 6080],
"portsAttributes": {
"5173": {
"label": "Vite Dev Server",
"onAutoForward": "notify"
},
"5900": {
"label": "VNC Server",
"onAutoForward": "silent"
},
"6080": {
"label": "noVNC Web (VNC via Browser)",
"protocol": "http"
}
},
"remoteEnv": { "VNC_PASSWORD": "devpass", "DISPLAY": "${localEnv:DISPLAY}" },
"runArgs": ["-e", "DISPLAY=${localEnv:DISPLAY}"],
"mounts": ["source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached"],
"workspaceFolder": "/workspace",
"remoteUser": "node"
}

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
# Manual wrapper to start the VNC services on demand.
# Use this instead of automatic postStartCommand.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="${SCRIPT_DIR%/.*}/.."
LOGDIR=/tmp
OUT_LOG="$LOGDIR/start-vnc.manual.log"
echo "Starting VNC services (logs -> $OUT_LOG)"
# Ensure underlying start script exists
if [ ! -x "$SCRIPT_DIR/start-vnc.sh" ] && [ -f "$SCRIPT_DIR/start-vnc.sh" ]; then
chmod +x "$SCRIPT_DIR/start-vnc.sh" || true
fi
if [ ! -f "$SCRIPT_DIR/start-vnc.sh" ]; then
echo "Error: start-vnc.sh not found in $SCRIPT_DIR"
exit 1
fi
# Run the original script in the background and stream logs to the terminal
nohup bash "$SCRIPT_DIR/start-vnc.sh" >"$OUT_LOG" 2>&1 &
PID=$!
echo "start-vnc.sh started with PID $PID"
echo "Tailing logs (press Ctrl-C to stop tailing):"
sleep 1
tail -n +1 -f "$OUT_LOG"

View File

@@ -0,0 +1,27 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>noVNC (local-scaling wrapper)</title>
<script>
// Try several possible setting keys used by different noVNC versions
try {
localStorage.setItem('resize', 'scale');
localStorage.setItem('scaleViewport', 'true');
localStorage.setItem('scalingMode', 'local');
localStorage.setItem('preferLocalScaling', 'true');
localStorage.setItem('viewOnly', localStorage.getItem('viewOnly') || 'false');
} catch (e) {
// ignore
}
// Load the original noVNC UI (we will rename the original to vnc.orig.html)
var orig = 'vnc.orig.html' + window.location.search + window.location.hash;
// Use replace so back button doesn't loop
window.location.replace(orig);
</script>
</head>
<body>
<p>Applying local-scaling defaults and loading noVNC...</p>
</body>
</html>

138
.devcontainer/start-vnc.sh Executable file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env bash
set -euo pipefail
# Start a headless X server, a lightweight desktop, x11vnc and noVNC proxy.
# Runs as the container user (configured via devcontainer.json remoteUser).
VNC_PASS=${VNC_PASSWORD:-devpass}
VNC_DIR="$HOME/.vnc"
mkdir -p "$VNC_DIR"
if [ ! -f "$VNC_DIR/passwd" ]; then
echo "Creating VNC password"
x11vnc -storepasswd "$VNC_PASS" "$VNC_DIR/passwd"
fi
# Prefer using an existing X server (host) when available. Set USE_EXISTING_DISPLAY=true
# to force using the existing DISPLAY; otherwise, if a host DISPLAY is propagated into the
# container (via devcontainer runArgs / mounts), prefer that instead of starting Xvfb.
if [ "${USE_EXISTING_DISPLAY:-}" = "true" ] || { [ -n "${DISPLAY:-}" ] && [ -d "/tmp/.X11-unix" ]; }; then
echo "Using existing X server on ${DISPLAY:-<unset>} (X11 forwarding to host)"
export USE_EXISTING_DISPLAY=true
else
echo "Prefer Xvfb — starting Xvfb :1"
if command -v Xvfb >/dev/null 2>&1; then
# Define virtual screen size once; will try to confirm at runtime
# using xdpyinfo and fall back to these values if xdpyinfo isn't available.
SCREEN_W=1280
SCREEN_H=800
# Use a slightly taller virtual screen so desktop panels/toolbars
# don't cause the noVNC client to show an unexpected area below
# the visible display; use variables so we can clip x11vnc to
# exactly the same region and avoid framebuffer artifacts.
Xvfb :1 -screen 0 "${SCREEN_W}x${SCREEN_H}x24" >/tmp/xvfb.log 2>&1 &
export DISPLAY=:1
else
echo "Warning: Xvfb not available; expecting an X server on DISPLAY"
fi
fi
sleep 1
echo "Starting desktop session (xfce)"
# startxfce4 backgrounds itself; if not available fall back to openbox
if command -v startxfce4 >/dev/null 2>&1; then
startxfce4 >/tmp/xfce4.log 2>&1 &
else
if command -v openbox-session >/dev/null 2>&1; then
openbox-session >/tmp/openbox.log 2>&1 &
fi
fi
sleep 1
echo "Starting x11vnc on ${DISPLAY:-:1}"
if [ "${USE_EXISTING_DISPLAY:-}" != "true" ]; then
# Query the actual display size (xdpyinfo) to compute a precise clip region
# and avoid sending framebuffer rows outside the visible desktop. Disable
# client-side caching which can sometimes produce visual artifacts.
GEOM=""
if command -v xdpyinfo >/dev/null 2>&1; then
GEOM=$(xdpyinfo -display "${DISPLAY:-:1}" 2>/dev/null | awk '/dimensions:/ {print $2}') || true
fi
if [ -n "$GEOM" ]; then
# GEOM looks like WxH (e.g. 1280x800)
W=${GEOM%x*}
H=${GEOM#*x}
else
W=${SCREEN_W}
H=${SCREEN_H}
fi
echo "Starting x11vnc with clip ${W}x${H}+0+0 (display ${DISPLAY:-:1})"
x11vnc -display "${DISPLAY:-:1}" -rfbauth "$VNC_DIR/passwd" -forever -shared -rfbport 5900 -clip ${W}x${H}+0+0 -bg || true
else
echo "USE_EXISTING_DISPLAY=true — skipping x11vnc (display forwarded to host)"
fi
sleep 1
# Locate noVNC installation. Prefer /opt/noVNC, then workspace/noVNC.
NOVNC_DIR=""
if [ -d /opt/noVNC ]; then
NOVNC_DIR="/opt/noVNC"
elif [ -d "$PWD/noVNC" ]; then
NOVNC_DIR="$PWD/noVNC"
# Ensure it's available under /opt for tools that expect it there
if [ ! -e /opt/noVNC ]; then
mkdir -p /opt
ln -s "$NOVNC_DIR" /opt/noVNC || true
fi
fi
# Start the noVNC proxy only when we are running an internal VNC server on port 5900
if [ -n "$NOVNC_DIR" ] && [ "${USE_EXISTING_DISPLAY:-}" != "true" ]; then
echo "Found noVNC at $NOVNC_DIR"
# If a wrapper exists in the devcontainer, install it into the noVNC web
# root so we can set localStorage defaults (e.g. prefer local scaling)
WRAPPER="${SCRIPT_DIR:-$(pwd)}/novnc/vnc-wrapper.html"
if [ -f "$WRAPPER" ]; then
# Backup original vnc.html once and replace with wrapper
if [ -f "$NOVNC_DIR/vnc.html" ] && [ ! -f "$NOVNC_DIR/vnc.orig.html" ]; then
echo "Backing up original noVNC vnc.html to vnc.orig.html and installing wrapper"
cp "$NOVNC_DIR/vnc.html" "$NOVNC_DIR/vnc.orig.html" || true
cp "$WRAPPER" "$NOVNC_DIR/vnc.html" || true
fi
fi
echo "Waiting for VNC server on localhost:5900..."
ATTEMPTS=0
until ss -ltnp | grep -q ':5900' || [ $ATTEMPTS -ge 10 ]; do
ATTEMPTS=$((ATTEMPTS+1))
sleep 1
done
if ss -ltnp | grep -q ':5900'; then
# If port 6080 is already bound, report the owner and skip starting a new proxy.
if ss -ltnp | grep -q ':6080'; then
echo "Port 6080 already in use. Current listener:"
ss -ltnp | egrep ':6080' || true
echo "Skipping starting another noVNC proxy to avoid address-in-use errors."
else
echo "Starting noVNC proxy on port 6080"
# Prefer the bundled novnc_proxy script; pass explicit --web and bind address
NOVNC_PROXY="$NOVNC_DIR/utils/novnc_proxy"
if [ -x "$NOVNC_PROXY" ]; then
nohup "$NOVNC_PROXY" --vnc localhost:5900 --listen 0.0.0.0:6080 --web "$NOVNC_DIR" >/tmp/novnc.log 2>&1 &
else
nohup python3 "$NOVNC_DIR/utils/novnc_proxy" --vnc localhost:5900 --listen 0.0.0.0:6080 --web "$NOVNC_DIR" >/tmp/novnc.log 2>&1 &
fi
fi
else
echo "VNC server not listening on :5900, skipping noVNC startup. See logs for details."
fi
else
if [ "${USE_EXISTING_DISPLAY:-}" = "true" ]; then
echo "Using host X11 forwarding; skipping noVNC/websockify startup."
fi
fi
echo "VNC service started. Connect to :5900 or open http://<host>:6080/vnc.html"