diff --git a/.gitignore b/.gitignore index 8612f7e..9afaad2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ _test* .DS_Store __pycache__ node_modules +.claude + +# Build outputs +src/clients/go/build/ .vscode/* !.vscode/*.shared.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3d40dbc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,96 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is @justbe/webview, a cross-platform library for building web-based desktop apps. The architecture consists of: + +- **Rust backend**: Core webview functionality using `tao` and `wry` +- **Multi-language clients**: Deno/TypeScript, Python, and Go clients that interface with the Rust binary via stdio + +## Essential Commands + +### Build Commands +```bash +# Build everything +mise build + +# Build specific targets +mise build:rust # Build the webview binary +mise build:deno # Build Deno client +mise build:python # Build Python client +mise build:go # Build Go client +``` + +### Code Generation +```bash +# Generate all schemas and clients +mise gen + +# Generate specific parts +mise gen:rust # Generate JSON schemas from Rust +mise gen:deno # Generate TypeScript client +mise gen:python # Generate Python client +mise gen:go # Generate Go client +``` + +### Linting and Type Checking +```bash +# Run all lints +mise lint + +# Specific lints +mise lint:rust # cargo fmt --check && cargo clippy +mise lint:deno # deno lint && deno check +mise lint:go # golangci-lint run +mise lint:ast-grep # AST-based linting +``` + +### Running Examples +```bash +# Run Deno example +mise run example:deno basic + +# Run Python example +mise run example:python basic + +# Run Go example (binaries built in src/clients/go/build/) +mise run example:go simple +``` + +### Version Management +```bash +# Sync version numbers across all packages +mise sync-versions +``` + +## Architecture + +### IPC Communication +- Client libraries communicate with the Rust binary via stdio (standard input/output) +- Messages are JSON-encoded and follow schemas defined in `schemas/` +- Schema-driven development ensures type safety across language boundaries + +### Directory Structure +- `src/` - Rust source code +- `src/clients/deno/` - Deno/TypeScript client +- `src/clients/python/` - Python client +- `src/clients/go/` - Go client +- `schemas/` - JSON schemas for IPC messages +- `scripts/` - Build and generation scripts +- `sg/` - AST-grep linting rules + +### Key Files +- `mise.toml` - Task runner configuration and tool versions +- `Cargo.toml` - Rust dependencies and build settings +- `src/clients/deno/deno.json` - Deno project configuration +- `src/clients/python/pyproject.toml` - Python project configuration +- `src/clients/go/go.mod` - Go module configuration + +### Development Workflow +1. Rust structs define the message protocol +2. `mise gen:rust` generates JSON schemas from Rust code +3. `mise gen:deno`, `mise gen:python`, and `mise gen:go` generate typed clients from schemas +4. Clients automatically download platform binaries if needed +5. Communication happens via JSON messages over stdio \ No newline at end of file diff --git a/mise.toml b/mise.toml index d7639a6..555fe37 100644 --- a/mise.toml +++ b/mise.toml @@ -4,6 +4,8 @@ deno = "2.3.7" rust = { version = "1.78.0", postinstall = "rustup component add rustfmt clippy rust-analyzer" } ruff = "0.12.0" uv = "0.6.2" +go = "1.23.4" +golangci-lint = "2.1.6" [settings] experimental = true @@ -36,16 +38,23 @@ outputs = ["schemas/*.json"] description = "Generate the deno client" run = "deno run -A scripts/generate-schema/index.ts --language typescript" depends = ["gen:rust"] -sources = ["schemas/*", "scripts/generate-schema.ts"] +sources = ["schemas/*", "scripts/generate-schema/**/*.ts"] outputs = ["src/clients/deno/schemas/*.ts"] [tasks."gen:python"] description = "Generate the python client" run = "deno run -A scripts/generate-schema/index.ts --language python" depends = ["gen:rust"] -sources = ["schemas/*", "scripts/generate-schema.ts"] +sources = ["schemas/*", "scripts/generate-schema/**/*.ts"] outputs = ["src/clients/python/src/justbe_webview/schemas/*.py"] +[tasks."gen:go"] +description = "Generate the go client" +run = "deno run -A scripts/generate-schema/index.ts --language go" +depends = ["gen:rust"] +sources = ["schemas/*", "scripts/generate-schema/**/*.ts"] +outputs = ["src/clients/go/webview/schemas.go"] + ## Debug [tasks."print-schema"] @@ -93,6 +102,10 @@ depends = ["gen:deno", "build:rust"] description = "Run code gen for python and ensure the binary is built" depends = ["gen:python", "build:rust"] +[tasks."build:go"] +description = "Run code gen for go and ensure the binary is built" +depends = ["gen:go", "build:rust"] + [tasks.build] description = "Build all targets" depends = ["build:*"] @@ -109,6 +122,11 @@ description = "Run deno lint" dir = "src/clients/deno" run = ["deno lint", "deno check ."] +[tasks."lint:go"] +description = "Run golangci-lint against go code" +dir = "src/clients/go" +run = "golangci-lint run ./webview/..." + [tasks."lint:ast-grep"] description = "Run ast-grep lint" run = """ @@ -138,3 +156,10 @@ depends = ["build:deno"] env = { LOG_LEVEL = "debug", WEBVIEW_BIN = "../../../target/debug/webview" } run = "deno run -E -R -N --allow-run examples/{{arg(name=\"example\")}}.ts" dir = "src/clients/deno" + +[tasks."example:go"] +description = "Run a go example" +depends = ["build:go"] +dir = "src/clients/go" +run = "mkdir -p build && go build -o build/{{arg(name=\"example\")}} examples/{{arg(name=\"example\")}}.go && ./build/{{arg(name=\"example\")}}" +env = { LOG_LEVEL = "debug", WEBVIEW_BIN = "../../../target/debug/webview" } diff --git a/scripts/generate-schema/gen-go.ts b/scripts/generate-schema/gen-go.ts new file mode 100644 index 0000000..c8b15b8 --- /dev/null +++ b/scripts/generate-schema/gen-go.ts @@ -0,0 +1,305 @@ +import type { Doc, Node } from "./parser.ts"; +import { Writer } from "./gen-helpers.ts"; +import { match } from "npm:ts-pattern"; + +const header = (relativePath: string) => + `// Code generated by ${relativePath}; DO NOT EDIT.\n` + + "package webview\n\n" + + "import (\n" + + "\t\"encoding/json\"\n" + + "\t\"fmt\"\n" + + ")\n\n"; + +// Track generated definitions to avoid duplicates +const generatedTypeDefinitions = new Set(); + +export function generateGo( + doc: Doc, + name: string, + relativePath: string, +) { + // Only include header for the first schema + const shouldIncludeHeader = generatedTypeDefinitions.size === 0; + const types = generateTypes(doc, name); + return (shouldIncludeHeader ? header(relativePath) : "") + types; +} + +function generateTypes(doc: Doc, typeName: string) { + const writer = new Writer(); + const { w, wn } = writer.shorthand(); + + // Generate definitions first + for (const [name, definition] of Object.entries(doc.definitions)) { + // Skip if we've already generated this definition + if (generatedTypeDefinitions.has(name)) { + continue; + } + generatedTypeDefinitions.add(name); + + // Special handling for Content type to create polymorphic structure + if (name === "Content") { + generateContentTypes(); + } else { + if (definition.description) { + wn(`// ${name} ${definition.description}`); + } + w("type", name, " "); + generateNode(definition, name); + wn("\n"); + } + } + + // Generate the main type if not already defined + if (!generatedTypeDefinitions.has(typeName)) { + // Special handling for Content type to create polymorphic structure + if (typeName === "Content") { + generateContentTypes(); + } else { + if (doc.description) { + wn(`// ${typeName} ${doc.description}`); + } + w("type", typeName, " "); + generateNode(doc.root, typeName); + wn("\n"); + } + } + + function generateNode(node: Node, parentName: string) { + match(node) + .with({ type: "reference" }, (node) => w(node.name)) + .with({ type: "int" }, () => w("int")) + .with({ type: "float" }, () => w("float64")) + .with({ type: "boolean" }, () => w("bool")) + .with({ type: "string" }, () => w("string")) + .with({ type: "literal" }, () => w("string")) // Literals become strings in Go + .with( + { type: "record" }, + (node) => w(`map[string]${goType(node.valueType)}`), + ) + .with({ type: "object" }, (node) => { + wn("struct {"); + for (const { key, required, description, value } of node.properties) { + if (description) { + // Handle multi-line descriptions + const descLines = description.split('\n'); + for (const line of descLines) { + wn(`\t// ${capitalizeFirst(key)} ${line}`); + } + } + // Use uppercase for public API structs, lowercase for internal structs + const fieldName = isPublicAPIStruct(parentName) ? capitalizeFirst(key) : lowercaseFirst(key); + let fieldType = getGoType(value); + // Make optional fields pointer types (except for interface{} and map types) + if (!required && fieldType !== "interface{}" && !fieldType.includes("map[")) { + fieldType = "*" + fieldType; + } + const jsonTag = required ? `\`json:"${key}"\`` : `\`json:"${key},omitempty"\``; + wn(`\t${fieldName} ${fieldType} ${jsonTag}`); + } + w("}"); + }) + .with({ type: "intersection" }, () => { + // Go doesn't have intersection types, so we'll need to generate a new struct + // For now, just use interface{} + w("interface{}") + }) + .with({ type: "union" }, () => { + // Go doesn't have union types, so we use interface{} + w("interface{}") + }) + .with({ type: "descriminated-union" }, (node) => { + // For discriminated unions, we'll create separate types for each variant + // and use interface{} for the main type + w("interface{}"); + + // Generate variant types + for (const [variantName, members] of Object.entries(node.members)) { + const variantTypeName = `${parentName}${capitalizeFirst(variantName)}`; + if (!generatedTypeDefinitions.has(variantTypeName)) { + generatedTypeDefinitions.add(variantTypeName); + wn("\n"); + wn(`// ${variantTypeName} represents the ${variantName} variant of ${parentName}`); + wn(`type ${variantTypeName} struct {`); + wn(`\tType string \`json:"${node.descriminator}"\``); + for (const { key, required, description, value } of members) { + if (description) { + wn(`\t// ${capitalizeFirst(key)} ${description}`); + } + // Use uppercase for public API structs, lowercase for internal structs + const fieldName = isPublicAPIStruct(parentName) ? capitalizeFirst(key) : lowercaseFirst(key); + let fieldType = getGoType(value); + // Make optional fields pointer types (except for interface{} and map types) + if (!required && fieldType !== "interface{}" && !fieldType.includes("map[")) { + fieldType = "*" + fieldType; + } + const jsonTag = required ? `\`json:"${key}"\`` : `\`json:"${key},omitempty"\``; + wn(`\t${fieldName} ${fieldType} ${jsonTag}`); + } + wn("}"); + } + } + }) + .with({ type: "enum" }, () => { + // Go doesn't have enums, so we use string + w("string") + }) + .with({ type: "unknown" }, () => w("interface{}")) + .exhaustive(); + } + + function generateContentTypes() { + // Generate the ContentProvider interface + wn("// ContentProvider interface that all content types must implement"); + wn("type ContentProvider interface {"); + wn("\tisContent() // marker method"); + wn("}"); + wn(""); + + // Generate HTMLContent struct + wn("// HtmlContent represents inline HTML content"); + wn("type HtmlContent struct {"); + wn("\tHtml string `json:\"html\"`"); + wn("\tOrigin string `json:\"origin,omitempty\"`"); + wn("}"); + wn(""); + + // Generate URLContent struct + wn("// UrlContent represents content loaded from a URL"); + wn("type UrlContent struct {"); + wn("\tUrl string `json:\"url\"`"); + wn("\tHeaders map[string]string `json:\"headers,omitempty\"`"); + wn("}"); + wn(""); + + // Generate interface implementations + wn("// Implement ContentProvider interface"); + wn("func (h HtmlContent) isContent() {}"); + wn("func (u UrlContent) isContent() {}"); + wn(""); + + // Generate constructor functions + wn("// NewHtmlContent creates HtmlContent with the specified HTML and optional origin"); + wn("func NewHtmlContent(html string, origin ...string) HtmlContent {"); + wn("\tc := HtmlContent{Html: html}"); + wn("\tif len(origin) > 0 {"); + wn("\t\tc.Origin = origin[0]"); + wn("\t}"); + wn("\treturn c"); + wn("}"); + wn(""); + + wn("// NewUrlContent creates UrlContent with the specified URL and optional headers"); + wn("func NewUrlContent(url string, headers ...map[string]string) UrlContent {"); + wn("\tc := UrlContent{Url: url}"); + wn("\tif len(headers) > 0 {"); + wn("\t\tc.Headers = headers[0]"); + wn("\t}"); + wn("\treturn c"); + wn("}"); + wn(""); + + // Generate Content wrapper struct + wn("// Content wraps a ContentProvider for JSON marshaling"); + wn("type Content struct {"); + wn("\tprovider ContentProvider"); + wn("}"); + wn(""); + + // Generate Content constructor function (as a package-level function) + wn("// ContentFrom creates a new Content from any ContentProvider"); + wn("func ContentFrom(provider ContentProvider) Content {"); + wn("\treturn Content{provider: provider}"); + wn("}"); + wn(""); + + // Generate MarshalJSON method + wn("// MarshalJSON implements custom JSON marshaling for Content"); + wn("func (c Content) MarshalJSON() ([]byte, error) {"); + wn("\treturn json.Marshal(c.provider)"); + wn("}"); + wn(""); + + // Generate UnmarshalJSON method + wn("// UnmarshalJSON implements custom JSON unmarshaling for Content"); + wn("func (c *Content) UnmarshalJSON(data []byte) error {"); + wn("\tvar raw map[string]interface{}"); + wn("\tif err := json.Unmarshal(data, &raw); err != nil {"); + wn("\t\treturn err"); + wn("\t}"); + wn(""); + wn("\tif _, hasURL := raw[\"url\"]; hasURL {"); + wn("\t\tvar content UrlContent"); + wn("\t\tif err := json.Unmarshal(data, &content); err != nil {"); + wn("\t\t\treturn err"); + wn("\t\t}"); + wn("\t\tc.provider = content"); + wn("\t\treturn nil"); + wn("\t}"); + wn(""); + wn("\tif _, hasHTML := raw[\"html\"]; hasHTML {"); + wn("\t\tvar content HtmlContent"); + wn("\t\tif err := json.Unmarshal(data, &content); err != nil {"); + wn("\t\t\treturn err"); + wn("\t\t}"); + wn("\t\tc.provider = content"); + wn("\t\treturn nil"); + wn("\t}"); + wn(""); + wn("\treturn fmt.Errorf(\"unknown content type\")"); + wn("}"); + wn(""); + } + + function getGoType(node: Node): string { + return match(node) + .with({ type: "reference" }, (node) => node.name) + .with({ type: "int" }, () => "int") + .with({ type: "float" }, () => "float64") + .with({ type: "boolean" }, () => "bool") + .with({ type: "string" }, () => "string") + .with({ type: "literal" }, () => "string") + .with({ type: "record" }, (node) => `map[string]${goType(node.valueType)}`) + .with({ type: "object" }, () => "struct { /* inline */ }") + .with({ type: "intersection" }, () => "interface{}") + .with({ type: "union" }, () => "interface{}") + .with({ type: "descriminated-union" }, () => "interface{}") + .with({ type: "enum" }, () => "string") + .with({ type: "unknown" }, () => "interface{}") + .exhaustive(); + } + + function goType(type: string): string { + switch (type) { + case "string": return "string"; + case "number": return "float64"; + case "boolean": return "bool"; + default: return "interface{}"; + } + } + + function capitalizeFirst(str: string): string { + // Handle snake_case to PascalCase conversion + return str.split('_').map(part => + part.charAt(0).toUpperCase() + part.slice(1) + ).join(''); + } + + function lowercaseFirst(str: string): string { + // Handle snake_case to camelCase conversion + const parts = str.split('_'); + return parts[0].toLowerCase() + parts.slice(1).map(part => + part.charAt(0).toUpperCase() + part.slice(1) + ).join(''); + } + + function isPublicAPIStruct(typeName: string): boolean { + // Most structs should have exported fields for public API + // Only internal implementation details should be private + const privateTypes = [ + 'Content' // Content.provider should remain private + ]; + return !privateTypes.includes(typeName); + } + + return writer.output(); +} \ No newline at end of file diff --git a/scripts/generate-schema/index.ts b/scripts/generate-schema/index.ts index 5b0fc89..add74cc 100644 --- a/scripts/generate-schema/index.ts +++ b/scripts/generate-schema/index.ts @@ -8,6 +8,7 @@ import { generateAll, generatePython, } from "./gen-python.ts"; +import { generateGo } from "./gen-go.ts"; import { parseSchema } from "./parser.ts"; const schemasDir = new URL("../../schemas", import.meta.url).pathname; @@ -15,6 +16,7 @@ const tsSchemaDir = new URL("../../src/clients/deno", import.meta.url).pathname; const pySchemaDir = new URL("../../src/clients/python/src/justbe_webview", import.meta.url) .pathname; +const goSchemaDir = new URL("../../src/clients/go/webview", import.meta.url).pathname; async function ensureDir(dir: string) { try { @@ -33,8 +35,8 @@ async function main() { }); const language = flags.language?.toLowerCase(); - if (language && !["typescript", "python"].includes(language)) { - console.error('Language must be either "typescript" or "python"'); + if (language && !["typescript", "python", "go"].includes(language)) { + console.error('Language must be either "typescript", "python", or "go"'); Deno.exit(1); } @@ -48,6 +50,9 @@ async function main() { if (!language || language === "python") { await ensureDir(pySchemaDir); } + if (!language || language === "go") { + await ensureDir(goSchemaDir); + } const entries = []; for await (const entry of walk(schemasDir, { exts: [".json"] })) { @@ -99,6 +104,16 @@ async function main() { console.log(`Generated Python schemas: ${pyFilePath}`); } + if (!language || language === "go") { + // Generate single Go file with all schemas + const goContent = schemas.map((doc) => + generateGo(doc, doc.title, relativePath) + ).join("\n\n"); + const goFilePath = join(goSchemaDir, "schemas.go"); + await Deno.writeTextFile(goFilePath, goContent); + console.log(`Generated Go schemas: ${goFilePath}`); + } + // Run deno fmt on TypeScript files if they were generated if (!language || language === "typescript") { const command = new Deno.Command("deno", { @@ -114,6 +129,14 @@ async function main() { }); await command.output(); } + + // Run go fmt on Go files if they were generated + if (!language || language === "go") { + const command = new Deno.Command("go", { + args: ["fmt", goSchemaDir], + }); + await command.output(); + } } main().catch(console.error); diff --git a/src/clients/deno/deno.lock b/src/clients/deno/deno.lock index 3fc948c..f0f21dc 100644 --- a/src/clients/deno/deno.lock +++ b/src/clients/deno/deno.lock @@ -1,5 +1,5 @@ { - "version": "4", + "version": "5", "specifiers": { "jsr:@bcheidemann/parse-params@0.5": "0.5.0", "jsr:@bcheidemann/tracing@~0.6.3": "0.6.3", @@ -11,6 +11,7 @@ "jsr:@std/internal@1": "1.0.5", "jsr:@std/path@^1.0.6": "1.0.8", "jsr:@std/path@^1.0.8": "1.0.8", + "npm:@types/node@*": "22.15.15", "npm:acorn@8.12.0": "8.12.0", "npm:type-fest@^4.26.1": "4.30.2", "npm:zod@^3.23.8": "3.23.8" @@ -57,12 +58,22 @@ } }, "npm": { + "@types/node@22.15.15": { + "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", + "dependencies": [ + "undici-types" + ] + }, "acorn@8.12.0": { - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==" + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "bin": true }, "type-fest@4.30.2": { "integrity": "sha512-UJShLPYi1aWqCdq9HycOL/gwsuqda1OISdBO3t8RlXQC4QvtuIz4b5FCfe2dQIWEpmlRExKmcTBfP1r9bhY7ig==" }, + "undici-types@6.21.0": { + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, "zod@3.23.8": { "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==" } diff --git a/src/clients/go/README.md b/src/clients/go/README.md new file mode 100644 index 0000000..8802c4c --- /dev/null +++ b/src/clients/go/README.md @@ -0,0 +1,133 @@ +# @justbe/webview Go Client + +A light, cross-platform library for building web-based desktop apps with Go. + +## Installation + +```bash +go get github.com/justbe-engineering/webview-client-go +``` + +## Example + +```go +package main + +import ( + "context" + "log" + + "github.com/justbe-engineering/webview-client-go/webview" +) + +func main() { + ctx := context.Background() + + loadContent := webview.Content(map[string]interface{}{ + "html": "

