Initial extract: templates + methodology guide from Ideation

Public, version-controlled source of truth for TeamIDE's project
templates, coding conventions, and the build-plan → PM scaffolding
rules. Mounted read-only by TeamIDE's planner agent at runtime.

Extracted from the personal Ideation repo as a precondition for
workspace-projects-agent-spec.md §9.4. Personal Ideation stays
private; this repo holds only the publishable methodology.
This commit is contained in:
2026-05-16 10:01:25 -05:00
commit 83c52bc011
95 changed files with 13727 additions and 0 deletions
+348
View File
@@ -0,0 +1,348 @@
#!/bin/bash
#
# Desktop Build Script Template (Tauri)
# Builds the Tauri desktop app for macOS
#
# Usage:
# ./build.sh -dev # Development build (runs from build folder)
# ./build.sh -dev -open # Development build and open app
# ./build.sh -prod # Production build (builds to build/prod/)
# ./build.sh -install # Build and open Finder for drag-to-Applications
# ./build.sh clean # Clean build folder (preserves logs)
# ./build.sh clean-logs # Clean all logs
# ./build.sh logs # View latest console log
#
# ============================================================
# PROJECT CONFIGURATION - Customize these for your project
# ============================================================
APP_NAME="TauriStarter" # Display name of the app
BUNDLE_ID_BASE="com.tauristarter.app" # Base bundle ID
VERSION="0.1.0" # App version
BINARY_NAME="TauriStarter" # Rust binary name (matches productName in tauri.conf.json)
# ============================================================
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() { echo -e "${BLUE}[Build]${NC} $1"; }
success() { echo -e "${GREEN}[Build]${NC} $1"; }
warn() { echo -e "${YELLOW}[Build]${NC} $1"; }
error() { echo -e "${RED}[Build]${NC} $1"; exit 1; }
# Paths
TAURI_DIR="$SCRIPT_DIR/src-tauri"
# Build folder structure
BUILD_DIR="$SCRIPT_DIR/build"
DEV_BUILD_DIR="$BUILD_DIR/dev"
PROD_BUILD_DIR="$BUILD_DIR/prod"
LOGS_DIR="$BUILD_DIR/logs"
CONSOLE_DIR="$BUILD_DIR/console"
PROD_INSTALL_DIR="$HOME/Applications"
# Timestamp for log files
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
# Parse arguments
BUILD_MODE=""
OPEN_APP=false
SHOW_INSTALL=false
CLEAN_BUILD=false
CLEAN_LOGS=false
SHOW_LOGS=false
SECURE_BUILD=false
while [[ $# -gt 0 ]]; do
case $1 in
-prod|--prod|--production)
BUILD_MODE="prod"
shift
;;
-dev|--dev|--development)
BUILD_MODE="dev"
shift
;;
-open|--open)
OPEN_APP=true
shift
;;
-install|--install)
SHOW_INSTALL=true
BUILD_MODE="prod"
shift
;;
-secure|--secure)
SECURE_BUILD=true
shift
;;
clean|--clean)
CLEAN_BUILD=true
shift
;;
clean-logs|--clean-logs)
CLEAN_LOGS=true
shift
;;
logs|--logs)
SHOW_LOGS=true
shift
;;
-h|--help)
echo "$APP_NAME Build Script"
echo ""
echo "Usage: $0 [-dev|-prod] [-open] [-secure] [clean|clean-logs|logs]"
echo ""
echo "Build Commands:"
echo " -dev, --dev Development build (runs from build folder)"
echo " -prod, --prod Production build (builds to build/prod/)"
echo " -install Build prod and open Finder for drag-to-Applications"
echo " -open, --open Open the app after building"
echo " -secure Build without automation (no HTTP server on port 9876)"
echo ""
echo "Other Commands:"
echo " clean Clean build artifacts (preserves logs)"
echo " clean-logs Remove all logs"
echo " logs Show latest console log (last 100 lines)"
echo " -h, --help Show this help"
echo ""
echo "Build folders:"
echo " Dev: $DEV_BUILD_DIR"
echo " Prod: $PROD_BUILD_DIR (also installs to $PROD_INSTALL_DIR)"
echo " Logs: $LOGS_DIR"
echo " Console: $CONSOLE_DIR"
echo ""
echo "Examples:"
echo " ./build.sh -dev -open # Build dev and open app"
echo " ./build.sh -prod # Build prod to build/prod/"
echo " ./build.sh -prod -secure # Build prod without automation"
echo " ./build.sh -install # Build and open Finder to drag to Applications"
echo " ./build.sh logs # View latest console output"
exit 0
;;
*)
error "Unknown option: $1. Use --help for usage."
;;
esac
done
# Handle logs command
if [ "$SHOW_LOGS" = true ]; then
if [ -f "$CONSOLE_DIR/console_latest.log" ]; then
log "Latest console log:"
echo "----------------------------------------"
tail -100 "$CONSOLE_DIR/console_latest.log"
else
warn "No console logs found at $CONSOLE_DIR/console_latest.log"
fi
exit 0
fi
# Handle clean-logs command
if [ "$CLEAN_LOGS" = true ]; then
log "Cleaning logs..."
rm -rf "$LOGS_DIR" "$CONSOLE_DIR"
success "Logs cleaned!"
exit 0
fi
# Handle clean command
if [ "$CLEAN_BUILD" = true ]; then
log "Cleaning build folder (preserving logs)..."
# Backup logs
if [ -d "$LOGS_DIR" ]; then
mv "$LOGS_DIR" "$SCRIPT_DIR/.logs_backup" 2>/dev/null || true
fi
if [ -d "$CONSOLE_DIR" ]; then
mv "$CONSOLE_DIR" "$SCRIPT_DIR/.console_backup" 2>/dev/null || true
fi
# Clean build dir
rm -rf "$BUILD_DIR"
# Clean other build artifacts
rm -rf "$SCRIPT_DIR/dist"
rm -rf "$SCRIPT_DIR/.quasar"
rm -rf "$TAURI_DIR/target"
# Restore logs
mkdir -p "$BUILD_DIR"
if [ -d "$SCRIPT_DIR/.logs_backup" ]; then
mv "$SCRIPT_DIR/.logs_backup" "$LOGS_DIR"
fi
if [ -d "$SCRIPT_DIR/.console_backup" ]; then
mv "$SCRIPT_DIR/.console_backup" "$CONSOLE_DIR"
fi
success "Build folder cleaned!"
exit 0
fi
# Require a build mode if not running a command
if [ -z "$BUILD_MODE" ]; then
error "No build mode specified. Use -dev or -prod. See --help for usage."
fi
# Create log directories
mkdir -p "$LOGS_DIR"
mkdir -p "$CONSOLE_DIR"
# Set up logging
BUILD_LOG="$LOGS_DIR/build_${BUILD_MODE}_$TIMESTAMP.log"
CONSOLE_LOG="$CONSOLE_DIR/console_$TIMESTAMP.log"
# Create symlinks to latest logs
ln -sf "build_${BUILD_MODE}_$TIMESTAMP.log" "$LOGS_DIR/build_latest.log"
ln -sf "console_$TIMESTAMP.log" "$CONSOLE_DIR/console_latest.log"
# Logging function that writes to both console and file
log_both() {
echo -e "${BLUE}[Build]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$BUILD_LOG"
}
# Set paths based on build mode
if [ "$BUILD_MODE" = "prod" ]; then
APP_BUNDLE="$PROD_BUILD_DIR/$APP_NAME.app"
INSTALL_BUNDLE="$PROD_INSTALL_DIR/$APP_NAME.app"
BUNDLE_ID="$BUNDLE_ID_BASE"
else
APP_BUNDLE="$DEV_BUILD_DIR/$APP_NAME.app"
INSTALL_BUNDLE=""
BUNDLE_ID="${BUNDLE_ID_BASE}.dev"
fi
log_both "=========================================="
log_both "Building $APP_NAME ($BUILD_MODE mode)"
log_both "=========================================="
log_both "Build log: $BUILD_LOG"
log_both "Console log: $CONSOLE_LOG"
# Create build directories
mkdir -p "$DEV_BUILD_DIR"
mkdir -p "$PROD_BUILD_DIR"
# Kill existing instances before building
log_both "Stopping existing instances..."
pkill -f "$DEV_BUILD_DIR/$APP_NAME.app/Contents/MacOS" 2>/dev/null || true
pkill -f "$PROD_BUILD_DIR/$APP_NAME.app/Contents/MacOS" 2>/dev/null || true
pkill -f "$PROD_INSTALL_DIR/$APP_NAME.app/Contents/MacOS" 2>/dev/null || true
sleep 1
# Check dependencies
command -v node >/dev/null 2>&1 || error "Node.js is required but not installed"
command -v cargo >/dev/null 2>&1 || error "Rust/Cargo is required but not installed"
# Step 1: Install npm dependencies if needed
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
log_both "Installing npm dependencies..."
npm install 2>&1 | tee -a "$BUILD_LOG"
fi
# Step 2: Build with Tauri (builds frontend + Rust + creates bundle)
log_both "Building with Tauri (this embeds frontend into binary)..."
cd "$SCRIPT_DIR"
# Build cargo args for secure mode (no automation)
CARGO_ARGS=""
if [ "$SECURE_BUILD" = true ]; then
log_both "Secure build: disabling automation feature..."
CARGO_ARGS="-- --no-default-features --features custom-protocol"
fi
if [ "$SHOW_INSTALL" = true ]; then
# No --ci flag: opens Finder window for drag-to-Applications
npx tauri build $CARGO_ARGS 2>&1 | tee -a "$BUILD_LOG"
else
# --ci flag: suppress Finder window
npx tauri build --ci $CARGO_ARGS 2>&1 | tee -a "$BUILD_LOG"
fi
# Find the generated app bundle
TAURI_BUNDLE_DIR="$TAURI_DIR/target/release/bundle/macos"
TAURI_APP_BUNDLE="$TAURI_BUNDLE_DIR/$APP_NAME.app"
if [ ! -d "$TAURI_APP_BUNDLE" ]; then
error "Tauri build failed - app bundle not found at $TAURI_APP_BUNDLE"
fi
# Step 3: Copy the Tauri-built bundle to our build directory
log_both "Copying app bundle to $APP_BUNDLE..."
rm -rf "$APP_BUNDLE"
cp -R "$TAURI_APP_BUNDLE" "$APP_BUNDLE"
# Step 4: Update Info.plist with our bundle ID (dev vs prod)
log_both "Updating Info.plist with bundle ID: $BUNDLE_ID..."
if [ "$BUILD_MODE" = "dev" ]; then
DISPLAY_NAME="$APP_NAME (Dev)"
else
DISPLAY_NAME="$APP_NAME"
fi
# Use PlistBuddy to update the plist
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$APP_BUNDLE/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $DISPLAY_NAME" "$APP_BUNDLE/Contents/Info.plist" 2>/dev/null || \
/usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $DISPLAY_NAME" "$APP_BUNDLE/Contents/Info.plist"
# Step 5: For prod, also install to ~/Applications
if [ "$BUILD_MODE" = "prod" ] && [ -n "$INSTALL_BUNDLE" ]; then
log_both "Installing to $INSTALL_BUNDLE..."
pkill -f "$INSTALL_BUNDLE/Contents/MacOS" 2>/dev/null || true
sleep 1
rm -rf "$INSTALL_BUNDLE"
mkdir -p "$PROD_INSTALL_DIR"
cp -R "$APP_BUNDLE" "$INSTALL_BUNDLE"
touch "$INSTALL_BUNDLE"
fi
# Touch the app to update Finder cache
touch "$APP_BUNDLE"
# Build complete
log_both "=========================================="
success "Build Complete!"
log_both "=========================================="
echo ""
echo " Build folder: $BUILD_DIR"
echo " App bundle: $APP_BUNDLE"
echo " Mode: $BUILD_MODE"
echo " Bundle ID: $BUNDLE_ID"
if [ "$SECURE_BUILD" = true ]; then
echo " Automation: DISABLED (secure build)"
else
echo " Automation: enabled (port 9876)"
fi
if [ "$BUILD_MODE" = "prod" ] && [ -n "$INSTALL_BUNDLE" ]; then
echo " Installed to: $INSTALL_BUNDLE"
fi
echo ""
echo " Build log: $BUILD_LOG"
echo " Console log: $CONSOLE_LOG"
echo ""
# Open the app and capture console output
if [ "$OPEN_APP" = true ]; then
log_both "Opening app (console output will be logged)..."
# Run the app in background with output to console log
"$APP_BUNDLE/Contents/MacOS/$BINARY_NAME" >> "$CONSOLE_LOG" 2>&1 &
APP_PID=$!
echo " App started (PID: $APP_PID)"
echo " Console log: $CONSOLE_LOG"
echo ""
echo " View logs with: ./build.sh logs"
else
echo " To run: open \"$APP_BUNDLE\""
echo " Or with logging: \"$APP_BUNDLE/Contents/MacOS/$BINARY_NAME\" 2>&1 | tee -a \"$CONSOLE_LOG\""
fi
echo ""
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri Starter</title>
</head>
<body>
<div id="q-app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
{
"name": "tauri-starter",
"version": "0.1.0",
"description": "Quasar + Vite + Tauri starter template",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
},
"dependencies": {
"@quasar/extras": "^1.16.9",
"pinia": "^2.1.7",
"quasar": "^2.14.2",
"vue": "^3.4.18",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@quasar/vite-plugin": "^1.10.0",
"@tauri-apps/api": "^1.5.3",
"@tauri-apps/cli": "^1.5.11",
"@types/node": "^20.11.16",
"@vitejs/plugin-vue": "^5.2.4",
"sass": "^1.70.0",
"typescript": "^5.3.3",
"vite": "^5.0.12"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
[package]
name = "tauri-starter"
version = "0.1.0"
description = "Tauri Starter App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
[build-dependencies]
tauri-build = { version = "1.5", features = [] }
[dependencies]
tauri = { version = "1.6", features = ["shell-open"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"
tiny_http = "0.12" # For automation HTTP server
[features]
default = ["custom-protocol", "automation"]
custom-protocol = ["tauri/custom-protocol"]
automation = [] # Enable automation HTTP server on port 9876
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,166 @@
# Tauri Automation Plugin
A lightweight, reusable automation layer for Tauri applications. Enables external tools to interact with your app via HTTP API.
## Features
- **HTTP API** on `localhost:9876` for external automation
- **Navigation**: Route to pages
- **Element interaction**: Click, type, clear, select
- **Reading**: Get text, values, attributes, check existence
- **Waiting**: Wait for elements with timeout
- **Screenshots**: Capture page as PNG
- **Debug mode only**: Automatically disabled in release builds
## Architecture
```
┌─────────────────┐ HTTP ┌──────────────────┐
│ External Tool │ ───────────▶ │ Rust HTTP Server │
│ (curl, tests) │ │ (port 9876) │
└─────────────────┘ └────────┬─────────┘
│
│ Tauri Command
▼
┌──────────────────┐
│ JavaScript │
│ Automation │
│ Service │
└────────┬─────────┘
│
│ DOM APIs
▼
┌──────────────────┐
│ WebView │
│ (Your App) │
└──────────────────┘
```
## HTTP API
### Endpoints
| Method | Path | Description |
|--------|------|-------------|
| POST | `/automation/execute` | Execute a command |
| GET | `/automation/screenshot` | Capture screenshot (PNG) |
| GET | `/automation/health` | Health check |
### Commands
```bash
# Navigate to a route
curl -X POST http://localhost:9876/automation/execute \
-H "Content-Type: application/json" \
-d '{"command": "navigate", "args": {"path": "/profile"}}'
# Click an element
curl -X POST http://localhost:9876/automation/execute \
-H "Content-Type: application/json" \
-d '{"command": "click", "args": {"selector": "#submit-btn"}}'
# Type into an input
curl -X POST http://localhost:9876/automation/execute \
-H "Content-Type: application/json" \
-d '{"command": "type", "args": {"selector": "#email", "text": "test@example.com"}}'
# Get element text
curl -X POST http://localhost:9876/automation/execute \
-H "Content-Type: application/json" \
-d '{"command": "getText", "args": {"selector": ".heading"}}'
# Wait for element
curl -X POST http://localhost:9876/automation/execute \
-H "Content-Type: application/json" \
-d '{"command": "waitFor", "args": {"selector": ".loaded", "timeout": 5000}}'
# Take screenshot
curl http://localhost:9876/automation/screenshot --output screenshot.png
# Health check
curl http://localhost:9876/automation/health
```
### Available Commands
| Command | Args | Returns | Description |
|---------|------|---------|-------------|
| `navigate` | `{path}` | `null` | Navigate to route |
| `click` | `{selector}` | `null` | Click element |
| `type` | `{selector, text}` | `null` | Type into input |
| `clear` | `{selector}` | `null` | Clear input value |
| `getText` | `{selector}` | `string` | Get element text |
| `getValue` | `{selector}` | `string` | Get input value |
| `getAttribute` | `{selector, attribute}` | `string` | Get attribute |
| `exists` | `{selector}` | `boolean` | Check if element exists |
| `waitFor` | `{selector, timeout?}` | `boolean` | Wait for element (default 5s) |
| `select` | `{selector, value}` | `null` | Select dropdown option |
| `check` | `{selector}` | `null` | Check checkbox |
| `uncheck` | `{selector}` | `null` | Uncheck checkbox |
| `getUrl` | `{}` | `string` | Get current URL |
| `getTitle` | `{}` | `string` | Get page title |
| `eval` | `{script}` | `any` | Evaluate JavaScript |
## Installation (Adding to Another Project)
### 1. Copy Rust files
Copy `src-tauri/src/automation/` to your project's `src-tauri/src/`.
### 2. Update Cargo.toml
Add dependencies:
```toml
[dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
warp = "0.3"
serde_json = "1.0"
```
### 3. Update main.rs
```rust
#[cfg(debug_assertions)]
mod automation;
fn main() {
let mut builder = tauri::Builder::default();
#[cfg(debug_assertions)]
{
builder = builder
.plugin(automation::init())
.invoke_handler(tauri::generate_handler![
automation::commands::execute_automation
]);
}
builder
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
### 4. Copy JS files
Copy `src/automation/` to your project's `src/`.
### 5. Initialize in App.vue
```typescript
import { initAutomation } from './automation'
onMounted(() => {
initAutomation()
})
```
## Security
- **Debug builds only**: The HTTP server only runs in debug builds (`#[cfg(debug_assertions)]`)
- **Localhost only**: Server binds to `127.0.0.1`, not accessible externally
- **No auth**: Designed for local development/testing only
## License
MIT - Use freely in your projects.
@@ -0,0 +1,78 @@
//! Tauri Automation Plugin
//!
//! Provides HTTP API for external automation of the application.
//! Enabled via the "automation" feature flag.
pub mod server;
use std::sync::{Arc, Mutex};
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime, Window,
};
/// Shared state for automation
pub struct AutomationState {
pub window: Arc<Mutex<Option<Window<tauri::Wry>>>>,
}
impl Default for AutomationState {
fn default() -> Self {
Self {
window: Arc::new(Mutex::new(None)),
}
}
}
/// Initialize the automation plugin
pub fn init() -> TauriPlugin<tauri::Wry> {
Builder::new("automation")
.setup(|app| {
let state = AutomationState::default();
app.manage(state);
// Start HTTP server in background thread
let app_handle = app.clone();
std::thread::spawn(move || {
server::start_server(app_handle);
});
println!("[Automation] Plugin initialized - HTTP server starting on port 9876");
Ok(())
})
.build()
}
/// Execute an automation command via JavaScript evaluation
pub fn execute_command(window: &Window<tauri::Wry>, command: &str, args: &serde_json::Value) -> Result<serde_json::Value, String> {
let args_json = serde_json::to_string(args).map_err(|e| e.to_string())?;
let script = format!(
r#"
(async function() {{
try {{
if (typeof window.__TAURI_AUTOMATION__ === 'undefined') {{
return JSON.stringify({{ error: 'Automation not initialized. Call initAutomation() first.' }});
}}
const result = await window.__TAURI_AUTOMATION__.execute('{}', {});
return JSON.stringify({{ success: true, result: result }});
}} catch (e) {{
return JSON.stringify({{ error: e.message || String(e) }});
}}
}})()
"#,
command,
args_json
);
// Execute JavaScript and get result
let result = window.eval(&script);
match result {
Ok(_) => {
// For Tauri 1.x, eval doesn't return a value directly
Ok(serde_json::json!({ "success": true, "note": "Command sent to webview" }))
}
Err(e) => Err(format!("Failed to execute script: {}", e))
}
}
@@ -0,0 +1,326 @@
//! HTTP server for automation commands
use std::io::Read;
use std::sync::Mutex;
use tauri::{AppHandle, Manager};
use tiny_http::{Server, Response, Header, Method};
const PORT: u16 = 9876;
// Global screenshot buffer (simple approach without lazy_static)
static SCREENSHOT_DATA: Mutex<Option<String>> = Mutex::new(None);
pub fn set_screenshot_data(data: String) {
if let Ok(mut guard) = SCREENSHOT_DATA.lock() {
*guard = Some(data);
}
}
pub fn take_screenshot_data() -> Option<String> {
if let Ok(mut guard) = SCREENSHOT_DATA.lock() {
guard.take()
} else {
None
}
}
/// Start the HTTP server
pub fn start_server(app_handle: AppHandle<tauri::Wry>) {
let addr = format!("127.0.0.1:{}", PORT);
let server = match Server::http(&addr) {
Ok(s) => s,
Err(e) => {
eprintln!("[Automation] Failed to start server on {}: {}", addr, e);
return;
}
};
println!("[Automation] HTTP server listening on http://{}", addr);
for mut request in server.incoming_requests() {
let method = request.method().clone();
let url = request.url().to_string();
println!("[Automation] {} {}", method, url);
let response = match (&method, url.as_str()) {
// Health check
(&Method::Get, "/automation/health") => {
json_response(serde_json::json!({
"status": "ok",
"port": PORT,
"version": "1.0.0"
}))
}
// Execute command
(&Method::Post, "/automation/execute") => {
handle_execute(&app_handle, &mut request)
}
// Screenshot
(&Method::Get, "/automation/screenshot") => {
handle_screenshot(&app_handle)
}
// CORS preflight
(&Method::Options, _) => {
cors_response()
}
// 404
_ => {
json_response_with_status(
serde_json::json!({ "error": "Not found" }),
404
)
}
};
if let Err(e) = request.respond(response) {
eprintln!("[Automation] Failed to send response: {}", e);
}
}
}
/// Handle execute command request
fn handle_execute(
app_handle: &AppHandle<tauri::Wry>,
request: &mut tiny_http::Request,
) -> Response<std::io::Cursor<Vec<u8>>> {
// Read body
let mut body = String::new();
if let Err(e) = request.as_reader().read_to_string(&mut body) {
return json_response_with_status(
serde_json::json!({ "error": format!("Failed to read body: {}", e) }),
400
);
}
// Parse JSON
let payload: serde_json::Value = match serde_json::from_str(&body) {
Ok(v) => v,
Err(e) => {
return json_response_with_status(
serde_json::json!({ "error": format!("Invalid JSON: {}", e) }),
400
);
}
};
let command = match payload.get("command").and_then(|v| v.as_str()) {
Some(c) => c.to_string(),
None => {
return json_response_with_status(
serde_json::json!({ "error": "Missing 'command' field" }),
400
);
}
};
let args = payload.get("args").cloned().unwrap_or(serde_json::json!({}));
// Get the main window
let window = match app_handle.get_window("main") {
Some(w) => w,
None => {
return json_response_with_status(
serde_json::json!({ "error": "Main window not found" }),
500
);
}
};
// Build JavaScript to execute
let args_json = serde_json::to_string(&args).unwrap_or_else(|_| "{}".to_string());
let script = format!(
r#"
(async function() {{
if (typeof window.__TAURI_AUTOMATION__ === 'undefined') {{
console.error('[Automation] Not initialized');
return;
}}
try {{
const result = await window.__TAURI_AUTOMATION__.execute('{}', {});
window.__TAURI_AUTOMATION__._lastResult = {{ success: true, result: result }};
}} catch (e) {{
window.__TAURI_AUTOMATION__._lastResult = {{ success: false, error: e.message || String(e) }};
}}
}})();
"#,
command,
args_json
);
// Execute the script
if let Err(e) = window.eval(&script) {
return json_response_with_status(
serde_json::json!({ "error": format!("Script execution failed: {}", e) }),
500
);
}
// Wait a bit for async commands to complete
std::thread::sleep(std::time::Duration::from_millis(100));
// Return success - the result is stored in the webview for debugging
json_response(serde_json::json!({
"success": true,
"message": "Command executed",
"command": command
}))
}
/// Handle screenshot request
fn handle_screenshot(app_handle: &AppHandle<tauri::Wry>) -> Response<std::io::Cursor<Vec<u8>>> {
let window = match app_handle.get_window("main") {
Some(w) => w,
None => {
return json_response_with_status(
serde_json::json!({ "error": "Main window not found" }),
500
);
}
};
// Request screenshot from JS
let script = r#"
(async function() {
if (typeof window.__TAURI_AUTOMATION__ === 'undefined') {
console.error('[Automation] Not initialized');
return;
}
try {
await window.__TAURI_AUTOMATION__.captureAndSend();
} catch (e) {
console.error('[Automation] Screenshot failed:', e);
}
})();
"#;
if let Err(e) = window.eval(script) {
return json_response_with_status(
serde_json::json!({ "error": format!("Screenshot request failed: {}", e) }),
500
);
}
// Wait for JS to send the screenshot data
// html2canvas can take a while, especially on first load
std::thread::sleep(std::time::Duration::from_millis(2000));
// Check if we have screenshot data
if let Some(data_url) = take_screenshot_data() {
// Parse data URL: data:image/png;base64,....
if let Some(base64_data) = data_url.strip_prefix("data:image/png;base64,") {
match base64_decode(base64_data) {
Ok(bytes) => {
return png_response(bytes);
}
Err(e) => {
return json_response_with_status(
serde_json::json!({ "error": format!("Base64 decode failed: {}", e) }),
500
);
}
}
}
}
json_response_with_status(
serde_json::json!({ "error": "Screenshot not available. Make sure html2canvas is loaded." }),
500
)
}
/// Simple base64 decoder
fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
let input = input.trim();
let chars: Vec<char> = input.chars().filter(|c| !c.is_whitespace()).collect();
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut output = Vec::new();
let mut buffer: u32 = 0;
let mut bits_collected = 0;
for c in chars {
if c == '=' {
break;
}
let value = ALPHABET.iter().position(|&x| x == c as u8)
.ok_or_else(|| format!("Invalid base64 character: {}", c))? as u32;
buffer = (buffer << 6) | value;
bits_collected += 6;
if bits_collected >= 8 {
bits_collected -= 8;
output.push((buffer >> bits_collected) as u8);
buffer &= (1 << bits_collected) - 1;
}
}
Ok(output)
}
/// Create a JSON response
fn json_response(data: serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
json_response_with_status(data, 200)
}
/// Create a JSON response with status
fn json_response_with_status(data: serde_json::Value, status: u16) -> Response<std::io::Cursor<Vec<u8>>> {
let body = serde_json::to_vec(&data).unwrap_or_else(|_| b"{}".to_vec());
let len = body.len();
let cursor = std::io::Cursor::new(body);
Response::new(
tiny_http::StatusCode(status),
vec![
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap(),
Header::from_bytes(&b"Access-Control-Allow-Methods"[..], &b"GET, POST, OPTIONS"[..]).unwrap(),
Header::from_bytes(&b"Access-Control-Allow-Headers"[..], &b"Content-Type"[..]).unwrap(),
],
cursor,
Some(len),
None,
)
}
/// Create a PNG response
fn png_response(data: Vec<u8>) -> Response<std::io::Cursor<Vec<u8>>> {
let len = data.len();
let cursor = std::io::Cursor::new(data);
Response::new(
tiny_http::StatusCode(200),
vec![
Header::from_bytes(&b"Content-Type"[..], &b"image/png"[..]).unwrap(),
Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap(),
],
cursor,
Some(len),
None,
)
}
/// CORS preflight response
fn cors_response() -> Response<std::io::Cursor<Vec<u8>>> {
let cursor = std::io::Cursor::new(Vec::new());
Response::new(
tiny_http::StatusCode(204),
vec![
Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap(),
Header::from_bytes(&b"Access-Control-Allow-Methods"[..], &b"GET, POST, OPTIONS"[..]).unwrap(),
Header::from_bytes(&b"Access-Control-Allow-Headers"[..], &b"Content-Type"[..]).unwrap(),
],
cursor,
Some(0),
None,
)
}
@@ -0,0 +1,52 @@
// Prevents additional console window on Windows in release
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use chrono::Local;
// Automation module - enabled via feature flag
#[cfg(feature = "automation")]
mod automation;
/// Log frontend console messages to stdout with timestamp
#[tauri::command]
fn log_to_stdout(level: String, message: String) {
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
println!("[{}] [{}] {}", timestamp, level, message);
}
/// Example command - replace with your own
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! Welcome to Tauri.", name)
}
/// Receive screenshot data from JS automation
#[cfg(feature = "automation")]
#[tauri::command]
fn automation_screenshot_data(data: String) {
automation::server::set_screenshot_data(data);
}
#[cfg(feature = "automation")]
fn main() {
tauri::Builder::default()
.plugin(automation::init())
.invoke_handler(tauri::generate_handler![
log_to_stdout,
greet,
automation_screenshot_data
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[cfg(not(feature = "automation"))]
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
log_to_stdout,
greet
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
@@ -0,0 +1,46 @@
{
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devPath": "http://localhost:9000",
"distDir": "../dist"
},
"package": {
"productName": "TauriStarter",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"open": true
}
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.tauristarter.app",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"security": {
"csp": null
},
"windows": [
{
"fullscreen": false,
"resizable": true,
"title": "Tauri Starter",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600
}
]
}
}
+7
View File
@@ -0,0 +1,7 @@
<template>
<router-view />
</template>
<script setup lang="ts">
// App root component
</script>
@@ -0,0 +1,141 @@
# Automation Service (JavaScript)
Frontend automation service that handles DOM manipulation commands from the Rust backend.
## Files
| File | Purpose |
|------|---------|
| `index.ts` | Main service initialization and screenshot capture |
| `commands.ts` | All DOM automation commands |
## Setup
Initialize in your app's entry point (App.vue or main.ts):
```typescript
import { initAutomation } from './automation'
// In your app setup (only runs in dev)
initAutomation()
```
## Available Commands
### Navigation
- `navigate` - Navigate to a Vue Router path
### Element Interaction
- `click` - Click an element
- `type` - Type text into an input
- `clear` - Clear an input value
- `select` - Select dropdown option
- `check` - Check a checkbox
- `uncheck` - Uncheck a checkbox
- `focus` - Focus an element
- `blur` - Blur an element
- `pressKey` - Simulate key press
- `submit` - Submit a form
- `scrollTo` - Scroll to element
### Reading
- `getText` - Get element text content
- `getValue` - Get input value
- `getAttribute` - Get element attribute
- `exists` - Check if element exists
- `getElements` - Get all matching elements
- `getHtml` - Get element HTML
- `getUrl` - Get current URL
- `getTitle` - Get page title
### Waiting
- `waitFor` - Wait for element to appear
- `wait` - Wait for specified milliseconds
### Advanced
- `eval` - Execute arbitrary JavaScript
## Adding Custom Commands
Add new commands to `commands.ts`:
```typescript
export const commands: Record<string, CommandHandler> = {
// ... existing commands
async myCustomCommand(args): Promise<void> {
const value = args.value as string
// Your implementation
},
}
```
## Screenshot Support
Screenshots use html2canvas, loaded dynamically from CDN:
- https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
For offline use, bundle html2canvas:
```bash
npm install html2canvas
```
Then update `index.ts` to import directly instead of CDN loading.
## Vue Router Integration
The `navigate` command tries to use Vue Router. For this to work, you need to expose the router on the window object. Update your `main.ts`:
```typescript
const app = createApp(App)
app.use(router)
// Expose for automation
if (import.meta.env.DEV) {
(window as any).__VUE_APP__ = app
}
app.mount('#app')
```
## Selectors
All selector arguments use standard CSS selectors:
```bash
# By ID
{"selector": "#my-id"}
# By class
{"selector": ".my-class"}
# By attribute
{"selector": "[data-testid='submit']"}
# Combined
{"selector": "form#login .btn-primary"}
# Quasar components often need to target inner elements
{"selector": ".q-input input"}
{"selector": ".q-btn"}
```
## Tips for Quasar
Quasar components have specific DOM structures:
| Component | Selector for inner element |
|-----------|---------------------------|
| q-input | `.q-input input` or `.q-input textarea` |
| q-select | `.q-select` (click to open) |
| q-btn | `.q-btn` |
| q-checkbox | `.q-checkbox` |
| q-tab | `.q-tab` |
Use `data-testid` attributes for reliable selectors:
```vue
<q-btn data-testid="submit-btn" label="Submit" />
```
```bash
{"selector": "[data-testid='submit-btn']"}
```
@@ -0,0 +1,387 @@
/**
* Automation Commands
*
* DOM manipulation commands for automated testing.
*/
export type CommandArgs = Record<string, unknown>
export type CommandResult = unknown
type CommandHandler = (args: CommandArgs) => Promise<CommandResult>
// Router type for type safety
interface VueRouter {
push: (path: string) => Promise<unknown>
currentRoute: { value: { path: string } }
}
// Get Vue Router instance from the app
function getRouter(): VueRouter | undefined {
// Access the router from the app instance stored on window
const app = (window as unknown as { __VUE_APP__?: { config: { globalProperties: { $router: VueRouter } } } }).__VUE_APP__
return app?.config.globalProperties.$router
}
/**
* Wait for an element to appear in the DOM
*/
async function waitForElement(selector: string, timeout = 5000): Promise<Element | null> {
const start = Date.now()
while (Date.now() - start < timeout) {
const element = document.querySelector(selector)
if (element) return element
await new Promise(r => setTimeout(r, 100))
}
return null
}
/**
* Get an element or throw
*/
function getElement(selector: string): Element {
const element = document.querySelector(selector)
if (!element) {
throw new Error(`Element not found: ${selector}`)
}
return element
}
/**
* Simulate realistic typing into an input
*/
function simulateTyping(element: HTMLInputElement | HTMLTextAreaElement, text: string): void {
element.focus()
element.value = text
// Dispatch events to trigger Vue reactivity
element.dispatchEvent(new Event('input', { bubbles: true }))
element.dispatchEvent(new Event('change', { bubbles: true }))
}
/**
* Available automation commands
*/
export const commands: Record<string, CommandHandler> = {
/**
* Navigate to a route
*/
async navigate(args): Promise<void> {
const path = args.path as string
if (!path) throw new Error('Missing path argument')
const router = getRouter()
if (router) {
await router.push(path)
} else {
// Fallback: use window.location
window.location.hash = path
}
// Wait for navigation to complete
await new Promise(r => setTimeout(r, 100))
},
/**
* Click an element
*/
async click(args): Promise<void> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const element = await waitForElement(selector, 5000)
if (!element) throw new Error(`Element not found: ${selector}`)
// Scroll into view
element.scrollIntoView({ behavior: 'instant', block: 'center' })
await new Promise(r => setTimeout(r, 50))
// Click
;(element as HTMLElement).click()
// Wait for any resulting actions
await new Promise(r => setTimeout(r, 100))
},
/**
* Type text into an input
*/
async type(args): Promise<void> {
const selector = args.selector as string
const text = args.text as string
if (!selector) throw new Error('Missing selector argument')
if (text === undefined) throw new Error('Missing text argument')
const element = await waitForElement(selector, 5000)
if (!element) throw new Error(`Element not found: ${selector}`)
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
simulateTyping(element, text)
} else {
throw new Error(`Element is not an input: ${selector}`)
}
},
/**
* Clear an input
*/
async clear(args): Promise<void> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const element = getElement(selector)
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
simulateTyping(element, '')
} else {
throw new Error(`Element is not an input: ${selector}`)
}
},
/**
* Get element text content
*/
async getText(args): Promise<string> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const element = await waitForElement(selector, 5000)
if (!element) throw new Error(`Element not found: ${selector}`)
return element.textContent?.trim() || ''
},
/**
* Get input value
*/
async getValue(args): Promise<string> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const element = getElement(selector)
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
return element.value
}
throw new Error(`Element is not an input: ${selector}`)
},
/**
* Get element attribute
*/
async getAttribute(args): Promise<string | null> {
const selector = args.selector as string
const attribute = args.attribute as string
if (!selector) throw new Error('Missing selector argument')
if (!attribute) throw new Error('Missing attribute argument')
const element = getElement(selector)
return element.getAttribute(attribute)
},
/**
* Check if element exists
*/
async exists(args): Promise<boolean> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
return document.querySelector(selector) !== null
},
/**
* Wait for element to appear
*/
async waitFor(args): Promise<boolean> {
const selector = args.selector as string
const timeout = (args.timeout as number) || 5000
if (!selector) throw new Error('Missing selector argument')
const element = await waitForElement(selector, timeout)
return element !== null
},
/**
* Select dropdown option
*/
async select(args): Promise<void> {
const selector = args.selector as string
const value = args.value as string
if (!selector) throw new Error('Missing selector argument')
if (value === undefined) throw new Error('Missing value argument')
const element = getElement(selector)
if (element instanceof HTMLSelectElement) {
element.value = value
element.dispatchEvent(new Event('change', { bubbles: true }))
} else {
throw new Error(`Element is not a select: ${selector}`)
}
},
/**
* Check a checkbox
*/
async check(args): Promise<void> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const element = getElement(selector)
if (element instanceof HTMLInputElement && element.type === 'checkbox') {
if (!element.checked) {
element.click()
}
} else {
throw new Error(`Element is not a checkbox: ${selector}`)
}
},
/**
* Uncheck a checkbox
*/
async uncheck(args): Promise<void> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const element = getElement(selector)
if (element instanceof HTMLInputElement && element.type === 'checkbox') {
if (element.checked) {
element.click()
}
} else {
throw new Error(`Element is not a checkbox: ${selector}`)
}
},
/**
* Get current URL/route
*/
async getUrl(): Promise<string> {
return window.location.href
},
/**
* Get page title
*/
async getTitle(): Promise<string> {
return document.title
},
/**
* Evaluate arbitrary JavaScript
*/
async eval(args): Promise<unknown> {
const script = args.script as string
if (!script) throw new Error('Missing script argument')
// Use Function constructor to evaluate in global scope
const fn = new Function(`return (async () => { ${script} })()`)
return await fn()
},
/**
* Get all elements matching selector
*/
async getElements(args): Promise<{ count: number; texts: string[] }> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const elements = document.querySelectorAll(selector)
const texts = Array.from(elements).map(el => el.textContent?.trim() || '')
return { count: elements.length, texts }
},
/**
* Scroll to element
*/
async scrollTo(args): Promise<void> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const element = getElement(selector)
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
await new Promise(r => setTimeout(r, 300))
},
/**
* Get page HTML
*/
async getHtml(args): Promise<string> {
const selector = (args.selector as string) || 'body'
const element = getElement(selector)
return element.innerHTML
},
/**
* Wait for a specified time
*/
async wait(args): Promise<void> {
const ms = (args.ms as number) || 1000
await new Promise(r => setTimeout(r, ms))
},
/**
* Focus an element
*/
async focus(args): Promise<void> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const element = getElement(selector) as HTMLElement
element.focus()
},
/**
* Blur (unfocus) an element
*/
async blur(args): Promise<void> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const element = getElement(selector) as HTMLElement
element.blur()
},
/**
* Press a key
*/
async pressKey(args): Promise<void> {
const key = args.key as string
const selector = args.selector as string
if (!key) throw new Error('Missing key argument')
const target = selector ? getElement(selector) : document.activeElement || document.body
target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
target.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }))
},
/**
* Submit a form
*/
async submit(args): Promise<void> {
const selector = args.selector as string
if (!selector) throw new Error('Missing selector argument')
const element = getElement(selector)
if (element instanceof HTMLFormElement) {
element.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
} else {
// Find parent form
const form = element.closest('form')
if (form) {
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
} else {
throw new Error('No form found')
}
}
},
}
@@ -0,0 +1,116 @@
/**
* Tauri Automation Service
*
* Provides automation capabilities for external testing tools.
* Only active in development builds.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { commands, type CommandArgs, type CommandResult } from './commands'
declare global {
interface Window {
__TAURI_AUTOMATION__: AutomationService
}
}
export interface AutomationService {
execute: (command: string, args: CommandArgs) => Promise<CommandResult>
screenshot: () => Promise<string>
captureAndSend: () => Promise<void>
_lastResult: { success: boolean; result?: unknown; error?: string } | null
}
/**
* Initialize the automation service
* Call this once when your app starts
* Note: Automation is controlled by Rust feature flag, JS side always initializes
*/
export function initAutomation(): void {
console.log('[Automation] Initializing...')
const service: AutomationService = {
_lastResult: null,
/**
* Execute an automation command
*/
async execute(command: string, args: CommandArgs = {}): Promise<CommandResult> {
console.log(`[Automation] Executing: ${command}`, args)
const handler = commands[command]
if (!handler) {
throw new Error(`Unknown command: ${command}`)
}
try {
const result = await handler(args)
console.log(`[Automation] Result:`, result)
return result
} catch (error) {
console.error(`[Automation] Error:`, error)
throw error
}
},
/**
* Capture screenshot and return as data URL
*/
async screenshot(): Promise<string> {
// Dynamically import html2canvas
const html2canvas = await loadHtml2Canvas()
const canvas = await html2canvas(document.body, {
backgroundColor: '#121212',
scale: 1,
logging: false,
useCORS: true,
})
return canvas.toDataURL('image/png')
},
/**
* Capture screenshot and send to Rust backend
*/
async captureAndSend(): Promise<void> {
try {
const dataUrl = await this.screenshot()
await invoke('automation_screenshot_data', { data: dataUrl })
console.log('[Automation] Screenshot sent to backend')
} catch (error) {
console.error('[Automation] Screenshot capture failed:', error)
throw error
}
},
}
// Expose globally for Rust to call via eval
window.__TAURI_AUTOMATION__ = service
console.log('[Automation] Ready. HTTP API available at http://localhost:9876')
}
/**
* Dynamically load html2canvas from CDN
*/
async function loadHtml2Canvas(): Promise<typeof import('html2canvas').default> {
if ((window as unknown as { html2canvas?: unknown }).html2canvas) {
return (window as unknown as { html2canvas: typeof import('html2canvas').default }).html2canvas
}
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js'
script.onload = () => {
const h2c = (window as unknown as { html2canvas: typeof import('html2canvas').default }).html2canvas
if (h2c) {
resolve(h2c)
} else {
reject(new Error('html2canvas failed to load'))
}
}
script.onerror = () => reject(new Error('Failed to load html2canvas'))
document.head.appendChild(script)
})
}
export { commands } from './commands'
@@ -0,0 +1,52 @@
// Console logger for Tauri - forwards console.log to Rust for file logging
// This allows us to capture frontend logs when running as a desktop app
import { invoke } from '@tauri-apps/api/tauri'
// Check if running in Tauri environment
function isTauri (): boolean {
return typeof window !== 'undefined' && '__TAURI__' in window
}
export function setupConsoleLogger () {
// Only activate in Tauri environment
if (!isTauri()) {
return
}
const originalLog = console.log
const originalWarn = console.warn
const originalError = console.error
const sendToRust = (level: string, args: unknown[]) => {
const message = args.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg)
} catch {
return String(arg)
}
}
return String(arg)
}).join(' ')
invoke('log_to_stdout', { level, message }).catch(() => {
// Silently fail if Rust command not available
})
}
console.log = (...args: unknown[]) => {
originalLog.apply(console, args)
sendToRust('INFO', args)
}
console.warn = (...args: unknown[]) => {
originalWarn.apply(console, args)
sendToRust('WARN', args)
}
console.error = (...args: unknown[]) => {
originalError.apply(console, args)
sendToRust('ERROR', args)
}
}
@@ -0,0 +1,2 @@
// App-specific styles
// Add your custom styles here
@@ -0,0 +1,14 @@
// Quasar SCSS variables
// Customize colors, breakpoints, etc.
$primary : #1976D2;
$secondary : #26A69A;
$accent : #9C27B0;
$dark : #1D1D1D;
$dark-page : #121212;
$positive : #21BA45;
$negative : #C10015;
$info : #31CCEC;
$warning : #F2C037;
@@ -0,0 +1,54 @@
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<q-toolbar-title>
Tauri Starter
</q-toolbar-title>
</q-toolbar>
</q-header>
<q-drawer
v-model="leftDrawerOpen"
show-if-above
bordered
>
<q-list>
<q-item-label header>
Navigation
</q-item-label>
<q-item clickable v-ripple to="/">
<q-item-section avatar>
<q-icon name="home" />
</q-item-section>
<q-item-section>
Home
</q-item-section>
</q-item>
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const leftDrawerOpen = ref(false)
function toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
</script>
+49
View File
@@ -0,0 +1,49 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { Quasar, Notify, Dialog, Loading } from 'quasar'
import router from './router'
import { setupConsoleLogger } from './boot/console-logger'
// Import Quasar css
import '@quasar/extras/roboto-font/roboto-font.css'
import '@quasar/extras/material-icons/material-icons.css'
import 'quasar/src/css/index.sass'
// Import app styles
import './css/app.scss'
// Import root component
import App from './App.vue'
// Setup console logger for Tauri
setupConsoleLogger()
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(Quasar, {
plugins: {
Notify,
Dialog,
Loading
},
config: {
dark: 'auto'
}
})
// Mount the app
app.mount('#q-app')
// Expose Vue app for automation
;(window as unknown as { __VUE_APP__: typeof app }).__VUE_APP__ = app
// Initialize automation service after app is mounted
setTimeout(() => {
import('./automation').then(({ initAutomation }) => {
initAutomation()
}).catch(err => {
console.warn('[Automation] Failed to load:', err)
})
}, 100)
@@ -0,0 +1,17 @@
<template>
<q-page class="flex flex-center">
<div class="text-center">
<h1 class="text-h1 text-weight-thin q-mb-md">404</h1>
<p class="text-grey-6 q-mb-lg">Page not found</p>
<q-btn
color="primary"
label="Go Home"
to="/"
/>
</div>
</q-page>
</template>
<script setup lang="ts">
// 404 page
</script>
@@ -0,0 +1,34 @@
<template>
<q-page class="flex flex-center">
<div class="text-center">
<h4 class="q-mb-md">Tauri Starter</h4>
<p class="text-grey-6">Quasar + Vite + Tauri template</p>
<q-btn
color="primary"
label="Get Started"
class="q-mt-md"
@click="showMessage"
/>
</div>
</q-page>
</template>
<script setup lang="ts">
import { useQuasar } from 'quasar'
import { onMounted } from 'vue'
const $q = useQuasar()
onMounted(() => {
console.log('App initialized')
})
function showMessage () {
console.log('Get Started clicked')
$q.notify({
message: 'Ready to build!',
color: 'positive',
icon: 'check'
})
}
</script>
@@ -0,0 +1,22 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/IndexPage.vue') }
]
},
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
@@ -0,0 +1,26 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useExampleStore = defineStore('example', () => {
// State
const count = ref(0)
// Getters
const doubleCount = computed(() => count.value * 2)
// Actions
function increment () {
count.value++
}
function reset () {
count.value = 0
}
return {
count,
doubleCount,
increment,
reset
}
})
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"src/*": ["src/*"],
"components/*": ["src/components/*"],
"layouts/*": ["src/layouts/*"],
"pages/*": ["src/pages/*"],
"stores/*": ["src/stores/*"],
"boot/*": ["src/boot/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules", "dist", "src-tauri"]
}
+31
View File
@@ -0,0 +1,31 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
import { fileURLToPath } from 'node:url'
export default defineConfig({
plugins: [
vue({
template: { transformAssetUrls }
}),
quasar({
sassVariables: 'src/css/quasar-variables.scss'
})
],
resolve: {
alias: {
src: fileURLToPath(new URL('./src', import.meta.url)),
components: fileURLToPath(new URL('./src/components', import.meta.url)),
layouts: fileURLToPath(new URL('./src/layouts', import.meta.url)),
pages: fileURLToPath(new URL('./src/pages', import.meta.url)),
stores: fileURLToPath(new URL('./src/stores', import.meta.url)),
boot: fileURLToPath(new URL('./src/boot', import.meta.url))
}
},
server: {
port: 9000,
strictPort: false
},
clearScreen: false,
envPrefix: ['VITE_', 'TAURI_']
})