Hello, World!

", + }) + + options := webview.Options{ + Title: "Example", + Devtools: &[]bool{true}[0], // pointer to true + Load: &loadContent, + } + + wv, err := webview.NewWebView(ctx, options) + if err != nil { + log.Fatal(err) + } + defer wv.Close() + + wv.On("started", func(event interface{}) { + wv.OpenDevTools() + wv.Eval("console.log('This is printed from eval!')") + }) + + // Wait for the webview to close + wv.Wait() +} +``` + +Check out the [examples directory](examples/) for more examples. + +## Binary Management + +When using the Go client, it will check for the required binary for interfacing with the OS's webview. If it doesn't exist, it downloads it to a cache directory and executes it. + +### Cache Directory Locations + +The binary is cached in OS-specific locations: + +- **macOS**: `~/Library/Caches/webview/` +- **Linux**: `~/.cache/webview/` +- **Windows**: `%LOCALAPPDATA%/webview/Cache/` + +### Using a Custom Binary + +You can specify a custom binary path using the `WEBVIEW_BIN` environment variable. When set, this will bypass the default binary resolution process and use the specified path instead. + +```bash +export WEBVIEW_BIN=/path/to/custom/webview +``` + +## API Reference + +### WebView + +The main WebView type provides methods for controlling the webview window. + +#### Methods + +- `Eval(js string) (interface{}, error)` - Execute JavaScript code +- `OpenDevTools() error` - Open developer tools +- `SetTitle(title string) error` - Set window title +- `LoadHTML(html string, origin string) error` - Load HTML content +- `LoadURL(url string, headers map[string]string) error` - Load URL +- `On(eventType string, handler EventHandler)` - Register event handler +- `Wait() error` - Wait for webview to close +- `Close() error` - Close the webview + +#### Events + +- `"started"` - Fired when the webview starts +- `"closed"` - Fired when the webview closes +- `"ipc"` - Fired when receiving IPC messages from JavaScript + +### Options + +Configuration options for creating a webview: + +```go +type Options struct { + Title string // required + Devtools *bool // optional + Load *Content // optional (Content is interface{}) + InitializationScript *string // optional + Ipc *bool // optional + UserAgent *string // optional + // ... and more options +} +``` + +Most optional fields are pointers. For Content, use type conversion: + +```go +// Helper functions for pointer types +func boolPtr(b bool) *bool { return &b } +func strPtr(s string) *string { return &s } + +// Content should be a map for union types +loadContent := webview.Content(map[string]interface{}{ + "html": "

Hello

", + // or for URLs: + // "url": "https://example.com", + // "headers": map[string]string{"Content-Type": "text/html"}, +}) + +options := webview.Options{ + Title: "My App", + Devtools: boolPtr(true), + Load: &loadContent, +} +``` \ No newline at end of file diff --git a/src/clients/go/examples/ipc.go b/src/clients/go/examples/ipc.go new file mode 100644 index 0000000..6c57676 --- /dev/null +++ b/src/clients/go/examples/ipc.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/justbe-engineering/webview-client-go/webview" +) + +func main() { + ctx := context.Background() + + html := `` + + loadContent := webview.ContentFrom(webview.NewHtmlContent(html)) + + options := webview.Options{ + Title: "IPC Example", + Load: &loadContent, + Ipc: boolPtr(true), + } + + wv, err := webview.NewWebView(ctx, options) + if err != nil { + log.Fatal(err) + } + defer wv.Close() + + // Register event handler for IPC messages + wv.On("ipc", func(event interface{}) { + eventMap, ok := event.(map[string]interface{}) + if !ok { + log.Printf("Invalid IPC event format") + return + } + + message, ok := eventMap["message"].(string) + if !ok { + log.Printf("Invalid IPC message format") + return + } + + fmt.Printf("Received IPC message: %s\n", message) + }) + + // Register event handler for window close + wv.On("closed", func(event interface{}) { + fmt.Println("WebView closed!") + }) + + // Wait for the webview to close + if err := wv.Wait(); err != nil { + log.Printf("WebView exited with error: %v", err) + } +} + +// Helper functions for pointer types +func boolPtr(b bool) *bool { + return &b +} \ No newline at end of file diff --git a/src/clients/go/examples/load_html.go b/src/clients/go/examples/load_html.go new file mode 100644 index 0000000..6c2ab22 --- /dev/null +++ b/src/clients/go/examples/load_html.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/justbe-engineering/webview-client-go/webview" +) + +func main() { + ctx := context.Background() + + loadContent := webview.ContentFrom(webview.NewHtmlContent("

Initial html

", "example.com")) + + options := webview.Options{ + Title: "Load Html Example", + Devtools: boolPtr(true), + Load: &loadContent, + } + + wv, err := webview.NewWebView(ctx, options) + if err != nil { + log.Fatal(err) + } + defer wv.Close() + + // Register event handler for "started" event + wv.On("started", func(event interface{}) { + fmt.Println("WebView started!") + + // Open dev tools + if err := wv.OpenDevTools(); err != nil { + log.Printf("Failed to open dev tools: %v", err) + } + + // Load new HTML content + if err := wv.LoadHTML("

Updated html!

", ""); err != nil { + log.Printf("Failed to load HTML: %v", err) + } + }) + + // Register event handler for window close + wv.On("closed", func(event interface{}) { + fmt.Println("WebView closed!") + }) + + // Wait for the webview to close + if err := wv.Wait(); err != nil { + log.Printf("WebView exited with error: %v", err) + } +} + +// Helper functions for pointer types +func boolPtr(b bool) *bool { + return &b +} + +func strPtr(s string) *string { + return &s +} \ No newline at end of file diff --git a/src/clients/go/examples/load_url.go b/src/clients/go/examples/load_url.go new file mode 100644 index 0000000..c0a8866 --- /dev/null +++ b/src/clients/go/examples/load_url.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/justbe-engineering/webview-client-go/webview" +) + +func main() { + ctx := context.Background() + + loadContent := webview.ContentFrom(webview.NewUrlContent("https://example.com", map[string]string{ + "Content-Type": "text/html", + })) + + options := webview.Options{ + Title: "Load Url Example", + Devtools: boolPtr(true), + UserAgent: strPtr("curl/7.81.0"), + Load: &loadContent, + } + + wv, err := webview.NewWebView(ctx, options) + if err != nil { + log.Fatal(err) + } + defer wv.Close() + + // Register event handler for "started" event + wv.On("started", func(event interface{}) { + fmt.Println("WebView started!") + + // Open dev tools + if err := wv.OpenDevTools(); err != nil { + log.Printf("Failed to open dev tools: %v", err) + } + + // Wait a bit then load a new URL + time.Sleep(2 * time.Second) + + headers := map[string]string{ + "Content-Type": "text/html", + } + + if err := wv.LoadURL("https://val.town/", headers); err != nil { + log.Printf("Failed to load URL: %v", err) + } + }) + + // Register event handler for window close + wv.On("closed", func(event interface{}) { + fmt.Println("WebView closed!") + }) + + // Wait for the webview to close + if err := wv.Wait(); err != nil { + log.Printf("WebView exited with error: %v", err) + } +} + +// Helper functions for pointer types +func boolPtr(b bool) *bool { + return &b +} + +func strPtr(s string) *string { + return &s +} \ No newline at end of file diff --git a/src/clients/go/examples/simple.go b/src/clients/go/examples/simple.go new file mode 100644 index 0000000..7917675 --- /dev/null +++ b/src/clients/go/examples/simple.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/justbe-engineering/webview-client-go/webview" +) + +func main() { + ctx := context.Background() + + loadContent := webview.ContentFrom(webview.NewHtmlContent("

Hello, World!

")) + + options := webview.Options{ + Title: "Simple", + Devtools: boolPtr(true), + Load: &loadContent, + InitializationScript: strPtr("console.log('This is printed from initializationScript!')"), + } + + wv, err := webview.NewWebView(ctx, options) + if err != nil { + log.Fatal(err) + } + defer wv.Close() + + // Register event handler for "started" event + wv.On("started", func(event interface{}) { + fmt.Println("WebView started!") + + // Set title + if err := wv.SetTitle("Title set from Go"); err != nil { + log.Printf("Failed to set title: %v", err) + } + + // Open dev tools + if err := wv.OpenDevTools(); err != nil { + log.Printf("Failed to open dev tools: %v", err) + } + + // Evaluate JavaScript + result, err := wv.Eval("console.log('This is printed from eval!'); 'Hello from Go!'") + if err != nil { + log.Printf("Failed to eval JS: %v", err) + } else { + fmt.Printf("JS result: %v\n", result) + } + }) + + // Register event handler for window close + wv.On("closed", func(event interface{}) { + fmt.Println("WebView closed!") + }) + + // Wait for the webview to close + if err := wv.Wait(); err != nil { + log.Printf("WebView exited with error: %v", err) + } +} + +// Helper functions for pointer types +func boolPtr(b bool) *bool { + return &b +} + +func strPtr(s string) *string { + return &s +} \ No newline at end of file diff --git a/src/clients/go/go.mod b/src/clients/go/go.mod new file mode 100644 index 0000000..d9a80c9 --- /dev/null +++ b/src/clients/go/go.mod @@ -0,0 +1,3 @@ +module github.com/justbe-engineering/webview-client-go + +go 1.23 diff --git a/src/clients/go/webview/client.go b/src/clients/go/webview/client.go new file mode 100644 index 0000000..88ea770 --- /dev/null +++ b/src/clients/go/webview/client.go @@ -0,0 +1,519 @@ +package webview + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" +) + +// BinVersion should match the cargo package version +const BinVersion = "0.3.1" + +// EventHandler is a function type for handling webview events +type EventHandler func(event interface{}) + +// WebView represents a webview instance +type WebView struct { + cmd *exec.Cmd + stdin io.WriteCloser + stdout io.ReadCloser + stderr io.ReadCloser + messageID int + mutex sync.RWMutex + handlers map[string][]EventHandler + responses map[int]chan Response + ctx context.Context + cancel context.CancelFunc + done chan struct{} +} + +// NewWebView creates a new webview instance +func NewWebView(ctx context.Context, options Options) (*WebView, error) { + binPath, err := getWebViewBin(options) + if err != nil { + return nil, fmt.Errorf("failed to get webview binary: %w", err) + } + + optionsJSON, err := json.Marshal(options) + if err != nil { + return nil, fmt.Errorf("failed to marshal options: %w", err) + } + + cmd := exec.CommandContext(ctx, binPath, string(optionsJSON)) + + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, fmt.Errorf("failed to create stdin pipe: %w", err) + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("failed to create stdout pipe: %w", err) + } + + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, fmt.Errorf("failed to create stderr pipe: %w", err) + } + + childCtx, cancel := context.WithCancel(ctx) + + wv := &WebView{ + cmd: cmd, + stdin: stdin, + stdout: stdout, + stderr: stderr, + handlers: make(map[string][]EventHandler), + responses: make(map[int]chan Response), + ctx: childCtx, + cancel: cancel, + done: make(chan struct{}), + } + + if err := cmd.Start(); err != nil { + cancel() + return nil, fmt.Errorf("failed to start webview process: %w", err) + } + + go wv.processMessages() + go wv.processStderr() + + return wv, nil +} + +// On registers an event handler for the specified event type +func (wv *WebView) On(eventType string, handler EventHandler) { + wv.mutex.Lock() + defer wv.mutex.Unlock() + wv.handlers[eventType] = append(wv.handlers[eventType], handler) +} + +// processMessages handles incoming messages from the webview process +func (wv *WebView) processMessages() { + defer close(wv.done) + defer wv.cancel() + + scanner := bufio.NewScanner(wv.stdout) + for scanner.Scan() { + line := scanner.Text() + if line == "" { + continue + } + + var message Message + if err := json.Unmarshal([]byte(line), &message); err != nil { + continue + } + + wv.handleMessage(message) + } +} + +// processStderr handles stderr from the webview process +func (wv *WebView) processStderr() { + scanner := bufio.NewScanner(wv.stderr) + for scanner.Scan() { + // For now, just print stderr to help with debugging + fmt.Fprintf(os.Stderr, "webview stderr: %s\n", scanner.Text()) + } +} + +// handleMessage processes incoming messages and dispatches events or responses +func (wv *WebView) handleMessage(message interface{}) { + messageMap, ok := message.(map[string]interface{}) + if !ok { + return + } + + msgType, ok := messageMap["$type"].(string) + if !ok { + return + } + + switch msgType { + case "notification": + wv.handleNotification(messageMap) + case "response": + wv.handleResponse(messageMap) + } +} + +// handleNotification processes notification messages +func (wv *WebView) handleNotification(messageMap map[string]interface{}) { + data, ok := messageMap["data"].(map[string]interface{}) + if !ok { + return + } + + notificationType, ok := data["$type"].(string) + if !ok { + return + } + + wv.mutex.RLock() + handlers := wv.handlers[notificationType] + wv.mutex.RUnlock() + + for _, handler := range handlers { + go handler(data) + } +} + +// handleResponse processes response messages +func (wv *WebView) handleResponse(messageMap map[string]interface{}) { + data, ok := messageMap["data"].(map[string]interface{}) + if !ok { + return + } + + idFloat, ok := data["id"].(float64) + if !ok { + return + } + id := int(idFloat) + + wv.mutex.RLock() + responseChan, exists := wv.responses[id] + wv.mutex.RUnlock() + + if exists { + responseChan <- data + close(responseChan) + + wv.mutex.Lock() + delete(wv.responses, id) + wv.mutex.Unlock() + } +} + +// send sends a request to the webview process and returns the response +func (wv *WebView) send(request map[string]interface{}) (Response, error) { + wv.mutex.Lock() + id := wv.messageID + wv.messageID++ + responseChan := make(chan Response, 1) + wv.responses[id] = responseChan + wv.mutex.Unlock() + + request["id"] = id + + requestJSON, err := json.Marshal(request) + if err != nil { + wv.mutex.Lock() + delete(wv.responses, id) + wv.mutex.Unlock() + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + if _, err := wv.stdin.Write(append(requestJSON, '\n')); err != nil { + wv.mutex.Lock() + delete(wv.responses, id) + wv.mutex.Unlock() + return nil, fmt.Errorf("failed to write request: %w", err) + } + + select { + case response := <-responseChan: + return response, nil + case <-wv.ctx.Done(): + return nil, wv.ctx.Err() + } +} + +// Eval executes JavaScript in the webview +func (wv *WebView) Eval(js string) (interface{}, error) { + request := map[string]interface{}{ + "$type": "eval", + "js": js, + } + + response, err := wv.send(request) + if err != nil { + return nil, err + } + + responseMap, ok := response.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response format") + } + + responseType, ok := responseMap["$type"].(string) + if !ok { + return nil, fmt.Errorf("missing response type") + } + + switch responseType { + case "result": + result, ok := responseMap["result"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid result format") + } + return result["value"], nil + case "err": + message, _ := responseMap["message"].(string) + return nil, fmt.Errorf("webview error: %s", message) + default: + return nil, fmt.Errorf("unexpected response type: %s", responseType) + } +} + +// OpenDevTools opens the developer tools +func (wv *WebView) OpenDevTools() error { + request := map[string]interface{}{ + "$type": "openDevTools", + } + + response, err := wv.send(request) + if err != nil { + return err + } + + responseMap, ok := response.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid response format") + } + + responseType, ok := responseMap["$type"].(string) + if !ok { + return fmt.Errorf("missing response type") + } + + switch responseType { + case "ack": + return nil + case "err": + message, _ := responseMap["message"].(string) + return fmt.Errorf("webview error: %s", message) + default: + return fmt.Errorf("unexpected response type: %s", responseType) + } +} + +// SetTitle sets the window title +func (wv *WebView) SetTitle(title string) error { + request := map[string]interface{}{ + "$type": "setTitle", + "title": title, + } + + response, err := wv.send(request) + if err != nil { + return err + } + + responseMap, ok := response.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid response format") + } + + responseType, ok := responseMap["$type"].(string) + if !ok { + return fmt.Errorf("missing response type") + } + + switch responseType { + case "ack": + return nil + case "err": + message, _ := responseMap["message"].(string) + return fmt.Errorf("webview error: %s", message) + default: + return fmt.Errorf("unexpected response type: %s", responseType) + } +} + +// LoadHTML loads HTML content into the webview +func (wv *WebView) LoadHTML(html string, origin string) error { + request := map[string]interface{}{ + "$type": "loadHtml", + "html": html, + } + + if origin != "" { + request["origin"] = origin + } + + response, err := wv.send(request) + if err != nil { + return err + } + + responseMap, ok := response.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid response format") + } + + responseType, ok := responseMap["$type"].(string) + if !ok { + return fmt.Errorf("missing response type") + } + + switch responseType { + case "ack": + return nil + case "err": + message, _ := responseMap["message"].(string) + return fmt.Errorf("webview error: %s", message) + default: + return fmt.Errorf("unexpected response type: %s", responseType) + } +} + +// LoadURL loads a URL into the webview +func (wv *WebView) LoadURL(url string, headers map[string]string) error { + request := map[string]interface{}{ + "$type": "loadUrl", + "url": url, + } + + if headers != nil { + request["headers"] = headers + } + + response, err := wv.send(request) + if err != nil { + return err + } + + responseMap, ok := response.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid response format") + } + + responseType, ok := responseMap["$type"].(string) + if !ok { + return fmt.Errorf("missing response type") + } + + switch responseType { + case "ack": + return nil + case "err": + message, _ := responseMap["message"].(string) + return fmt.Errorf("webview error: %s", message) + default: + return fmt.Errorf("unexpected response type: %s", responseType) + } +} + +// Wait waits for the webview process to finish +func (wv *WebView) Wait() error { + <-wv.done + return wv.cmd.Wait() +} + +// Close closes the webview +func (wv *WebView) Close() error { + wv.cancel() + if err := wv.stdin.Close(); err != nil { + // Stdin close errors are not critical for cleanup + _ = err + } + return wv.cmd.Process.Kill() +} + +// getWebViewBin gets the path to the webview binary, downloading it if necessary +func getWebViewBin(options Options) (string, error) { + // Check for WEBVIEW_BIN environment variable + if binPath := os.Getenv("WEBVIEW_BIN"); binPath != "" { + return binPath, nil + } + + flags := "" + if options.Devtools != nil && *options.Devtools { + flags = "-devtools" + } else if options.Transparent != nil && *options.Transparent && runtime.GOOS == "darwin" { + flags = "-transparent" + } + + cacheDir := getCacheDir() + fileName := fmt.Sprintf("webview-%s%s", BinVersion, flags) + if runtime.GOOS == "windows" { + fileName += ".exe" + } + filePath := filepath.Join(cacheDir, fileName) + + // Check if the file already exists in cache + if _, err := os.Stat(filePath); err == nil { + return filePath, nil + } + + // If not in cache, download it + url := fmt.Sprintf("https://github.com/zephraph/webview/releases/download/webview-v%s/webview", BinVersion) + + switch runtime.GOOS { + case "darwin": + url += "-mac" + if runtime.GOARCH == "arm64" { + url += "-arm64" + } + url += flags + case "linux": + url += "-linux" + flags + case "windows": + url += "-windows" + flags + ".exe" + default: + return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS) + } + + // Download the binary + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("failed to download webview binary: %w", err) + } + defer func() { + if err := resp.Body.Close(); err != nil { + // Response body close errors are typically not critical + _ = err + } + }() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to download webview binary: HTTP %d", resp.StatusCode) + } + + // Ensure the cache directory exists + if err := os.MkdirAll(cacheDir, 0755); err != nil { + return "", fmt.Errorf("failed to create cache directory: %w", err) + } + + // Write the binary to disk + file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return "", fmt.Errorf("failed to create binary file: %w", err) + } + defer func() { + if err := file.Close(); err != nil { + // File close errors are typically not critical + _ = err + } + }() + + if _, err := io.Copy(file, resp.Body); err != nil { + return "", fmt.Errorf("failed to write binary file: %w", err) + } + + return filePath, nil +} + +// getCacheDir returns the OS-specific cache directory +func getCacheDir() string { + switch runtime.GOOS { + case "darwin": + return filepath.Join(os.Getenv("HOME"), "Library", "Caches", "webview") + case "linux": + return filepath.Join(os.Getenv("HOME"), ".cache", "webview") + case "windows": + return filepath.Join(os.Getenv("LOCALAPPDATA"), "webview", "Cache") + default: + return filepath.Join(os.TempDir(), "webview") + } +} \ No newline at end of file diff --git a/src/clients/go/webview/schemas.go b/src/clients/go/webview/schemas.go new file mode 100644 index 0000000..ed84e45 --- /dev/null +++ b/src/clients/go/webview/schemas.go @@ -0,0 +1,392 @@ +// Code generated by generate-schema/index.ts; DO NOT EDIT. +package webview + +import ( + "encoding/json" + "fmt" +) + +// Notification Messages that are sent unbidden from the webview to the client. +type Notification interface{} + +// NotificationStarted represents the started variant of Notification +type NotificationStarted struct { + Type string `json:"$type"` + // Version The version of the webview binary + Version string `json:"version"` +} + + +// NotificationIpc represents the ipc variant of Notification +type NotificationIpc struct { + Type string `json:"$type"` + // Message The message sent from the webview UI to the client. + Message string `json:"message"` +} + + +// NotificationClosed represents the closed variant of Notification +type NotificationClosed struct { + Type string `json:"$type"` +} + + +type SizeWithScale struct { + // Height The height of the window in logical pixels. + Height float64 `json:"height"` + // ScaleFactor The ratio between physical and logical sizes. + ScaleFactor float64 `json:"scaleFactor"` + // Width The width of the window in logical pixels. + Width float64 `json:"width"` +} + +// ResultType Types that can be returned from webview results. +type ResultType interface{} + +// ResultTypeString represents the string variant of ResultType +type ResultTypeString struct { + Type string `json:"$type"` + Value string `json:"value"` +} + + +// ResultTypeBoolean represents the boolean variant of ResultType +type ResultTypeBoolean struct { + Type string `json:"$type"` + Value bool `json:"value"` +} + + +// ResultTypeFloat represents the float variant of ResultType +type ResultTypeFloat struct { + Type string `json:"$type"` + Value float64 `json:"value"` +} + + +// ResultTypeSize represents the size variant of ResultType +type ResultTypeSize struct { + Type string `json:"$type"` + Value SizeWithScale `json:"value"` +} + + +// Response Responses from the webview to the client. +type Response interface{} + +// ResponseAck represents the ack variant of Response +type ResponseAck struct { + Type string `json:"$type"` + Id int `json:"id"` +} + + +// ResponseResult represents the result variant of Response +type ResponseResult struct { + Type string `json:"$type"` + Id int `json:"id"` + Result ResultType `json:"result"` +} + + +// ResponseErr represents the err variant of Response +type ResponseErr struct { + Type string `json:"$type"` + Id int `json:"id"` + Message string `json:"message"` +} + + +// Message Complete definition of all outbound messages from the webview to the client. +type Message interface{} + +// MessageNotification represents the notification variant of Message +type MessageNotification struct { + Type string `json:"$type"` + Data Notification `json:"data"` +} + + +// MessageResponse represents the response variant of Message +type MessageResponse struct { + Type string `json:"$type"` + Data Response `json:"data"` +} + + + + +// ContentProvider interface that all content types must implement +type ContentProvider interface { + isContent() // marker method +} + +// HtmlContent represents inline HTML content +type HtmlContent struct { + Html string `json:"html"` + Origin string `json:"origin,omitempty"` +} + +// UrlContent represents content loaded from a URL +type UrlContent struct { + Url string `json:"url"` + Headers map[string]string `json:"headers,omitempty"` +} + +// Implement ContentProvider interface +func (h HtmlContent) isContent() {} +func (u UrlContent) isContent() {} + +// NewHtmlContent creates HtmlContent with the specified HTML and optional origin +func NewHtmlContent(html string, origin ...string) HtmlContent { + c := HtmlContent{Html: html} + if len(origin) > 0 { + c.Origin = origin[0] + } + return c +} + +// NewUrlContent creates UrlContent with the specified URL and optional headers +func NewUrlContent(url string, headers ...map[string]string) UrlContent { + c := UrlContent{Url: url} + if len(headers) > 0 { + c.Headers = headers[0] + } + return c +} + +// Content wraps a ContentProvider for JSON marshaling +type Content struct { + provider ContentProvider +} + +// ContentFrom creates a new Content from any ContentProvider +func ContentFrom(provider ContentProvider) Content { + return Content{provider: provider} +} + +// MarshalJSON implements custom JSON marshaling for Content +func (c Content) MarshalJSON() ([]byte, error) { + return json.Marshal(c.provider) +} + +// UnmarshalJSON implements custom JSON unmarshaling for Content +func (c *Content) UnmarshalJSON(data []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if _, hasURL := raw["url"]; hasURL { + var content UrlContent + if err := json.Unmarshal(data, &content); err != nil { + return err + } + c.provider = content + return nil + } + + if _, hasHTML := raw["html"]; hasHTML { + var content HtmlContent + if err := json.Unmarshal(data, &content); err != nil { + return err + } + c.provider = content + return nil + } + + return fmt.Errorf("unknown content type") +} + +type Size struct { + // Height The height of the window in logical pixels. + Height float64 `json:"height"` + // Width The width of the window in logical pixels. + Width float64 `json:"width"` +} + +type WindowSizeStates string + +type WindowSize interface{} + +// Options Options for creating a webview. +type Options struct { + // AcceptFirstMouse Sets whether clicking an inactive window also clicks through to the webview. Default is false. + AcceptFirstMouse *bool `json:"acceptFirstMouse,omitempty"` + // Autoplay When true, all media can be played without user interaction. Default is false. + Autoplay *bool `json:"autoplay,omitempty"` + // Clipboard Enables clipboard access for the page rendered on Linux and Windows. + // Clipboard + // Clipboard macOS doesn’t provide such method and is always enabled by default. But your app will still need to add menu item accelerators to use the clipboard shortcuts. + Clipboard *bool `json:"clipboard,omitempty"` + // Decorations When true, the window will have a border, a title bar, etc. Default is true. + Decorations *bool `json:"decorations,omitempty"` + // Devtools Enable or disable webview devtools. + // Devtools + // Devtools Note this only enables devtools to the webview. To open it, you can call `webview.open_devtools()`, or right click the page and open it from the context menu. + Devtools *bool `json:"devtools,omitempty"` + // Focused Sets whether the webview should be focused when created. Default is false. + Focused *bool `json:"focused,omitempty"` + // Incognito Run the WebView with incognito mode. Note that WebContext will be ingored if incognito is enabled. + // Incognito + // Incognito Platform-specific: - Windows: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 + Incognito *bool `json:"incognito,omitempty"` + // InitializationScript Run JavaScript code when loading new pages. When the webview loads a new page, this code will be executed. It is guaranteed that the code is executed before window.onload. + InitializationScript *string `json:"initializationScript,omitempty"` + // Ipc Sets whether host should be able to receive messages from the webview via `window.ipc.postMessage`. + Ipc *bool `json:"ipc,omitempty"` + // Load The content to load into the webview. + Load *Content `json:"load,omitempty"` + // Size The size of the window. + Size *WindowSize `json:"size,omitempty"` + // Title Sets the title of the window. + Title string `json:"title"` + // Transparent Sets whether the window should be transparent. + Transparent *bool `json:"transparent,omitempty"` + // UserAgent Sets the user agent to use when loading pages. + UserAgent *string `json:"userAgent,omitempty"` +} + + + +// Request Explicit requests from the client to the webview. +type Request interface{} + +// RequestGetVersion represents the getVersion variant of Request +type RequestGetVersion struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` +} + + +// RequestEval represents the eval variant of Request +type RequestEval struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` + // Js The javascript to evaluate. + Js string `json:"js"` +} + + +// RequestSetTitle represents the setTitle variant of Request +type RequestSetTitle struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` + // Title The title to set. + Title string `json:"title"` +} + + +// RequestGetTitle represents the getTitle variant of Request +type RequestGetTitle struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` +} + + +// RequestSetVisibility represents the setVisibility variant of Request +type RequestSetVisibility struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` + // Visible Whether the window should be visible or hidden. + Visible bool `json:"visible"` +} + + +// RequestIsVisible represents the isVisible variant of Request +type RequestIsVisible struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` +} + + +// RequestOpenDevTools represents the openDevTools variant of Request +type RequestOpenDevTools struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` +} + + +// RequestGetSize represents the getSize variant of Request +type RequestGetSize struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` + // IncludeDecorations Whether to include the title bar and borders in the size measurement. + IncludeDecorations *bool `json:"include_decorations,omitempty"` +} + + +// RequestSetSize represents the setSize variant of Request +type RequestSetSize struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` + // Size The size to set. + Size Size `json:"size"` +} + + +// RequestFullscreen represents the fullscreen variant of Request +type RequestFullscreen struct { + Type string `json:"$type"` + // Fullscreen Whether to enter fullscreen mode. If left unspecified, the window will enter fullscreen mode if it is not already in fullscreen mode or exit fullscreen mode if it is currently in fullscreen mode. + Fullscreen *bool `json:"fullscreen,omitempty"` + // Id The id of the request. + Id int `json:"id"` +} + + +// RequestMaximize represents the maximize variant of Request +type RequestMaximize struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` + // Maximized Whether to maximize the window. If left unspecified, the window will be maximized if it is not already maximized or restored if it was previously maximized. + Maximized *bool `json:"maximized,omitempty"` +} + + +// RequestMinimize represents the minimize variant of Request +type RequestMinimize struct { + Type string `json:"$type"` + // Id The id of the request. + Id int `json:"id"` + // Minimized Whether to minimize the window. If left unspecified, the window will be minimized if it is not already minimized or restored if it was previously minimized. + Minimized *bool `json:"minimized,omitempty"` +} + + +// RequestLoadHtml represents the loadHtml variant of Request +type RequestLoadHtml struct { + Type string `json:"$type"` + // Html HTML to set as the content of the webview. + Html string `json:"html"` + // Id The id of the request. + Id int `json:"id"` + // Origin What to set as the origin of the webview when loading html. If not specified, the origin will be set to the value of the `origin` field when the webview was created. + Origin *string `json:"origin,omitempty"` +} + + +// RequestLoadUrl represents the loadUrl variant of Request +type RequestLoadUrl struct { + Type string `json:"$type"` + // Headers Optional headers to send with the request. + Headers map[string]string `json:"headers,omitempty"` + // Id The id of the request. + Id int `json:"id"` + // Url URL to load in the webview. + Url string `json:"url"` +} + + + +