Merge branch 'v3-alpha' into fix/systemtray-menu-realtime-update

This commit is contained in:
Lea Anthony 2026-01-25 13:10:59 +11:00 committed by GitHub
commit c44f5df9d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
186 changed files with 13799 additions and 3120 deletions

View file

@ -23,6 +23,7 @@ env:
jobs:
check-permissions:
name: Check Release Permissions
permissions: {}
runs-on: ubuntu-latest
outputs:
authorized: ${{ steps.check.outputs.authorized }}

View file

@ -1,4 +1,6 @@
name: Test Nightly Releases (Dry Run)
permissions:
contents: read
on:
workflow_dispatch:

View file

@ -135,6 +135,7 @@ For example: `bd create --help` shows `--parent`, `--deps`, `--assignee`, etc.
- Check `bd ready` before asking "what should I work on?"
- Store AI planning docs in `history/` directory
- Run `bd <cmd> --help` to discover available flags
- **ALWAYS run `coderabbit --plain` before committing** to get code analysis and catch issues early
- Do NOT create markdown TODO lists
- Do NOT use external issue trackers
- Do NOT duplicate tracking systems

View file

@ -171,7 +171,11 @@ export default defineConfig({
collapsed: true,
autogenerate: { directory: "features/browser" },
},
{ label: "Drag & Drop", link: "/features/drag-drop" },
{
label: "Drag & Drop",
collapsed: true,
autogenerate: { directory: "features/drag-and-drop" },
},
{
label: "Keyboard",
collapsed: true,

View file

@ -30,6 +30,120 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
*/
## [Unreleased]
## v3.0.0-alpha.62 - 2026-01-22
## Fixed
- Fix SetProcessDpiAwarenessContext "Access is denied" error when DPI awareness is already set via application manifest (#4803)
## v3.0.0-alpha.61 - 2026-01-20
## Fixed
- Update the docs page for keyboard shortcuts and corrects the type of the callback parameter for `KeyBinding.Add` by @ndianabasi
- Fix documentation regarding generating custom binding, must use `-d String` instead of `-o String`
## v3.0.0-alpha.60 - 2026-01-14
## Fixed
- Fix menu not clearing children on `menu.Update()`
## v3.0.0-alpha.59 - 2026-01-11
## Changed
- Update the README for the `Drag N Drop` example and highlights that `Internal Drag and Drop` is demonstrated with the example @ndianabasi
## v3.0.0-alpha.58 - 2026-01-09
## Fixed
- Fix outdated Manager API references in documentation (31 files updated to use new pattern like `app.Window.New()`, `app.Event.Emit()`, etc.) by @leaanthony
- Fix Linux crash on panic in JS-bound Go methods due to WebKit overriding signal handlers (#3965) by @leaanthony
## v3.0.0-alpha.57 - 2026-01-05
## Changed
- Replace various debug logs from Info to Debug (by @mbaklor)
## Fixed
- Fix SaveFileDialog.SetFilename() having no effect on Linux (#4841) by @samstanier
- Fix drop coordinates showing as undefined in drag-n-drop example
- Fix macOS app bundle creation failing when APP_NAME contains spaces (brace expansion issue)
- Fix index out of bounds panic on Windows when calling service methods (revert goccy/go-json)
## v3.0.0-alpha.56 - 2026-01-04
## Added
- Add `internal/libpath` package for finding native library paths on Linux with parallel search, caching, and support for Flatpak/Snap/Nix
## Changed
- **BREAKING:** Rename `EnableDragAndDrop` to `EnableFileDrop` in window options
- **BREAKING:** Rename `DropZoneDetails` to `DropTargetDetails` in event context
- **BREAKING:** Rename `DropZoneDetails()` method to `DropTargetDetails()` on `WindowEventContext`
- **BREAKING:** Remove `WindowDropZoneFilesDropped` event, use `WindowFilesDropped` instead
- **BREAKING:** Change HTML attribute from `data-wails-dropzone` to `data-file-drop-target`
- **BREAKING:** Change CSS hover class from `wails-dropzone-hover` to `file-drop-target-active`
- **BREAKING:** Remove `DragEffect`, `OnEnterEffect`, `OnOverEffect` options from Windows (were part of removed IDropTarget)
## Fixed
- Fix file drag-and-drop on Windows not working at non-100% display scaling
- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Windows
- Fix file drop coordinates being in wrong pixel space on Windows (physical vs CSS pixels)
- Fix file drag-and-drop on Linux not working reliably with hover effects
- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Linux
## Removed
- Remove native `IDropTarget` implementation on Windows in favor of JavaScript-based approach (matches v2 behavior)
## v3.0.0-alpha.55 - 2026-01-02
## Changed
- Switch to goccy/go-json for all runtime JSON processing (method bindings, events, webview requests, notifications, kvstore), improving performance by 21-63% and reducing memory allocations by 40-60%
- Optimize BoundMethod struct layout and cache isVariadic flag to reduce per-call overhead
- Use stack-allocated argument buffer for methods with `<=8` arguments to avoid heap allocations
- Optimize result collection in method calls to avoid slice allocation for single return values
- Use sync.Map for MIME type cache to improve concurrent performance
- Use buffer pool for HTTP transport request body reading
- Lazily allocate CloseNotify channel in content type sniffer to reduce per-request allocations
- Remove debug CSS logging from asset server
- Expand MIME type extension map to cover 50+ common web formats (fonts, audio, video, etc.)
## Fixed
- Update all commands in Taskfile.yml files for all operating systems to accommodate spaces in variables such as `APP_NAME` by @ndianabasi
## Removed
- Remove github.com/wailsapp/mimetype dependency in favor of expanded extension map + stdlib http.DetectContentType, reducing binary size by ~1.2MB
- Remove gopkg.in/ini.v1 dependency by implementing minimal .desktop file parser for Linux file explorer, saving ~45KB
- Remove samber/lo from runtime code by using Go 1.21+ stdlib slices package and minimal internal helpers, saving ~310KB
## v3.0.0-alpha.54 - 2025-12-29
## Added
- Add `CollectionBehavior` option to `MacWindow` for controlling window behavior across macOS Spaces and fullscreen (#4756) by @leaanthony
## Fixed
- Fix command argument error when executing 'build:universal:lipo:go' task on Linux by @wux1an
- Fix Docker error "undefined symbol: ___ubsan_handle_xxxxxxx" when running 'wails3 build GOOS=darwin GOARCH=arm64' on Linux by @wux1an
## Removed
- Remove debug printf statements from Darwin URL scheme handler (#4834)
## v3.0.0-alpha.53 - 2025-12-27
## Added
- Add unit tests for pkg/application by @leaanthony
- Add custom protocol support to MSIX packaging by @leaanthony
## Fixed
- Consolidate custom protocol documentation and add Universal Links sections by @leaanthony
## v3.0.0-alpha.52 - 2025-12-26
## Fixed
- Fix Windows systray menu crash when clicking icon repeatedly by adding guard against concurrent TrackPopupMenuEx calls (#4151) by @leaanthony
## v3.0.0-alpha.51 - 2025-12-23
## Fixed
- Prevent app crashing when calling systray.Run() before app.Run() by @leaanthony
## v3.0.0-alpha.50 - 2025-12-21
## Changed

View file

@ -264,7 +264,7 @@ Frontend2 -> EventBus: "On('data-updated', handler)"
**Example:**
```go
// Go: Emit an event
app.EmitEvent("user-logged-in", user)
app.Event.Emit("user-logged-in", user)
```
```javascript

View file

@ -538,7 +538,7 @@ func ProcessLargeFile(path string) error {
for scanner.Scan() {
lineNum++
// Emit progress events
app.EmitEvent("file-progress", map[string]interface{}{
app.Event.Emit("file-progress", map[string]interface{}{
"line": lineNum,
"text": scanner.Text(),
})

View file

@ -325,7 +325,7 @@ func main() {
},
})
app.NewWebviewWindow()
app.Window.New()
app.Run()
}
```

View file

@ -39,7 +39,7 @@ win := app.Window.New(&application.WebviewWindowOptions{
win.Show()
```
`NewWebviewWindow` delegates to `internal/runtime/webview_window_*.go` where
`app.Window.New()` delegates to `internal/runtime/webview_window_*.go` where
platform-specific constructors live:
```
@ -170,6 +170,80 @@ Follow this checklist and you'll keep the cross-platform contract intact.
---
## 9. Drag-and-Drop
File drag-and-drop uses a **JavaScript-first approach** on all platforms. The native layer intercepts OS drag events, but the actual drop handling and DOM interaction happens in JavaScript.
### Flow
1. User drags files from OS over the Wails window
2. Native layer detects the drag and notifies JavaScript for hover effects
3. User drops files
4. Native layer sends file paths + coordinates to JavaScript
5. JavaScript finds the drop target element (`data-file-drop-target`)
6. JavaScript sends file paths + element details to Go backend
7. Go emits `WindowFilesDropped` event with full context
### Platform Implementations
| Platform | Native Layer | Key Challenge |
|----------|--------------|---------------|
| **Windows** | WebView2's built-in drag support | Coordinates in CSS pixels, no conversion needed |
| **macOS** | NSWindow drag delegates | Convert window-relative to webview-relative coords |
| **Linux** | GTK3 drag signals | Must distinguish file drags from internal HTML5 drags |
### Linux: Distinguishing Drag Types
GTK and WebKit both want to handle drag events. The key is checking the drag target type:
```c
static gboolean is_file_drag(GdkDragContext *context) {
GList *targets = gdk_drag_context_list_targets(context);
for (GList *l = targets; l != NULL; l = l->next) {
GdkAtom atom = GDK_POINTER_TO_ATOM(l->data);
gchar *name = gdk_atom_name(atom);
if (name && g_strcmp0(name, "text/uri-list") == 0) {
g_free(name);
return TRUE; // External file drag
}
g_free(name);
}
return FALSE; // Internal HTML5 drag
}
```
Signal handlers return `FALSE` for internal drags (letting WebKit handle them) and `TRUE` for file drags (handling them ourselves).
### Blocking File Drops
When `EnableFileDrop` is `false`, we still need to prevent the browser from navigating to dropped files. Each platform handles this differently:
- **Windows**: JavaScript calls `preventDefault()` on drag events
- **macOS**: JavaScript calls `preventDefault()` on drag events
- **Linux**: GTK signal handlers intercept and reject file drags at the native level
### Key Files
| File | Purpose |
|------|---------|
| `pkg/application/linux_cgo.go` | GTK drag signal handlers (C code in cgo preamble) |
| `pkg/application/webview_window_darwin.go` | macOS drag delegates |
| `pkg/application/webview_window_windows.go` | WebView2 message handling |
| `internal/runtime/desktop/@wailsio/runtime/src/window.ts` | JavaScript drop handling |
### Debugging
- **Linux**: Add `printf` in C code (remember `fflush(stdout)`)
- **Windows**: Use `globalApplication.debug()`
- **JavaScript**: Check browser console, enable debug mode
Common issues:
1. **Internal HTML5 drag not working**: Native handler intercepting it (return `FALSE` for non-file drags)
2. **Hover effects not showing**: JavaScript handlers not being called
3. **Wrong coordinates**: Check coordinate space conversions
---
You now have a guided tour of the runtime internals. Combine this knowledge with
the **Codebase Layout** map and the **Asset Server** docs to navigate confidently
and make impactful contributions. Happy coding!

View file

@ -123,8 +123,8 @@ Use SignTool with your certificate.
Yes! Wails v3 has native multi-window support:
```go
window1 := app.NewWebviewWindow()
window2 := app.NewWebviewWindow()
window1 := app.Window.New()
window2 := app.Window.New()
```
### Does Wails support system tray?
@ -132,7 +132,7 @@ window2 := app.NewWebviewWindow()
Yes! Create system tray applications:
```go
tray := app.NewSystemTray()
tray := app.SystemTray.New()
tray.SetIcon(iconBytes)
tray.SetMenu(menu)
```
@ -142,7 +142,7 @@ tray.SetMenu(menu)
Yes! Wails provides native dialogs:
```go
path, _ := app.OpenFileDialog().
path, _ := app.Dialog.OpenFile().
SetTitle("Select File").
PromptForSingleSelection()
```
@ -194,7 +194,7 @@ wails3 generate bindings
Check if you called `Show()`:
```go
window := app.NewWebviewWindow()
window := app.Window.New()
window.Show() // Don't forget this!
```
@ -204,7 +204,7 @@ Ensure event names match exactly:
```go
// Go
app.EmitEvent("my-event", data)
app.Event.Emit("my-event", data)
// JavaScript
OnEvent("my-event", handler) // Must match

View file

@ -274,7 +274,7 @@ func (s *Service) ProcessLargeFile(path string) error {
processed++
// Emit progress
s.app.EmitEvent("progress", map[string]interface{}{
s.app.Event.Emit("progress", map[string]interface{}{
"processed": processed,
"total": total,
"percent": int(float64(processed) / float64(total) * 100),

View file

@ -200,7 +200,7 @@ wails3 generate bindings -ts
### Custom Output Directory
```bash
wails3 generate bindings -o ./src/bindings
wails3 generate bindings -d ./src/bindings
```
### Watch Mode (Development)
@ -461,7 +461,7 @@ const config = await GetConfig()
func ProcessLargeFile(path string) error {
// Emit progress events
for line := range lines {
app.EmitEvent("progress", line)
app.Event.Emit("progress", line)
}
return nil
}
@ -530,7 +530,7 @@ func main() {
},
})
app.NewWebviewWindow()
app.Window.New()
app.Run()
}
```

View file

@ -628,7 +628,7 @@ func main() {
},
})
app.NewWebviewWindow()
app.Window.New()
app.Run()
}
```

View file

@ -438,7 +438,7 @@ func (o *OrderService) CreateOrder(items []Item) (*Order, error) {
}
// Emit event
o.app.EmitEvent("order-created", order)
o.app.Event.Emit("order-created", order)
return order, nil
}
@ -485,7 +485,7 @@ func NewNotificationService(app *application.Application) *NotificationService {
func (n *NotificationService) Notify(message string) {
// Use application to emit events
n.app.EmitEvent("notification", message)
n.app.Event.Emit("notification", message)
// Or show system notification
n.app.ShowNotification(message)
@ -780,7 +780,7 @@ func main() {
},
})
app.NewWebviewWindow()
app.Window.New()
app.Run()
}
```

View file

@ -64,12 +64,12 @@ await CopyToClipboard("Text to copy")
```go
func copyWithFeedback(text string) {
if app.Clipboard.SetText(text) {
app.InfoDialog().
app.Dialog.Info().
SetTitle("Copied").
SetMessage("Text copied to clipboard!").
Show()
} else {
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Copy Failed").
SetMessage("Failed to copy to clipboard.").
Show()
@ -296,7 +296,7 @@ func (cm *ClipboardMonitor) checkClipboard() {
if text != cm.lastText {
cm.lastText = text
cm.app.EmitEvent("clipboard-changed", text)
cm.app.Event.Emit("clipboard-changed", text)
}
}
```

View file

@ -14,7 +14,7 @@ Create **custom dialog windows** using regular Wails windows with dialog-like be
```go
// Create custom dialog window
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Custom dialog",
Width: 400,
Height: 300,
@ -48,7 +48,7 @@ func NewCustomdialog(app *application.Application) *Customdialog {
result: make(chan string, 1),
}
dialog.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
dialog.window = app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Custom dialog",
Width: 400,
Height: 300,
@ -79,7 +79,7 @@ func (d *Customdialog) Close(result string) {
```go
func ShowModaldialog(parent *application.WebviewWindow, title string) string {
// Create dialog
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: title,
Width: 400,
Height: 200,
@ -119,7 +119,7 @@ func NewFormdialog(app *application.Application) *Formdialog {
done: make(chan bool, 1),
}
fd.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
fd.window = app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Enter Information",
Width: 500,
Height: 400,
@ -156,7 +156,7 @@ func (fd *Formdialog) Cancel() {
```go
func ShowConfirmdialog(message string) bool {
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Confirm",
Width: 400,
Height: 150,
@ -172,12 +172,12 @@ func ShowConfirmdialog(message string) bool {
result := make(chan bool, 1)
// Handle responses
app.OnEvent("confirm-yes", func(e *application.CustomEvent) {
app.Event.On("confirm-yes", func(e *application.CustomEvent) {
result <- true
dialog.Close()
})
app.OnEvent("confirm-no", func(e *application.CustomEvent) {
app.Event.On("confirm-no", func(e *application.CustomEvent) {
result <- false
dialog.Close()
})
@ -215,7 +215,7 @@ function confirm(result) {
```go
func ShowInputdialog(prompt string, defaultValue string) (string, bool) {
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Input",
Width: 400,
Height: 150,
@ -234,7 +234,7 @@ func ShowInputdialog(prompt string, defaultValue string) (string, bool) {
})
})
app.OnEvent("input-submit", func(e *application.CustomEvent) {
app.Event.On("input-submit", func(e *application.CustomEvent) {
result <- struct {
value string
ok bool
@ -242,7 +242,7 @@ func ShowInputdialog(prompt string, defaultValue string) (string, bool) {
dialog.Close()
})
app.OnEvent("input-cancel", func(e *application.CustomEvent) {
app.Event.On("input-cancel", func(e *application.CustomEvent) {
result <- struct {
value string
ok bool
@ -266,7 +266,7 @@ type Progressdialog struct {
func NewProgressdialog(title string) *Progressdialog {
pd := &Progressdialog{}
pd.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
pd.window = app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: title,
Width: 400,
Height: 150,
@ -336,7 +336,7 @@ func NewLogindialog(app *application.Application) *Logindialog {
}, 1),
}
ld.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
ld.window = app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Login",
Width: 400,
Height: 250,
@ -421,7 +421,7 @@ func NewSettingsdialog(app *application.Application, current map[string]interfac
done: make(chan bool, 1),
}
sd.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
sd.window = app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Settings",
Width: 600,
Height: 500,
@ -470,7 +470,7 @@ func NewWizarddialog(app *application.Application) *Wizarddialog {
done: make(chan bool, 1),
}
wd.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
wd.window = app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Setup Wizard",
Width: 600,
Height: 400,

View file

@ -0,0 +1,212 @@
---
title: File Drop
description: Accept files dragged from the operating system into your application
sidebar:
order: 1
---
Wails lets users drag files from the operating system (file manager, desktop) into your application. Unlike HTML5 drag-and-drop which only works within the browser, this gives you access to actual file paths on disk.
## Enable File Drop
File drop is disabled by default. To enable it, set `EnableFileDrop: true` in your window options:
```go
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My App",
Width: 800,
Height: 600,
EnableFileDrop: true,
})
```
When `EnableFileDrop` is `false` (the default), files dragged from the OS are blocked - they won't open in the webview or trigger any events. This prevents accidental navigation when users drag files over your app.
## Define Drop Zones
Drop zones tell Wails which elements should accept files. Files dropped outside a drop zone are ignored.
Add the `data-file-drop-target` attribute to any element:
```html
<div id="upload" class="drop-zone" data-file-drop-target>
Drop files here
</div>
```
You can have multiple drop zones. The element's `id` and CSS classes are passed to your Go code, so you can handle drops differently depending on where files land.
## Style Drag Hover
When files are dragged over a drop zone, Wails adds the `file-drop-target-active` class. This lets you provide visual feedback so users know where they can drop:
```css
.drop-zone {
border: 2px dashed #ccc;
padding: 40px;
text-align: center;
transition: all 0.2s ease;
}
.drop-zone.file-drop-target-active {
border-color: #007bff;
background-color: rgba(0, 123, 255, 0.1);
}
```
The class is removed automatically when files leave the zone or are dropped.
## Detect Dropped Files
When files are dropped on a valid drop zone, Wails fires a `WindowFilesDropped` event. The event context contains the full filesystem paths of all dropped files:
```go
import "github.com/wailsapp/wails/v3/pkg/events"
window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
files := event.Context().DroppedFiles()
for _, file := range files {
fmt.Println("Dropped:", file)
}
})
```
The paths are absolute, like `/home/user/documents/report.pdf` or `C:\Users\Name\Documents\report.pdf`.
## Get Drop Target Info
When you have multiple drop zones, you can find out which one received the files using `DropTargetDetails()`:
```go
window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
files := event.Context().DroppedFiles()
details := event.Context().DropTargetDetails()
fmt.Printf("Dropped on element: id=%s, classes=%v\n",
details.ElementID, details.ClassList)
fmt.Printf("Position: x=%d, y=%d\n", details.X, details.Y)
})
```
This lets you route files to different handlers:
```go
switch details.ElementID {
case "images":
handleImageUpload(files)
case "documents":
handleDocumentUpload(files)
}
```
## Complete Example
**Go:**
```go
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "File Uploader",
EnableFileDrop: true,
})
window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
files := event.Context().DroppedFiles()
details := event.Context().DropTargetDetails()
// Send to frontend
app.Event.Emit("files-dropped", map[string]any{
"files": files,
"target": details.ElementID,
})
})
```
**HTML:**
```html
<div id="images" class="drop-zone" data-file-drop-target>
Drop images here
</div>
<div id="documents" class="drop-zone" data-file-drop-target>
Drop documents here
</div>
<style>
.drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 40px;
text-align: center;
margin: 20px;
transition: all 0.2s ease;
}
.drop-zone.file-drop-target-active {
border-color: #007bff;
background-color: rgba(0, 123, 255, 0.1);
}
</style>
```
## Full Window Drop
If you want files to be droppable anywhere in your app, add the attribute to the body element:
```html
<body data-file-drop-target>
<!-- Your app content -->
</body>
```
You can use a CSS overlay to indicate the entire window is a drop target:
```css
body.file-drop-target-active::after {
content: "Drop files anywhere";
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #007bff;
background: rgba(255, 255, 255, 0.9);
pointer-events: none;
}
```
## Combining with HTML Drag & Drop
You can use both external file drops and internal HTML drag-and-drop in the same application. When `EnableFileDrop` is `true`, Wails intercepts external file drags but lets internal HTML5 drags pass through normally.
To distinguish between them in your HTML drop zone handlers, check if the drag contains files:
```javascript
zone.addEventListener('dragenter', (e) => {
// Skip external file drags - Wails handles these
if (e.dataTransfer?.types.includes('Files')) {
return;
}
// Handle internal HTML5 drags
zone.classList.add('drag-over');
});
zone.addEventListener('drop', (e) => {
// Skip external file drops - Wails handles these
if (e.dataTransfer?.types.includes('Files')) {
return;
}
e.preventDefault();
zone.classList.remove('drag-over');
// Handle internal drop
});
```
This ensures your HTML drop handlers only respond to internal drags (like moving list items), while Wails handles external file drops separately via the `WindowFilesDropped` event.
## Next Steps
- [HTML Drag & Drop](./html) - Drag elements within your app
- [Window Options](/features/windows/options) - All window configuration options

View file

@ -0,0 +1,232 @@
---
title: HTML Drag & Drop
description: Drag and drop elements within your application
sidebar:
order: 2
---
HTML5 drag-and-drop lets users drag elements within your app's UI - for example, reordering a list or moving items between columns. This is standard web functionality that works in Wails without any special setup.
## Make an Element Draggable
By default, most elements can't be dragged. To make an element draggable, add `draggable="true"`:
```html
<div class="item" draggable="true">Drag me</div>
```
The element will now show a drag preview when the user clicks and drags it.
## Define a Drop Zone
Elements don't accept drops by default. To make an element accept drops, you need to cancel the default behaviour on `dragover`:
```html
<div class="drop-zone" id="target">Drop here</div>
<script>
const target = document.getElementById('target');
target.addEventListener('dragover', (e) => {
e.preventDefault(); // Allow the drop
});
target.addEventListener('drop', (e) => {
e.preventDefault();
// Handle the drop
});
</script>
```
Calling `preventDefault()` on `dragover` is required - it signals that this element accepts drops. Without it, the drop event won't fire.
## Style Drag Hover
To show users where they can drop, add visual feedback when dragging over a drop zone. The `dragenter` event fires when something enters the zone, and `dragleave` fires when it leaves:
```css
.drop-zone {
border: 2px dashed #ccc;
padding: 40px;
transition: all 0.2s ease;
}
.drop-zone.drag-over {
border-color: #007bff;
background-color: rgba(0, 123, 255, 0.1);
}
```
```javascript
const target = document.getElementById('target');
target.addEventListener('dragenter', () => {
target.classList.add('drag-over');
});
target.addEventListener('dragleave', () => {
target.classList.remove('drag-over');
});
target.addEventListener('drop', (e) => {
e.preventDefault();
target.classList.remove('drag-over');
// Handle the drop
});
```
Note: `dragleave` also fires when entering a child element, which can cause flickering. The complete example below shows how to handle this.
## Complete Example
A task list where items can be dragged between priority columns. This tracks the dragged element in a variable, which is the simplest approach when everything is on the same page:
```html
<div class="tasks">
<div class="item" draggable="true">Fix login bug</div>
<div class="item" draggable="true">Update docs</div>
<div class="item" draggable="true">Add dark mode</div>
</div>
<div class="columns">
<div class="drop-zone" data-priority="high">
<h3>High Priority</h3>
<ul></ul>
</div>
<div class="drop-zone" data-priority="low">
<h3>Low Priority</h3>
<ul></ul>
</div>
</div>
<script>
let draggedItem = null;
// Track which item is being dragged
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('dragstart', () => {
draggedItem = item;
item.classList.add('dragging');
});
item.addEventListener('dragend', () => {
item.classList.remove('dragging');
});
});
// Handle drops on each zone
document.querySelectorAll('.drop-zone').forEach(zone => {
zone.addEventListener('dragover', (e) => {
e.preventDefault();
});
zone.addEventListener('dragenter', () => {
zone.classList.add('drag-over');
});
zone.addEventListener('dragleave', (e) => {
// Only remove the class if we're leaving the zone entirely,
// not just entering a child element
if (!zone.contains(e.relatedTarget)) {
zone.classList.remove('drag-over');
}
});
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.classList.remove('drag-over');
if (draggedItem) {
const li = document.createElement('li');
li.textContent = draggedItem.textContent;
zone.querySelector('ul').appendChild(li);
draggedItem.remove();
}
});
});
</script>
<style>
.item {
padding: 12px 16px;
background: #f0f0f0;
margin: 8px 0;
border-radius: 8px;
cursor: grab;
}
.item.dragging {
opacity: 0.5;
}
.drop-zone {
min-height: 150px;
border: 2px dashed #ccc;
border-radius: 8px;
padding: 15px;
transition: all 0.2s ease;
}
.drop-zone.drag-over {
border-color: #007bff;
background: rgba(0, 123, 255, 0.1);
}
</style>
```
## Combining with File Drop
If your app uses both HTML drag-and-drop and [File Drop](./files), your HTML drop zones will also receive events when users drag files from the operating system. To prevent confusion, filter out file drags in your handlers:
```javascript
zone.addEventListener('dragenter', (e) => {
// Ignore external file drags
if (e.dataTransfer?.types.includes('Files')) return;
zone.classList.add('drag-over');
});
zone.addEventListener('dragover', (e) => {
// Ignore external file drags
if (e.dataTransfer?.types.includes('Files')) return;
e.preventDefault();
});
zone.addEventListener('drop', (e) => {
// Ignore external file drags
if (e.dataTransfer?.types.includes('Files')) return;
e.preventDefault();
zone.classList.remove('drag-over');
// Handle the internal drop
});
```
The `dataTransfer.types` array contains `'Files'` when the user is dragging files from the OS, but contains types like `'text/plain'` for internal HTML drags. This lets you distinguish between the two.
## Passing Data with dataTransfer
The example above tracks the dragged element in a JavaScript variable. This works well when everything is on the same page. But if you need to drag between iframes or pass data that isn't tied to a DOM element, use the `dataTransfer` API:
```javascript
// When drag starts, store data
item.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', item.id);
});
// When dropped, retrieve the data
target.addEventListener('drop', (e) => {
e.preventDefault();
const itemId = e.dataTransfer.getData('text/plain');
const item = document.getElementById(itemId);
// Move or copy the item
});
```
The data is stored as strings, so you'll need to serialize objects with `JSON.stringify()` if needed.
## Next Steps
- [File Drop](./files) - Accept files from the operating system
- [MDN Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API) - Full browser API reference

View file

@ -16,7 +16,7 @@ Wails provides a **unified event system** for pub/sub communication. Emit events
**Go (emit):**
```go
app.EmitEvent("user-logged-in", map[string]interface{}{
app.Event.Emit("user-logged-in", map[string]interface{}{
"userId": 123,
"name": "Alice",
})
@ -42,9 +42,9 @@ Your application-specific events:
```go
// Emit from Go
app.EmitEvent("order-created", order)
app.EmitEvent("payment-processed", payment)
app.EmitEvent("notification", message)
app.Event.Emit("order-created", order)
app.Event.Emit("payment-processed", payment)
app.Event.Emit("notification", message)
```
```javascript
@ -62,14 +62,14 @@ Built-in OS and application events:
import "github.com/wailsapp/wails/v3/pkg/events"
// Theme changes
app.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
if e.Context().IsDarkMode() {
app.Logger.Info("Dark mode enabled")
}
})
// Application lifecycle
app.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) {
app.Logger.Info("Application started")
})
```
@ -95,29 +95,29 @@ window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEven
**Basic emit:**
```go
app.EmitEvent("event-name", data)
app.Event.Emit("event-name", data)
```
**With different data types:**
```go
// String
app.EmitEvent("message", "Hello")
app.Event.Emit("message", "Hello")
// Number
app.EmitEvent("count", 42)
app.Event.Emit("count", 42)
// Struct
app.EmitEvent("user", User{ID: 1, Name: "Alice"})
app.Event.Emit("user", User{ID: 1, Name: "Alice"})
// Map
app.EmitEvent("config", map[string]interface{}{
app.Event.Emit("config", map[string]interface{}{
"theme": "dark",
"fontSize": 14,
})
// Array
app.EmitEvent("items", []string{"a", "b", "c"})
app.Event.Emit("items", []string{"a", "b", "c"})
```
**To specific window:**
@ -145,7 +145,7 @@ Emit("broadcast-message", "Hello everyone")
**Application events:**
```go
app.OnEvent("custom-event", func(e *application.CustomEvent) {
app.Event.On("custom-event", func(e *application.CustomEvent) {
data := e.Data
// Handle event
})
@ -154,7 +154,7 @@ app.OnEvent("custom-event", func(e *application.CustomEvent) {
**With type assertion:**
```go
app.OnEvent("user-updated", func(e *application.CustomEvent) {
app.Event.On("user-updated", func(e *application.CustomEvent) {
user := e.Data.(User)
app.Logger.Info("User updated", "name", user.Name)
})
@ -164,9 +164,9 @@ app.OnEvent("user-updated", func(e *application.CustomEvent) {
```go
// All handlers will be called
app.OnEvent("order-created", logOrder)
app.OnEvent("order-created", sendEmail)
app.OnEvent("order-created", updateInventory)
app.Event.On("order-created", logOrder)
app.Event.On("order-created", sendEmail)
app.Event.On("order-created", updateInventory)
```
### In JavaScript
@ -208,18 +208,18 @@ OnEvent("data-updated", logChange)
import "github.com/wailsapp/wails/v3/pkg/events"
// Application started
app.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) {
app.Logger.Info("App started")
})
// Theme changed
app.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
isDark := e.Context().IsDarkMode()
app.EmitEvent("theme-changed", isDark)
app.Event.Emit("theme-changed", isDark)
})
// File opened
app.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *application.ApplicationEvent) {
filePath := e.Context().OpenedFile()
openFile(filePath)
})
@ -231,12 +231,12 @@ app.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *applicat
<TabItem label="macOS" icon="apple">
```go
// Application became active
app.OnApplicationEvent(events.Mac.ApplicationDidBecomeActive, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Mac.ApplicationDidBecomeActive, func(e *application.ApplicationEvent) {
app.Logger.Info("App became active")
})
// Application will terminate
app.OnApplicationEvent(events.Mac.ApplicationWillTerminate, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Mac.ApplicationWillTerminate, func(e *application.ApplicationEvent) {
cleanup()
})
```
@ -245,12 +245,12 @@ app.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *applicat
<TabItem label="Windows" icon="seti:windows">
```go
// Power status changed
app.OnApplicationEvent(events.Windows.APMPowerStatusChange, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Windows.APMPowerStatusChange, func(e *application.ApplicationEvent) {
app.Logger.Info("Power status changed")
})
// System suspending
app.OnApplicationEvent(events.Windows.APMSuspend, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Windows.APMSuspend, func(e *application.ApplicationEvent) {
saveState()
})
```
@ -259,12 +259,12 @@ app.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *applicat
<TabItem label="Linux" icon="linux">
```go
// Application startup
app.OnApplicationEvent(events.Linux.ApplicationStartup, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(e *application.ApplicationEvent) {
app.Logger.Info("App starting")
})
// Theme changed
app.OnApplicationEvent(events.Linux.SystemThemeChanged, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Linux.SystemThemeChanged, func(e *application.ApplicationEvent) {
updateTheme()
})
```
@ -347,23 +347,23 @@ func (o *OrderService) CreateOrder(items []Item) (*Order, error) {
}
// Publish event
o.app.EmitEvent("order-created", order)
o.app.Event.Emit("order-created", order)
return order, nil
}
// Subscribers
app.OnEvent("order-created", func(e *application.CustomEvent) {
app.Event.On("order-created", func(e *application.CustomEvent) {
order := e.Data.(*Order)
sendConfirmationEmail(order)
})
app.OnEvent("order-created", func(e *application.CustomEvent) {
app.Event.On("order-created", func(e *application.CustomEvent) {
order := e.Data.(*Order)
updateInventory(order)
})
app.OnEvent("order-created", func(e *application.CustomEvent) {
app.Event.On("order-created", func(e *application.CustomEvent) {
order := e.Data.(*Order)
logOrder(order)
})
@ -376,14 +376,14 @@ app.OnEvent("order-created", func(e *application.CustomEvent) {
Emit("get-user-data", { userId: 123 })
// Backend responds
app.OnEvent("get-user-data", func(e *application.CustomEvent) {
app.Event.On("get-user-data", func(e *application.CustomEvent) {
data := e.Data.(map[string]interface{})
userId := int(data["userId"].(float64))
user := getUserFromDB(userId)
// Send response
app.EmitEvent("user-data-response", user)
app.Event.Emit("user-data-response", user)
})
// Frontend receives response
@ -398,7 +398,7 @@ OnEvent("user-data-response", (user) => {
```go
// Broadcast to all windows
app.EmitEvent("global-notification", "System update available")
app.Event.Emit("global-notification", "System update available")
// Each window handles it
OnEvent("global-notification", (message) => {
@ -422,7 +422,7 @@ func (ea *EventAggregator) Add(event Event) {
// Emit batch every 100 events
if len(ea.events) >= 100 {
app.EmitEvent("event-batch", ea.events)
app.Event.Emit("event-batch", ea.events)
ea.events = nil
}
}
@ -446,7 +446,7 @@ type NotificationService struct {
func (n *NotificationService) Notify(message string) {
// Emit to all windows
n.app.EmitEvent("notification", map[string]interface{}{
n.app.Event.Emit("notification", map[string]interface{}{
"message": message,
"timestamp": time.Now(),
})
@ -460,13 +460,13 @@ func main() {
notifService := &NotificationService{app: app}
// System events
app.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
isDark := e.Context().IsDarkMode()
app.EmitEvent("theme-changed", isDark)
app.Event.Emit("theme-changed", isDark)
})
// Custom events from frontend
app.OnEvent("user-action", func(e *application.CustomEvent) {
app.Event.On("user-action", func(e *application.CustomEvent) {
data := e.Data.(map[string]interface{})
action := data["action"].(string)
@ -477,15 +477,15 @@ func main() {
})
// Window events
window := app.NewWebviewWindow()
window := app.Window.New()
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
app.EmitEvent("window-focused", window.Name())
app.Event.Emit("window-focused", window.Name())
})
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
// Confirm before close
app.EmitEvent("confirm-close", nil)
app.Event.Emit("confirm-close", nil)
e.Cancel() // Wait for confirmation
})

View file

@ -29,7 +29,7 @@ keyBindings := app.KeyBinding
Register a simple keyboard shortcut:
```go
app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+S", func(window application.Window) {
// Handle save action
app.Logger.Info("Save shortcut triggered")
// Perform save operation...
@ -42,12 +42,12 @@ Register multiple shortcuts for common operations:
```go
// File operations
app.KeyBinding.Add("Ctrl+N", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+N", func(window application.Window) {
// New file
window.EmitEvent("file:new", nil)
})
app.KeyBinding.Add("Ctrl+O", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+O", func(window application.Window) {
// Open file
dialog := app.Dialog.OpenFile()
if file, err := dialog.PromptForSingleSelection(); err == nil {
@ -55,23 +55,23 @@ app.KeyBinding.Add("Ctrl+O", func(window *application.WebviewWindow) {
}
})
app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+S", func(window application.Window) {
// Save file
window.EmitEvent("file:save", nil)
})
// Edit operations
app.KeyBinding.Add("Ctrl+Z", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+Z", func(window application.Window) {
// Undo
window.EmitEvent("edit:undo", nil)
})
app.KeyBinding.Add("Ctrl+Y", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+Y", func(window application.Window) {
// Redo (Windows/Linux)
window.EmitEvent("edit:redo", nil)
})
app.KeyBinding.Add("Cmd+Shift+Z", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Cmd+Shift+Z", func(window application.Window) {
// Redo (macOS)
window.EmitEvent("edit:redo", nil)
})
@ -139,7 +139,7 @@ Remove key bindings when they're no longer needed:
app.KeyBinding.Remove("Ctrl+S")
// Example: Temporary key binding for a modal
app.KeyBinding.Add("Escape", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Escape", func(window application.Window) {
// Close modal
window.EmitEvent("modal:close", nil)
// Remove this temporary binding
@ -165,7 +165,7 @@ for _, binding := range allBindings {
Make key bindings context-aware by checking application state:
```go
app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+S", func(window application.Window) {
// Check current application state
if isEditMode() {
// Save document
@ -184,7 +184,7 @@ app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) {
Key bindings receive the active window, allowing window-specific behavior:
```go
app.KeyBinding.Add("F11", func(window *application.WebviewWindow) {
app.KeyBinding.Add("F11", func(window application.Window) {
// Toggle fullscreen for the active window
if window.Fullscreen() {
window.SetFullscreen(false)
@ -193,7 +193,7 @@ app.KeyBinding.Add("F11", func(window *application.WebviewWindow) {
}
})
app.KeyBinding.Add("Ctrl+W", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+W", func(window application.Window) {
// Close the active window
window.Close()
})
@ -206,15 +206,15 @@ Dynamically add and remove key bindings based on application state:
```go
func enableEditMode() {
// Add edit-specific key bindings
app.KeyBinding.Add("Ctrl+B", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+B", func(window application.Window) {
window.EmitEvent("format:bold", nil)
})
app.KeyBinding.Add("Ctrl+I", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+I", func(window application.Window) {
window.EmitEvent("format:italic", nil)
})
app.KeyBinding.Add("Ctrl+U", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+U", func(window application.Window) {
window.EmitEvent("format:underline", nil)
})
}
@ -313,7 +313,7 @@ func disableEditMode() {
2. **Provide Visual Feedback**: Let users know when shortcuts are triggered:
```go
app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+S", func(window application.Window) {
saveDocument()
// Show brief notification
window.EmitEvent("notification:show", "Document saved")
@ -330,7 +330,7 @@ func disableEditMode() {
4. **Document Shortcuts**: Provide help or documentation for available shortcuts:
```go
app.KeyBinding.Add("F1", func(window *application.WebviewWindow) {
app.KeyBinding.Add("F1", func(window application.Window) {
// Show help dialog with available shortcuts
showKeyboardShortcutsHelp()
})
@ -342,7 +342,7 @@ func disableEditMode() {
app.KeyBinding.Add("Escape", exitEditModeHandler)
}
func exitEditModeHandler(window *application.WebviewWindow) {
func exitEditModeHandler(window application.Window) {
exitEditMode()
app.KeyBinding.Remove("Escape") // Clean up temporary binding
}
@ -367,33 +367,33 @@ func main() {
// File operations
if runtime.GOOS == "darwin" {
app.KeyBinding.Add("Cmd+N", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Cmd+N", func(window application.Window) {
window.EmitEvent("file:new", nil)
})
app.KeyBinding.Add("Cmd+O", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Cmd+O", func(window application.Window) {
openFile(app, window)
})
app.KeyBinding.Add("Cmd+S", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Cmd+S", func(window application.Window) {
window.EmitEvent("file:save", nil)
})
} else {
app.KeyBinding.Add("Ctrl+N", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+N", func(window application.Window) {
window.EmitEvent("file:new", nil)
})
app.KeyBinding.Add("Ctrl+O", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+O", func(window application.Window) {
openFile(app, window)
})
app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) {
app.KeyBinding.Add("Ctrl+S", func(window application.Window) {
window.EmitEvent("file:save", nil)
})
}
// View operations
app.KeyBinding.Add("F11", func(window *application.WebviewWindow) {
app.KeyBinding.Add("F11", func(window application.Window) {
window.SetFullscreen(!window.Fullscreen())
})
app.KeyBinding.Add("F1", func(window *application.WebviewWindow) {
app.KeyBinding.Add("F1", func(window application.Window) {
showKeyboardShortcuts(window)
})
@ -407,7 +407,7 @@ func main() {
}
}
func openFile(app *application.App, window *application.WebviewWindow) {
func openFile(app *application.App, window application.Window) {
dialog := app.Dialog.OpenFile()
dialog.AddFilter("Text Files", "*.txt;*.md")
@ -416,7 +416,7 @@ func openFile(app *application.App, window *application.WebviewWindow) {
}
}
func showKeyboardShortcuts(window *application.WebviewWindow) {
func showKeyboardShortcuts(window application.Window) {
shortcuts := `
Keyboard Shortcuts:
- Ctrl/Cmd+N: New file
@ -435,4 +435,4 @@ Test your key bindings on all target platforms to ensure they work correctly and
:::danger[Warning]
Be careful not to override critical system shortcuts. Some key combinations are reserved by the operating system and cannot be captured by applications.
:::
:::

View file

@ -60,7 +60,7 @@ func main() {
app.SetMenu(menu)
// Create window and run
app.NewWebviewWindow()
app.Window.New()
app.Run()
}
```
@ -487,7 +487,7 @@ func main() {
createMenu(app)
// Create main window
app.NewWebviewWindow()
app.Window.New()
app.Run()
}

View file

@ -482,7 +482,7 @@ func main() {
textMenu := createTextMenu(app)
app.RegisterContextMenu("text-menu", textMenu)
app.NewWebviewWindow()
app.Window.New()
app.Run()
}

View file

@ -38,7 +38,7 @@ func main() {
})
// Create system tray
systray := app.NewSystemTray()
systray := app.SystemTray.New()
systray.SetIcon(icon)
systray.SetLabel("My App")
@ -53,7 +53,7 @@ func main() {
systray.SetMenu(menu)
// Create hidden window
window := app.NewWebviewWindow()
window := app.Window.New()
window.Hide()
app.Run()
@ -68,7 +68,7 @@ func main() {
```go
// Create system tray
systray := app.NewSystemTray()
systray := app.SystemTray.New()
// Set icon
systray.SetIcon(iconBytes)
@ -95,7 +95,7 @@ func main() {
Name: "My App",
})
systray := app.NewSystemTray()
systray := app.SystemTray.New()
systray.SetIcon(icon)
systray.SetDarkModeIcon(iconDark) // macOS dark mode
@ -162,7 +162,7 @@ Attach a window to the tray icon for automatic show/hide:
```go
// Create window
window := app.NewWebviewWindow()
window := app.Window.New()
// Attach to tray
systray.AttachWindow(window)
@ -181,7 +181,7 @@ systray.SetWindowDebounce(200 * time.Millisecond) // Click debounce
**Example: Popup window**
```go
window := app.NewWebviewWindow(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Quick Access",
Width: 300,
Height: 400,
@ -198,7 +198,7 @@ systray.SetWindowOffset(5)
Handle tray icon clicks:
```go
systray := app.NewSystemTray()
systray := app.SystemTray.New()
// Left click
systray.OnClick(func() {
@ -445,7 +445,7 @@ func main() {
func (t *TrayApp) setup() {
// Create system tray
t.systray = t.app.NewSystemTray()
t.systray = t.app.SystemTray.New()
t.systray.SetIcon(icon)
t.systray.SetLabel("Inactive")
@ -453,7 +453,7 @@ func (t *TrayApp) setup() {
t.createMenu()
// Create window (hidden by default)
t.window = t.app.NewWebviewWindow(application.WebviewWindowOptions{
t.window = t.app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Tray Application",
Width: 400,
Height: 600,

View file

@ -266,7 +266,7 @@ func (m *MultiMonitorManager) CreateWindowOnScreen(screenIndex int) error {
screen := screens[screenIndex]
// Create window
window := m.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := m.app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: fmt.Sprintf("Window on %s", screen.Name),
Width: 800,
Height: 600,
@ -346,7 +346,7 @@ func createDPIAwareWindow(screen *Screen) *application.WebviewWindow {
width := int(float32(baseWidth) * screen.ScaleFactor)
height := int(float32(baseHeight) * screen.ScaleFactor)
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "DPI-Aware Window",
Width: width,
Height: height,

View file

@ -24,7 +24,7 @@ func main() {
})
// Create a window
window := app.NewWebviewWindow()
window := app.Window.New()
// Configure it
window.SetTitle("Hello Wails")
@ -47,7 +47,7 @@ func main() {
The simplest way to create a window:
```go
window := app.NewWebviewWindow()
window := app.Window.New()
```
**What you get:**
@ -61,7 +61,7 @@ window := app.NewWebviewWindow()
Create a window with custom configuration:
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My Application",
Width: 1200,
Height: 800,
@ -101,7 +101,7 @@ window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Give windows names for easy retrieval:
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "main-window",
Title: "Main Application",
})
@ -259,7 +259,7 @@ foundWindow := app.GetWindowByID(id)
Get the currently focused window:
```go
current := app.CurrentWindow()
current := app.Window.Current()
if current != nil {
current.SetTitle("Active Window")
}
@ -270,7 +270,7 @@ if current != nil {
Get all windows:
```go
windows := app.GetAllWindows()
windows := app.Window.GetAll()
fmt.Printf("Total windows: %d\n", len(windows))
for _, w := range windows {
@ -323,7 +323,7 @@ window.OnDestroy(func() {
```go
// Main window
mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
mainWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Main Application",
Width: 1200,
@ -331,7 +331,7 @@ mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
})
// Settings window
settingsWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
settingsWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
@ -349,12 +349,12 @@ Windows can communicate via events:
```go
// In main window
app.EmitEvent("data-updated", map[string]interface{}{
app.Event.Emit("data-updated", map[string]interface{}{
"value": 42,
})
// In settings window
app.OnEvent("data-updated", func(event *application.WailsEvent) {
app.Event.On("data-updated", func(event *application.WailsEvent) {
data := event.Data.(map[string]interface{})
value := data["value"].(int)
fmt.Printf("Received: %d\n", value)
@ -367,7 +367,7 @@ app.OnEvent("data-updated", func(event *application.WailsEvent) {
```go
// Create child window
childWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
childWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Child Window",
Parent: mainWindow, // Set parent
})
@ -410,22 +410,29 @@ childWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
<TabItem label="macOS" icon="apple">
**macOS-specific features:**
```go
// Transparent title bar
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Mac: application.MacOptions{
TitleBarAppearsTransparent: true,
Backdrop: application.MacBackdropTranslucent,
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Mac: application.MacWindow{
TitleBar: application.MacTitleBar{
AppearsTransparent: true,
},
Backdrop: application.MacBackdropTranslucent,
},
})
```
**Backdrop types:**
- `MacBackdropNormal` - Standard window
- `MacBackdropTranslucent` - Translucent background
- `MacBackdropTransparent` - Fully transparent
**Collection behavior:**
Control how windows behave across Spaces:
- `MacWindowCollectionBehaviorCanJoinAllSpaces` - Visible on all Spaces
- `MacWindowCollectionBehaviorFullScreenAuxiliary` - Can overlay fullscreen apps
**Native fullscreen:**
macOS fullscreen creates a new Space (virtual desktop).
</TabItem>
@ -435,7 +442,7 @@ childWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
```go
// Set window icon
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Linux: application.LinuxOptions{
Icon: iconBytes,
},
@ -460,7 +467,7 @@ childWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
```go
// Create splash screen
splash := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
splash := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Loading...",
Width: 400,
Height: 300,
@ -486,7 +493,7 @@ var settingsWindow *application.WebviewWindow
func showSettings() {
if settingsWindow == nil {
settingsWindow = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
settingsWindow = app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,

View file

@ -74,7 +74,7 @@ window.OnClose(func() bool {
}
// Show confirmation dialog
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Confirm Close",
Width: 400,
Height: 150,
@ -164,7 +164,7 @@ window.OnFocus(func() {
refreshContent()
// Notify other windows
app.EmitEvent("window-focused", window.ID())
app.Event.Emit("window-focused", window.ID())
})
```
@ -437,7 +437,7 @@ func main() {
}
func (mw *ManagedWindow) CreateWindow() {
mw.window = mw.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
mw.window = mw.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Event Demo",
Width: 800,
@ -566,11 +566,11 @@ Coordinate between multiple windows:
// In main window
mainWindow.OnFocus(func() {
// Notify all windows
app.EmitEvent("main-window-focused", nil)
app.Event.Emit("main-window-focused", nil)
})
// In other windows
app.OnEvent("main-window-focused", func(event *application.WailsEvent) {
app.Event.On("main-window-focused", func(event *application.WailsEvent) {
// Update UI
updateRelativeToMain()
})
@ -589,7 +589,7 @@ window.OnMaximise(func() {
window.EmitEvent("layout-changed", "maximised")
// Notify other windows
app.EmitEvent("window-maximised", window.ID())
app.Event.Emit("window-maximised", window.ID())
})
```
@ -656,7 +656,7 @@ window.Destroy()
```go
// Register handlers immediately after creation
window := app.NewWebviewWindow()
window := app.Window.New()
window.OnClose(func() bool { return true })
```

View file

@ -14,7 +14,7 @@ Wails provides **frameless window support** with CSS-based drag regions and plat
## Quick Start
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Frameless App",
Width: 800,
Height: 600,
@ -52,7 +52,7 @@ window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
### Basic Frameless Window
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
Width: 800,
Height: 600,
@ -73,7 +73,7 @@ window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
### With Transparent Background
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
BackgroundType: application.BackgroundTypeTransparent,
})
@ -336,7 +336,7 @@ body {
**Windows frameless windows:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
Windows: application.WindowsOptions{
DisableFramelessWindowDecorations: false,
@ -371,7 +371,7 @@ body {
**macOS frameless windows:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
Mac: application.MacOptions{
TitleBarAppearsTransparent: true,
@ -406,7 +406,7 @@ body {
**Linux frameless windows:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
})
```
@ -500,7 +500,7 @@ body {
### Pattern 2: Splash Screen
```go
splash := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
splash := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Loading...",
Width: 400,
Height: 300,
@ -530,7 +530,7 @@ body {
### Pattern 3: Rounded Window
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
BackgroundType: application.BackgroundTypeTransparent,
})
@ -560,7 +560,7 @@ body {
### Pattern 4: Overlay Window
```go
overlay := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
overlay := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
AlwaysOnTop: true,
BackgroundType: application.BackgroundTypeTransparent,
@ -602,7 +602,7 @@ func main() {
Name: "Frameless App",
})
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Frameless Application",
Width: 1000,
Height: 700,

View file

@ -32,7 +32,7 @@ func main() {
})
// Create main window
app.mainWindow = app.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
app.mainWindow = app.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Main Application",
Width: 1200,
@ -40,7 +40,7 @@ func main() {
})
// Create settings window (hidden initially)
app.settingsWindow = app.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
app.settingsWindow = app.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
@ -71,7 +71,7 @@ func (a *App) ShowSettings() {
### Get All Windows
```go
windows := app.GetAllWindows()
windows := app.Window.GetAll()
fmt.Printf("Total windows: %d\n", len(windows))
for _, window := range windows {
@ -92,7 +92,7 @@ if settings != nil {
window := app.GetWindowByID(123)
// Current (focused) window
current := app.CurrentWindow()
current := app.Window.Current()
```
### Window Registry Pattern
@ -132,13 +132,13 @@ Windows communicate via the event system:
```go
// In main window - emit event
app.EmitEvent("settings-changed", map[string]interface{}{
app.Event.Emit("settings-changed", map[string]interface{}{
"theme": "dark",
"fontSize": 14,
})
// In settings window - listen for event
app.OnEvent("settings-changed", func(event *application.WailsEvent) {
app.Event.On("settings-changed", func(event *application.WailsEvent) {
data := event.Data.(map[string]interface{})
theme := data["theme"].(string)
fontSize := data["fontSize"].(int)
@ -170,7 +170,7 @@ func (s *AppState) SetTheme(theme string) {
s.mu.Unlock()
// Notify all windows
app.EmitEvent("theme-changed", theme)
app.Event.Emit("theme-changed", theme)
}
func (s *AppState) GetTheme() string {
@ -204,7 +204,7 @@ var settingsWindow *application.WebviewWindow
func ShowSettings(app *application.Application) {
// Create if doesn't exist
if settingsWindow == nil {
settingsWindow = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
settingsWindow = app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
@ -245,7 +245,7 @@ func OpenDocument(app *application.Application, filePath string) {
}
// Create new document window
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: filepath.Base(filePath),
Width: 800,
Height: 600,
@ -275,7 +275,7 @@ Floating windows that stay on top:
```go
func CreateToolPalette(app *application.Application) *application.WebviewWindow {
palette := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
palette := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "tools",
Title: "Tools",
Width: 200,
@ -294,7 +294,7 @@ Child windows that block parent:
```go
func ShowModaldialog(parent *application.WebviewWindow, title string) {
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: title,
Width: 400,
Height: 200,
@ -332,7 +332,7 @@ func (e *EditorApp) UpdatePreview(content string) {
func (e *EditorApp) TogglePreview() {
if e.preview == nil {
e.preview = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
e.preview = app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "preview",
Title: "Preview",
Width: 600,
@ -357,7 +357,7 @@ func (e *EditorApp) TogglePreview() {
### Creating Child Windows
```go
childWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
childWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Child Window",
Parent: parentWindow,
})
@ -382,7 +382,7 @@ Create modal-like behaviour:
```go
func ShowModal(parent *application.WebviewWindow) {
modal := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
modal := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Modal dialog",
Width: 400,
Height: 200,
@ -463,7 +463,7 @@ Always clean up window references:
var windows = make(map[string]*application.WebviewWindow)
func CreateWindow(name string) {
window := app.NewWebviewWindow()
window := app.Window.New()
windows[name] = window
// IMPORTANT: Clean up on destroy
@ -530,7 +530,7 @@ func (wp *WindowPool) Acquire() *application.WebviewWindow {
}
// Create new window
window := app.NewWebviewWindow()
window := app.Window.New()
wp.inUse[window.ID()] = window
return window
}
@ -598,7 +598,7 @@ type WindowState struct {
func SaveLayout() *WindowLayout {
layout := &WindowLayout{}
for _, window := range app.GetAllWindows() {
for _, window := range app.Window.GetAll() {
x, y := window.Position()
width, height := window.Size()
@ -667,7 +667,7 @@ func main() {
}
func (mwa *MultiWindowApp) CreateMainWindow() {
window := mwa.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := mwa.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Main Application",
Width: 1200,
@ -684,7 +684,7 @@ func (mwa *MultiWindowApp) ShowSettings() {
return
}
window := mwa.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := mwa.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
@ -703,7 +703,7 @@ func (mwa *MultiWindowApp) OpenDocument(path string) {
return
}
window := mwa.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := mwa.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: name,
Title: path,
Width: 800,

View file

@ -86,7 +86,7 @@ Name: "main-window"
**Example:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings-window",
})
@ -161,7 +161,7 @@ Y: 100, // 100px from top edge
**Example:**
```go
settings := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
settings := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "coordinate-window",
InitialPosition: application.WindowXY, // use coordinate system
X: 100,
@ -171,7 +171,7 @@ settings := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
**Best practice:** Use `Center()` to center the window:
```go
window := app.NewWebviewWindow()
window := app.Window.New()
window.Center()
```
@ -248,7 +248,7 @@ Hidden: true,
```go
// Create hidden window
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "main-window",
Hidden: true,
BackgroundColour: application.RGBA{R: 30, G: 30, B: 30, A: 255}, // Match your theme
@ -264,7 +264,7 @@ window.Show()
**Example:**
```go
settings := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
settings := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Hidden: true,
})
@ -546,6 +546,61 @@ Assets: application.AssetOptions{
**See [Build System](/concepts/build-system) for details.**
## Input Options
### EnableFileDrop
**Type:** `bool`
**Default:** `false`
**Platform:** All
```go
EnableFileDrop: true,
```
**Purpose:** Enable drag-and-drop of files from the operating system into the window.
When enabled:
- Files dragged from file managers can be dropped into your application
- The `WindowFilesDropped` event fires with the dropped file paths
- Elements with `data-file-drop-target` attribute provide detailed drop information
**Use cases:**
- File upload interfaces
- Document editors
- Media importers
- Any app that accepts files
**Example:**
```go
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "File Uploader",
EnableFileDrop: true,
})
// Handle dropped files
window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
files := event.Context().DroppedFiles()
details := event.Context().DropTargetDetails()
for _, file := range files {
fmt.Println("Dropped:", file)
}
})
```
**HTML drop zones:**
```html
<!-- Mark elements as drop targets -->
<div id="upload" data-file-drop-target>
Drop files here
</div>
```
**See [File Drop](/features/drag-and-drop/files) for complete documentation.**
## Security Options
### ContentProtectionEnabled
@ -580,7 +635,7 @@ ContentProtectionEnabled: true,
**Example:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Secure Window",
ContentProtectionEnabled: true,
})
@ -665,39 +720,85 @@ OnDestroy: func() {
### Mac Options
```go
Mac: application.MacOptions{
TitleBarAppearsTransparent: true,
Backdrop: application.MacBackdropTranslucent,
InvisibleTitleBarHeight: 50,
TitleBarStyle: application.MacTitleBarStyleHidden,
Mac: application.MacWindow{
TitleBar: application.MacTitleBar{
AppearsTransparent: true,
Hide: false,
HideTitle: true,
FullSizeContent: true,
},
Backdrop: application.MacBackdropTranslucent,
InvisibleTitleBarHeight: 50,
WindowLevel: application.MacWindowLevelNormal,
CollectionBehavior: application.MacWindowCollectionBehaviorDefault,
},
```
**TitleBarAppearsTransparent** (`bool`)
- Makes title bar transparent
- Content extends into title bar area
**TitleBar** (`MacTitleBar`)
- `AppearsTransparent` - Makes title bar transparent, content extends into title bar area
- `Hide` - Hides the title bar completely
- `HideTitle` - Hides only the title text
- `FullSizeContent` - Extends content to full window size
**Backdrop** (`MacBackdrop`)
- `MacBackdropNormal` - Standard
- `MacBackdropTranslucent` - Blurred translucent
- `MacBackdropTransparent` - Fully transparent
- `MacBackdropNormal` - Standard opaque background
- `MacBackdropTranslucent` - Blurred translucent background
- `MacBackdropTransparent` - Fully transparent background
**InvisibleTitleBarHeight** (`int`)
- Height of invisible title bar (for dragging)
- Only when `TitleBarStyle` is `MacTitleBarStyleHidden`
- Height of invisible title bar area (for dragging)
- Useful when title bar is hidden
**TitleBarStyle** (`MacTitleBarStyle`)
- `MacTitleBarStyleDefault` - Standard title bar
- `MacTitleBarStyleHidden` - Hidden title bar
- `MacTitleBarStyleHiddenInset` - Hidden with inset
**WindowLevel** (`MacWindowLevel`)
- `MacWindowLevelNormal` - Standard window level (default)
- `MacWindowLevelFloating` - Floats above normal windows
- `MacWindowLevelTornOffMenu` - Torn-off menu level
- `MacWindowLevelModalPanel` - Modal panel level
- `MacWindowLevelMainMenu` - Main menu level
- `MacWindowLevelStatus` - Status window level
- `MacWindowLevelPopUpMenu` - Pop-up menu level
- `MacWindowLevelScreenSaver` - Screen saver level
**Example:**
**CollectionBehavior** (`MacWindowCollectionBehavior`)
Controls how the window behaves across macOS Spaces and fullscreen. These are bitmask values that can be combined using bitwise OR (`|`).
**Space behavior:**
- `MacWindowCollectionBehaviorDefault` - Uses FullScreenPrimary (default, backwards compatible)
- `MacWindowCollectionBehaviorCanJoinAllSpaces` - Window appears on all Spaces
- `MacWindowCollectionBehaviorMoveToActiveSpace` - Moves to active Space when shown
- `MacWindowCollectionBehaviorManaged` - Default managed window behavior
- `MacWindowCollectionBehaviorTransient` - Temporary/transient window
- `MacWindowCollectionBehaviorStationary` - Stays stationary during Space switches
**Window cycling:**
- `MacWindowCollectionBehaviorParticipatesInCycle` - Included in Cmd+` cycling
- `MacWindowCollectionBehaviorIgnoresCycle` - Excluded from Cmd+` cycling
**Fullscreen behavior:**
- `MacWindowCollectionBehaviorFullScreenPrimary` - Can enter fullscreen mode
- `MacWindowCollectionBehaviorFullScreenAuxiliary` - Can overlay fullscreen apps
- `MacWindowCollectionBehaviorFullScreenNone` - Disables fullscreen capability
- `MacWindowCollectionBehaviorFullScreenAllowsTiling` - Allows side-by-side tiling (macOS 10.11+)
- `MacWindowCollectionBehaviorFullScreenDisallowsTiling` - Prevents tiling (macOS 10.11+)
**Example - Spotlight-like window:**
```go
Mac: application.MacOptions{
TitleBarAppearsTransparent: true,
Backdrop: application.MacBackdropTranslucent,
InvisibleTitleBarHeight: 50,
// Window that appears on all Spaces AND can overlay fullscreen apps
Mac: application.MacWindow{
CollectionBehavior: application.MacWindowCollectionBehaviorCanJoinAllSpaces |
application.MacWindowCollectionBehaviorFullScreenAuxiliary,
WindowLevel: application.MacWindowLevelFloating,
},
```
**Example - Single behavior:**
```go
// Window that can appear over fullscreen applications
Mac: application.MacWindow{
CollectionBehavior: application.MacWindowCollectionBehaviorFullScreenAuxiliary,
},
```
@ -791,7 +892,7 @@ func main() {
Name: "My Application",
})
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
// Identity
Name: "main-window",
Title: "My Application",

View file

@ -146,7 +146,7 @@ sudo pacman -S base-devel
Alternatively, run `wails3 task setup:docker` and the build system will use Docker automatically.
### AppImage strip compatibility {#appimage-strip-compatibility}
### AppImage strip compatibility
On modern Linux distributions (Arch Linux, Fedora 39+, Ubuntu 24.04+), system libraries are compiled with `.relr.dyn` ELF sections for more efficient relocations. The `linuxdeploy` tool used to create AppImages bundles an older `strip` binary that cannot process these modern sections.

View file

@ -1,216 +0,0 @@
---
title: Custom Protocol Schemes (Deep Linking)
description: Guide to implementing custom URL schemes for deep linking in Wails applications across macOS, Windows, and Linux.
---
import { Aside } from '@astrojs/starlight/components';
# Custom Protocol Schemes (Deep Linking)
Custom protocol schemes (also known as custom URL schemes or deep linking) allow your Wails application to be launched or brought to the foreground by clicking a URL with a scheme you define (e.g., `myapp://some/data`). This is useful for various purposes, such as:
- OAuth authentication flows.
- Inter-application communication.
- Launching your app with a specific context or to perform a particular action.
Wails provides a unified way to handle these custom URL invocations across macOS, Windows, and Linux through the `events.Common.ApplicationLaunchedWithUrl` event.
## Defining Your Protocols
First, you need to define the custom protocol schemes your application will use. This is done in your `wails.json` project configuration file. Wails reads this file during the build process (`wails build`) to configure the necessary platform-specific assets like `Info.plist` for macOS, NSIS installer scripts for Windows, and `.desktop` files for Linux.
**Example: `wails.json`**
```json title="wails.json"
{
"name": "My App",
"description": "An amazing Wails app!",
"info": {
"companyName": "My Company",
"productName": "My Product",
// ... other info fields ...
"protocols": [
{
"scheme": "myapp",
"description": "My Application Custom Protocol"
},
{
"scheme": "anotherprotocol",
"description": "Another protocol for specific actions"
}
]
}
// ... other wails.json fields ...
}
```
This `info.protocols` array is what Wails uses to generate the necessary entries in platform-specific files. For example, in template files, you might access this via a path like `{{.Info.Protocols}}`.
<Aside type="note">
While `application.Options` in your `main.go` is used for runtime application settings, the definition of custom protocols for build-time asset generation (like `Info.plist`, NSIS scripts, `.desktop` files) should be managed in `wails.json`.
</Aside>
## Handling the Event in Your Application
When your application is launched or activated via a custom URL, Wails emits an `events.Common.ApplicationLaunchedWithUrl` event. You can listen for this event and retrieve the URL that triggered the launch.
```go title="main.go"
import (
"log"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
func main() {
app := application.New(application.Options{
Name: "My App", // Ensure this matches relevant info from wails.json if needed
Description: "An amazing Wails app!",
// ... other runtime options ...
})
app.Event.OnApplicationEvent(events.Common.ApplicationLaunchedWithUrl, func(e *application.ApplicationEvent) {
launchedURL := e.Context().URL() // Retrieve the URL from the event context
log.Printf("Application launched with URL: %s", launchedURL)
// TODO: Process the URL (e.g., navigate, perform action, etc.)
// Example: app.Event.Emit("frontend:ShowURL", launchedURL)
})
// ... rest of your main function ...
err := app.Run()
if err != nil {
log.Fatal(err)
}
}
```
<Aside type="note">
The `e.Context().URL()` method returns the full URL string that was used to launch the application (e.g., `myapp://some/data?param=value`).
</Aside>
## Platform-Specific Setup and Behavior
While Wails aims for a unified event, the underlying mechanism for custom protocol registration and URL delivery varies by operating system.
### macOS
- **Setup:** Wails automatically configures your application's `Info.plist` file during the build process. It adds `CFBundleURLTypes` entries based on the `info.protocols` defined in your `wails.json` file.
```xml title="Info.plist (excerpt generated by Wails)"
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>My Application Custom Protocol</string> <!-- From Protocol.Description in wails.json -->
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string> <!-- From Protocol.Scheme in wails.json -->
</array>
</dict>
<!-- ... other protocols ... -->
</array>
```
- **How it Works:** When a URL like `myapp://` is opened, macOS uses LaunchServices to find the application registered for that scheme and sends it an Apple Event (`kAEGetURL`). Wails intercepts this event and translates it into the common `events.Common.ApplicationLaunchedWithUrl` Wails event, providing the URL via `e.Context().URL()`.
#### Universal Links
In addition to custom protocol schemes, macOS also supports **Universal Links**, which allow your app to be launched by regular HTTPS links (e.g., `https://myawesomeapp.com/path`). Universal Links provide a seamless user experience between your web and desktop app.
<Aside type="caution">
Universal Links require your macOS app to be **code-signed** with a valid Apple Developer certificate and provisioning profile. Unsigned or ad-hoc signed builds will not be able to open Universal Links. Ensure your app is properly signed before testing.
</Aside>
To enable Universal Links, follow the [Apple guide on supporting Universal Links in your app](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app). You'll need to:
1. **Add entitlements** in your `entitlements.plist`:
```xml title="entitlements.plist"
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:myawesomeapp.com</string>
</array>
```
2. **Add NSUserActivityTypes to Info.plist**:
```xml title="Info.plist"
<key>NSUserActivityTypes</key>
<array>
<string>NSUserActivityTypeBrowsingWeb</string>
</array>
```
3. **Configure `apple-app-site-association` on your website:** Host an `apple-app-site-association` file at `https://myawesomeapp.com/.well-known/apple-app-site-association`.
When a Universal Link triggers your app, you'll receive the same `events.Common.ApplicationLaunchedWithUrl` event, making the handling code identical to custom protocol schemes.
### Windows
- **Setup:** Custom protocol schemes on Windows are registered in the Windows Registry. Wails facilitates this through its NSIS installer template.
- When you build your application with the `-nsis` flag, Wails uses the `v3/internal/commands/updatable_build_assets/windows/nsis/wails_tools.nsh.tmpl` file.
- This template contains macros like `CUSTOM_PROTOCOL_ASSOCIATE` and `wails.associateCustomProtocols` which use the `info.protocols` from your `wails.json` (passed as `{{.Info.Protocols}}` to the template) to create the necessary registry entries during installation.
```nsis title="wails_tools.nsh.tmpl (excerpt)"
!macro wails.associateCustomProtocols
; Create custom protocols associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
{{end}}
!macroend
```
- **How it Works:** The installer registers your application executable to be called with the URL as a command-line argument (`%1`). For example, `your_app.exe "myapp://some/data"`.
- The Wails runtime for Windows (`v3/pkg/application/application_windows.go`) has been updated to check `os.Args` upon startup. If it detects an argument that looks like a URL (e.g., `os.Args[1]` contains `"://"`), it now emits the `events.Common.ApplicationLaunchedWithUrl` event with this URL.
<Aside type="caution">
For Windows, custom protocol schemes are typically only registered when your application is installed via an installer (like the one generated by Wails with NSIS). Running the bare executable might not have the schemes registered system-wide.
</Aside>
#### Universal Links (Web-to-App Linking)
Windows supports **Web-to-App linking**, which works similarly to Universal Links on macOS. When deploying your application as an MSIX package, you can enable HTTPS links to launch your app directly.
To enable Web-to-App linking, follow the [Microsoft guide on web-to-app linking](https://learn.microsoft.com/en-us/windows/apps/develop/launch/web-to-app-linking). You'll need to:
1. **Add App URI Handler in your MSIX manifest**:
```xml title="AppPackage.appxmanifest (excerpt)"
<uap3:Extension Category="windows.appUriHandler">
<uap3:AppUriHandler>
<uap3:Host Name="myawesomeapp.com"/>
</uap3:AppUriHandler>
</uap3:Extension>
```
2. **Configure `windows-app-web-link` on your website:** Host a `windows-app-web-link` file at `https://my.app.org/.well-known/windows-app-web-link`. This file should contain your app's package information and the paths it handles.
When a Web-to-App link launches your application, you'll receive the same `events.Common.ApplicationLaunchedWithUrl` event as with custom protocol schemes.
### Linux
- **Setup:** On Linux, custom protocol handling is typically managed via `.desktop` files and the MIME type system.
- Wails uses a `.desktop` file template (e.g., `v3/internal/commands/updatable_build_assets/linux/desktop.tmpl`) which is populated during the build using information from `wails.json`.
```desktop title="desktop.tmpl (excerpt)"
[Desktop Entry]
Name={{.ProductName}}
Exec=/usr/local/bin/{{.BinaryName}} %u
MimeType={{range $index, $protocol := .Info.Protocols}}x-scheme-handler/{{$protocol.Scheme}};{{end}}
```
The `Exec` line uses `%u` which gets replaced by the URL. The `MimeType` line registers your application as a handler for `x-scheme-handler/your-scheme` for each protocol defined in `wails.json` (via `{{.Info.Protocols}}`).
- When packaging for Linux (e.g., using `nfpm`), this `.desktop` file is installed to `/usr/share/applications/`.
- A `postinstall.sh` script (e.g., `v3/internal/commands/build_assets/linux/nfpm/scripts/postinstall.sh`) is used to update the system's application and MIME databases:
```sh title="postinstall.sh (excerpt)"
#!/bin/sh
update-desktop-database -q /usr/share/applications
update-mime-database -n /usr/share/mime
```
- **How it Works:** When a URL like `myapp://` is opened, the desktop environment uses the MIME database to find the associated `.desktop` file and executes the command specified in its `Exec` line, substituting `%u` with the URL. Your application receives this URL as a command-line argument.
- The Wails runtime for Linux (`v3/pkg/application/application_linux.go`) checks `os.Args` on startup. If it detects an argument that looks like a URL, it emits the `events.Common.ApplicationLaunchedWithUrl` event.
## Testing Your Custom Protocols
- **macOS:** Open Terminal and type `open "your-scheme://your/data"`.
- **Linux:** Open a terminal and type `xdg-open "your-scheme://your/data"` (requires `xdg-utils` to be installed and the app to be properly packaged and registered).
- **Windows:** After installation via NSIS:
- You can try running `start your-scheme://your/data` from Command Prompt or PowerShell.
- Create a simple HTML file with a link `<a href="your-scheme://your/data">Test Link</a>` and open it in a browser.
<Aside type="tip">
Always ensure your application is properly built and installed (especially for Windows and Linux) for the system to recognize the custom protocol schemes.
</Aside>
By following this guide, you can effectively use custom protocol schemes to enhance your Wails application's interactivity and integration with other applications or web services.

View file

@ -5,7 +5,7 @@ sidebar:
order: 3
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
Custom URL protocols (also called URL schemes) allow your application to be launched when users click links with your custom protocol, such as `myapp://action` or `myapp://open/document`.
@ -42,7 +42,7 @@ func main() {
})
// Register handler for protocol events
app.OnEvent(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
app.Event.On(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
url := event.Context().ClickedURL()
handleCustomURL(url)
})
@ -62,7 +62,7 @@ func handleCustomURL(url string) {
Listen for protocol events to handle incoming URLs:
```go
app.OnEvent(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
app.Event.On(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
url := event.Context().ClickedURL()
// Parse the URL
@ -159,6 +159,47 @@ Start-Process "myapp://test/action"
start myapp://test/action
```
### Windows MSIX Package
Custom protocols are also automatically registered when using MSIX packaging.
#### Automatic Registration
When you build your application with MSIX, the manifest automatically includes protocol registrations from your `build/config.yml` protocols configuration.
The generated manifest includes:
```xml
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="myapp">
<uap:DisplayName>My Application Protocol</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
```
#### Universal Links (Web-to-App Linking)
Windows supports **Web-to-App linking**, which works similarly to Universal Links on macOS. When deploying your application as an MSIX package, you can enable HTTPS links to launch your app directly.
<Aside type="note">
Web-to-App linking requires manual manifest configuration. Custom protocol schemes are automatically configured from `build/config.yml`, but associated domains must be added manually to your MSIX manifest.
</Aside>
To enable Web-to-App linking, follow the [Microsoft guide on web-to-app linking](https://learn.microsoft.com/en-us/windows/apps/develop/launch/web-to-app-linking). You'll need to:
1. **Manually add App URI Handler to your MSIX manifest** (`build/windows/msix/app_manifest.xml`):
```xml
<uap3:Extension Category="windows.appUriHandler">
<uap3:AppUriHandler>
<uap3:Host Name="myawesomeapp.com"/>
</uap3:AppUriHandler>
</uap3:Extension>
```
2. **Configure `windows-app-web-link` on your website:** Host a `windows-app-web-link` file at `https://myawesomeapp.com/.well-known/windows-app-web-link`. This file should contain your app's package information and the paths it handles.
When a Web-to-App link launches your application, you'll receive the same `ApplicationOpenedWithURL` event as with custom protocol schemes.
</TabItem>
<TabItem label="macOS" icon="apple">
@ -199,6 +240,36 @@ open "myapp://test/action"
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump | grep myapp
```
#### Universal Links
In addition to custom protocol schemes, macOS also supports **Universal Links**, which allow your app to be launched by regular HTTPS links (e.g., `https://myawesomeapp.com/path`). Universal Links provide a seamless user experience between your web and desktop app.
<Aside type="caution">
Universal Links require your macOS app to be **code-signed** with a valid Apple Developer certificate and provisioning profile. Unsigned or ad-hoc signed builds will not be able to open Universal Links. Ensure your app is properly signed before testing.
</Aside>
To enable Universal Links, follow the [Apple guide on supporting Universal Links in your app](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app). You'll need to:
1. **Add entitlements** in your `entitlements.plist`:
```xml
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:myawesomeapp.com</string>
</array>
```
2. **Add NSUserActivityTypes to Info.plist**:
```xml
<key>NSUserActivityTypes</key>
<array>
<string>NSUserActivityTypeBrowsingWeb</string>
</array>
```
3. **Configure `apple-app-site-association` on your website:** Host an `apple-app-site-association` file at `https://myawesomeapp.com/.well-known/apple-app-site-association`.
When a Universal Link triggers your app, you'll receive the same `ApplicationOpenedWithURL` event, making the handling code identical to custom protocol schemes.
</TabItem>
<TabItem label="Linux" icon="linux">
@ -291,7 +362,7 @@ func main() {
func (a *App) setup() {
// Create window
a.window = a.app.NewWebviewWindow(application.WebviewWindowOptions{
a.window = a.app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "DeepLink Demo",
Width: 800,
Height: 600,
@ -299,7 +370,7 @@ func (a *App) setup() {
})
// Handle custom protocol URLs
a.app.OnEvent(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
a.app.Event.On(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
customURL := event.Context().ClickedURL()
a.handleDeepLink(customURL)
})

View file

@ -116,10 +116,10 @@ func (s *Service) ProcessLargeFile(path string) error {
go func() {
result, err := s.process(path)
if err != nil {
s.app.EmitEvent("process-error", err.Error())
s.app.Event.Emit("process-error", err.Error())
return
}
s.app.EmitEvent("process-complete", result)
s.app.Event.Emit("process-complete", result)
}()
return nil

View file

@ -47,7 +47,7 @@ func main() {
},
})
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My App",
Name: "main",
})
@ -291,7 +291,7 @@ func main() {
},
})
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Raw Message Demo",
Name: "main",
Width: 400,
@ -382,7 +382,7 @@ RawMessageHandler: func(window application.Window, message string, originInfo *a
window.EmitEvent("response", result)
// Or broadcast to all windows
app.EmitEvent("broadcast", result)
app.Event.Emit("broadcast", result)
}
```

View file

@ -49,7 +49,7 @@ app := application.New(application.Options{
},
})
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My App",
Width: 1024,
Height: 768,
@ -137,12 +137,12 @@ type MyService struct {
}
func (s *MyService) UpdateTitle() {
window := s.app.CurrentWindow()
window := s.app.Window.Current()
window.SetTitle("New Title")
}
func (s *MyService) EmitEvent() {
s.app.EmitEvent("event-name", data)
s.app.Event.Emit("event-name", data)
}
```
@ -203,19 +203,19 @@ runtime.EventsEmit(ctx, "event-name", data)
**v3:**
```go
app.OnEvent("event-name", func(e *application.CustomEvent) {
app.Event.On("event-name", func(e *application.CustomEvent) {
data := e.Data
// Handle event
})
app.EmitEvent("event-name", data)
app.Event.Emit("event-name", data)
```
**Why this is better:**
- **Type safety**: Events use proper event objects instead of `...interface{}`
- **Better debugging**: Event objects contain metadata like event name, making debugging easier
- **Clearer API**: `app.OnEvent()` and `app.EmitEvent()` are more intuitive than runtime functions
- **Clearer API**: `app.Event.On()` and `app.Event.Emit()` are more intuitive than runtime functions
- **No context needed**: Events work directly on the app object without threading context
- **Simpler handlers**: Event handlers have a clear signature instead of variadic parameters
@ -236,10 +236,10 @@ runtime.WindowSetSize(ctx, 800, 600)
```go
// Multiple windows supported
window1 := app.NewWebviewWindow()
window1 := app.Window.New()
window1.SetSize(800, 600)
window2 := app.NewWebviewWindow()
window2 := app.Window.New()
window2.SetSize(1024, 768)
```
@ -339,7 +339,7 @@ func main() {
},
})
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My App",
Width: 1024,
Height: 768,
@ -416,10 +416,10 @@ func (a *App) DoSomething() {
```go
func (s *MyService) DoSomething() {
window := s.app.CurrentWindow()
window := s.app.Window.Current()
window.SetTitle("New Title")
s.app.EmitEvent("update", data)
s.app.Event.Emit("update", data)
s.app.Logger.Info("Message")
}
@ -510,7 +510,7 @@ selection, err := runtime.OpenFileDialog(ctx, runtime.OpenDialogOptions{
**v3:**
```go
selection, err := app.OpenFileDialog(application.OpenFileDialogOptions{
selection, err := app.Dialog.OpenFile(application.OpenFileDialogOptions{
Title: "Select File",
})
```
@ -549,7 +549,7 @@ fileMenu.Add("Quit").OnClick(func(ctx *application.Context) {
**v3:**
```go
systray := app.NewSystemTray()
systray := app.SystemTray.New()
systray.SetIcon(iconBytes)
systray.SetLabel("My App")
@ -602,7 +602,7 @@ func NewMyService(app *application.Application) *MyService {
Use window methods directly:
```go
window := s.app.CurrentWindow()
window := s.app.Window.Current()
window.SetTitle("New Title")
```
@ -616,7 +616,7 @@ Check event names match exactly:
```go
// Go
app.EmitEvent("my-event", data)
app.Event.Emit("my-event", data)
// JavaScript
OnEvent("my-event", handler) // Must match exactly

View file

@ -24,7 +24,7 @@ Wails provides native desktop capabilities:
- [Events](/features/events/system) - Communication between components
- [Bindings](/features/bindings/methods) - Type-safe Go ↔ JavaScript calls
- [Clipboard](/features/clipboard) - Copy/paste operations
- [Drag & Drop](/features/drag-drop) - File drag and drop
- [Drag & Drop](/features/drag-and-drop/files) - File drag and drop
- [Keyboard](/features/keyboard) - Global shortcuts
## Get Help

View file

@ -80,33 +80,33 @@ fmt.Println("App name:", config.Name)
## Window Management
### NewWebviewWindow()
### app.Window.New()
Creates a new webview window with default options.
```go
func (a *App) NewWebviewWindow() *WebviewWindow
func (wm *WindowManager) New() *WebviewWindow
```
**Example:**
```go
window := app.NewWebviewWindow()
window := app.Window.New()
window.Show()
```
### NewWebviewWindowWithOptions()
### app.Window.NewWithOptions()
Creates a new webview window with custom options.
```go
func (a *App) NewWebviewWindowWithOptions(options WebviewWindowOptions) *WebviewWindow
func (wm *WindowManager) NewWithOptions(options WebviewWindowOptions) *WebviewWindow
```
**Example:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My Window",
Width: 800,
Height: 600,
@ -114,35 +114,35 @@ window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
})
```
### GetWindowByName()
### app.Window.GetByName()
Gets a window by its name.
```go
func (a *App) GetWindowByName(name string) Window
func (wm *WindowManager) GetByName(name string) Window
```
**Example:**
```go
window := app.GetWindowByName("main")
window := app.Window.GetByName("main")
if window != nil {
window.Show()
}
```
### GetWindows()
### app.Window.GetAll()
Returns all application windows.
```go
func (a *App) GetWindows() []Window
func (wm *WindowManager) GetAll() []Window
```
**Example:**
```go
windows := app.GetWindows()
windows := app.Window.GetAll()
for _, window := range windows {
fmt.Println("Window:", window.Name())
}
@ -168,7 +168,7 @@ app.Env // Environment variables
```go
// Create window
window := app.Window.NewWebviewWindow()
window := app.Window.New()
// Show dialog
app.Dialog.Info().SetMessage("Hello!").Show()
@ -207,55 +207,55 @@ app.RegisterService(application.NewService(NewMyService(app)))
## Event Management
### EmitEvent()
### app.Event.Emit()
Emits a custom event.
```go
func (a *App) EmitEvent(name string, data ...interface{})
func (em *EventManager) Emit(name string, data ...interface{})
```
**Example:**
```go
// Emit event with data
app.EmitEvent("user-logged-in", map[string]interface{}{
app.Event.Emit("user-logged-in", map[string]interface{}{
"username": "john",
"timestamp": time.Now(),
})
```
### OnEvent()
### app.Event.On()
Listens for custom events.
```go
func (a *App) OnEvent(name string, callback func(*CustomEvent))
func (em *EventManager) On(name string, callback func(*CustomEvent))
```
**Example:**
```go
app.OnEvent("user-logged-in", func(e *application.CustomEvent) {
app.Event.On("user-logged-in", func(e *application.CustomEvent) {
data := e.Data.(map[string]interface{})
username := data["username"].(string)
fmt.Println("User logged in:", username)
})
```
### OnApplicationEvent()
### app.Event.OnApplicationEvent()
Listens for application lifecycle events.
```go
func (a *App) OnApplicationEvent(eventType ApplicationEventType, callback func(*ApplicationEvent)) func()
func (em *EventManager) OnApplicationEvent(eventType ApplicationEventType, callback func(*ApplicationEvent)) func()
```
**Example:**
```go
// Listen for shutdown
app.OnApplicationEvent(application.EventApplicationShutdown, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(application.EventApplicationShutdown, func(e *application.ApplicationEvent) {
fmt.Println("Application shutting down")
// Cleanup
})
@ -415,7 +415,7 @@ func main() {
})
// Create main window
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My App",
Width: 1024,
Height: 768,

View file

@ -24,12 +24,12 @@ The Events API provides methods to emit and listen to events, enabling communica
## Event Methods (Go)
### EmitEvent()
### app.Event.Emit()
Emits a custom event to all windows.
```go
func (a *App) EmitEvent(name string, data ...interface{})
func (em *EventManager) Emit(name string, data ...interface{})
```
**Parameters:**
@ -39,24 +39,24 @@ func (a *App) EmitEvent(name string, data ...interface{})
**Example:**
```go
// Emit simple event
app.EmitEvent("user-logged-in")
app.Event.Emit("user-logged-in")
// Emit with data
app.EmitEvent("data-updated", map[string]interface{}{
app.Event.Emit("data-updated", map[string]interface{}{
"count": 42,
"status": "success",
})
// Emit multiple values
app.EmitEvent("progress", 75, "Processing files...")
app.Event.Emit("progress", 75, "Processing files...")
```
### OnEvent()
### app.Event.On()
Listens for custom events in Go.
```go
func (a *App) OnEvent(name string, callback func(*CustomEvent)) func()
func (em *EventManager) On(name string, callback func(*CustomEvent)) func()
```
**Parameters:**
@ -68,7 +68,7 @@ func (a *App) OnEvent(name string, callback func(*CustomEvent)) func()
**Example:**
```go
// Listen for events
cleanup := app.OnEvent("user-action", func(e *application.CustomEvent) {
cleanup := app.Event.On("user-action", func(e *application.CustomEvent) {
data := e.Data.(map[string]interface{})
action := data["action"].(string)
app.Logger.Info("User action", "action", action)
@ -87,7 +87,7 @@ Emit events to a specific window:
window.EmitEvent("notification", "Hello from Go!")
// Emit to all windows
app.EmitEvent("global-update", data)
app.Event.Emit("global-update", data)
```
## Event Methods (Frontend)
@ -187,12 +187,12 @@ OffAll('data-updated')
## Application Events
### OnApplicationEvent()
### app.Event.OnApplicationEvent()
Listens for application lifecycle events.
```go
func (a *App) OnApplicationEvent(eventType ApplicationEventType, callback func(*ApplicationEvent)) func()
func (em *EventManager) OnApplicationEvent(eventType ApplicationEventType, callback func(*ApplicationEvent)) func()
```
**Event Types:**
@ -203,13 +203,13 @@ func (a *App) OnApplicationEvent(eventType ApplicationEventType, callback func(*
**Example:**
```go
// Handle application startup
app.OnApplicationEvent(application.EventApplicationStarted, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(application.EventApplicationStarted, func(e *application.ApplicationEvent) {
app.Logger.Info("Application started")
// Initialize resources
})
// Handle application shutdown
app.OnApplicationEvent(application.EventApplicationShutdown, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(application.EventApplicationShutdown, func(e *application.ApplicationEvent) {
app.Logger.Info("Application shutting down")
// Cleanup resources, save state
database.Close()
@ -268,7 +268,7 @@ func (s *DataService) FetchData(query string) ([]Item, error) {
items := fetchFromDatabase(query)
// Emit event when done
s.app.EmitEvent("data-fetched", map[string]interface{}{
s.app.Event.Emit("data-fetched", map[string]interface{}{
"query": query,
"count": len(items),
})
@ -309,7 +309,7 @@ func (s *Service) ProcessFiles(files []string) error {
processFile(file)
// Emit progress event
s.app.EmitEvent("progress", map[string]interface{}{
s.app.Event.Emit("progress", map[string]interface{}{
"current": i + 1,
"total": total,
"percent": float64(i+1) / float64(total) * 100,
@ -317,7 +317,7 @@ func (s *Service) ProcessFiles(files []string) error {
})
}
s.app.EmitEvent("processing-complete")
s.app.Event.Emit("processing-complete")
return nil
}
```
@ -349,7 +349,7 @@ Perfect for applications with multiple windows like settings panels, dashboards,
```go
// Broadcast to all windows
app.EmitEvent("theme-changed", "dark")
app.Event.Emit("theme-changed", "dark")
// Send to specific window
preferencesWindow.EmitEvent("settings-updated", settings)
@ -391,7 +391,7 @@ func (s *StateService) UpdateState(key string, value interface{}) {
s.mu.Unlock()
// Notify all windows
s.app.EmitEvent("state-updated", map[string]interface{}{
s.app.Event.Emit("state-updated", map[string]interface{}{
"key": key,
"value": value,
})
@ -435,21 +435,21 @@ type NotificationService struct {
}
func (s *NotificationService) Success(message string) {
s.app.EmitEvent("notification", map[string]interface{}{
s.app.Event.Emit("notification", map[string]interface{}{
"type": "success",
"message": message,
})
}
func (s *NotificationService) Error(message string) {
s.app.EmitEvent("notification", map[string]interface{}{
s.app.Event.Emit("notification", map[string]interface{}{
"type": "error",
"message": message,
})
}
func (s *NotificationService) Info(message string) {
s.app.EmitEvent("notification", map[string]interface{}{
s.app.Event.Emit("notification", map[string]interface{}{
"type": "info",
"message": message,
})
@ -498,7 +498,7 @@ func NewEventDemoService(app *application.Application) *EventDemoService {
service := &EventDemoService{app: app}
// Listen for custom events
app.OnEvent("user-action", func(e *application.CustomEvent) {
app.Event.On("user-action", func(e *application.CustomEvent) {
data := e.Data.(map[string]interface{})
app.Logger.Info("User action received", "data", data)
})
@ -508,26 +508,26 @@ func NewEventDemoService(app *application.Application) *EventDemoService {
func (s *EventDemoService) StartLongTask() {
go func() {
s.app.EmitEvent("task-started")
s.app.Event.Emit("task-started")
for i := 1; i <= 10; i++ {
time.Sleep(500 * time.Millisecond)
s.app.EmitEvent("task-progress", map[string]interface{}{
s.app.Event.Emit("task-progress", map[string]interface{}{
"step": i,
"total": 10,
"percent": i * 10,
})
}
s.app.EmitEvent("task-completed", map[string]interface{}{
s.app.Event.Emit("task-completed", map[string]interface{}{
"message": "Task finished successfully!",
})
}()
}
func (s *EventDemoService) BroadcastMessage(message string) {
s.app.EmitEvent("broadcast", message)
s.app.Event.Emit("broadcast", message)
}
func main() {
@ -536,11 +536,11 @@ func main() {
})
// Handle application lifecycle
app.OnApplicationEvent(application.EventApplicationStarted, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(application.EventApplicationStarted, func(e *application.ApplicationEvent) {
app.Logger.Info("Application started!")
})
app.OnApplicationEvent(application.EventApplicationShutdown, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(application.EventApplicationShutdown, func(e *application.ApplicationEvent) {
app.Logger.Info("Application shutting down...")
})
@ -549,7 +549,7 @@ func main() {
app.RegisterService(application.NewService(service))
// Create window
window := app.NewWebviewWindow()
window := app.Window.New()
// Handle window events
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
@ -673,11 +673,11 @@ This mapping happens automatically in the background, so when you listen for `ev
```go
import "github.com/wailsapp/wails/v3/pkg/events"
app.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) {
app.Logger.Info("Application ready!")
})
app.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
// Update app theme
})
```
@ -720,7 +720,7 @@ window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent)
// Cancel window close
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
result, _ := app.QuestionDialog().
result, _ := app.Dialog.Question().
SetMessage("Close window?").
SetButtons("Yes", "No").
Show()
@ -749,14 +749,14 @@ window.OnWindowEvent(events.Common.WindowRuntimeReady, func(e *application.Windo
```go
// Good - descriptive and specific
app.EmitEvent("user:logged-in", user)
app.EmitEvent("data:fetch:complete", results)
app.EmitEvent("ui:theme:changed", theme)
app.Event.Emit("user:logged-in", user)
app.Event.Emit("data:fetch:complete", results)
app.Event.Emit("ui:theme:changed", theme)
// Bad - vague and unclear
app.EmitEvent("event1", data)
app.EmitEvent("update", stuff)
app.EmitEvent("e", value)
app.Event.Emit("event1", data)
app.Event.Emit("update", stuff)
app.Event.Emit("e", value)
```
## Performance Considerations
@ -776,7 +776,7 @@ func (s *Service) EmitWithDebounce(event string, data interface{}) {
return // Skip this emission
}
s.app.EmitEvent(event, data)
s.app.Event.Emit(event, data)
s.lastEmit = now
}
```

View file

@ -316,12 +316,12 @@ menu.Update()
## Application Menu
### SetApplicationMenu()
### app.Menu.Set()
Sets the application's main menu bar.
```go
func (a *App) SetApplicationMenu(menu *Menu)
func (mm *MenuManager) Set(menu *Menu)
```
**Example:**
@ -347,7 +347,7 @@ editMenu.Add("Cut").SetAccelerator("Ctrl+X").OnClick(cut)
editMenu.Add("Copy").SetAccelerator("Ctrl+C").OnClick(copy)
editMenu.Add("Paste").SetAccelerator("Ctrl+V").OnClick(paste)
app.SetApplicationMenu(menu)
app.Menu.Set(menu)
```
**Platform notes:**
@ -357,12 +357,12 @@ app.SetApplicationMenu(menu)
## Context Menus
### RegisterContextMenu()
### app.ContextMenu.Add()
Registers a context menu (right-click menu) with a specific name.
```go
func (a *App) RegisterContextMenu(name string, menu *Menu)
func (cm *ContextMenuManager) Add(name string, menu *Menu)
```
**Parameters:**
@ -412,17 +412,17 @@ func updateContextMenu() {
## System Tray Menu
### NewSystemTray()
### app.SystemTray.New()
Creates a new system tray icon.
```go
func (a *App) NewSystemTray() *SystemTray
func (sm *SystemTrayManager) New() *SystemTray
```
**Example:**
```go
tray := app.NewSystemTray()
tray := app.SystemTray.New()
```
### SetIcon()
@ -572,9 +572,9 @@ func main() {
})
menu := createMenu(app)
app.SetApplicationMenu(menu)
app.Menu.Set(menu)
window := app.NewWebviewWindow()
window := app.Window.New()
window.Show()
app.Run()
@ -586,7 +586,7 @@ func main() {
```go
func setupSystemTray(app *application.Application, window *application.Window) {
// Create system tray
tray := app.NewSystemTray()
tray := app.SystemTray.New()
// Set icon
iconData, _ := os.ReadFile("icon.png")
@ -667,7 +667,7 @@ func (e *Editor) createMenu() {
})
e.updateMenuState()
e.app.SetApplicationMenu(e.menu)
e.app.Menu.Set(e.menu)
}
func (e *Editor) updateMenuState() {

View file

@ -37,7 +37,7 @@ This is the complete API reference for Wails v3. It documents every public type,
Most methods that can fail return `error` as the last return value:
```go
window, err := app.NewWebviewWindow()
window, err := app.Window.New()
if err != nil {
log.Fatal(err)
}

View file

@ -30,7 +30,7 @@ func (w *Window) Show()
**Example:**
```go
window := app.NewWebviewWindow()
window := app.Window.New()
window.Show()
```
@ -202,7 +202,7 @@ func (w *Window) Centre()
**Example:**
```go
window := app.NewWebviewWindow()
window := app.Window.New()
window.Centre()
window.Show()
```
@ -441,7 +441,7 @@ func (w *Window) RegisterHook(
import "github.com/wailsapp/wails/v3/pkg/events"
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
result, _ := app.QuestionDialog().
result, _ := app.Dialog.Question().
SetTitle("Confirm Close").
SetMessage("Are you sure you want to close this window?").
SetButtons("Yes", "No").
@ -457,7 +457,7 @@ window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent
```go
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
if hasUnsavedChanges {
result, _ := app.QuestionDialog().
result, _ := app.Dialog.Question().
SetMessage("Save changes before closing?").
SetButtons("Save", "Don't Save", "Cancel").
Show()
@ -620,7 +620,7 @@ func main() {
})
// Create window with options
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My Application",
Width: 1024,
Height: 768,
@ -638,7 +638,7 @@ func main() {
// Set up event hooks
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
// Confirm before closing
result, _ := app.QuestionDialog().
result, _ := app.Dialog.Question().
SetTitle("Confirm Close").
SetMessage("Are you sure you want to close this window?").
SetButtons("Yes", "No").

View file

@ -181,7 +181,6 @@ tasks:
gin-routing
gin-service
hide-window
html-dnd-api
ignore-mouse
keybindings
liquid-glass
@ -306,7 +305,6 @@ tasks:
gin-routing
gin-service
hide-window
html-dnd-api
ignore-mouse
keybindings
liquid-glass

View file

@ -1,6 +1,6 @@
# Unreleased Changes
<!--
<!--
This file is used to collect changelog entries for the next v3-alpha release.
Add your changes under the appropriate sections below.
@ -25,6 +25,7 @@ After processing, the content will be moved to the main changelog and this file
<!-- Bug fixes -->
- Fix macOS system tray menu real-time updates using NSMenuDelegate (#4630)
- Implement macOS system tray onMenuOpen/onMenuClose callbacks for parity with Windows and Linux
- Fix `Position()` and `SetPosition()` using inconsistent coordinate systems on macOS, causing window position drift when saving/restoring state (#4816) by @leaanthony
## Deprecated
<!-- Soon-to-be removed features -->

View file

@ -1,27 +1,75 @@
# Drag-n-drop Example
# File Drop Example
This example demonstrates how to handle files being dragged into the application.
This example demonstrates how to handle files being dragged from the operating system (Finder, Explorer, file managers) into a Wails application.
Dropped files are automatically categorised by type and displayed in separate buckets: documents, images, or other files.
## How it works
1. Enable file drops in window options:
```go
EnableFileDrop: true
```
2. Mark elements as drop targets in HTML:
```html
<div data-file-drop-target>Drop files here</div>
```
3. Listen for the `WindowFilesDropped` event:
```go
win.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
files := event.Context().DroppedFiles()
details := event.Context().DropTargetDetails()
// Handle the dropped files
})
```
4. Optionally forward to frontend:
```go
application.Get().Event.Emit("files-dropped", map[string]any{
"files": files,
"details": details,
})
```
## Drop Target Details
When files are dropped, you can get information about the drop location:
- `ElementID` - The ID of the element that received the drop
- `ClassList` - CSS classes on the drop target
- `X`, `Y` - Coordinates of the drop within the element
## Styling
When files are dragged over a valid drop target, Wails adds the `file-drop-target-active` class:
```css
.file-drop-target-active {
border-color: #4a9eff;
background: rgba(74, 158, 255, 0.1);
}
```
## Running the example
To run the example, simply run the following command:
```bash
go run main.go
```
## Building the example
Then drag files from your desktop or file manager into the drop zone.
To build the example in debug mode, simply run the following command:
## HTML5 Drag and Drop API
```bash
wails3 task build
```
This example also includes a demonstration for dragging elements *within* your application via the HTML5 Drag and Drop API.
# Status
Scroll down to the `Internal Drag and Drop` section within the launched application to interact with the demo.
| Platform | Status |
|----------|-------------|
| Mac | Working |
| Windows | Not Working |
| Linux | |
## Status
| Platform | Status |
|----------|---------|
| Mac | Working |
| Windows | Working |
| Linux | Working |

View file

@ -3,422 +3,433 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Tree Drag-and-Drop Example</title>
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Material UI lite CSS -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
<script defer src="https://code.getmdl.io/1.3.0/material.min.js" integrity="sha384-7/3UJ+C4EZRMEh+yDUhEZJ5YH9Ul3XW1U6AlTjHlyMowgIkG7svPJf0BN3b2CEm1" crossorigin="anonymous"></script>
<title>Drag and Drop Demo</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
background-color: #1e1e1e;
color: #f0f0f0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
}
.container {
display: flex;
height: calc(100vh - 140px);
gap: 20px;
h1 {
margin-top: 40px;
text-align: center;
color: #fff;
}
.file-tree {
flex: 1;
background-color: #2a2a2a;
border-radius: 4px;
padding: 16px;
overflow: auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.info-panel {
flex: 1;
background-color: #2a2a2a;
border-radius: 4px;
padding: 16px;
overflow: auto;
display: flex;
flex-direction: column;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.tree-node {
padding: 4px;
margin: 2px 0;
display: flex;
align-items: center;
border-radius: 4px;
cursor: pointer;
}
.tree-node:hover {
background-color: #3a3a3a;
}
.folder {
color: #ffca28; /* Amber for folders */
}
.file {
color: #81c784; /* Light green for files */
}
.node-icon {
margin-right: 8px;
}
.tree-children {
padding-left: 24px;
}
.dropzone {
transition: background-color 0.3s, box-shadow 0.3s;
}
/* Wails applied class for active dropzones during drag */
.wails-dropzone-hover {
background-color: rgba(63, 81, 181, 0.2) !important;
box-shadow: 0 0 8px rgba(63, 81, 181, 0.5) !important;
}
/* Custom styles for folder highlight */
.folder-dropzone {
transition: all 0.2s ease;
border: 1px solid transparent;
}
.folder-dropzone.wails-dropzone-hover {
border: 1px dashed #7986cb;
box-shadow: inset 0 0 5px rgba(121, 134, 203, 0.5) !important;
}
#drop-output {
background-color: #3a3a3a;
color: #f0f0f0;
font-family: monospace;
padding: 16px;
border-radius: 4px;
flex-grow: 1;
white-space: pre-wrap;
overflow: auto;
margin-top: 16px;
}
.header {
margin-bottom: 20px;
h2 {
color: #888;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 1px;
margin: 40px 0 20px;
text-align: center;
}
.info-header {
margin-bottom: 16px;
color: #7986cb;
.instructions {
max-width: 900px;
margin: 0 auto 20px;
text-align: center;
color: #888;
line-height: 1.6;
}
.material-icons {
font-size: 20px;
}
/* Path breadcrumb */
.path-display {
background-color: #3a3a3a;
padding: 8px 16px;
.instructions code {
background: #16213e;
padding: 2px 6px;
border-radius: 4px;
margin-bottom: 16px;
color: #4a9eff;
}
/* ===== External File Drop Section ===== */
.external-section {
max-width: 900px;
margin: 0 auto 40px;
}
.drop-zone {
padding: 40px;
border: 3px dashed #444;
border-radius: 16px;
text-align: center;
transition: all 0.2s ease;
}
.drop-zone p {
margin: 0;
color: #666;
font-size: 18px;
}
/* Wails adds this class when dragging files over */
.file-drop-target-active {
border-color: #4a9eff !important;
background: rgba(74, 158, 255, 0.1) !important;
box-shadow: 0 0 30px rgba(74, 158, 255, 0.2);
}
.file-drop-target-active p {
color: #4a9eff;
}
.buckets {
display: flex;
gap: 20px;
margin-top: 20px;
}
.bucket {
flex: 1;
min-height: 150px;
background: #16213e;
border-radius: 12px;
padding: 15px;
}
.bucket h3 {
margin: 0 0 15px 0;
padding-bottom: 10px;
border-bottom: 1px solid #2a3a5e;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
}
.bucket.documents h3 { color: #4a9eff; }
.bucket.images h3 { color: #9b59b6; }
.bucket.other h3 { color: #27ae60; }
.bucket ul {
margin: 0;
padding: 0;
list-style: none;
}
.bucket li {
padding: 6px 0;
font-family: monospace;
color: #bbdefb;
overflow-x: auto;
white-space: nowrap;
font-size: 12px;
color: #aaa;
word-break: break-all;
}
.bucket .empty {
color: #444;
font-style: italic;
font-family: inherit;
}
/* ===== Internal Drag Section ===== */
.internal-section {
max-width: 900px;
margin: 0 auto 40px;
}
.internal-container {
display: flex;
gap: 20px;
align-items: flex-start;
}
.draggable-items {
flex: 1;
background: #16213e;
border-radius: 12px;
padding: 15px;
min-height: 200px;
}
.draggable-items h3 {
margin: 0 0 15px 0;
padding-bottom: 10px;
border-bottom: 1px solid #2a3a5e;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
color: #e67e22;
}
.draggable-item {
background: #2a3a5e;
padding: 12px 16px;
margin-bottom: 8px;
border-radius: 8px;
cursor: grab;
transition: all 0.2s ease;
user-select: none;
}
.draggable-item:hover {
background: #3a4a6e;
}
.draggable-item.dragging {
opacity: 0.5;
cursor: grabbing;
}
.drop-targets {
flex: 2;
display: flex;
gap: 15px;
}
.internal-drop-zone {
flex: 1;
min-height: 200px;
background: #16213e;
border: 2px dashed #333;
border-radius: 12px;
padding: 15px;
transition: all 0.2s ease;
}
.internal-drop-zone h3 {
margin: 0 0 15px 0;
padding-bottom: 10px;
border-bottom: 1px solid #2a3a5e;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
}
.internal-drop-zone.priority-high h3 { color: #e74c3c; }
.internal-drop-zone.priority-medium h3 { color: #f39c12; }
.internal-drop-zone.priority-low h3 { color: #27ae60; }
.internal-drop-zone.drag-over {
border-color: #4a9eff;
background: rgba(74, 158, 255, 0.1);
}
.internal-drop-zone ul {
margin: 0;
padding: 0;
list-style: none;
}
.internal-drop-zone li {
background: #2a3a5e;
padding: 10px 14px;
margin-bottom: 6px;
border-radius: 6px;
font-size: 13px;
}
.internal-drop-zone .empty {
color: #444;
font-style: italic;
background: none;
padding: 0;
}
/* ===== Info Section ===== */
.drop-info {
max-width: 900px;
margin: 20px auto 0;
padding: 15px;
background: #16213e;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
color: #666;
}
.drop-info strong {
color: #888;
}
</style>
</head>
<body>
<div class="header">
<h1>File Tree Drag & Drop Example</h1>
<p>Drag files onto folders to upload them to that location</p>
<h1>Drag and Drop Demo</h1>
<!-- ===== External File Drop ===== -->
<h2>External File Drop</h2>
<div class="instructions">
<p>
Drop files from your operating system (Finder, Explorer, file managers).
Uses <code>EnableFileDrop: true</code> and <code>data-file-drop-target</code>
</p>
</div>
<div class="container">
<div class="file-tree">
<div class="path-display" id="current-path">/home/user</div>
<!-- Root folder -->
<div class="tree-node folder folder-dropzone" data-path="/home/user" data-wails-dropzone data-folder-id="root" data-folder-name="Home">
<span class="material-icons node-icon">folder</span>
<span>Home</span>
<div class="external-section">
<div class="drop-zone" data-file-drop-target>
<p>Drop files from your desktop or file manager here</p>
</div>
<div class="buckets">
<div class="bucket documents">
<h3>Documents</h3>
<ul id="documents-list">
<li class="empty">No documents yet</li>
</ul>
</div>
<div class="tree-children">
<!-- Documents folder -->
<div class="tree-node folder folder-dropzone" data-path="/home/user/Documents" data-wails-dropzone data-folder-id="docs" data-folder-name="Documents">
<span class="material-icons node-icon">folder</span>
<span>Documents</span>
<div class="bucket images">
<h3>Images</h3>
<ul id="images-list">
<li class="empty">No images yet</li>
</ul>
</div>
<div class="bucket other">
<h3>Other Files</h3>
<ul id="other-list">
<li class="empty">No other files yet</li>
</ul>
</div>
</div>
</div>
<!-- ===== Internal Drag and Drop ===== -->
<h2>Internal Drag and Drop</h2>
<div class="instructions">
<p>
Drag items between zones using the HTML5 Drag and Drop API.
Uses <code>draggable="true"</code> and standard DOM events.
</p>
</div>
<div class="internal-section">
<div class="internal-container">
<div class="draggable-items">
<h3>Tasks</h3>
<div class="draggable-item" draggable="true" data-task="1">Fix login bug</div>
<div class="draggable-item" draggable="true" data-task="2">Update documentation</div>
<div class="draggable-item" draggable="true" data-task="3">Add dark mode</div>
<div class="draggable-item" draggable="true" data-task="4">Refactor API calls</div>
<div class="draggable-item" draggable="true" data-task="5">Write unit tests</div>
</div>
<div class="drop-targets">
<div class="internal-drop-zone priority-high" data-priority="high">
<h3>High Priority</h3>
<ul></ul>
</div>
<div class="tree-children">
<div class="tree-node file" data-path="/home/user/Documents/report.pdf">
<span class="material-icons node-icon">description</span>
<span>report.pdf</span>
</div>
<div class="tree-node file" data-path="/home/user/Documents/notes.txt">
<span class="material-icons node-icon">description</span>
<span>notes.txt</span>
</div>
<div class="internal-drop-zone priority-medium" data-priority="medium">
<h3>Medium Priority</h3>
<ul></ul>
</div>
<!-- Pictures folder -->
<div class="tree-node folder folder-dropzone" data-path="/home/user/Pictures" data-wails-dropzone data-folder-id="pics" data-folder-name="Pictures">
<span class="material-icons node-icon">folder</span>
<span>Pictures</span>
</div>
<div class="tree-children">
<div class="tree-node file" data-path="/home/user/Pictures/vacation.jpg">
<span class="material-icons node-icon">image</span>
<span>vacation.jpg</span>
</div>
<div class="tree-node file" data-path="/home/user/Pictures/profile.png">
<span class="material-icons node-icon">image</span>
<span>profile.png</span>
</div>
</div>
<!-- Downloads folder -->
<div class="tree-node folder folder-dropzone" data-path="/home/user/Downloads" data-wails-dropzone data-folder-id="downloads" data-folder-name="Downloads">
<span class="material-icons node-icon">folder</span>
<span>Downloads</span>
</div>
<div class="tree-children">
<div class="tree-node file" data-path="/home/user/Downloads/app.dmg">
<span class="material-icons node-icon">archive</span>
<span>app.dmg</span>
</div>
<div class="tree-node file" data-path="/home/user/Downloads/data.zip">
<span class="material-icons node-icon">archive</span>
<span>data.zip</span>
</div>
</div>
<!-- Projects folder with nested structure -->
<div class="tree-node folder folder-dropzone" data-path="/home/user/Projects" data-wails-dropzone data-folder-id="projects" data-folder-name="Projects">
<span class="material-icons node-icon">folder</span>
<span>Projects</span>
</div>
<div class="tree-children">
<!-- Nested folder with its own dropzone -->
<div class="tree-node folder folder-dropzone" data-path="/home/user/Projects/Wails" data-wails-dropzone data-folder-id="wails" data-folder-name="Wails">
<span class="material-icons node-icon">folder</span>
<span>Wails</span>
</div>
<div class="tree-children">
<div class="tree-node file" data-path="/home/user/Projects/Wails/main.go">
<span class="material-icons node-icon">code</span>
<span>main.go</span>
</div>
<div class="tree-node file" data-path="/home/user/Projects/Wails/go.mod">
<span class="material-icons node-icon">code</span>
<span>go.mod</span>
</div>
</div>
<div class="internal-drop-zone priority-low" data-priority="low">
<h3>Low Priority</h3>
<ul></ul>
</div>
</div>
</div>
<div class="info-panel">
<h2 class="info-header">Drop Information</h2>
<div id="drop-output">Drag and drop files onto any folder in the file tree...
</div>
<div class="drop-info" id="drop-info">
<strong>Last action:</strong> <span id="drop-details">No actions yet</span>
</div>
The folder elements have the following attributes:
- data-wails-dropzone: Marks the element as a dropzone
- data-path: Contains the full path (used for destination)
- data-folder-id: A unique ID for the folder
- data-folder-name: The display name of the folder</div>
</div>
</div>
<script type="module">
import * as wails from "/wails/runtime.js";
import { Events } from '/wails/runtime.js';
const outputDiv = document.getElementById('drop-output');
const pathDisplay = document.getElementById('current-path');
const folderNodes = document.querySelectorAll('.folder-dropzone');
const documentsEl = document.getElementById('documents-list');
const imagesEl = document.getElementById('images-list');
const otherEl = document.getElementById('other-list');
const dropDetails = document.getElementById('drop-details');
// Add debug coordinate overlay
let debugOverlay = null;
// ===== External File Drop =====
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.webp', '.ico', '.tiff', '.tif'];
const documentExtensions = ['.pdf', '.doc', '.docx', '.txt', '.rtf', '.odt', '.xls', '.xlsx', '.ppt', '.pptx', '.md', '.csv', '.json', '.xml', '.html', '.htm'];
function createDebugOverlay() {
if (debugOverlay) return;
debugOverlay = document.createElement('div');
debugOverlay.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px;
border-radius: 5px;
font-family: monospace;
font-size: 12px;
z-index: 10000;
pointer-events: none;
white-space: pre-line;
`;
document.body.appendChild(debugOverlay);
function getFileName(path) {
return path.split(/[/\\]/).pop();
}
function updateDebugOverlay(info) {
if (!debugOverlay) createDebugOverlay();
debugOverlay.textContent = info;
function getExtension(path) {
const name = getFileName(path);
const idx = name.lastIndexOf('.');
return idx > 0 ? name.substring(idx).toLowerCase() : '';
}
// Track mouse position and other debug info
let mouseInfo = { x: 0, y: 0 };
let windowInfo = { width: window.innerWidth, height: window.innerHeight };
function categoriseFile(path) {
const ext = getExtension(path);
if (imageExtensions.includes(ext)) return 'images';
if (documentExtensions.includes(ext)) return 'documents';
return 'other';
}
document.addEventListener('mousemove', (e) => {
mouseInfo.x = e.clientX;
mouseInfo.y = e.clientY;
function addFileToList(listEl, fileName) {
const empty = listEl.querySelector('.empty');
if (empty) empty.remove();
const debugInfo = `Mouse: ${e.clientX}, ${e.clientY}
Page: ${e.pageX}, ${e.pageY}
Screen: ${e.screenX}, ${e.screenY}
Window: ${windowInfo.width}x${windowInfo.height}
Viewport offset: ${window.pageXOffset}, ${window.pageYOffset}`;
const li = document.createElement('li');
li.textContent = fileName;
listEl.appendChild(li);
}
Events.On('files-dropped', (event) => {
const { files, details } = event.data;
updateDebugOverlay(debugInfo);
files.forEach(filePath => {
const fileName = getFileName(filePath);
const category = categoriseFile(filePath);
switch (category) {
case 'documents':
addFileToList(documentsEl, fileName);
break;
case 'images':
addFileToList(imagesEl, fileName);
break;
default:
addFileToList(otherEl, fileName);
}
});
let info = `External: ${files.length} file(s) dropped`;
if (details) {
info += ` at (${details.x}, ${details.y})`;
}
dropDetails.textContent = info;
});
// Add drag event listeners to show coordinates during drag
document.addEventListener('dragover', (e) => {
e.preventDefault();
const debugInfo = `[DRAGOVER]
Mouse: ${e.clientX}, ${e.clientY}
Page: ${e.pageX}, ${e.pageY}
Screen: ${e.screenX}, ${e.screenY}
Window: ${windowInfo.width}x${windowInfo.height}
Target: ${e.target.tagName} ${e.target.className}`;
updateDebugOverlay(debugInfo);
});
// ===== Internal Drag and Drop =====
const draggableItems = document.querySelectorAll('.draggable-item');
const dropZones = document.querySelectorAll('.internal-drop-zone');
document.addEventListener('drop', (e) => {
e.preventDefault();
const rect = e.target.getBoundingClientRect();
const debugInfo = `[DROP EVENT]
Client: ${e.clientX}, ${e.clientY}
Page: ${e.pageX}, ${e.pageY}
Screen: ${e.screenX}, ${e.screenY}
Target rect: ${rect.left}, ${rect.top}, ${rect.width}x${rect.height}
Relative to target: ${e.clientX - rect.left}, ${e.clientY - rect.top}
Window: ${windowInfo.width}x${windowInfo.height}`;
let draggedItem = null;
draggableItems.forEach(item => {
item.addEventListener('dragstart', (e) => {
draggedItem = item;
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', item.dataset.task);
});
updateDebugOverlay(debugInfo);
console.log('Drop event debug info:', {
clientX: e.clientX,
clientY: e.clientY,
pageX: e.pageX,
pageY: e.pageY,
screenX: e.screenX,
screenY: e.screenY,
targetRect: rect,
relativeX: e.clientX - rect.left,
relativeY: e.clientY - rect.top
item.addEventListener('dragend', () => {
item.classList.remove('dragging');
draggedItem = null;
});
});
window.addEventListener('resize', () => {
windowInfo.width = window.innerWidth;
windowInfo.height = window.innerHeight;
});
// Update path display when clicking folders
folderNodes.forEach(folder => {
folder.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent event bubbling
const path = folder.getAttribute('data-path');
if (path) {
pathDisplay.textContent = path;
outputDiv.textContent = `Selected folder: ${path}\nReady to receive files...`;
dropZones.forEach(zone => {
zone.addEventListener('dragenter', (e) => {
// Ignore external file drags - only respond to internal HTML drags
if (e.dataTransfer?.types.includes('Files')) return;
e.preventDefault();
zone.classList.add('drag-over');
});
zone.addEventListener('dragover', (e) => {
// Ignore external file drags - only respond to internal HTML drags
if (e.dataTransfer?.types.includes('Files')) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
zone.addEventListener('dragleave', (e) => {
// Ignore external file drags - only respond to internal HTML drags
if (e.dataTransfer?.types.includes('Files')) return;
// Only remove if leaving the zone entirely
if (!zone.contains(e.relatedTarget)) {
zone.classList.remove('drag-over');
}
});
zone.addEventListener('drop', (e) => {
// Ignore external file drags - only respond to internal HTML drags
if (e.dataTransfer?.types.includes('Files')) return;
e.preventDefault();
zone.classList.remove('drag-over');
if (draggedItem) {
const taskText = draggedItem.textContent;
const priority = zone.dataset.priority;
// Add to drop zone
const li = document.createElement('li');
li.textContent = taskText;
zone.querySelector('ul').appendChild(li);
// Remove from source
draggedItem.remove();
dropDetails.textContent = `Internal: "${taskText}" moved to ${priority} priority`;
}
});
});
// Listen for the file drop event from Wails
wails.Events.On("frontend:FileDropInfo", (eventData) => {
console.log("=============== Frontend: File Drop Debug Info ===============");
console.log("Full event data:", eventData);
const { files, targetID, targetClasses, dropX, dropY, attributes } = eventData.data[0];
console.log("Extracted data:", {
files,
targetID,
targetClasses,
dropX,
dropY,
attributes
});
// Get additional folder information from the attributes
const folderPath = attributes ? attributes['data-path'] : 'Unknown path';
const folderName = attributes ? attributes['data-folder-name'] : 'Unknown folder';
let message = `=============== FILE DROP DEBUG REPORT ===============\n`;
message += `Files dropped on folder: ${folderName}\n`;
message += `Target path: ${folderPath}\n`;
message += `Element ID: ${targetID || 'N/A'}\n`;
message += `Element Classes: ${targetClasses && targetClasses.length > 0 ? targetClasses.join(', ') : 'N/A'}\n`;
message += `\n=== COORDINATE DEBUG INFO ===\n`;
message += `Drop Coordinates from Wails: X=${dropX.toFixed(2)}, Y=${dropY.toFixed(2)}\n`;
message += `Current Mouse Position: X=${mouseInfo.x}, Y=${mouseInfo.y}\n`;
message += `Window Size: ${windowInfo.width}x${windowInfo.height}\n`;
// Get the target element to show its position
const targetElement = document.querySelector(`[data-folder-id="${targetID}"]`);
if (targetElement) {
const rect = targetElement.getBoundingClientRect();
message += `Target Element Position:\n`;
message += ` - Bounding rect: left=${rect.left}, top=${rect.top}, right=${rect.right}, bottom=${rect.bottom}\n`;
message += ` - Size: ${rect.width}x${rect.height}\n`;
message += ` - Center: ${rect.left + rect.width/2}, ${rect.top + rect.height/2}\n`;
message += ` - Drop relative to element: X=${dropX - rect.left}, Y=${dropY - rect.top}\n`;
}
message += `\n=== ELEMENT ATTRIBUTES ===\n`;
if (attributes && Object.keys(attributes).length > 0) {
Object.entries(attributes).forEach(([key, value]) => {
message += ` ${key}: "${value}"\n`;
});
} else {
message += " No attributes found\n";
}
message += `\n=== DROPPED FILES ===\n`;
files.forEach((file, index) => {
message += ` ${index + 1}. ${file}\n`;
});
message += `\n=== SIMULATION ===\n`;
message += `Simulating upload to ${folderPath}...\n`;
message += `===========================================`;
outputDiv.textContent = message;
console.log("=============== End Frontend Debug ===============");
});
console.log("File Tree Drag-and-Drop example initialized with enhanced debugging.");
createDebugOverlay();
</script>
</body>
</html>
</html>

View file

@ -1,10 +1,7 @@
package main
import (
"context"
"embed"
_ "embed"
"fmt"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
@ -14,153 +11,50 @@ import (
//go:embed assets
var assets embed.FS
// App struct
type App struct {
ctx context.Context
app *application.App
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// Startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
a.app = application.Get()
}
// FileDropInfo defines the payload for the file drop event sent to the frontend.
type FileDropInfo struct {
Files []string `json:"files"`
TargetID string `json:"targetID"`
TargetClasses []string `json:"targetClasses"`
DropX float64 `json:"dropX"`
DropY float64 `json:"dropY"`
Attributes map[string]string `json:"attributes,omitempty"`
}
// FilesDroppedOnTarget is called when files are dropped onto a registered drop target
// or the window if no specific target is hit.
func FilesDroppedOnTarget(
files []string,
targetID string,
targetClasses []string,
dropX float64,
dropY float64,
isTargetDropzone bool, // This parameter is kept for logging but not sent to frontend in this event
attributes map[string]string,
) {
log.Println("=============== Go: FilesDroppedOnTarget Debug Info ===============")
log.Println(fmt.Sprintf(" Files: %v", files))
log.Println(fmt.Sprintf(" Target ID: '%s'", targetID))
log.Println(fmt.Sprintf(" Target Classes: %v", targetClasses))
log.Println(fmt.Sprintf(" Drop X: %f, Drop Y: %f", dropX, dropY))
log.Println(
fmt.Sprintf(
" Drop occurred on a designated dropzone (runtime validated before this Go event): %t",
isTargetDropzone,
),
)
log.Println(fmt.Sprintf(" Element Attributes: %v", attributes))
log.Println("================================================================")
payload := FileDropInfo{
Files: files,
TargetID: targetID,
TargetClasses: targetClasses,
DropX: dropX,
DropY: dropY,
Attributes: attributes,
}
log.Println("Go: Emitted 'frontend:FileDropInfo' event with payload:", payload)
}
func main() {
appInstance := NewApp()
app := application.New(application.Options{
Name: "Drag-n-drop Demo",
Description: "A demo of the Drag-n-drop API",
Name: "File Drop Demo",
Description: "A demo of file drag and drop",
Assets: application.AssetOptions{
Handler: application.BundledAssetFileServer(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
Services: []application.Service{
application.NewService(appInstance),
},
})
win := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Drag-n-drop Demo",
Title: "File Drop Demo",
Width: 800,
Height: 600,
EnableFileDrop: true,
Mac: application.MacWindow{
Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInsetUnified,
InvisibleTitleBarHeight: 50,
},
EnableDragAndDrop: true,
})
log.Println("Setting up event listener for 'WindowDropZoneFilesDropped'...")
win.OnWindowEvent(
events.Common.WindowDropZoneFilesDropped,
func(event *application.WindowEvent) {
// Listen for file drop events
win.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
files := event.Context().DroppedFiles()
details := event.Context().DropTargetDetails()
droppedFiles := event.Context().DroppedFiles()
details := event.Context().DropZoneDetails()
log.Printf("Files dropped: %v", files)
if details != nil {
log.Printf("Drop target: id=%s, classes=%v, x=%d, y=%d",
details.ElementID, details.ClassList, details.X, details.Y)
}
log.Printf("Dropped files count: %d", len(droppedFiles))
log.Printf("Event context: %+v", event.Context())
if details != nil {
log.Printf("DropZone details found:")
log.Printf(" ElementID: '%s'", details.ElementID)
log.Printf(" ClassList: %v", details.ClassList)
log.Printf(" X: %d, Y: %d", details.X, details.Y)
log.Printf(" Attributes: %+v", details.Attributes)
// Call the App method with the extracted data
FilesDroppedOnTarget(
droppedFiles,
details.ElementID,
details.ClassList,
float64(details.X),
float64(details.Y),
details.ElementID != "", // isTargetDropzone based on whether an ID was found
details.Attributes,
)
} else {
log.Println("DropZone details are nil - drop was not on a specific registered zone")
// This case might occur if DropZoneDetails are nil, meaning the drop was not on a specific registered zone
// or if the context itself was problematic.
FilesDroppedOnTarget(droppedFiles, "", nil, 0, 0, false, nil)
}
payload := FileDropInfo{
Files: droppedFiles,
TargetID: details.ElementID,
TargetClasses: details.ClassList,
DropX: float64(details.X),
DropY: float64(details.Y),
Attributes: details.Attributes, // Add the attributes
}
log.Printf("Emitting event payload: %+v", payload)
application.Get().Event.Emit("frontend:FileDropInfo", payload)
log.Println(
"=============== End WindowDropZoneFilesDropped Event Debug ===============",
)
},
)
// Emit event to frontend
application.Get().Event.Emit("files-dropped", map[string]any{
"files": files,
"details": details,
})
})
err := app.Run()
if err != nil {
log.Fatal(err.Error())
log.Fatal(err)
}
}

View file

@ -1,43 +0,0 @@
# HTML Drag and Drop API Example
This example should demonstrate whether the [HTML Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API") works correctly.
## Expected Behaviour
When dragging the "draggable", in the console should be printed:
1. "dragstart" once
2. "drag" many times
3. "dragend" once
When dragging the "draggable" on the drop target, the inner text of the latter shoud change and in the console should be printed:
1. "dragstart" once
2. "drag" many times
3. "dragenter" once
4. "dragover" many times (alternating with "drag")
5. - "drop" once (in case of a drop inside the drop target)
- "dragleave" once (in case the draggable div leaves the drop target)
6. "dragend" once
## Running the example
To run the example, simply run the following command:
```bash
go run main.go
```
## Building the example
To build the example in debug mode, simply run the following command:
```bash
wails3 task build
```
# Status
| Platform | Status |
|----------|-------------|
| Mac | Working |
| Windows | Not Working |
| Linux | |

View file

@ -1,75 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body{
background-color: white;
}
#draggable {
width: 100px;
height: 100px;
background-color: yellow;
text-align: center;
}
#dropTarget {
width: 200px;
height: 200px;
border: 2px solid red;
text-align: center;
}
</style>
</head>
<body>
<h1>HTML Drag and Drop API Demo</h1>
<br/>
<div id="draggable" draggable="true" >draggable</div>
<div id="dropTarget" >drop target</div>
</body>
<script type="module">
const draggable = document.getElementById('draggable');
draggable.addEventListener('dragstart', (event) => {
console.log('dragstart');
dropTarget.innerText = 'drop target';
});
draggable.addEventListener("drag", (event) => {
console.log('drag');
});
draggable.addEventListener("dragend", (event) => {
console.log('dragend');
});
const dropTarget = document.getElementById('dropTarget');
dropTarget.addEventListener('dragenter', (event) => {
console.log('dragenter');
});
dropTarget.addEventListener('dragleave', (event) => {
console.log('dragleave');
dropTarget.innerText = 'left drop target';
});
dropTarget.addEventListener('dragover', (event) => {
event.preventDefault()
console.log('dragover');
dropTarget.innerText = 'dragged over';
});
dropTarget.addEventListener('drop', (event) => {
console.log('drop');
dropTarget.innerText = 'dropped';
});
</script>
</html>

View file

@ -1,40 +0,0 @@
package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed assets
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "HTML Drag and Drop API Demo",
Description: "A demo of the HTML Drag and drop API",
Assets: application.AssetOptions{
Handler: application.BundledAssetFileServer(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Drag-n-drop Demo",
Mac: application.MacWindow{
Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInsetUnified,
InvisibleTitleBarHeight: 50,
},
})
err := app.Run()
if err != nil {
log.Fatal(err.Error())
}
}

View file

@ -61,15 +61,15 @@ func main() {
// Window 1: Light style with no tint
window1 := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Light Glass",
Width: 350,
Height: 280,
X: 100,
Y: 100,
Frameless: true,
EnableDragAndDrop: false,
HTML: lightHTML,
InitialPosition: application.WindowXY,
Title: "Light Glass",
Width: 350,
Height: 280,
X: 100,
Y: 100,
Frameless: true,
EnableFileDrop: false,
HTML: lightHTML,
InitialPosition: application.WindowXY,
Mac: application.MacWindow{
Backdrop: application.MacBackdropLiquidGlass,
InvisibleTitleBarHeight: 500,
@ -84,15 +84,15 @@ func main() {
// Window 2: Dark style
window2 := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Dark Glass",
Width: 350,
Height: 280,
X: 500,
Y: 100,
Frameless: true,
EnableDragAndDrop: false,
HTML: darkHTML,
InitialPosition: application.WindowXY,
Title: "Dark Glass",
Width: 350,
Height: 280,
X: 500,
Y: 100,
Frameless: true,
EnableFileDrop: false,
HTML: darkHTML,
InitialPosition: application.WindowXY,
Mac: application.MacWindow{
Backdrop: application.MacBackdropLiquidGlass,
InvisibleTitleBarHeight: 500,
@ -107,15 +107,15 @@ func main() {
// Window 3: Vibrant style
window3 := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Vibrant Glass",
Width: 350,
Height: 280,
X: 900,
Y: 100,
Frameless: true,
EnableDragAndDrop: false,
HTML: vibrantHTML,
InitialPosition: application.WindowXY,
Title: "Vibrant Glass",
Width: 350,
Height: 280,
X: 900,
Y: 100,
Frameless: true,
EnableFileDrop: false,
HTML: vibrantHTML,
InitialPosition: application.WindowXY,
Mac: application.MacWindow{
Backdrop: application.MacBackdropLiquidGlass,
InvisibleTitleBarHeight: 500,
@ -130,15 +130,15 @@ func main() {
// Window 4: Blue tinted glass
window4 := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Tinted Glass",
Width: 350,
Height: 280,
X: 300,
Y: 420,
Frameless: true,
EnableDragAndDrop: false,
HTML: tintedHTML,
InitialPosition: application.WindowXY,
Title: "Tinted Glass",
Width: 350,
Height: 280,
X: 300,
Y: 420,
Frameless: true,
EnableFileDrop: false,
HTML: tintedHTML,
InitialPosition: application.WindowXY,
Mac: application.MacWindow{
Backdrop: application.MacBackdropLiquidGlass,
InvisibleTitleBarHeight: 500,
@ -153,15 +153,15 @@ func main() {
// Window 5: Using specific NSVisualEffectMaterial
window5 := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Sheet Material",
Width: 350,
Height: 280,
X: 700,
Y: 420,
Frameless: true,
EnableDragAndDrop: false,
HTML: sheetHTML,
InitialPosition: application.WindowXY,
Title: "Sheet Material",
Width: 350,
Height: 280,
X: 700,
Y: 420,
Frameless: true,
EnableFileDrop: false,
HTML: sheetHTML,
InitialPosition: application.WindowXY,
Mac: application.MacWindow{
Backdrop: application.MacBackdropLiquidGlass,
InvisibleTitleBarHeight: 500,
@ -176,15 +176,15 @@ func main() {
// Window 6: HUD Window Material (very light, translucent)
window6 := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "HUD Window",
Width: 350,
Height: 280,
X: 100,
Y: 740,
Frameless: true,
EnableDragAndDrop: false,
HTML: hudHTML,
InitialPosition: application.WindowXY,
Title: "HUD Window",
Width: 350,
Height: 280,
X: 100,
Y: 740,
Frameless: true,
EnableFileDrop: false,
HTML: hudHTML,
InitialPosition: application.WindowXY,
Mac: application.MacWindow{
Backdrop: application.MacBackdropLiquidGlass,
InvisibleTitleBarHeight: 500,
@ -199,15 +199,15 @@ func main() {
// Window 7: Content Background Material
window7 := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Content Background",
Width: 350,
Height: 280,
X: 500,
Y: 740,
Frameless: true,
EnableDragAndDrop: false,
HTML: contentHTML,
InitialPosition: application.WindowXY,
Title: "Content Background",
Width: 350,
Height: 280,
X: 500,
Y: 740,
Frameless: true,
EnableFileDrop: false,
HTML: contentHTML,
InitialPosition: application.WindowXY,
Mac: application.MacWindow{
Backdrop: application.MacBackdropLiquidGlass,
InvisibleTitleBarHeight: 500,

View file

@ -0,0 +1,66 @@
# Spotlight Example
This example demonstrates how to create a Spotlight-like launcher window using the `CollectionBehavior` option on macOS.
## Features
- **Appears on all Spaces**: Using `MacWindowCollectionBehaviorCanJoinAllSpaces`, the window is visible across all virtual desktops
- **Overlays fullscreen apps**: Using `MacWindowCollectionBehaviorFullScreenAuxiliary`, the window can appear over fullscreen applications
- **Combined behaviors**: Demonstrates combining multiple behaviors with bitwise OR
- **Floating window**: `MacWindowLevelFloating` keeps the window above other windows
- **Accessory app**: Doesn't appear in the Dock (uses `ActivationPolicyAccessory`)
- **Frameless design**: Clean, borderless appearance with translucent backdrop
## Running the example
```bash
go run .
```
**Note**: This example is macOS-specific due to the use of `CollectionBehavior`.
## Combining CollectionBehaviors
Behaviors can be combined using bitwise OR (`|`):
```go
CollectionBehavior: application.MacWindowCollectionBehaviorCanJoinAllSpaces |
application.MacWindowCollectionBehaviorFullScreenAuxiliary,
```
## CollectionBehavior Options
These are bitmask values that can be combined:
**Space behavior:**
| Option | Description |
|--------|-------------|
| `MacWindowCollectionBehaviorDefault` | Uses FullScreenPrimary (default) |
| `MacWindowCollectionBehaviorCanJoinAllSpaces` | Window appears on all Spaces |
| `MacWindowCollectionBehaviorMoveToActiveSpace` | Moves to active Space when shown |
| `MacWindowCollectionBehaviorManaged` | Default managed window behavior |
| `MacWindowCollectionBehaviorTransient` | Temporary/transient window |
| `MacWindowCollectionBehaviorStationary` | Stays stationary during Space switches |
**Fullscreen behavior:**
| Option | Description |
|--------|-------------|
| `MacWindowCollectionBehaviorFullScreenPrimary` | Can enter fullscreen mode |
| `MacWindowCollectionBehaviorFullScreenAuxiliary` | Can overlay fullscreen apps |
| `MacWindowCollectionBehaviorFullScreenNone` | Disables fullscreen |
| `MacWindowCollectionBehaviorFullScreenAllowsTiling` | Allows side-by-side tiling |
## Use Cases
- **Launcher apps** (like Spotlight, Alfred, Raycast)
- **Quick capture tools** (notes, screenshots)
- **System utilities** that need to be accessible anywhere
- **Overlay widgets** that should appear over fullscreen apps
## Status
| Platform | Status |
|----------|--------|
| Mac | Working |
| Windows | N/A (macOS-specific feature) |
| Linux | N/A (macOS-specific feature) |

View file

@ -0,0 +1,142 @@
package main
import (
"log"
"net/http"
"github.com/wailsapp/wails/v3/pkg/application"
)
// This example demonstrates how to create a Spotlight-like launcher window
// that appears on all macOS Spaces and can overlay fullscreen applications.
//
// Key features:
// - Window appears on all Spaces (virtual desktops)
// - Can overlay fullscreen applications
// - Floating window level keeps it above other windows
// - Accessory activation policy hides from Dock
// - Frameless design with translucent backdrop
func main() {
app := application.New(application.Options{
Name: "Spotlight Example",
Description: "A Spotlight-like launcher demonstrating CollectionBehavior",
Mac: application.MacOptions{
// Accessory apps don't appear in the Dock
ActivationPolicy: application.ActivationPolicyAccessory,
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
Assets: application.AssetOptions{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(spotlightHTML))
}),
},
})
// Create a Spotlight-like window
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Spotlight",
Width: 680,
Height: 80,
Frameless: true,
// Center the window
InitialPosition: application.WindowCentered,
// Prevent resizing
DisableResize: true,
Mac: application.MacWindow{
// Combine multiple behaviors using bitwise OR:
// - CanJoinAllSpaces: window appears on ALL Spaces (virtual desktops)
// - FullScreenAuxiliary: window can overlay fullscreen applications
CollectionBehavior: application.MacWindowCollectionBehaviorCanJoinAllSpaces |
application.MacWindowCollectionBehaviorFullScreenAuxiliary,
// Float above other windows
WindowLevel: application.MacWindowLevelFloating,
// Translucent vibrancy effect
Backdrop: application.MacBackdropTranslucent,
// Hidden title bar for clean look
TitleBar: application.MacTitleBar{
AppearsTransparent: true,
Hide: true,
},
},
URL: "/",
})
err := app.Run()
if err != nil {
log.Fatal(err)
}
}
const spotlightHTML = `<!DOCTYPE html>
<html>
<head>
<title>Spotlight</title>
<script type="module" src="/wails/runtime.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
padding: 16px;
user-select: none;
-webkit-user-select: none;
}
.search-container {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
}
.search-icon {
width: 24px;
height: 24px;
opacity: 0.6;
}
.search-input {
flex: 1;
background: transparent;
border: none;
outline: none;
font-size: 24px;
color: white;
font-weight: 300;
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
@media (prefers-color-scheme: light) {
.search-container {
background: rgba(0, 0, 0, 0.05);
}
.search-input {
color: #333;
}
.search-input::placeholder {
color: rgba(0, 0, 0, 0.4);
}
}
</style>
</head>
<body>
<div class="search-container">
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
</svg>
<input type="text" class="search-input" placeholder="Spotlight Search" autofocus>
</div>
</body>
</html>`

View file

@ -1,6 +1,6 @@
module github.com/wailsapp/wails/v3
go 1.24.0
go 1.25
require (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3
@ -12,6 +12,7 @@ require (
github.com/charmbracelet/huh v0.8.0
github.com/ebitengine/purego v0.8.2
github.com/go-git/go-git/v5 v5.13.2
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e
github.com/go-ole/go-ole v1.3.0
github.com/godbus/dbus/v5 v5.1.0
github.com/google/go-cmp v0.7.0
@ -38,13 +39,11 @@ require (
github.com/stretchr/testify v1.10.0
github.com/tc-hib/winres v0.3.1
github.com/wailsapp/go-webview2 v1.0.22
github.com/wailsapp/mimetype v1.4.1
github.com/wailsapp/task/v3 v3.40.1-patched3
github.com/zalando/go-keyring v0.2.6
golang.org/x/sys v0.33.0
golang.org/x/term v0.30.0
golang.org/x/tools v0.31.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.36.0
)
@ -160,7 +159,7 @@ require (
golang.org/x/text v0.23.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
howett.net/plist v1.0.1 // indirect
howett.net/plist v1.0.1
modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect

View file

@ -157,6 +157,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@ -363,8 +365,6 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/task/v3 v3.40.1-patched3 h1:i6O1WNdSur9CGaiMDIYGjsmj/qS4465zqv+WEs6sPRs=
github.com/wailsapp/task/v3 v3.40.1-patched3/go.mod h1:jIP48r8ftoSQNlxFP4+aEnkvGQqQXqCnRi/B7ROaecE=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@ -406,7 +406,6 @@ golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@ -464,8 +463,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=

View file

@ -69,7 +69,7 @@ func (a *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
req = req.WithContext(contextWithLogger(req.Context(), a.options.Logger))
a.handler.ServeHTTP(wrapped, req)
a.options.Logger.Info(
a.options.Logger.Debug(
"Asset Request:",
"windowName", req.Header.Get(webViewRequestHeaderWindowName),
"windowID", req.Header.Get(webViewRequestHeaderWindowId),

View file

@ -0,0 +1,242 @@
//go:build bench
package assetserver
import (
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"sync"
"testing"
)
// resetMimeCache clears the mime cache for benchmark isolation
func resetMimeCache() {
mimeCache = sync.Map{}
}
// BenchmarkGetMimetype measures MIME type detection performance
func BenchmarkGetMimetype(b *testing.B) {
// Reset cache between runs
resetMimeCache()
b.Run("ByExtension/JS", func(b *testing.B) {
data := []byte("function test() {}")
for b.Loop() {
_ = GetMimetype("script.js", data)
}
})
resetMimeCache()
b.Run("ByExtension/CSS", func(b *testing.B) {
data := []byte(".class { color: red; }")
for b.Loop() {
_ = GetMimetype("style.css", data)
}
})
resetMimeCache()
b.Run("ByExtension/HTML", func(b *testing.B) {
data := []byte("<!DOCTYPE html><html></html>")
for b.Loop() {
_ = GetMimetype("index.html", data)
}
})
resetMimeCache()
b.Run("ByExtension/JSON", func(b *testing.B) {
data := []byte(`{"key": "value"}`)
for b.Loop() {
_ = GetMimetype("data.json", data)
}
})
resetMimeCache()
b.Run("Detection/Unknown", func(b *testing.B) {
data := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}
for b.Loop() {
_ = GetMimetype("unknown.bin", data)
}
})
resetMimeCache()
b.Run("Detection/PNG", func(b *testing.B) {
// PNG magic bytes
data := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00}
for b.Loop() {
_ = GetMimetype("image.unknown", data)
}
})
resetMimeCache()
b.Run("CacheHit", func(b *testing.B) {
data := []byte{0x00, 0x01, 0x02}
// Prime the cache
_ = GetMimetype("cached.bin", data)
b.ResetTimer()
for b.Loop() {
_ = GetMimetype("cached.bin", data)
}
})
}
// BenchmarkGetMimetype_Concurrent tests concurrent MIME type lookups
func BenchmarkGetMimetype_Concurrent(b *testing.B) {
resetMimeCache()
data := []byte("function test() {}")
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = GetMimetype("script.js", data)
}
})
}
// BenchmarkAssetServerServeHTTP measures request handling overhead
func BenchmarkAssetServerServeHTTP(b *testing.B) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError + 1}))
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("<!DOCTYPE html><html><body>Hello</body></html>"))
})
server, err := NewAssetServer(&Options{
Handler: handler,
Logger: logger,
})
if err != nil {
b.Fatal(err)
}
b.Run("SimpleRequest", func(b *testing.B) {
req := httptest.NewRequest("GET", "/index.html", nil)
for b.Loop() {
rr := httptest.NewRecorder()
server.ServeHTTP(rr, req)
}
})
b.Run("WithHeaders", func(b *testing.B) {
req := httptest.NewRequest("GET", "/index.html", nil)
req.Header.Set("x-wails-window-id", "1")
req.Header.Set("x-wails-window-name", "main")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
for b.Loop() {
rr := httptest.NewRecorder()
server.ServeHTTP(rr, req)
}
})
}
// BenchmarkAssetServerServeHTTP_Concurrent tests concurrent request handling
func BenchmarkAssetServerServeHTTP_Concurrent(b *testing.B) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError + 1}))
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("<!DOCTYPE html><html><body>Hello</body></html>"))
})
server, err := NewAssetServer(&Options{
Handler: handler,
Logger: logger,
})
if err != nil {
b.Fatal(err)
}
b.RunParallel(func(pb *testing.PB) {
req := httptest.NewRequest("GET", "/index.html", nil)
for pb.Next() {
rr := httptest.NewRecorder()
server.ServeHTTP(rr, req)
}
})
}
// BenchmarkContentTypeSniffer measures the content type sniffer overhead
func BenchmarkContentTypeSniffer(b *testing.B) {
b.Run("SmallResponse", func(b *testing.B) {
data := []byte("Hello, World!")
for b.Loop() {
rr := httptest.NewRecorder()
sniffer := newContentTypeSniffer(rr)
_, _ = sniffer.Write(data)
_, _ = sniffer.complete()
}
})
b.Run("HTMLResponse", func(b *testing.B) {
data := []byte("<!DOCTYPE html><html><head><title>Test</title></head><body><h1>Hello</h1></body></html>")
for b.Loop() {
rr := httptest.NewRecorder()
sniffer := newContentTypeSniffer(rr)
_, _ = sniffer.Write(data)
_, _ = sniffer.complete()
}
})
b.Run("LargeResponse", func(b *testing.B) {
data := make([]byte, 64*1024) // 64KB
for i := range data {
data[i] = byte(i % 256)
}
for b.Loop() {
rr := httptest.NewRecorder()
sniffer := newContentTypeSniffer(rr)
_, _ = sniffer.Write(data)
_, _ = sniffer.complete()
}
})
}
// BenchmarkServiceRouting measures service route matching performance
func BenchmarkServiceRouting(b *testing.B) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError + 1}))
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
server, err := NewAssetServer(&Options{
Handler: dummyHandler,
Logger: logger,
})
if err != nil {
b.Fatal(err)
}
// Attach multiple service routes
for i := 0; i < 10; i++ {
server.AttachServiceHandler(fmt.Sprintf("/api/v%d/", i), dummyHandler)
}
b.Run("FirstRoute", func(b *testing.B) {
req := httptest.NewRequest("GET", "/api/v0/users", nil)
for b.Loop() {
rr := httptest.NewRecorder()
server.ServeHTTP(rr, req)
}
})
b.Run("LastRoute", func(b *testing.B) {
req := httptest.NewRequest("GET", "/api/v9/users", nil)
for b.Loop() {
rr := httptest.NewRecorder()
server.ServeHTTP(rr, req)
}
})
b.Run("NoMatch", func(b *testing.B) {
req := httptest.NewRequest("GET", "/static/app.js", nil)
for b.Loop() {
rr := httptest.NewRecorder()
server.ServeHTTP(rr, req)
}
})
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,10 +1,8 @@
package assetserver
import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
@ -26,32 +24,23 @@ type assetServerLogger struct{}
var assetServerLoggerKey assetServerLogger
// ServeFile writes the provided blob to rw as an HTTP 200 response, ensuring appropriate
// Content-Length and Content-Type headers are set.
//
// If the Content-Type header is not already present, ServeFile determines an appropriate
// MIME type from the filename and blob and sets the Content-Type header. It then writes
// the 200 status and the blob body to the response, returning any error encountered while
// writing the body.
func ServeFile(rw http.ResponseWriter, filename string, blob []byte) error {
header := rw.Header()
header.Set(HeaderContentLength, fmt.Sprintf("%d", len(blob)))
if mimeType := header.Get(HeaderContentType); mimeType == "" {
mimeType = GetMimetype(filename, blob)
header.Set(HeaderContentType, mimeType)
// Debug CSS serving with clear markers
if strings.HasSuffix(filename, ".css") {
fmt.Printf("\n🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨\n")
fmt.Printf("CSS FILE BEING SERVED:\n")
fmt.Printf(" Filename: %s\n", filename)
fmt.Printf(" MimeType: %s\n", mimeType)
fmt.Printf(" Size: %d bytes\n", len(blob))
if len(blob) > 0 {
preview := string(blob)
if len(preview) > 100 {
preview = preview[:100] + "..."
}
fmt.Printf(" Preview: %s\n", preview)
}
fmt.Printf("🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨\n\n")
}
}
rw.WriteHeader(http.StatusOK)
_, err := io.Copy(rw, bytes.NewReader(blob))
_, err := rw.Write(blob)
return err
}
@ -74,4 +63,4 @@ func logError(ctx context.Context, message string, args ...interface{}) {
if logger, _ := ctx.Value(assetServerLoggerKey).(*slog.Logger); logger != nil {
logger.Error(message, args...)
}
}
}

View file

@ -4,20 +4,21 @@ import (
"net/http"
)
// newContentTypeSniffer creates a contentTypeSniffer that wraps the provided http.ResponseWriter.
// The returned sniffer does not allocate a close notification channel; it will be initialized lazily by CloseNotify.
func newContentTypeSniffer(rw http.ResponseWriter) *contentTypeSniffer {
return &contentTypeSniffer{
rw: rw,
closeChannel: make(chan bool, 1),
rw: rw,
}
}
type contentTypeSniffer struct {
rw http.ResponseWriter
prefix []byte
closeChannel chan bool // lazily allocated only if CloseNotify is called
status int
headerCommitted bool
headerWritten bool
closeChannel chan bool
}
// Unwrap returns the wrapped [http.ResponseWriter] for use with [http.ResponseController].
@ -118,12 +119,19 @@ func (rw *contentTypeSniffer) complete() (n int, err error) {
}
// CloseNotify implements the http.CloseNotifier interface.
// The channel is lazily allocated to avoid allocation overhead for requests
// that don't use this deprecated interface.
func (rw *contentTypeSniffer) CloseNotify() <-chan bool {
if rw.closeChannel == nil {
rw.closeChannel = make(chan bool, 1)
}
return rw.closeChannel
}
func (rw *contentTypeSniffer) closeClient() {
rw.closeChannel <- true
if rw.closeChannel != nil {
rw.closeChannel <- true
}
}
// Flush implements the http.Flusher interface.
@ -131,4 +139,4 @@ func (rw *contentTypeSniffer) Flush() {
if f, ok := rw.rw.(http.Flusher); ok {
f.Flush()
}
}
}

View file

@ -4,64 +4,113 @@ import (
"net/http"
"path/filepath"
"sync"
"github.com/wailsapp/mimetype"
)
var (
mimeCache = map[string]string{}
mimeMutex sync.Mutex
// mimeCache uses sync.Map for better concurrent read performance
// since reads are far more common than writes
mimeCache sync.Map
// The list of builtin mime-types by extension as defined by
// the golang standard lib package "mime"
// The standard lib also takes into account mime type definitions from
// /etc files like '/etc/apache2/mime.types' but we want to have the
// same behaviour on all platforms and not depend on some external file.
// mimeTypesByExt maps file extensions to MIME types for common web formats.
// This approach is preferred over content-based detection because:
// 1. Extension-based lookup is O(1) vs O(n) content scanning
// 2. Web assets typically have correct extensions
// 3. stdlib's http.DetectContentType handles remaining cases adequately
// 4. Saves ~208KB binary size by not using github.com/wailsapp/mimetype
mimeTypesByExt = map[string]string{
".avif": "image/avif",
".css": "text/css; charset=utf-8",
".gif": "image/gif",
// HTML
".htm": "text/html; charset=utf-8",
".html": "text/html; charset=utf-8",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".js": "text/javascript; charset=utf-8",
// CSS/JS
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".ts": "application/x-typescript; charset=utf-8",
".tsx": "application/x-typescript; charset=utf-8",
".jsx": "text/javascript; charset=utf-8",
// Data formats
".json": "application/json",
".mjs": "text/javascript; charset=utf-8",
".pdf": "application/pdf",
".png": "image/png",
".svg": "image/svg+xml",
".wasm": "application/wasm",
".webp": "image/webp",
".xml": "text/xml; charset=utf-8",
".yaml": "text/yaml; charset=utf-8",
".yml": "text/yaml; charset=utf-8",
".toml": "text/toml; charset=utf-8",
// Images
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".avif": "image/avif",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".bmp": "image/bmp",
".tiff": "image/tiff",
".tif": "image/tiff",
// Fonts
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".otf": "font/otf",
".eot": "application/vnd.ms-fontobject",
// Audio
".mp3": "audio/mpeg",
".wav": "audio/wav",
".ogg": "audio/ogg",
".m4a": "audio/mp4",
".aac": "audio/aac",
".flac": "audio/flac",
".opus": "audio/opus",
// Video
".mp4": "video/mp4",
".webm": "video/webm",
".ogv": "video/ogg",
".mov": "video/quicktime",
".avi": "video/x-msvideo",
".mkv": "video/x-matroska",
".m4v": "video/mp4",
// Documents
".pdf": "application/pdf",
".txt": "text/plain; charset=utf-8",
".md": "text/markdown; charset=utf-8",
// Archives
".zip": "application/zip",
".gz": "application/gzip",
".tar": "application/x-tar",
// WebAssembly
".wasm": "application/wasm",
// Source maps
".map": "application/json",
}
)
// "application/octet-stream".
func GetMimetype(filename string, data []byte) string {
mimeMutex.Lock()
defer mimeMutex.Unlock()
result := mimeTypesByExt[filepath.Ext(filename)]
if result != "" {
// Fast path: check extension map first (no lock needed)
if result := mimeTypesByExt[filepath.Ext(filename)]; result != "" {
return result
}
result = mimeCache[filename]
if result != "" {
return result
}
detect := mimetype.Detect(data)
if detect == nil {
result = http.DetectContentType(data)
} else {
result = detect.String()
// Check cache (lock-free read)
if cached, ok := mimeCache.Load(filename); ok {
return cached.(string)
}
// Slow path: use stdlib content-based detection and cache
result := http.DetectContentType(data)
if result == "" {
result = "application/octet-stream"
}
mimeCache[filename] = result
mimeCache.Store(filename, result)
return result
}
}

View file

@ -0,0 +1,277 @@
package assetserver
import (
"net/http"
"path/filepath"
"strings"
"testing"
)
// TestMimeTypeDetection_WebFormats validates that extension-based detection
// plus stdlib fallback correctly handles all common web asset formats.
// This test ensures we can safely remove the github.com/wailsapp/mimetype dependency.
func TestMimeTypeDetection_WebFormats(t *testing.T) {
// webMimeTests covers all common web formats that Wails applications typically serve
webMimeTests := []struct {
name string
filename string
data []byte
wantPrefix string // Use prefix matching since charset may vary
}{
// === TEXT FORMATS (extension-based) ===
{"HTML file", "index.html", []byte("<!DOCTYPE html><html></html>"), "text/html"},
{"HTM file", "page.htm", []byte("<html></html>"), "text/html"},
{"CSS file", "styles.css", []byte(".class { color: red; }"), "text/css"},
{"JavaScript file", "app.js", []byte("function test() {}"), "text/javascript"},
{"ES Module file", "module.mjs", []byte("export default {}"), "text/javascript"},
{"JSON file", "data.json", []byte(`{"key": "value"}`), "application/json"},
{"XML file", "data.xml", []byte("<?xml version=\"1.0\"?><root/>"), "text/xml"},
// === IMAGE FORMATS (extension-based) ===
{"PNG file", "image.png", pngData, "image/png"},
{"JPEG file", "photo.jpg", jpegData, "image/jpeg"},
{"JPEG alt ext", "photo.jpeg", jpegData, "image/jpeg"},
{"GIF file", "anim.gif", gifData, "image/gif"},
{"WebP file", "image.webp", webpData, "image/webp"},
{"AVIF file", "image.avif", avifData, "image/avif"},
{"SVG file", "icon.svg", []byte("<svg></svg>"), "image/svg+xml"},
{"PDF file", "doc.pdf", pdfData, "application/pdf"},
// === WASM (extension-based) ===
{"WASM file", "app.wasm", wasmData, "application/wasm"},
// === FONT FORMATS (need detection or extension map) ===
{"WOFF file", "font.woff", woffData, "font/woff"},
{"WOFF2 file", "font.woff2", woff2Data, "font/woff2"},
{"TTF file", "font.ttf", ttfData, "font/ttf"},
{"OTF file", "font.otf", otfData, "font/otf"},
{"EOT file", "font.eot", eotData, "application/vnd.ms-fontobject"},
// === AUDIO/VIDEO (common web formats) ===
{"MP3 file", "audio.mp3", mp3Data, "audio/mpeg"},
{"MP4 file", "video.mp4", mp4Data, "video/mp4"},
{"WebM file", "video.webm", webmData, "video/webm"},
{"OGG file", "audio.ogg", oggData, "audio/ogg"},
// === ARCHIVES (sometimes served by web apps) ===
{"ZIP file", "archive.zip", zipData, "application/zip"},
{"GZIP file", "data.gz", gzipData, "application/"},
// === SOURCE MAPS (common in dev mode) ===
{"Source map", "app.js.map", []byte(`{"version":3}`), "application/json"},
// === ICO (favicon) ===
{"ICO file", "favicon.ico", icoData, "image/"},
// === FALLBACK TESTS ===
{"Unknown binary", "data.bin", []byte{0x00, 0x01, 0x02, 0x03}, "application/octet-stream"},
{"Plain text (no ext)", "readme", []byte("Hello World"), "text/plain"},
}
for _, tt := range webMimeTests {
t.Run(tt.name, func(t *testing.T) {
got := getMimeTypeStdlib(tt.filename, tt.data)
if !hasPrefix(got, tt.wantPrefix) {
t.Errorf("getMimeTypeStdlib(%q) = %q, want prefix %q", tt.filename, got, tt.wantPrefix)
}
})
}
}
// getMimeTypeStdlib is the proposed replacement that uses only stdlib
func getMimeTypeStdlib(filename string, data []byte) string {
// Fast path: check extension map first
if result := extMimeTypes[filepath.Ext(filename)]; result != "" {
return result
}
// Fallback to stdlib content-based detection
result := http.DetectContentType(data)
if result == "" {
result = "application/octet-stream"
}
return result
}
// extMimeTypes is an expanded map covering all common web formats
// This replaces the need for the mimetype library for web assets
var extMimeTypes = map[string]string{
// HTML
".htm": "text/html; charset=utf-8",
".html": "text/html; charset=utf-8",
// CSS/JS
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".ts": "application/x-typescript; charset=utf-8",
".tsx": "application/x-typescript; charset=utf-8",
".jsx": "text/javascript; charset=utf-8",
// Data formats
".json": "application/json",
".xml": "text/xml; charset=utf-8",
".yaml": "text/yaml; charset=utf-8",
".yml": "text/yaml; charset=utf-8",
".toml": "text/toml; charset=utf-8",
// Images
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".avif": "image/avif",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".bmp": "image/bmp",
".tiff": "image/tiff",
".tif": "image/tiff",
// Fonts
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".otf": "font/otf",
".eot": "application/vnd.ms-fontobject",
// Audio
".mp3": "audio/mpeg",
".wav": "audio/wav",
".ogg": "audio/ogg",
".m4a": "audio/mp4",
".aac": "audio/aac",
".flac": "audio/flac",
".opus": "audio/opus",
// Video
".mp4": "video/mp4",
".webm": "video/webm",
".ogv": "video/ogg",
".mov": "video/quicktime",
".avi": "video/x-msvideo",
".mkv": "video/x-matroska",
".m4v": "video/mp4",
// Documents
".pdf": "application/pdf",
".txt": "text/plain; charset=utf-8",
".md": "text/markdown; charset=utf-8",
// Archives
".zip": "application/zip",
".gz": "application/gzip",
".tar": "application/x-tar",
// WebAssembly
".wasm": "application/wasm",
// Source maps
".map": "application/json",
}
func hasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
// Magic bytes for various formats
var (
// PNG: 89 50 4E 47 0D 0A 1A 0A
pngData = []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D}
// JPEG: FF D8 FF
jpegData = []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46}
// GIF: 47 49 46 38
gifData = []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61}
// WebP: 52 49 46 46 ... 57 45 42 50
webpData = []byte{0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50}
// AVIF: ... ftypavif or ftypavis
avifData = []byte{0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66}
// PDF: 25 50 44 46
pdfData = []byte{0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E}
// WASM: 00 61 73 6D
wasmData = []byte{0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00}
// WOFF: 77 4F 46 46
woffData = []byte{0x77, 0x4F, 0x46, 0x46, 0x00, 0x01, 0x00, 0x00}
// WOFF2: 77 4F 46 32
woff2Data = []byte{0x77, 0x4F, 0x46, 0x32, 0x00, 0x01, 0x00, 0x00}
// TTF: 00 01 00 00
ttfData = []byte{0x00, 0x01, 0x00, 0x00, 0x00}
// OTF: 4F 54 54 4F (OTTO)
otfData = []byte{0x4F, 0x54, 0x54, 0x4F, 0x00}
// EOT: varies, but starts with size bytes then magic
eotData = []byte{0x00, 0x00, 0x01, 0x00, 0x00, 0x00}
// MP3: FF FB or FF FA or ID3
mp3Data = []byte{0xFF, 0xFB, 0x90, 0x00}
// MP4: ... ftyp
mp4Data = []byte{0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D}
// WebM: 1A 45 DF A3 (EBML header)
webmData = []byte{0x1A, 0x45, 0xDF, 0xA3}
// OGG: 4F 67 67 53
oggData = []byte{0x4F, 0x67, 0x67, 0x53, 0x00, 0x02}
// ZIP: 50 4B 03 04
zipData = []byte{0x50, 0x4B, 0x03, 0x04}
// GZIP: 1F 8B
gzipData = []byte{0x1F, 0x8B, 0x08}
// ICO: 00 00 01 00
icoData = []byte{0x00, 0x00, 0x01, 0x00, 0x01, 0x00}
)
// TestMimeTypeExtensionMapCompleteness checks that all extensions in the
// original mimeTypesByExt are covered by the expanded extMimeTypes
func TestMimeTypeExtensionMapCompleteness(t *testing.T) {
for ext, mime := range mimeTypesByExt {
if newMime, ok := extMimeTypes[ext]; !ok {
t.Errorf("extension %q missing from extMimeTypes (was: %q)", ext, mime)
} else if newMime != mime {
// Allow differences as long as they're equivalent (compare base MIME type)
mimeBase := mime
if idx := strings.Index(mime, ";"); idx > 0 {
mimeBase = mime[:idx]
}
if !hasPrefix(newMime, mimeBase) {
t.Logf("extension %q changed: %q -> %q (verify this is correct)", ext, mime, newMime)
}
}
}
}
// BenchmarkMimeType_StdlibOnly benchmarks the stdlib-only implementation
func BenchmarkMimeType_StdlibOnly(b *testing.B) {
testCases := []struct {
name string
filename string
data []byte
}{
{"ExtHit_JS", "app.js", []byte("function() {}")},
{"ExtHit_CSS", "styles.css", []byte(".class { }")},
{"ExtHit_PNG", "image.png", pngData},
{"ExtMiss_Binary", "data.bin", []byte{0x00, 0x01, 0x02}},
{"ContentDetect_PNG", "unknown", pngData},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
for b.Loop() {
_ = getMimeTypeStdlib(tc.filename, tc.data)
}
})
}
}

View file

@ -109,12 +109,13 @@ import "C"
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"unsafe"
"encoding/json"
)
// NewRequest creates as new WebViewRequest based on a pointer to an `id<WKURLSchemeTask>`

View file

@ -107,12 +107,13 @@ import "C"
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"unsafe"
"encoding/json"
)
// NewRequest creates as new WebViewRequest based on a pointer to an `id<WKURLSchemeTask>`

View file

@ -68,9 +68,10 @@ static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCod
import "C"
import (
"encoding/json"
"net/http"
"unsafe"
"encoding/json"
)
var _ ResponseWriter = &responseWriter{}

View file

@ -68,11 +68,12 @@ static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCod
import "C"
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"unsafe"
"encoding/json"
)
var _ ResponseWriter = &responseWriter{}

View file

@ -125,7 +125,7 @@ tasks:
- |
cd build/android
./gradlew assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk ../../{{.BIN_DIR}}/{{.APP_NAME}}.apk
cp app/build/outputs/apk/debug/app-debug.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}.apk"
echo "APK created: {{.BIN_DIR}}/{{.APP_NAME}}.apk"
assemble:apk:release:
@ -134,7 +134,7 @@ tasks:
- |
cd build/android
./gradlew assembleRelease
cp app/build/outputs/apk/release/app-release-unsigned.apk ../../{{.BIN_DIR}}/{{.APP_NAME}}-release.apk
cp app/build/outputs/apk/release/app-release-unsigned.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}-release.apk"
echo "Release APK created: {{.BIN_DIR}}/{{.APP_NAME}}-release.apk"
generate:android:bindings:
@ -202,7 +202,7 @@ tasks:
deps: [package]
cmds:
- adb uninstall {{.APP_ID}} 2>/dev/null || true
- adb install {{.BIN_DIR}}/{{.APP_NAME}}.apk
- adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk"
- adb shell am start -n {{.APP_ID}}/.MainActivity
run:
@ -215,7 +215,7 @@ tasks:
cmds:
- task: assemble:apk
- adb uninstall {{.APP_ID}} 2>/dev/null || true
- adb install {{.BIN_DIR}}/{{.APP_NAME}}.apk
- adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk"
- adb shell am start -n {{.APP_ID}}/.MainActivity
logs:

View file

@ -65,10 +65,10 @@ tasks:
Docker image '{{.CROSS_IMAGE}}' not found.
Build it first: wails3 task setup:docker
cmds:
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME={{.APP_NAME}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
- mkdir -p {{.BIN_DIR}}
- mv bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}} {{.OUTPUT}}
- mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
vars:
DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
@ -117,7 +117,7 @@ tasks:
summary: Creates universal binary using wails3 tool lipo (Linux/Windows)
internal: true
cmds:
- wails3 tool lipo -o "{{.BIN_DIR}}/{{.APP_NAME}}" -i "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -i "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
- wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
- rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
package:
@ -138,17 +138,18 @@ tasks:
create:app:bundle:
summary: Creates an `.app` bundle
cmds:
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
- cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
- cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
- cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents"
- task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}'
codesign:adhoc:
summary: Ad-hoc signs the app bundle (macOS only)
internal: true
cmds:
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app
- codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app"
codesign:skip:
summary: Skips codesigning when cross-compiling
@ -158,11 +159,12 @@ tasks:
run:
cmds:
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources}
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS
- cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
- cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
- cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist"
- codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}'
sign:
@ -173,7 +175,7 @@ tasks:
deps:
- task: package
cmds:
- wails3 tool sign --input {{.BIN_DIR}}/{{.APP_NAME}}.app --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}}
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
@ -189,7 +191,7 @@ tasks:
deps:
- task: package
cmds:
- wails3 tool sign --input {{.BIN_DIR}}/{{.APP_NAME}}.app --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}}
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}}
preconditions:
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"

View file

@ -46,7 +46,7 @@ for arg in "$@"; do
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-darwin-arm64
@ -66,7 +66,7 @@ for arg in "$@"; do
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-darwin-amd64

View file

@ -61,7 +61,7 @@ tasks:
sh: xcrun --sdk iphonesimulator --show-sdk-path
cmds:
- xcrun -sdk iphonesimulator clang -target arm64-apple-ios15.0-simulator -isysroot {{.SDK_PATH}} -framework Foundation -framework UIKit -framework WebKit -o {{.BIN_DIR}}/{{.APP_NAME}} build/ios/main.m
- codesign --force --sign - {{.BIN_DIR}}/{{.APP_NAME}}
- codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}"
package:
summary: Packages a production build of the application into a `.app` bundle
@ -75,10 +75,10 @@ tasks:
create:app:bundle:
summary: Creates an iOS `.app` bundle
cmds:
- rm -rf {{.BIN_DIR}}/{{.APP_NAME}}.app
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/
- cp build/ios/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/
- rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.app"
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app"
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/"
- cp build/ios/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/"
- |
# Compile asset catalog and embed icons in the app bundle
APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.app"
@ -103,7 +103,7 @@ tasks:
/usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true
fi
fi
- codesign --force --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app
- codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app"
deploy-simulator:
summary: Deploy to iOS Simulator
@ -111,7 +111,7 @@ tasks:
cmds:
- xcrun simctl terminate booted {{.BUNDLE_ID}} 2>/dev/null || true
- xcrun simctl uninstall booted {{.BUNDLE_ID}} 2>/dev/null || true
- xcrun simctl install booted {{.BIN_DIR}}/{{.APP_NAME}}.app
- xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.app"
- xcrun simctl launch booted {{.BUNDLE_ID}}
compile:ios:
@ -133,8 +133,8 @@ tasks:
-framework Foundation -framework UIKit -framework WebKit \
-framework Security -framework CoreFoundation \
-lresolv \
-o {{.BIN_DIR}}/{{.APP_NAME | lower}} \
"$MAIN_M" {{.BIN_DIR}}/{{.APP_NAME}}.a
-o "{{.BIN_DIR}}/{{.APP_NAME | lower}}" \
"$MAIN_M" "{{.BIN_DIR}}/{{.APP_NAME}}.a"
generate:ios:bindings:
internal: true
@ -225,10 +225,10 @@ tasks:
- task: ensure-simulator
- task: compile:ios
cmds:
- rm -rf {{.BIN_DIR}}/{{.APP_NAME}}.dev.app
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app
- cp {{.BIN_DIR}}/{{.APP_NAME | lower}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/{{.APP_NAME | lower}}
- cp build/ios/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Info.plist
- rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- cp "{{.BIN_DIR}}/{{.APP_NAME | lower}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/{{.APP_NAME | lower}}"
- cp build/ios/Info.dev.plist "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Info.plist"
- |
# Compile asset catalog and embed icons for dev bundle
APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
@ -253,11 +253,11 @@ tasks:
/usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true
fi
fi
- codesign --force --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app
- xcrun simctl terminate booted com.wails.{{.APP_NAME | lower}}.dev 2>/dev/null || true
- xcrun simctl uninstall booted com.wails.{{.APP_NAME | lower}}.dev 2>/dev/null || true
- xcrun simctl install booted {{.BIN_DIR}}/{{.APP_NAME}}.dev.app
- xcrun simctl launch booted com.wails.{{.APP_NAME | lower}}.dev
- codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- xcrun simctl terminate booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true
- xcrun simctl uninstall booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true
- xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- xcrun simctl launch booted "com.wails.{{.APP_NAME | lower}}.dev"
xcode:
summary: Open the generated Xcode project for this app

View file

@ -69,10 +69,10 @@ tasks:
Docker image '{{.CROSS_IMAGE}}' not found.
Build it first: wails3 task setup:docker
cmds:
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME={{.APP_NAME}} {{.CROSS_IMAGE}} linux {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
- mkdir -p {{.BIN_DIR}}
- mv bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}} {{.OUTPUT}}
- mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
vars:
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
@ -112,9 +112,9 @@ tasks:
- task: build
- task: generate:dotdesktop
cmds:
- cp {{.APP_BINARY}} {{.APP_NAME}}
- cp ../../appicon.png {{.APP_NAME}}.png
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
- cp "{{.APP_BINARY}}" "{{.APP_NAME}}"
- cp ../../appicon.png "{{.APP_NAME}}.png"
- wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
vars:
APP_NAME: '{{.APP_NAME}}'
APP_BINARY: '../../../bin/{{.APP_NAME}}'
@ -149,24 +149,24 @@ tasks:
generate:deb:
summary: Creates a deb package
cmds:
- wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
- wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:rpm:
summary: Creates a rpm package
cmds:
- wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
- wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:aur:
summary: Creates a arch linux packager package
cmds:
- wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
- wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:dotdesktop:
summary: Generates a `.desktop` file
dir: build
cmds:
- mkdir -p {{.ROOT_DIR}}/build/linux/appimage
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}"
vars:
APP_NAME: '{{.APP_NAME}}'
EXEC: '{{.APP_NAME}}'
@ -187,7 +187,7 @@ tasks:
deps:
- task: create:deb
cmds:
- wails3 tool sign --input {{.BIN_DIR}}/{{.APP_NAME}}*.deb --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}}
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}}
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
@ -201,7 +201,7 @@ tasks:
deps:
- task: create:rpm
cmds:
- wails3 tool sign --input {{.BIN_DIR}}/{{.APP_NAME}}*.rpm --pgp-key {{.PGP_KEY}}
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" --pgp-key {{.PGP_KEY}}
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"

View file

@ -41,7 +41,7 @@ tasks:
- task: common:generate:icons
cmds:
- task: generate:syso
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
- go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe"
- cmd: powershell Remove-item *.syso
platforms: [windows]
- cmd: rm -f *.syso
@ -68,7 +68,7 @@ tasks:
Build it first: wails3 task setup:docker
cmds:
- task: generate:syso
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME={{.APP_NAME}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
- rm -f *.syso
vars:
@ -163,7 +163,7 @@ tasks:
deps:
- task: build
cmds:
- wails3 tool sign --input {{.BIN_DIR}}/{{.APP_NAME}}.exe {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"
@ -177,7 +177,7 @@ tasks:
deps:
- task: create:nsis:installer
cmds:
- wails3 tool sign --input build/windows/nsis/{{.APP_NAME}}-installer.exe {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
- wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"

View file

@ -2,8 +2,10 @@
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10">
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap3">
<Identity
Name="{{.ProductIdentifier}}"
@ -54,6 +56,13 @@
</uap:FileTypeAssociation>
</uap:Extension>
{{end}}
{{range .Protocols}}
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="{{.Scheme}}">
<uap:DisplayName>{{.Description}}</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
{{end}}
</Extensions>
</Application>
</Applications>

View file

@ -4,6 +4,7 @@ import (
"bytes"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@ -156,8 +157,8 @@ func TestCLIParameterFormats(t *testing.T) {
},
},
{
name: "Empty value",
otherArgs: []string{"build", "EMPTY=", "KEY=value"},
name: "Empty value",
otherArgs: []string{"build", "EMPTY=", "KEY=value"},
expectedVars: map[string]string{
"EMPTY": "",
"KEY": "value",
@ -224,7 +225,7 @@ func captureTaskOutput(t *testing.T, options *RunTaskOptions, otherArgs []string
// Wait for task to complete
<-done
// Check for errors (might be expected in some tests)
if taskErr != nil && !strings.Contains(taskErr.Error(), "expected") {
t.Logf("Task error (might be expected): %v", taskErr)
@ -286,4 +287,56 @@ func TestBackwardCompatibility(t *testing.T) {
}
})
}
}
}
func TestMkdirWithSpacesInPath(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("Skipping: macOS app bundle test only applies to darwin")
}
if os.Getenv("CI") == "true" && os.Getenv("SKIP_INTEGRATION_TESTS") == "true" {
t.Skip("Skipping integration test in CI")
}
tmpDir, err := os.MkdirTemp("", "wails task test with spaces-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
taskfileContent := `version: '3'
vars:
BIN_DIR: "` + tmpDir + `/bin"
APP_NAME: "My App"
tasks:
create-bundle:
cmds:
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
`
taskfilePath := filepath.Join(tmpDir, "Taskfile.yml")
err = os.WriteFile(taskfilePath, []byte(taskfileContent), 0644)
require.NoError(t, err)
originalWd, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(originalWd)
err = os.Chdir(tmpDir)
require.NoError(t, err)
err = RunTask(&RunTaskOptions{Name: "create-bundle"}, []string{})
require.NoError(t, err)
appContentsDir := filepath.Join(tmpDir, "bin", "My App.app", "Contents")
macOSDir := filepath.Join(appContentsDir, "MacOS")
info, err := os.Stat(macOSDir)
require.NoError(t, err, "MacOS directory should exist")
assert.True(t, info.IsDir(), "MacOS should be a directory")
resourcesDir := filepath.Join(appContentsDir, "Resources")
info, err = os.Stat(resourcesDir)
require.NoError(t, err, "Resources directory should exist")
assert.True(t, info.IsDir(), "Resources should be a directory")
}

View file

@ -0,0 +1,96 @@
package fileexplorer
import (
"bufio"
"io"
"os"
"strings"
)
// DesktopEntry represents a parsed .desktop file's [Desktop Entry] section.
// This is a minimal parser that only extracts the fields we need,
// replacing the full gopkg.in/ini.v1 dependency (~34KB + 68 transitive deps).
type DesktopEntry struct {
Exec string
}
// ParseDesktopFile parses a .desktop file and returns the Desktop Entry section.
// It follows the Desktop Entry Specification:
// ParseDesktopFile parses the `[Desktop Entry]` section of the desktop file at path and returns a DesktopEntry.
// It returns an error if the file cannot be opened or if parsing the file fails.
func ParseDesktopFile(path string) (*DesktopEntry, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return ParseDesktopReader(f)
}
// ParseDesktopReader parses the [Desktop Entry] section of a .desktop file from r and extracts the Exec value.
// It ignores empty lines and lines starting with '#', treats section names as case-sensitive, and stops parsing after leaving the [Desktop Entry] section.
// The returned *DesktopEntry has Exec set to the exact value of the Exec key if present (whitespace preserved).
// An error is returned if reading from r fails.
func ParseDesktopReader(r io.Reader) (*DesktopEntry, error) {
scanner := bufio.NewScanner(r)
entry := &DesktopEntry{}
inDesktopEntry := false
for scanner.Scan() {
line := scanner.Text()
// Skip empty lines
if len(line) == 0 {
continue
}
// Skip comments (# at start of line)
if line[0] == '#' {
continue
}
// Handle section headers
if line[0] == '[' {
// Check if this is the [Desktop Entry] section
// The spec says section names are case-sensitive
trimmed := strings.TrimSpace(line)
if trimmed == "[Desktop Entry]" {
inDesktopEntry = true
} else if inDesktopEntry {
// We've left the [Desktop Entry] section
// (e.g., entering [Desktop Action new-window])
// We already have what we need, so we can stop
break
}
continue
}
// Only process key=value pairs in [Desktop Entry] section
if !inDesktopEntry {
continue
}
// Parse key=value (spec says no spaces around =, but be lenient)
eqIdx := strings.Index(line, "=")
if eqIdx == -1 {
continue
}
key := strings.TrimSpace(line[:eqIdx])
value := line[eqIdx+1:] // Don't trim value - preserve intentional whitespace
// We only need the Exec key
// Per spec, keys are case-sensitive and Exec is always "Exec"
if key == "Exec" {
entry.Exec = value
// Continue parsing in case there are multiple Exec lines (shouldn't happen but be safe)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return entry, nil
}

View file

@ -0,0 +1,532 @@
package fileexplorer
import (
"strings"
"testing"
)
func TestParseDesktopReader(t *testing.T) {
tests := []struct {
name string
input string
wantExec string
wantErr bool
}{
{
name: "simple desktop file",
input: `[Desktop Entry]
Name=Files
Exec=nautilus %U
Icon=org.gnome.Nautilus
`,
wantExec: "nautilus %U",
},
{
name: "exec with full path",
input: `[Desktop Entry]
Name=1Password
Exec=/opt/1Password/1password %U
`,
wantExec: "/opt/1Password/1password %U",
},
{
name: "exec without arguments",
input: `[Desktop Entry]
Name=Btop
Exec=btop
Terminal=true
`,
wantExec: "btop",
},
{
name: "exec with spaces in path",
input: `[Desktop Entry]
Name=My App
Exec="/path/with spaces/myapp" %f
`,
wantExec: `"/path/with spaces/myapp" %f`,
},
{
name: "comments are ignored",
input: `# This is a comment
[Desktop Entry]
# Another comment
Name=Files
Exec=nautilus
# Comment after
`,
wantExec: "nautilus",
},
{
name: "empty lines are ignored",
input: `
[Desktop Entry]
Name=Files
Exec=nautilus
`,
wantExec: "nautilus",
},
{
name: "key before section is ignored",
input: `Exec=ignored
[Desktop Entry]
Exec=nautilus
`,
wantExec: "nautilus",
},
{
name: "other sections after Desktop Entry are ignored",
input: `[Desktop Entry]
Exec=nautilus --new-window %U
Icon=nautilus
[Desktop Action new-window]
Name=New Window
Exec=nautilus --new-window
`,
wantExec: "nautilus --new-window %U",
},
{
name: "section before Desktop Entry is ignored",
input: `[Some Other Section]
Exec=ignored
[Desktop Entry]
Exec=nautilus
`,
wantExec: "nautilus",
},
{
name: "case sensitive section name",
input: `[desktop entry]
Exec=ignored
[Desktop Entry]
Exec=correct
`,
wantExec: "correct",
},
{
name: "case sensitive key name",
input: `[Desktop Entry]
exec=ignored
EXEC=also ignored
Exec=correct
`,
wantExec: "correct",
},
{
name: "value with equals sign",
input: `[Desktop Entry]
Exec=env VAR=value myapp
`,
wantExec: "env VAR=value myapp",
},
{
name: "value with multiple equals signs",
input: `[Desktop Entry]
Exec=env A=1 B=2 C=3 myapp
`,
wantExec: "env A=1 B=2 C=3 myapp",
},
{
name: "localized keys are separate",
input: `[Desktop Entry]
Name[en]=Files
Name=Default Files
Exec[en]=ignored
Exec=nautilus
`,
wantExec: "nautilus",
},
{
name: "whitespace in section header",
input: `[Desktop Entry]
Exec=nautilus
`,
wantExec: "nautilus",
},
{
name: "no exec key",
input: `[Desktop Entry]
Name=Files
Icon=nautilus
`,
wantExec: "",
},
{
name: "empty file",
input: ``,
wantExec: "",
},
{
name: "only comments",
input: `# Comment 1
# Comment 2
`,
wantExec: "",
},
{
name: "no Desktop Entry section",
input: `[Other Section]
Exec=ignored
`,
wantExec: "",
},
{
name: "real nautilus desktop file structure",
input: `[Desktop Entry]
Name[en_CA]=Files
Name[en_GB]=Files
Name=Files
Comment=Access and organize files
Keywords=folder;manager;explore;disk;filesystem;nautilus;
Exec=nautilus --new-window %U
Icon=org.gnome.Nautilus
Terminal=false
Type=Application
DBusActivatable=true
StartupNotify=true
Categories=GNOME;GTK;Utility;Core;FileManager;
MimeType=inode/directory;application/x-7z-compressed;
X-GNOME-UsesNotifications=true
Actions=new-window;
[Desktop Action new-window]
Name=New Window
Exec=nautilus --new-window
`,
wantExec: "nautilus --new-window %U",
},
{
name: "thunar style",
input: `[Desktop Entry]
Version=1.0
Name=Thunar File Manager
Exec=thunar %F
Icon=Thunar
Type=Application
Categories=System;FileTools;FileManager;
`,
wantExec: "thunar %F",
},
{
name: "dolphin style",
input: `[Desktop Entry]
Type=Application
Exec=dolphin %u
Icon=system-file-manager
Name=Dolphin
GenericName=File Manager
`,
wantExec: "dolphin %u",
},
{
name: "pcmanfm style",
input: `[Desktop Entry]
Type=Application
Name=PCManFM
GenericName=File Manager
Exec=pcmanfm %U
Icon=system-file-manager
`,
wantExec: "pcmanfm %U",
},
{
name: "exec with environment variable",
input: `[Desktop Entry]
Exec=env GDK_BACKEND=x11 nautilus %U
`,
wantExec: "env GDK_BACKEND=x11 nautilus %U",
},
{
name: "trailing whitespace in value preserved",
input: `[Desktop Entry]
Exec=nautilus
`,
wantExec: "nautilus ",
},
{
name: "leading whitespace in key",
input: `[Desktop Entry]
Exec=nautilus
`,
wantExec: "nautilus",
},
{
name: "space around equals",
input: `[Desktop Entry]
Exec = nautilus
`,
wantExec: " nautilus", // We trim the key, value starts after =
},
{
name: "line without equals is ignored",
input: `[Desktop Entry]
InvalidLine
Exec=nautilus
AnotherInvalidLine
`,
wantExec: "nautilus",
},
{
name: "UTF-8 in exec path",
input: `[Desktop Entry]
Exec=/usr/bin/文件管理器 %U
`,
wantExec: "/usr/bin/文件管理器 %U",
},
{
name: "special characters in exec",
input: `[Desktop Entry]
Exec=sh -c "echo 'hello world' && nautilus %U"
`,
wantExec: `sh -c "echo 'hello world' && nautilus %U"`,
},
{
name: "multiple Desktop Entry sections (invalid file, last value wins)",
input: `[Desktop Entry]
Exec=first
[Desktop Entry]
Exec=second
`,
wantExec: "second", // Invalid file, but we handle it gracefully
},
{
name: "very long exec line",
input: `[Desktop Entry]
Exec=` + strings.Repeat("a", 1000) + `
`,
wantExec: strings.Repeat("a", 1000),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
entry, err := ParseDesktopReader(strings.NewReader(tt.input))
if (err != nil) != tt.wantErr {
t.Errorf("ParseDesktopReader() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
return
}
if entry.Exec != tt.wantExec {
t.Errorf("ParseDesktopReader() Exec = %q, want %q", entry.Exec, tt.wantExec)
}
})
}
}
func TestParseDesktopReader_LineScanning(t *testing.T) {
// Test that we handle lines at the scanner's limit (64KB default)
// bufio.Scanner returns an error for lines > 64KB, which is acceptable
// since real .desktop files never have lines that long
t.Run("line at buffer limit returns error", func(t *testing.T) {
// Create a line that exceeds the buffer size (64KB)
longValue := strings.Repeat("x", 65536)
input := "[Desktop Entry]\nExec=" + longValue + "\n"
_, err := ParseDesktopReader(strings.NewReader(input))
if err == nil {
t.Error("Expected error for line exceeding buffer size, got nil")
}
})
t.Run("line under buffer limit works", func(t *testing.T) {
// Create a line that's under the limit (should work fine)
longValue := strings.Repeat("x", 60000)
input := "[Desktop Entry]\nExec=" + longValue + "\n"
entry, err := ParseDesktopReader(strings.NewReader(input))
if err != nil {
t.Errorf("Unexpected error for long but valid line: %v", err)
return
}
if entry.Exec != longValue {
t.Errorf("Long line not parsed correctly, got length %d, want %d", len(entry.Exec), len(longValue))
}
})
}
func TestParseDesktopReader_RealWorldFiles(t *testing.T) {
// These are actual .desktop file contents from real systems
realWorldTests := []struct {
name string
content string
wantExec string
}{
{
name: "GNOME Nautilus 43.x",
content: `[Desktop Entry]
Name=Files
Comment=Access and organize files
Keywords=folder;manager;explore;disk;filesystem;nautilus;
Exec=nautilus --new-window %U
Icon=org.gnome.Nautilus
Terminal=false
Type=Application
DBusActivatable=true
StartupNotify=true
Categories=GNOME;GTK;Utility;Core;FileManager;
MimeType=inode/directory;application/x-7z-compressed;
Actions=new-window;
[Desktop Action new-window]
Name=New Window
Exec=nautilus --new-window`,
wantExec: "nautilus --new-window %U",
},
{
name: "KDE Dolphin",
content: `[Desktop Entry]
Type=Application
Exec=dolphin %u
Icon=system-file-manager
Terminal=false
InitialPreference=9
Name=Dolphin
GenericName=File Manager
MimeType=inode/directory;
Categories=Qt;KDE;System;FileTools;FileManager;
Actions=new-window;
[Desktop Action new-window]
Name=Open a New Window
Exec=dolphin %u`,
wantExec: "dolphin %u",
},
{
name: "Thunar",
content: `[Desktop Entry]
Version=1.0
Name=Thunar File Manager
GenericName=File Manager
Comment=Browse the filesystem with the file manager
Exec=thunar %F
Icon=Thunar
Terminal=false
StartupNotify=true
Type=Application
Categories=System;FileTools;FileManager;
`,
wantExec: "thunar %F",
},
{
name: "PCManFM",
content: `[Desktop Entry]
Type=Application
Name=PCManFM
GenericName=File Manager
Comment=Browse the file system
Exec=pcmanfm %U
Icon=system-file-manager
Terminal=false
StartupNotify=true
Categories=Utility;FileManager;`,
wantExec: "pcmanfm %U",
},
{
name: "Caja (MATE)",
content: `[Desktop Entry]
Name=Files
Comment=Access and organize files
Exec=caja %U
Icon=system-file-manager
Terminal=false
Type=Application
Categories=MATE;System;FileManager;
StartupNotify=true`,
wantExec: "caja %U",
},
{
name: "Nemo (Cinnamon)",
content: `[Desktop Entry]
Name=Files
Comment=Access and organize files
Exec=nemo %U
Icon=folder
Terminal=false
Type=Application
StartupNotify=true
Categories=GNOME;GTK;Utility;Core;
MimeType=inode/directory;`,
wantExec: "nemo %U",
},
}
for _, tt := range realWorldTests {
t.Run(tt.name, func(t *testing.T) {
entry, err := ParseDesktopReader(strings.NewReader(tt.content))
if err != nil {
t.Fatalf("ParseDesktopReader() error = %v", err)
}
if entry.Exec != tt.wantExec {
t.Errorf("ParseDesktopReader() Exec = %q, want %q", entry.Exec, tt.wantExec)
}
})
}
}
// BenchmarkParseDesktopReader measures parsing performance
func BenchmarkParseDesktopReader(b *testing.B) {
// Real Nautilus .desktop file content
content := `[Desktop Entry]
Name=Files
Comment=Access and organize files
Keywords=folder;manager;explore;disk;filesystem;nautilus;
Exec=nautilus --new-window %U
Icon=org.gnome.Nautilus
Terminal=false
Type=Application
DBusActivatable=true
StartupNotify=true
Categories=GNOME;GTK;Utility;Core;FileManager;
MimeType=inode/directory;application/x-7z-compressed;
Actions=new-window;
[Desktop Action new-window]
Name=New Window
Exec=nautilus --new-window
`
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := ParseDesktopReader(strings.NewReader(content))
if err != nil {
b.Fatal(err)
}
}
}
// BenchmarkParseDesktopReader_Large tests parsing a file with many localized entries
func BenchmarkParseDesktopReader_Large(b *testing.B) {
// Simulate a desktop file with many localized Name entries (like Nautilus)
var sb strings.Builder
sb.WriteString("[Desktop Entry]\n")
for i := 0; i < 100; i++ {
sb.WriteString("Name[lang")
sb.WriteString(strings.Repeat("x", 5))
sb.WriteString("]=Localized Name\n")
}
sb.WriteString("Exec=nautilus %U\n")
sb.WriteString("[Desktop Action new-window]\n")
sb.WriteString("Name=New Window\n")
sb.WriteString("Exec=nautilus\n")
content := sb.String()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := ParseDesktopReader(strings.NewReader(content))
if err != nil {
b.Fatal(err)
}
}
}

View file

@ -11,10 +11,9 @@ import (
"path/filepath"
"strings"
"syscall"
ini "gopkg.in/ini.v1"
)
// when possible; the fallback method does not support selecting a file.
func explorerBinArgs(path string, selectFile bool) (string, []string, error) {
// Map of field codes to their replacements
var fieldCodes = map[string]string{
@ -38,22 +37,22 @@ func explorerBinArgs(path string, selectFile bool) (string, []string, error) {
return fallbackExplorerBinArgs(path, selectFile)
}
desktopFile, err := findDesktopFile(strings.TrimSpace((buf.String())))
desktopFilePath, err := findDesktopFile(strings.TrimSpace((buf.String())))
if err != nil {
return fallbackExplorerBinArgs(path, selectFile)
}
cfg, err := ini.Load(desktopFile)
entry, err := ParseDesktopFile(desktopFilePath)
if err != nil {
// Opting to fallback rather than fail
return fallbackExplorerBinArgs(path, selectFile)
}
exec := cfg.Section("Desktop Entry").Key("Exec").String()
execCmd := entry.Exec
for fieldCode, replacement := range fieldCodes {
exec = strings.ReplaceAll(exec, fieldCode, replacement)
execCmd = strings.ReplaceAll(execCmd, fieldCode, replacement)
}
args := strings.Fields(exec)
args := strings.Fields(execCmd)
if !strings.Contains(strings.Join(args, " "), path) {
args = append(args, path)
}
@ -111,4 +110,4 @@ func findDesktopFile(xdgFileName string) (string, error) {
}
err := fmt.Errorf("desktop file not found: %s", xdgFileName)
return "", err
}
}

View file

@ -6,218 +6,217 @@ func IsKnownEvent(name string) bool {
}
var knownEvents = map[string]struct{}{
"common:ApplicationOpenedWithFile": {},
"common:ApplicationStarted": {},
"common:ApplicationLaunchedWithUrl": {},
"common:ThemeChanged": {},
"common:WindowClosing": {},
"common:WindowDidMove": {},
"common:WindowDidResize": {},
"common:WindowDPIChanged": {},
"common:WindowFilesDropped": {},
"common:WindowFocus": {},
"common:WindowFullscreen": {},
"common:WindowHide": {},
"common:WindowLostFocus": {},
"common:WindowMaximise": {},
"common:WindowMinimise": {},
"common:WindowToggleFrameless": {},
"common:WindowRestore": {},
"common:WindowRuntimeReady": {},
"common:WindowShow": {},
"common:WindowUnFullscreen": {},
"common:WindowUnMaximise": {},
"common:WindowUnMinimise": {},
"common:WindowZoom": {},
"common:WindowZoomIn": {},
"common:WindowZoomOut": {},
"common:WindowZoomReset": {},
"common:WindowDropZoneFilesDropped": {},
"linux:ApplicationStartup": {},
"linux:SystemThemeChanged": {},
"linux:WindowDeleteEvent": {},
"linux:WindowDidMove": {},
"linux:WindowDidResize": {},
"linux:WindowFocusIn": {},
"linux:WindowFocusOut": {},
"linux:WindowLoadStarted": {},
"linux:WindowLoadRedirected": {},
"linux:WindowLoadCommitted": {},
"linux:WindowLoadFinished": {},
"mac:ApplicationDidBecomeActive": {},
"mac:ApplicationDidChangeBackingProperties": {},
"mac:ApplicationDidChangeEffectiveAppearance": {},
"mac:ApplicationDidChangeIcon": {},
"mac:ApplicationDidChangeOcclusionState": {},
"mac:ApplicationDidChangeScreenParameters": {},
"mac:ApplicationDidChangeStatusBarFrame": {},
"mac:ApplicationDidChangeStatusBarOrientation": {},
"mac:ApplicationDidChangeTheme": {},
"mac:ApplicationDidFinishLaunching": {},
"mac:ApplicationDidHide": {},
"mac:ApplicationDidResignActive": {},
"mac:ApplicationDidUnhide": {},
"mac:ApplicationDidUpdate": {},
"mac:ApplicationShouldHandleReopen": {},
"mac:ApplicationWillBecomeActive": {},
"mac:ApplicationWillFinishLaunching": {},
"mac:ApplicationWillHide": {},
"mac:ApplicationWillResignActive": {},
"mac:ApplicationWillTerminate": {},
"mac:ApplicationWillUnhide": {},
"mac:ApplicationWillUpdate": {},
"mac:MenuDidAddItem": {},
"mac:MenuDidBeginTracking": {},
"mac:MenuDidClose": {},
"mac:MenuDidDisplayItem": {},
"mac:MenuDidEndTracking": {},
"mac:MenuDidHighlightItem": {},
"mac:MenuDidOpen": {},
"mac:MenuDidPopUp": {},
"mac:MenuDidRemoveItem": {},
"mac:MenuDidSendAction": {},
"mac:MenuDidSendActionToItem": {},
"mac:MenuDidUpdate": {},
"mac:MenuWillAddItem": {},
"mac:MenuWillBeginTracking": {},
"mac:MenuWillDisplayItem": {},
"mac:MenuWillEndTracking": {},
"mac:MenuWillHighlightItem": {},
"mac:MenuWillOpen": {},
"mac:MenuWillPopUp": {},
"mac:MenuWillRemoveItem": {},
"mac:MenuWillSendAction": {},
"mac:MenuWillSendActionToItem": {},
"mac:MenuWillUpdate": {},
"mac:WebViewDidCommitNavigation": {},
"mac:WebViewDidFinishNavigation": {},
"common:ApplicationOpenedWithFile": {},
"common:ApplicationStarted": {},
"common:ApplicationLaunchedWithUrl": {},
"common:ThemeChanged": {},
"common:WindowClosing": {},
"common:WindowDidMove": {},
"common:WindowDidResize": {},
"common:WindowDPIChanged": {},
"common:WindowFilesDropped": {},
"common:WindowFocus": {},
"common:WindowFullscreen": {},
"common:WindowHide": {},
"common:WindowLostFocus": {},
"common:WindowMaximise": {},
"common:WindowMinimise": {},
"common:WindowToggleFrameless": {},
"common:WindowRestore": {},
"common:WindowRuntimeReady": {},
"common:WindowShow": {},
"common:WindowUnFullscreen": {},
"common:WindowUnMaximise": {},
"common:WindowUnMinimise": {},
"common:WindowZoom": {},
"common:WindowZoomIn": {},
"common:WindowZoomOut": {},
"common:WindowZoomReset": {},
"linux:ApplicationStartup": {},
"linux:SystemThemeChanged": {},
"linux:WindowDeleteEvent": {},
"linux:WindowDidMove": {},
"linux:WindowDidResize": {},
"linux:WindowFocusIn": {},
"linux:WindowFocusOut": {},
"linux:WindowLoadStarted": {},
"linux:WindowLoadRedirected": {},
"linux:WindowLoadCommitted": {},
"linux:WindowLoadFinished": {},
"mac:ApplicationDidBecomeActive": {},
"mac:ApplicationDidChangeBackingProperties": {},
"mac:ApplicationDidChangeEffectiveAppearance": {},
"mac:ApplicationDidChangeIcon": {},
"mac:ApplicationDidChangeOcclusionState": {},
"mac:ApplicationDidChangeScreenParameters": {},
"mac:ApplicationDidChangeStatusBarFrame": {},
"mac:ApplicationDidChangeStatusBarOrientation": {},
"mac:ApplicationDidChangeTheme": {},
"mac:ApplicationDidFinishLaunching": {},
"mac:ApplicationDidHide": {},
"mac:ApplicationDidResignActive": {},
"mac:ApplicationDidUnhide": {},
"mac:ApplicationDidUpdate": {},
"mac:ApplicationShouldHandleReopen": {},
"mac:ApplicationWillBecomeActive": {},
"mac:ApplicationWillFinishLaunching": {},
"mac:ApplicationWillHide": {},
"mac:ApplicationWillResignActive": {},
"mac:ApplicationWillTerminate": {},
"mac:ApplicationWillUnhide": {},
"mac:ApplicationWillUpdate": {},
"mac:MenuDidAddItem": {},
"mac:MenuDidBeginTracking": {},
"mac:MenuDidClose": {},
"mac:MenuDidDisplayItem": {},
"mac:MenuDidEndTracking": {},
"mac:MenuDidHighlightItem": {},
"mac:MenuDidOpen": {},
"mac:MenuDidPopUp": {},
"mac:MenuDidRemoveItem": {},
"mac:MenuDidSendAction": {},
"mac:MenuDidSendActionToItem": {},
"mac:MenuDidUpdate": {},
"mac:MenuWillAddItem": {},
"mac:MenuWillBeginTracking": {},
"mac:MenuWillDisplayItem": {},
"mac:MenuWillEndTracking": {},
"mac:MenuWillHighlightItem": {},
"mac:MenuWillOpen": {},
"mac:MenuWillPopUp": {},
"mac:MenuWillRemoveItem": {},
"mac:MenuWillSendAction": {},
"mac:MenuWillSendActionToItem": {},
"mac:MenuWillUpdate": {},
"mac:WebViewDidCommitNavigation": {},
"mac:WebViewDidFinishNavigation": {},
"mac:WebViewDidReceiveServerRedirectForProvisionalNavigation": {},
"mac:WebViewDidStartProvisionalNavigation": {},
"mac:WindowDidBecomeKey": {},
"mac:WindowDidBecomeMain": {},
"mac:WindowDidBeginSheet": {},
"mac:WindowDidChangeAlpha": {},
"mac:WindowDidChangeBackingLocation": {},
"mac:WindowDidChangeBackingProperties": {},
"mac:WindowDidChangeCollectionBehavior": {},
"mac:WindowDidChangeEffectiveAppearance": {},
"mac:WindowDidChangeOcclusionState": {},
"mac:WindowDidChangeOrderingMode": {},
"mac:WindowDidChangeScreen": {},
"mac:WindowDidChangeScreenParameters": {},
"mac:WindowDidChangeScreenProfile": {},
"mac:WindowDidChangeScreenSpace": {},
"mac:WindowDidChangeScreenSpaceProperties": {},
"mac:WindowDidChangeSharingType": {},
"mac:WindowDidChangeSpace": {},
"mac:WindowDidChangeSpaceOrderingMode": {},
"mac:WindowDidChangeTitle": {},
"mac:WindowDidChangeToolbar": {},
"mac:WindowDidDeminiaturize": {},
"mac:WindowDidEndSheet": {},
"mac:WindowDidEnterFullScreen": {},
"mac:WindowDidEnterVersionBrowser": {},
"mac:WindowDidExitFullScreen": {},
"mac:WindowDidExitVersionBrowser": {},
"mac:WindowDidExpose": {},
"mac:WindowDidFocus": {},
"mac:WindowDidMiniaturize": {},
"mac:WindowDidMove": {},
"mac:WindowDidOrderOffScreen": {},
"mac:WindowDidOrderOnScreen": {},
"mac:WindowDidResignKey": {},
"mac:WindowDidResignMain": {},
"mac:WindowDidResize": {},
"mac:WindowDidUpdate": {},
"mac:WindowDidUpdateAlpha": {},
"mac:WindowDidUpdateCollectionBehavior": {},
"mac:WindowDidUpdateCollectionProperties": {},
"mac:WindowDidUpdateShadow": {},
"mac:WindowDidUpdateTitle": {},
"mac:WindowDidUpdateToolbar": {},
"mac:WindowDidZoom": {},
"mac:WindowFileDraggingEntered": {},
"mac:WindowFileDraggingExited": {},
"mac:WindowFileDraggingPerformed": {},
"mac:WindowHide": {},
"mac:WindowMaximise": {},
"mac:WindowUnMaximise": {},
"mac:WindowMinimise": {},
"mac:WindowUnMinimise": {},
"mac:WindowShouldClose": {},
"mac:WindowShow": {},
"mac:WindowWillBecomeKey": {},
"mac:WindowWillBecomeMain": {},
"mac:WindowWillBeginSheet": {},
"mac:WindowWillChangeOrderingMode": {},
"mac:WindowWillClose": {},
"mac:WindowWillDeminiaturize": {},
"mac:WindowWillEnterFullScreen": {},
"mac:WindowWillEnterVersionBrowser": {},
"mac:WindowWillExitFullScreen": {},
"mac:WindowWillExitVersionBrowser": {},
"mac:WindowWillFocus": {},
"mac:WindowWillMiniaturize": {},
"mac:WindowWillMove": {},
"mac:WindowWillOrderOffScreen": {},
"mac:WindowWillOrderOnScreen": {},
"mac:WindowWillResignMain": {},
"mac:WindowWillResize": {},
"mac:WindowWillUnfocus": {},
"mac:WindowWillUpdate": {},
"mac:WindowWillUpdateAlpha": {},
"mac:WindowWillUpdateCollectionBehavior": {},
"mac:WindowWillUpdateCollectionProperties": {},
"mac:WindowWillUpdateShadow": {},
"mac:WindowWillUpdateTitle": {},
"mac:WindowWillUpdateToolbar": {},
"mac:WindowWillUpdateVisibility": {},
"mac:WindowWillUseStandardFrame": {},
"mac:WindowZoomIn": {},
"mac:WindowZoomOut": {},
"mac:WindowZoomReset": {},
"windows:APMPowerSettingChange": {},
"windows:APMPowerStatusChange": {},
"windows:APMResumeAutomatic": {},
"windows:APMResumeSuspend": {},
"windows:APMSuspend": {},
"windows:ApplicationStarted": {},
"windows:SystemThemeChanged": {},
"windows:WebViewNavigationCompleted": {},
"windows:WindowActive": {},
"windows:WindowBackgroundErase": {},
"windows:WindowClickActive": {},
"windows:WindowClosing": {},
"windows:WindowDidMove": {},
"windows:WindowDidResize": {},
"windows:WindowDPIChanged": {},
"windows:WindowDragDrop": {},
"windows:WindowDragEnter": {},
"windows:WindowDragLeave": {},
"windows:WindowDragOver": {},
"windows:WindowEndMove": {},
"windows:WindowEndResize": {},
"windows:WindowFullscreen": {},
"windows:WindowHide": {},
"windows:WindowInactive": {},
"windows:WindowKeyDown": {},
"windows:WindowKeyUp": {},
"windows:WindowKillFocus": {},
"windows:WindowNonClientHit": {},
"windows:WindowNonClientMouseDown": {},
"windows:WindowNonClientMouseLeave": {},
"windows:WindowNonClientMouseMove": {},
"windows:WindowNonClientMouseUp": {},
"windows:WindowPaint": {},
"windows:WindowRestore": {},
"windows:WindowSetFocus": {},
"windows:WindowShow": {},
"windows:WindowStartMove": {},
"windows:WindowStartResize": {},
"windows:WindowUnFullscreen": {},
"windows:WindowZOrderChanged": {},
"windows:WindowMinimise": {},
"windows:WindowUnMinimise": {},
"windows:WindowMaximise": {},
"windows:WindowUnMaximise": {},
"mac:WebViewDidStartProvisionalNavigation": {},
"mac:WindowDidBecomeKey": {},
"mac:WindowDidBecomeMain": {},
"mac:WindowDidBeginSheet": {},
"mac:WindowDidChangeAlpha": {},
"mac:WindowDidChangeBackingLocation": {},
"mac:WindowDidChangeBackingProperties": {},
"mac:WindowDidChangeCollectionBehavior": {},
"mac:WindowDidChangeEffectiveAppearance": {},
"mac:WindowDidChangeOcclusionState": {},
"mac:WindowDidChangeOrderingMode": {},
"mac:WindowDidChangeScreen": {},
"mac:WindowDidChangeScreenParameters": {},
"mac:WindowDidChangeScreenProfile": {},
"mac:WindowDidChangeScreenSpace": {},
"mac:WindowDidChangeScreenSpaceProperties": {},
"mac:WindowDidChangeSharingType": {},
"mac:WindowDidChangeSpace": {},
"mac:WindowDidChangeSpaceOrderingMode": {},
"mac:WindowDidChangeTitle": {},
"mac:WindowDidChangeToolbar": {},
"mac:WindowDidDeminiaturize": {},
"mac:WindowDidEndSheet": {},
"mac:WindowDidEnterFullScreen": {},
"mac:WindowDidEnterVersionBrowser": {},
"mac:WindowDidExitFullScreen": {},
"mac:WindowDidExitVersionBrowser": {},
"mac:WindowDidExpose": {},
"mac:WindowDidFocus": {},
"mac:WindowDidMiniaturize": {},
"mac:WindowDidMove": {},
"mac:WindowDidOrderOffScreen": {},
"mac:WindowDidOrderOnScreen": {},
"mac:WindowDidResignKey": {},
"mac:WindowDidResignMain": {},
"mac:WindowDidResize": {},
"mac:WindowDidUpdate": {},
"mac:WindowDidUpdateAlpha": {},
"mac:WindowDidUpdateCollectionBehavior": {},
"mac:WindowDidUpdateCollectionProperties": {},
"mac:WindowDidUpdateShadow": {},
"mac:WindowDidUpdateTitle": {},
"mac:WindowDidUpdateToolbar": {},
"mac:WindowDidZoom": {},
"mac:WindowFileDraggingEntered": {},
"mac:WindowFileDraggingExited": {},
"mac:WindowFileDraggingPerformed": {},
"mac:WindowHide": {},
"mac:WindowMaximise": {},
"mac:WindowUnMaximise": {},
"mac:WindowMinimise": {},
"mac:WindowUnMinimise": {},
"mac:WindowShouldClose": {},
"mac:WindowShow": {},
"mac:WindowWillBecomeKey": {},
"mac:WindowWillBecomeMain": {},
"mac:WindowWillBeginSheet": {},
"mac:WindowWillChangeOrderingMode": {},
"mac:WindowWillClose": {},
"mac:WindowWillDeminiaturize": {},
"mac:WindowWillEnterFullScreen": {},
"mac:WindowWillEnterVersionBrowser": {},
"mac:WindowWillExitFullScreen": {},
"mac:WindowWillExitVersionBrowser": {},
"mac:WindowWillFocus": {},
"mac:WindowWillMiniaturize": {},
"mac:WindowWillMove": {},
"mac:WindowWillOrderOffScreen": {},
"mac:WindowWillOrderOnScreen": {},
"mac:WindowWillResignMain": {},
"mac:WindowWillResize": {},
"mac:WindowWillUnfocus": {},
"mac:WindowWillUpdate": {},
"mac:WindowWillUpdateAlpha": {},
"mac:WindowWillUpdateCollectionBehavior": {},
"mac:WindowWillUpdateCollectionProperties": {},
"mac:WindowWillUpdateShadow": {},
"mac:WindowWillUpdateTitle": {},
"mac:WindowWillUpdateToolbar": {},
"mac:WindowWillUpdateVisibility": {},
"mac:WindowWillUseStandardFrame": {},
"mac:WindowZoomIn": {},
"mac:WindowZoomOut": {},
"mac:WindowZoomReset": {},
"windows:APMPowerSettingChange": {},
"windows:APMPowerStatusChange": {},
"windows:APMResumeAutomatic": {},
"windows:APMResumeSuspend": {},
"windows:APMSuspend": {},
"windows:ApplicationStarted": {},
"windows:SystemThemeChanged": {},
"windows:WebViewNavigationCompleted": {},
"windows:WindowActive": {},
"windows:WindowBackgroundErase": {},
"windows:WindowClickActive": {},
"windows:WindowClosing": {},
"windows:WindowDidMove": {},
"windows:WindowDidResize": {},
"windows:WindowDPIChanged": {},
"windows:WindowDragDrop": {},
"windows:WindowDragEnter": {},
"windows:WindowDragLeave": {},
"windows:WindowDragOver": {},
"windows:WindowEndMove": {},
"windows:WindowEndResize": {},
"windows:WindowFullscreen": {},
"windows:WindowHide": {},
"windows:WindowInactive": {},
"windows:WindowKeyDown": {},
"windows:WindowKeyUp": {},
"windows:WindowKillFocus": {},
"windows:WindowNonClientHit": {},
"windows:WindowNonClientMouseDown": {},
"windows:WindowNonClientMouseLeave": {},
"windows:WindowNonClientMouseMove": {},
"windows:WindowNonClientMouseUp": {},
"windows:WindowPaint": {},
"windows:WindowRestore": {},
"windows:WindowSetFocus": {},
"windows:WindowShow": {},
"windows:WindowStartMove": {},
"windows:WindowStartResize": {},
"windows:WindowUnFullscreen": {},
"windows:WindowZOrderChanged": {},
"windows:WindowMinimise": {},
"windows:WindowUnMinimise": {},
"windows:WindowMaximise": {},
"windows:WindowUnMaximise": {},
}

View file

@ -0,0 +1,79 @@
//go:build linux
package libpath
import "sync"
// pathCache holds cached dynamic library paths to avoid repeated
// expensive filesystem and subprocess operations.
type pathCache struct {
mu sync.RWMutex
flatpak []string
snap []string
nix []string
initOnce sync.Once
inited bool
}
var cache pathCache
// init populates the cache with dynamic paths from package managers.
// This is called lazily on first access.
func (c *pathCache) init() {
c.initOnce.Do(func() {
// Discover paths without holding the lock
flatpak := discoverFlatpakLibPaths()
snap := discoverSnapLibPaths()
nix := discoverNixLibPaths()
// Hold lock only while updating the cache
c.mu.Lock()
c.flatpak = flatpak
c.snap = snap
c.nix = nix
c.inited = true
c.mu.Unlock()
})
}
// getFlatpak returns cached Flatpak library paths.
func (c *pathCache) getFlatpak() []string {
c.init()
c.mu.RLock()
defer c.mu.RUnlock()
return c.flatpak
}
// getSnap returns cached Snap library paths.
func (c *pathCache) getSnap() []string {
c.init()
c.mu.RLock()
defer c.mu.RUnlock()
return c.snap
}
// getNix returns cached Nix library paths.
func (c *pathCache) getNix() []string {
c.init()
c.mu.RLock()
defer c.mu.RUnlock()
return c.nix
}
// invalidate clears the cache and forces re-discovery on next access.
func (c *pathCache) invalidate() {
c.mu.Lock()
defer c.mu.Unlock()
c.flatpak = nil
c.snap = nil
c.nix = nil
c.initOnce = sync.Once{} // Reset so init() runs again
c.inited = false
}
// InvalidateCache clears the cached dynamic library paths.
// Call this if packages are installed or removed during runtime
// and you need to re-discover library paths.
func InvalidateCache() {
cache.invalidate()
}

View file

@ -0,0 +1,53 @@
//go:build linux
package libpath
import (
"os"
"os/exec"
"path/filepath"
"strings"
)
// getFlatpakLibPaths returns cached library paths from installed Flatpak runtimes.
func getFlatpakLibPaths() []string {
return cache.getFlatpak()
}
// discoverFlatpakLibPaths scans for Flatpak runtime library directories.
// Uses `flatpak --installations` and scans for runtime lib directories.
func discoverFlatpakLibPaths() []string {
var paths []string
// Get system and user installation directories
installDirs := []string{
"/var/lib/flatpak", // System default
os.ExpandEnv("$HOME/.local/share/flatpak"), // User default
}
// Try to get actual installation path from flatpak
if out, err := exec.Command("flatpak", "--installations").Output(); err == nil {
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if line != "" {
installDirs = append(installDirs, line)
}
}
}
// Scan for runtime lib directories
for _, installDir := range installDirs {
runtimeDir := filepath.Join(installDir, "runtime")
if _, err := os.Stat(runtimeDir); err != nil {
continue
}
// Look for lib directories in runtimes
// Structure: runtime/<name>/<arch>/<version>/<hash>/files/lib
matches, err := filepath.Glob(filepath.Join(runtimeDir, "*", "*", "*", "*", "files", "lib"))
if err == nil {
paths = append(paths, matches...)
}
}
return paths
}

View file

@ -0,0 +1,104 @@
// Package libpath provides utilities for finding native library paths on Linux.
//
// # Overview
//
// This package helps locate shared libraries (.so files) on Linux systems,
// supporting multiple distributions and package managers. It's particularly
// useful for applications that need to link against libraries like GTK,
// WebKit2GTK, or other system libraries at runtime.
//
// # Search Strategy
//
// The package uses a multi-tier search strategy, trying each method in order
// until a library is found:
//
// 1. pkg-config: Queries the pkg-config database for library paths
// 2. ldconfig: Searches the dynamic linker cache
// 3. Filesystem: Scans common library directories
//
// # Supported Distributions
//
// The package includes default search paths for:
//
// - Debian/Ubuntu (multiarch paths like /usr/lib/x86_64-linux-gnu)
// - Fedora/RHEL/CentOS (/usr/lib64, /usr/lib64/gtk-*)
// - Arch Linux (/usr/lib/webkit2gtk-*, /usr/lib/gtk-*)
// - openSUSE (/usr/lib64/gcc/x86_64-suse-linux)
// - NixOS and Nix package manager
//
// # Package Manager Support
//
// Dynamic paths are discovered from:
//
// - Flatpak: Scans runtime directories via `flatpak --installations`
// - Snap: Globs /snap/*/current/usr/lib* directories
// - Nix: Checks ~/.nix-profile/lib and /run/current-system/sw/lib
//
// # Caching
//
// Dynamic path discovery (Flatpak, Snap, Nix) is cached for performance.
// The cache is populated on first access and persists for the process lifetime.
// Use [InvalidateCache] to force re-discovery if packages are installed/removed
// during runtime.
//
// # Security
//
// The current directory (".") is never included in search paths by default,
// as this is a security risk. Use [FindLibraryPathWithOptions] with
// IncludeCurrentDir if you explicitly need this behavior (not recommended
// for production).
//
// # Performance
//
// Typical lookup times (cached):
//
// - Found via pkg-config: ~2ms (spawns external process)
// - Found via ldconfig: ~1.3ms (spawns external process)
// - Found via filesystem: ~0.1ms (uses cached paths)
// - Not found (worst case): ~20ms (searches all paths)
//
// # Example Usage
//
// // Find a library by its pkg-config name
// path, err := libpath.FindLibraryPath("webkit2gtk-4.1")
// if err != nil {
// log.Fatal("WebKit2GTK not found:", err)
// }
// fmt.Println("Found at:", path)
//
// // Find a specific .so file
// soPath, err := libpath.FindLibraryFile("libgtk-3.so")
// if err != nil {
// log.Fatal("GTK3 library file not found:", err)
// }
// fmt.Println("Library file:", soPath)
//
// // Get all library search paths
// for _, p := range libpath.GetAllLibPaths() {
// fmt.Println(p)
// }
//
// # Multi-Library Search
//
// When you don't know which version of a library is installed, use the
// multi-library search functions:
//
// // Find any available WebKit2GTK version (first found wins)
// match, err := libpath.FindFirstLibrary("webkit2gtk-4.1", "webkit2gtk-4.0", "webkit2gtk-6.0")
// if err != nil {
// log.Fatal("No WebKit2GTK found")
// }
// fmt.Printf("Found %s at %s\n", match.Name, match.Path)
//
// // Prefer newer versions (ordered search)
// match, err := libpath.FindFirstLibraryOrdered("gtk4", "gtk+-3.0")
//
// // Discover all available versions
// matches := libpath.FindAllLibraries("gtk+-3.0", "gtk4", "webkit2gtk-4.0", "webkit2gtk-4.1")
// for _, m := range matches {
// fmt.Printf("Available: %s at %s\n", m.Name, m.Path)
// }
//
// On non-Linux platforms, stub implementations are provided that always
// return [LibraryNotFoundError].
package libpath

View file

@ -0,0 +1,551 @@
//go:build linux
package libpath
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
)
// Common library search paths on Linux systems
var defaultLibPaths = []string{
// Standard paths
"/usr/lib",
"/usr/lib64",
"/lib",
"/lib64",
// Debian/Ubuntu multiarch
"/usr/lib/x86_64-linux-gnu",
"/usr/lib/aarch64-linux-gnu",
"/usr/lib/i386-linux-gnu",
"/usr/lib/arm-linux-gnueabihf",
"/lib/x86_64-linux-gnu",
"/lib/aarch64-linux-gnu",
// Fedora/RHEL/CentOS
"/usr/lib64/gtk-3.0",
"/usr/lib64/gtk-4.0",
"/usr/lib/gcc/x86_64-redhat-linux",
"/usr/lib/gcc/aarch64-redhat-linux",
// Arch Linux
"/usr/lib/webkit2gtk-4.0",
"/usr/lib/webkit2gtk-4.1",
"/usr/lib/gtk-3.0",
"/usr/lib/gtk-4.0",
// openSUSE
"/usr/lib64/gcc/x86_64-suse-linux",
// Local installations
"/usr/local/lib",
"/usr/local/lib64",
}
// searchResult holds the result from a parallel search goroutine.
type searchResult struct {
path string
source string // for debugging: "pkg-config", "ldconfig", "filesystem"
}
// FindLibraryPath attempts to find the path to a library using multiple methods
// in parallel. It searches via pkg-config, ldconfig, and filesystem simultaneously,
// returning as soon as any method finds the library.
//
// The libName should be the pkg-config name (e.g., "gtk+-3.0", "webkit2gtk-4.1").
// Returns the library directory path and any error encountered.
func FindLibraryPath(libName string) (string, error) {
return findLibraryPathCtx(context.Background(), libName)
}
// FindLibraryPathSequential is the original sequential implementation.
// Use this if you need deterministic search order (pkg-config → ldconfig → filesystem).
func FindLibraryPathSequential(libName string) (string, error) {
// Try pkg-config first (most reliable when available)
if path, err := findWithPkgConfig(libName); err == nil {
return path, nil
}
// Try ldconfig cache
if path, err := findWithLdconfig(libName); err == nil {
return path, nil
}
// Fall back to searching common paths
return findInCommonPaths(libName)
}
// FindLibraryFile finds the full path to a specific library file (e.g., "libgtk-3.so").
func FindLibraryFile(fileName string) (string, error) {
// Try ldconfig first
if path, err := findFileWithLdconfig(fileName); err == nil {
return path, nil
}
// Search all paths including dynamic ones
for _, dir := range GetAllLibPaths() {
// Check exact match
fullPath := filepath.Join(dir, fileName)
if _, err := os.Stat(fullPath); err == nil {
return fullPath, nil
}
// Check with .so suffix variations
matches, err := filepath.Glob(filepath.Join(dir, fileName+"*"))
if err == nil && len(matches) > 0 {
return matches[0], nil
}
}
return "", &LibraryNotFoundError{Name: fileName}
}
// findWithPkgConfig uses pkg-config to find library paths.
func findWithPkgConfig(libName string) (string, error) {
return findWithPkgConfigCtx(context.Background(), libName)
}
// findWithPkgConfigCtx uses pkg-config to find library paths with context support.
func findWithPkgConfigCtx(ctx context.Context, libName string) (string, error) {
// Check if already cancelled
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
cmd := exec.CommandContext(ctx, "pkg-config", "--libs-only-L", libName)
output, err := cmd.Output()
if err != nil {
return "", err
}
// Parse -L flags from output
parts := strings.Fields(string(output))
for _, part := range parts {
if strings.HasPrefix(part, "-L") {
path := strings.TrimPrefix(part, "-L")
if _, err := os.Stat(path); err == nil {
return path, nil
}
}
}
// Check context before second command
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
// If no -L flag, try --variable=libdir
cmd = exec.CommandContext(ctx, "pkg-config", "--variable=libdir", libName)
output, err = cmd.Output()
if err != nil {
return "", err
}
path := strings.TrimSpace(string(output))
if path != "" {
if _, err := os.Stat(path); err == nil {
return path, nil
}
}
return "", &LibraryNotFoundError{Name: libName}
}
// findWithLdconfig searches the ldconfig cache for library paths.
func findWithLdconfig(libName string) (string, error) {
return findWithLdconfigCtx(context.Background(), libName)
}
// findWithLdconfigCtx searches the ldconfig cache for library paths with context support.
func findWithLdconfigCtx(ctx context.Context, libName string) (string, error) {
// Check if already cancelled
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
// Convert pkg-config name to library name pattern
// e.g., "gtk+-3.0" -> "libgtk-3", "webkit2gtk-4.1" -> "libwebkit2gtk-4.1"
searchName := pkgConfigToLibName(libName)
cmd := exec.CommandContext(ctx, "ldconfig", "-p")
output, err := cmd.Output()
if err != nil {
return "", err
}
for _, line := range strings.Split(string(output), "\n") {
if strings.Contains(line, searchName) {
// Line format: " libname.so.X (libc6,x86-64) => /path/to/lib"
parts := strings.Split(line, "=>")
if len(parts) == 2 {
libPath := strings.TrimSpace(parts[1])
return filepath.Dir(libPath), nil
}
}
}
return "", &LibraryNotFoundError{Name: libName}
}
// findFileWithLdconfig finds a specific library file using ldconfig.
func findFileWithLdconfig(fileName string) (string, error) {
cmd := exec.Command("ldconfig", "-p")
output, err := cmd.Output()
if err != nil {
return "", err
}
baseName := strings.TrimSuffix(fileName, ".so")
for _, line := range strings.Split(string(output), "\n") {
if strings.Contains(line, baseName) {
parts := strings.Split(line, "=>")
if len(parts) == 2 {
return strings.TrimSpace(parts[1]), nil
}
}
}
return "", &LibraryNotFoundError{Name: fileName}
}
// findInCommonPaths searches common library directories including
// dynamically discovered Flatpak, Snap, and Nix paths.
func findInCommonPaths(libName string) (string, error) {
return findInCommonPathsCtx(context.Background(), libName)
}
// findInCommonPathsCtx searches common library directories with context support.
func findInCommonPathsCtx(ctx context.Context, libName string) (string, error) {
searchName := pkgConfigToLibName(libName)
// Search all paths including dynamic ones
allPaths := GetAllLibPaths()
for _, dir := range allPaths {
// Check if cancelled periodically
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
if _, err := os.Stat(dir); err != nil {
continue
}
// Look for the library file
pattern := filepath.Join(dir, searchName+"*.so*")
matches, err := filepath.Glob(pattern)
if err == nil && len(matches) > 0 {
return dir, nil
}
// Also check pkgconfig subdirectory for .pc files
pcPath := filepath.Join(dir, "pkgconfig", libName+".pc")
if _, err := os.Stat(pcPath); err == nil {
return dir, nil
}
}
return "", &LibraryNotFoundError{Name: libName}
}
// pkgConfigToLibName converts a pkg-config package name to a library name pattern.
func pkgConfigToLibName(pkgName string) string {
// Common transformations
name := pkgName
// Remove version suffix like "-3.0", "-4.1"
// but keep it for webkit2gtk-4.1 style names
if strings.HasPrefix(name, "gtk+-") {
// gtk+-3.0 -> libgtk-3
name = "libgtk-" + strings.TrimPrefix(name, "gtk+-")
name = strings.Split(name, ".")[0]
} else if strings.HasPrefix(name, "webkit2gtk-") {
// webkit2gtk-4.1 -> libwebkit2gtk-4.1
name = "lib" + name
} else if !strings.HasPrefix(name, "lib") {
name = "lib" + name
}
return name
}
// GetAllLibPaths returns all library paths from LD_LIBRARY_PATH, default paths,
// and dynamically discovered paths from Flatpak, Snap, and Nix.
// It does NOT include the current directory for security reasons.
func GetAllLibPaths() []string {
var paths []string
// Add LD_LIBRARY_PATH entries first (highest priority)
if ldPath := os.Getenv("LD_LIBRARY_PATH"); ldPath != "" {
for _, p := range strings.Split(ldPath, ":") {
if p != "" {
paths = append(paths, p)
}
}
}
// Add default system paths
paths = append(paths, defaultLibPaths...)
// Add dynamically discovered paths from package managers
paths = append(paths, getFlatpakLibPaths()...)
paths = append(paths, getSnapLibPaths()...)
paths = append(paths, getNixLibPaths()...)
return paths
}
// FindOptions controls library search behavior.
type FindOptions struct {
// IncludeCurrentDir includes "." in the search path.
// WARNING: This is a security risk and should only be used for development.
IncludeCurrentDir bool
// ExtraPaths are additional paths to search before the defaults.
ExtraPaths []string
}
// FindLibraryPathWithOptions attempts to find the path to a library with custom options.
func FindLibraryPathWithOptions(libName string, opts FindOptions) (string, error) {
// Try pkg-config first (most reliable when available)
if path, err := findWithPkgConfig(libName); err == nil {
return path, nil
}
// Try ldconfig cache
if path, err := findWithLdconfig(libName); err == nil {
return path, nil
}
// Build search paths - include all dynamic paths too
allPaths := GetAllLibPaths()
searchPaths := make([]string, 0, len(opts.ExtraPaths)+len(allPaths)+1)
if opts.IncludeCurrentDir {
if cwd, err := os.Getwd(); err == nil {
searchPaths = append(searchPaths, cwd)
}
}
searchPaths = append(searchPaths, opts.ExtraPaths...)
searchPaths = append(searchPaths, allPaths...)
// Search the paths
searchName := pkgConfigToLibName(libName)
for _, dir := range searchPaths {
if _, err := os.Stat(dir); err != nil {
continue
}
pattern := filepath.Join(dir, searchName+"*.so*")
matches, err := filepath.Glob(pattern)
if err == nil && len(matches) > 0 {
return dir, nil
}
pcPath := filepath.Join(dir, "pkgconfig", libName+".pc")
if _, err := os.Stat(pcPath); err == nil {
return dir, nil
}
}
return "", &LibraryNotFoundError{Name: libName}
}
// LibraryNotFoundError is returned when a library cannot be found.
type LibraryNotFoundError struct {
Name string
}
func (e *LibraryNotFoundError) Error() string {
return "library not found: " + e.Name
}
// LibraryMatch holds information about a found library.
type LibraryMatch struct {
// Name is the pkg-config name that was searched for.
Name string
// Path is the directory containing the library.
Path string
}
// FindFirstLibrary searches for multiple libraries in parallel and returns
// the first one found. This is useful when you don't know the exact version
// of a library installed (e.g., gtk+-3.0 vs gtk+-4.0).
//
// The search order among candidates is non-deterministic - whichever is found
// first wins. If you need a specific preference order, list preferred libraries
// first and use FindFirstLibraryOrdered instead.
//
// Example:
//
// match, err := FindFirstLibrary("webkit2gtk-4.1", "webkit2gtk-4.0", "webkit2gtk-6.0")
// if err != nil {
// log.Fatal("No WebKit2GTK found")
// }
// fmt.Printf("Found %s at %s\n", match.Name, match.Path)
func FindFirstLibrary(libNames ...string) (*LibraryMatch, error) {
if len(libNames) == 0 {
return nil, &LibraryNotFoundError{Name: "no libraries specified"}
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
results := make(chan *LibraryMatch, len(libNames))
var wg sync.WaitGroup
for _, name := range libNames {
wg.Add(1)
go func(libName string) {
defer wg.Done()
if path, err := findLibraryPathCtx(ctx, libName); err == nil {
select {
case results <- &LibraryMatch{Name: libName, Path: path}:
case <-ctx.Done():
}
}
}(name)
}
// Close results when all goroutines complete
go func() {
wg.Wait()
close(results)
}()
if result := <-results; result != nil {
return result, nil
}
return nil, &LibraryNotFoundError{Name: strings.Join(libNames, ", ")}
}
// FindFirstLibraryOrdered searches for libraries in order of preference,
// returning the first one found. Unlike FindFirstLibrary, this respects
// the order of candidates - earlier entries are preferred.
//
// This is useful when you want to prefer newer library versions:
//
// match, err := FindFirstLibraryOrdered("gtk+-4.0", "gtk+-3.0")
// // Will return gtk+-4.0 if available, otherwise gtk+-3.0
func FindFirstLibraryOrdered(libNames ...string) (*LibraryMatch, error) {
if len(libNames) == 0 {
return nil, &LibraryNotFoundError{Name: "no libraries specified"}
}
for _, name := range libNames {
if path, err := FindLibraryPath(name); err == nil {
return &LibraryMatch{Name: name, Path: path}, nil
}
}
return nil, &LibraryNotFoundError{Name: strings.Join(libNames, ", ")}
}
// FindAllLibraries searches for multiple libraries in parallel and returns
// all that are found. This is useful for discovering which library versions
// are available on the system.
//
// Example:
//
// matches := FindAllLibraries("gtk+-3.0", "gtk+-4.0", "webkit2gtk-4.0", "webkit2gtk-4.1")
// for _, m := range matches {
// fmt.Printf("Found %s at %s\n", m.Name, m.Path)
// }
func FindAllLibraries(libNames ...string) []LibraryMatch {
if len(libNames) == 0 {
return nil
}
results := make(chan *LibraryMatch, len(libNames))
var wg sync.WaitGroup
for _, name := range libNames {
wg.Add(1)
go func(libName string) {
defer wg.Done()
if path, err := FindLibraryPath(libName); err == nil {
results <- &LibraryMatch{Name: libName, Path: path}
}
}(name)
}
// Close results when all goroutines complete
go func() {
wg.Wait()
close(results)
}()
var matches []LibraryMatch
for result := range results {
matches = append(matches, *result)
}
return matches
}
// findLibraryPathCtx is FindLibraryPath with context support.
func findLibraryPathCtx(ctx context.Context, libName string) (string, error) {
// Create a child context for this search
ctx, cancel := context.WithCancel(ctx)
defer cancel()
results := make(chan searchResult, 3)
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
if path, err := findWithPkgConfigCtx(ctx, libName); err == nil {
select {
case results <- searchResult{path: path, source: "pkg-config"}:
case <-ctx.Done():
}
}
}()
go func() {
defer wg.Done()
if path, err := findWithLdconfigCtx(ctx, libName); err == nil {
select {
case results <- searchResult{path: path, source: "ldconfig"}:
case <-ctx.Done():
}
}
}()
go func() {
defer wg.Done()
if path, err := findInCommonPathsCtx(ctx, libName); err == nil {
select {
case results <- searchResult{path: path, source: "filesystem"}:
case <-ctx.Done():
}
}
}()
go func() {
wg.Wait()
close(results)
}()
if result, ok := <-results; ok {
return result.path, nil
}
return "", &LibraryNotFoundError{Name: libName}
}

View file

@ -0,0 +1,769 @@
//go:build linux
package libpath
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestPkgConfigToLibName(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"gtk+-3.0", "libgtk-3"},
{"gtk+-4.0", "libgtk-4"},
{"webkit2gtk-4.1", "libwebkit2gtk-4.1"},
{"webkit2gtk-4.0", "libwebkit2gtk-4.0"},
{"glib-2.0", "libglib-2.0"},
{"libsoup-3.0", "libsoup-3.0"},
{"cairo", "libcairo"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := pkgConfigToLibName(tt.input)
if result != tt.expected {
t.Errorf("pkgConfigToLibName(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestGetAllLibPaths(t *testing.T) {
paths := GetAllLibPaths()
if len(paths) == 0 {
t.Error("GetAllLibPaths() returned empty slice")
}
// Check that default paths are included
hasUsrLib := false
for _, p := range paths {
if p == "/usr/lib" || p == "/usr/lib64" {
hasUsrLib = true
break
}
}
if !hasUsrLib {
t.Error("GetAllLibPaths() should include /usr/lib or /usr/lib64")
}
}
func TestGetAllLibPaths_WithLDPath(t *testing.T) {
// Save and restore LD_LIBRARY_PATH
original := os.Getenv("LD_LIBRARY_PATH")
defer os.Setenv("LD_LIBRARY_PATH", original)
testPath := "/test/custom/lib:/another/path"
os.Setenv("LD_LIBRARY_PATH", testPath)
paths := GetAllLibPaths()
// First paths should be from LD_LIBRARY_PATH
if len(paths) < 2 {
t.Fatal("Expected at least 2 paths")
}
if paths[0] != "/test/custom/lib" {
t.Errorf("First path should be /test/custom/lib, got %s", paths[0])
}
if paths[1] != "/another/path" {
t.Errorf("Second path should be /another/path, got %s", paths[1])
}
}
func TestLibraryNotFoundError(t *testing.T) {
err := &LibraryNotFoundError{Name: "testlib"}
expected := "library not found: testlib"
if err.Error() != expected {
t.Errorf("Error() = %q, want %q", err.Error(), expected)
}
}
func TestFindLibraryPath_NotFound(t *testing.T) {
_, err := FindLibraryPath("nonexistent-library-xyz-123")
if err == nil {
t.Error("Expected error for nonexistent library")
}
var notFoundErr *LibraryNotFoundError
if _, ok := err.(*LibraryNotFoundError); !ok {
t.Errorf("Expected LibraryNotFoundError, got %T", err)
} else {
notFoundErr = err.(*LibraryNotFoundError)
if notFoundErr.Name != "nonexistent-library-xyz-123" {
t.Errorf("Error name = %q, want %q", notFoundErr.Name, "nonexistent-library-xyz-123")
}
}
}
func TestFindLibraryFile_NotFound(t *testing.T) {
_, err := FindLibraryFile("libnonexistent-xyz-123.so")
if err == nil {
t.Error("Expected error for nonexistent library file")
}
}
// Integration tests - these depend on system state
// They're skipped if the required tools/libraries aren't available
func TestFindLibraryPath_WithPkgConfig(t *testing.T) {
// Skip if pkg-config is not available
if _, err := exec.LookPath("pkg-config"); err != nil {
t.Skip("pkg-config not available")
}
// Try to find a common library that's likely installed
commonLibs := []string{"glib-2.0", "zlib"}
for _, lib := range commonLibs {
// Check if pkg-config knows about this library
cmd := exec.Command("pkg-config", "--exists", lib)
if cmd.Run() != nil {
continue
}
t.Run(lib, func(t *testing.T) {
path, err := FindLibraryPath(lib)
if err != nil {
t.Errorf("FindLibraryPath(%q) failed: %v", lib, err)
return
}
// Verify the path exists
if _, err := os.Stat(path); err != nil {
t.Errorf("Returned path %q does not exist", path)
}
})
return // Only need to test one
}
t.Skip("No common libraries found via pkg-config")
}
func TestFindLibraryFile_Integration(t *testing.T) {
// Try to find libc which should exist on any Linux system
libcNames := []string{"libc.so.6", "libc.so"}
for _, name := range libcNames {
path, err := FindLibraryFile(name)
if err == nil {
// Verify the path exists
if _, err := os.Stat(path); err != nil {
t.Errorf("Returned path %q does not exist", path)
}
return
}
}
t.Skip("Could not find libc.so - unusual system configuration")
}
func TestFindInCommonPaths(t *testing.T) {
// Create a temporary directory structure for testing
tmpDir := t.TempDir()
// Create a fake library directory with a fake .so file
libDir := filepath.Join(tmpDir, "lib")
if err := os.MkdirAll(libDir, 0755); err != nil {
t.Fatal(err)
}
// Create a fake library file
fakeLib := filepath.Join(libDir, "libfaketest.so.1")
if err := os.WriteFile(fakeLib, []byte{}, 0644); err != nil {
t.Fatal(err)
}
// Temporarily add our test dir to defaultLibPaths
originalPaths := defaultLibPaths
defaultLibPaths = append([]string{libDir}, defaultLibPaths...)
defer func() { defaultLibPaths = originalPaths }()
// Now test finding it
path, err := findInCommonPaths("faketest")
if err != nil {
t.Errorf("findInCommonPaths(\"faketest\") failed: %v", err)
return
}
if path != libDir {
t.Errorf("findInCommonPaths(\"faketest\") = %q, want %q", path, libDir)
}
}
func TestFindWithLdconfig(t *testing.T) {
// Skip if ldconfig is not available
if _, err := exec.LookPath("ldconfig"); err != nil {
t.Skip("ldconfig not available")
}
// Check if we can run ldconfig -p
cmd := exec.Command("ldconfig", "-p")
output, err := cmd.Output()
if err != nil {
t.Skip("ldconfig -p failed")
}
// Find any library from the output to test with
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, "=>") && strings.Contains(line, "libc.so") {
// We found libc, try to find it
path, err := findWithLdconfig("glib-2.0") // Common library
if err == nil {
if _, statErr := os.Stat(path); statErr != nil {
t.Errorf("Returned path %q does not exist", path)
}
return
}
// If glib not found, that's okay - just means it's not installed
break
}
}
}
func TestFindLibraryPathWithOptions_IncludeCurrentDir(t *testing.T) {
// Create a temporary directory and change to it
tmpDir := t.TempDir()
// Create a fake library file in the temp dir
fakeLib := filepath.Join(tmpDir, "libcwdtest.so.1")
if err := os.WriteFile(fakeLib, []byte{}, 0644); err != nil {
t.Fatal(err)
}
// Save current directory
origDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer os.Chdir(origDir)
// Change to temp directory
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
// Without IncludeCurrentDir, should not find it
_, err = FindLibraryPathWithOptions("cwdtest", FindOptions{IncludeCurrentDir: false})
if err == nil {
t.Error("Expected error without IncludeCurrentDir")
}
// With IncludeCurrentDir, should find it
path, err := FindLibraryPathWithOptions("cwdtest", FindOptions{IncludeCurrentDir: true})
if err != nil {
t.Errorf("FindLibraryPathWithOptions with IncludeCurrentDir failed: %v", err)
return
}
if path != tmpDir {
t.Errorf("Expected path %q, got %q", tmpDir, path)
}
}
func TestFindLibraryPathWithOptions_ExtraPaths(t *testing.T) {
// Create a temporary directory with a fake library
tmpDir := t.TempDir()
fakeLib := filepath.Join(tmpDir, "libextratest.so.1")
if err := os.WriteFile(fakeLib, []byte{}, 0644); err != nil {
t.Fatal(err)
}
// Should find it with ExtraPaths
path, err := FindLibraryPathWithOptions("extratest", FindOptions{
ExtraPaths: []string{tmpDir},
})
if err != nil {
t.Errorf("FindLibraryPathWithOptions with ExtraPaths failed: %v", err)
return
}
if path != tmpDir {
t.Errorf("Expected path %q, got %q", tmpDir, path)
}
}
func TestDefaultLibPaths_ContainsDistros(t *testing.T) {
// Verify that paths for various distros are included
expectedPaths := map[string][]string{
"Debian/Ubuntu": {"/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu"},
"Fedora/RHEL": {"/usr/lib64/gtk-3.0", "/usr/lib64/gtk-4.0"},
"Arch": {"/usr/lib/webkit2gtk-4.0", "/usr/lib/webkit2gtk-4.1"},
"openSUSE": {"/usr/lib64/gcc/x86_64-suse-linux"},
"Local": {"/usr/local/lib", "/usr/local/lib64"},
}
for distro, paths := range expectedPaths {
for _, path := range paths {
found := false
for _, defaultPath := range defaultLibPaths {
if defaultPath == path {
found = true
break
}
}
if !found {
t.Errorf("Missing %s path: %s", distro, path)
}
}
}
}
func TestGetFlatpakLibPaths(t *testing.T) {
// This test just ensures the function doesn't panic
// Actual paths depend on system state
paths := getFlatpakLibPaths()
t.Logf("Found %d Flatpak lib paths", len(paths))
for _, p := range paths {
t.Logf(" %s", p)
}
}
func TestGetSnapLibPaths(t *testing.T) {
// This test just ensures the function doesn't panic
// Actual paths depend on system state
paths := getSnapLibPaths()
t.Logf("Found %d Snap lib paths", len(paths))
for _, p := range paths {
t.Logf(" %s", p)
}
}
func TestGetNixLibPaths(t *testing.T) {
// This test just ensures the function doesn't panic
paths := getNixLibPaths()
t.Logf("Found %d Nix lib paths", len(paths))
for _, p := range paths {
t.Logf(" %s", p)
}
}
func TestGetAllLibPaths_IncludesDynamicPaths(t *testing.T) {
paths := GetAllLibPaths()
// Should have at least the default paths
if len(paths) < len(defaultLibPaths) {
t.Errorf("GetAllLibPaths returned fewer paths (%d) than defaultLibPaths (%d)",
len(paths), len(defaultLibPaths))
}
// Log all paths for debugging
t.Logf("Total paths: %d", len(paths))
}
func TestGetAllLibPaths_DoesNotIncludeCurrentDir(t *testing.T) {
paths := GetAllLibPaths()
for _, p := range paths {
if p == "." {
t.Error("GetAllLibPaths should not include '.' for security reasons")
}
}
}
func TestInvalidateCache(t *testing.T) {
// First call populates cache
paths1 := GetAllLibPaths()
// Invalidate and call again
InvalidateCache()
paths2 := GetAllLibPaths()
// Should get same results (assuming no system changes)
if len(paths1) != len(paths2) {
t.Logf("Path counts differ after invalidation: %d vs %d", len(paths1), len(paths2))
// This is not necessarily an error, just informational
}
// Verify cache is working by checking getFlatpakLibPaths is fast
// (would be slow if cache wasn't working)
for i := 0; i < 100; i++ {
_ = getFlatpakLibPaths()
}
}
func TestFindLibraryPath_ParallelConsistency(t *testing.T) {
// Skip if pkg-config is not available
if _, err := exec.LookPath("pkg-config"); err != nil {
t.Skip("pkg-config not available")
}
// Check if glib-2.0 is available
cmd := exec.Command("pkg-config", "--exists", "glib-2.0")
if cmd.Run() != nil {
t.Skip("glib-2.0 not installed")
}
// Run parallel and sequential versions multiple times
// to ensure they return consistent results
for i := 0; i < 10; i++ {
parallelPath, parallelErr := FindLibraryPath("glib-2.0")
seqPath, seqErr := FindLibraryPathSequential("glib-2.0")
if parallelErr != nil && seqErr == nil {
t.Errorf("Parallel failed but sequential succeeded: %v", parallelErr)
}
if parallelErr == nil && seqErr != nil {
t.Errorf("Sequential failed but parallel succeeded: %v", seqErr)
}
// Both should find the library (path might differ if found by different methods)
if parallelErr != nil {
t.Errorf("Iteration %d: parallel search failed: %v", i, parallelErr)
}
if seqErr != nil {
t.Errorf("Iteration %d: sequential search failed: %v", i, seqErr)
}
// Log paths for debugging
t.Logf("Iteration %d: parallel=%s, sequential=%s", i, parallelPath, seqPath)
}
}
func TestFindLibraryPath_ParallelNotFound(t *testing.T) {
// Both parallel and sequential should return the same error for non-existent libs
_, parallelErr := FindLibraryPath("nonexistent-library-xyz-123")
_, seqErr := FindLibraryPathSequential("nonexistent-library-xyz-123")
if parallelErr == nil {
t.Error("Parallel search should fail for nonexistent library")
}
if seqErr == nil {
t.Error("Sequential search should fail for nonexistent library")
}
// Both should return LibraryNotFoundError
if _, ok := parallelErr.(*LibraryNotFoundError); !ok {
t.Errorf("Parallel: expected LibraryNotFoundError, got %T", parallelErr)
}
if _, ok := seqErr.(*LibraryNotFoundError); !ok {
t.Errorf("Sequential: expected LibraryNotFoundError, got %T", seqErr)
}
}
// Benchmarks
// BenchmarkFindLibraryPath benchmarks finding a library via the full search chain.
func BenchmarkFindLibraryPath(b *testing.B) {
// Test with glib-2.0 which is commonly installed
if _, err := exec.LookPath("pkg-config"); err != nil {
b.Skip("pkg-config not available")
}
cmd := exec.Command("pkg-config", "--exists", "glib-2.0")
if cmd.Run() != nil {
b.Skip("glib-2.0 not installed")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = FindLibraryPath("glib-2.0")
}
}
// BenchmarkFindLibraryPath_NotFound benchmarks the worst case (library not found).
func BenchmarkFindLibraryPath_NotFound(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = FindLibraryPath("nonexistent-library-xyz-123")
}
}
// BenchmarkFindLibraryFile benchmarks finding a specific library file.
func BenchmarkFindLibraryFile(b *testing.B) {
// libc.so.6 should exist on any Linux system
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = FindLibraryFile("libc.so.6")
}
}
// BenchmarkGetAllLibPaths benchmarks collecting all library paths.
func BenchmarkGetAllLibPaths(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = GetAllLibPaths()
}
}
// BenchmarkFindWithPkgConfig benchmarks pkg-config lookup directly.
func BenchmarkFindWithPkgConfig(b *testing.B) {
if _, err := exec.LookPath("pkg-config"); err != nil {
b.Skip("pkg-config not available")
}
cmd := exec.Command("pkg-config", "--exists", "glib-2.0")
if cmd.Run() != nil {
b.Skip("glib-2.0 not installed")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = findWithPkgConfig("glib-2.0")
}
}
// BenchmarkFindWithLdconfig benchmarks ldconfig lookup directly.
func BenchmarkFindWithLdconfig(b *testing.B) {
if _, err := exec.LookPath("ldconfig"); err != nil {
b.Skip("ldconfig not available")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = findWithLdconfig("glib-2.0")
}
}
// BenchmarkFindInCommonPaths benchmarks filesystem scanning.
func BenchmarkFindInCommonPaths(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = findInCommonPaths("glib-2.0")
}
}
// BenchmarkGetFlatpakLibPaths benchmarks Flatpak path discovery.
func BenchmarkGetFlatpakLibPaths(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = getFlatpakLibPaths()
}
}
// BenchmarkGetSnapLibPaths benchmarks Snap path discovery.
func BenchmarkGetSnapLibPaths(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = getSnapLibPaths()
}
}
// BenchmarkGetNixLibPaths benchmarks Nix path discovery.
func BenchmarkGetNixLibPaths(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = getNixLibPaths()
}
}
// BenchmarkPkgConfigToLibName benchmarks the name conversion function.
func BenchmarkPkgConfigToLibName(b *testing.B) {
names := []string{"gtk+-3.0", "webkit2gtk-4.1", "glib-2.0", "cairo", "libsoup-3.0"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, name := range names {
_ = pkgConfigToLibName(name)
}
}
}
// BenchmarkFindLibraryPathSequential benchmarks the sequential search.
func BenchmarkFindLibraryPathSequential(b *testing.B) {
if _, err := exec.LookPath("pkg-config"); err != nil {
b.Skip("pkg-config not available")
}
cmd := exec.Command("pkg-config", "--exists", "glib-2.0")
if cmd.Run() != nil {
b.Skip("glib-2.0 not installed")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = FindLibraryPathSequential("glib-2.0")
}
}
// BenchmarkFindLibraryPathSequential_NotFound benchmarks the sequential worst case.
func BenchmarkFindLibraryPathSequential_NotFound(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = FindLibraryPathSequential("nonexistent-library-xyz-123")
}
}
// BenchmarkFindLibraryPathParallel explicitly tests parallel performance.
func BenchmarkFindLibraryPathParallel(b *testing.B) {
if _, err := exec.LookPath("pkg-config"); err != nil {
b.Skip("pkg-config not available")
}
cmd := exec.Command("pkg-config", "--exists", "glib-2.0")
if cmd.Run() != nil {
b.Skip("glib-2.0 not installed")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = FindLibraryPath("glib-2.0")
}
}
// Tests for multi-library search functions
func TestFindFirstLibrary(t *testing.T) {
if _, err := exec.LookPath("pkg-config"); err != nil {
t.Skip("pkg-config not available")
}
// Test with a mix of existing and non-existing libraries
match, err := FindFirstLibrary("nonexistent-xyz", "glib-2.0", "also-nonexistent")
if err != nil {
t.Skipf("glib-2.0 not installed: %v", err)
}
if match.Name != "glib-2.0" {
t.Errorf("Expected glib-2.0, got %s", match.Name)
}
if match.Path == "" {
t.Error("Expected non-empty path")
}
}
func TestFindFirstLibrary_AllNotFound(t *testing.T) {
_, err := FindFirstLibrary("nonexistent-1", "nonexistent-2", "nonexistent-3")
if err == nil {
t.Error("Expected error for all non-existent libraries")
}
}
func TestFindFirstLibrary_Empty(t *testing.T) {
_, err := FindFirstLibrary()
if err == nil {
t.Error("Expected error for empty library list")
}
}
func TestFindFirstLibraryOrdered(t *testing.T) {
if _, err := exec.LookPath("pkg-config"); err != nil {
t.Skip("pkg-config not available")
}
// glib-2.0 should be found, and since it's first, it should be returned
match, err := FindFirstLibraryOrdered("glib-2.0", "nonexistent-xyz")
if err != nil {
t.Skipf("glib-2.0 not installed: %v", err)
}
if match.Name != "glib-2.0" {
t.Errorf("Expected glib-2.0, got %s", match.Name)
}
}
func TestFindFirstLibraryOrdered_PreferFirst(t *testing.T) {
if _, err := exec.LookPath("pkg-config"); err != nil {
t.Skip("pkg-config not available")
}
// Check what GTK versions are available
gtk4Available := exec.Command("pkg-config", "--exists", "gtk4").Run() == nil
gtk3Available := exec.Command("pkg-config", "--exists", "gtk+-3.0").Run() == nil
if !gtk4Available && !gtk3Available {
t.Skip("Neither GTK3 nor GTK4 installed")
}
// If both available, test that order is respected
if gtk4Available && gtk3Available {
match, err := FindFirstLibraryOrdered("gtk4", "gtk+-3.0")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if match.Name != "gtk4" {
t.Errorf("Expected gtk4 (first in order), got %s", match.Name)
}
// Reverse order
match, err = FindFirstLibraryOrdered("gtk+-3.0", "gtk4")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if match.Name != "gtk+-3.0" {
t.Errorf("Expected gtk+-3.0 (first in order), got %s", match.Name)
}
}
}
func TestFindAllLibraries(t *testing.T) {
if _, err := exec.LookPath("pkg-config"); err != nil {
t.Skip("pkg-config not available")
}
matches := FindAllLibraries("glib-2.0", "nonexistent-xyz", "zlib")
// Should find at least glib-2.0 on most systems
if len(matches) == 0 {
t.Skip("No common libraries found")
}
t.Logf("Found %d libraries:", len(matches))
for _, m := range matches {
t.Logf(" %s at %s", m.Name, m.Path)
}
// Verify no duplicates and no nonexistent library
seen := make(map[string]bool)
for _, m := range matches {
if m.Name == "nonexistent-xyz" {
t.Error("Should not have found nonexistent library")
}
if seen[m.Name] {
t.Errorf("Duplicate match for %s", m.Name)
}
seen[m.Name] = true
}
}
func TestFindAllLibraries_Empty(t *testing.T) {
matches := FindAllLibraries()
if len(matches) != 0 {
t.Error("Expected empty result for empty input")
}
}
func TestFindAllLibraries_AllNotFound(t *testing.T) {
matches := FindAllLibraries("nonexistent-1", "nonexistent-2")
if len(matches) != 0 {
t.Errorf("Expected empty result, got %d matches", len(matches))
}
}
// Benchmarks for multi-library search
func BenchmarkFindFirstLibrary(b *testing.B) {
if _, err := exec.LookPath("pkg-config"); err != nil {
b.Skip("pkg-config not available")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = FindFirstLibrary("nonexistent-1", "glib-2.0", "nonexistent-2")
}
}
func BenchmarkFindFirstLibraryOrdered(b *testing.B) {
if _, err := exec.LookPath("pkg-config"); err != nil {
b.Skip("pkg-config not available")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = FindFirstLibraryOrdered("nonexistent-1", "glib-2.0", "nonexistent-2")
}
}
func BenchmarkFindAllLibraries(b *testing.B) {
if _, err := exec.LookPath("pkg-config"); err != nil {
b.Skip("pkg-config not available")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = FindAllLibraries("glib-2.0", "zlib", "nonexistent-xyz")
}
}

View file

@ -0,0 +1,73 @@
//go:build !linux
package libpath
// FindLibraryPath is a stub for non-Linux platforms.
func FindLibraryPath(libName string) (string, error) {
return "", &LibraryNotFoundError{Name: libName}
}
// FindLibraryFile is a stub for non-Linux platforms.
func FindLibraryFile(fileName string) (string, error) {
return "", &LibraryNotFoundError{Name: fileName}
}
// GetAllLibPaths returns an empty slice on non-Linux platforms.
func GetAllLibPaths() []string {
return nil
}
// InvalidateCache is a no-op on non-Linux platforms.
func InvalidateCache() {}
// FindOptions controls library search behavior.
type FindOptions struct {
IncludeCurrentDir bool
ExtraPaths []string
}
// FindLibraryPathWithOptions is a stub for non-Linux platforms.
func FindLibraryPathWithOptions(libName string, opts FindOptions) (string, error) {
return "", &LibraryNotFoundError{Name: libName}
}
// LibraryNotFoundError is returned when a library cannot be found.
type LibraryNotFoundError struct {
Name string
}
func (e *LibraryNotFoundError) Error() string {
return "library not found: " + e.Name
}
// LibraryMatch holds information about a found library.
type LibraryMatch struct {
Name string
Path string
}
// FindFirstLibrary is a stub for non-Linux platforms.
func FindFirstLibrary(libNames ...string) (*LibraryMatch, error) {
if len(libNames) == 0 {
return nil, &LibraryNotFoundError{Name: "no libraries specified"}
}
return nil, &LibraryNotFoundError{Name: libNames[0]}
}
// FindFirstLibraryOrdered is a stub for non-Linux platforms.
func FindFirstLibraryOrdered(libNames ...string) (*LibraryMatch, error) {
if len(libNames) == 0 {
return nil, &LibraryNotFoundError{Name: "no libraries specified"}
}
return nil, &LibraryNotFoundError{Name: libNames[0]}
}
// FindAllLibraries is a stub for non-Linux platforms.
func FindAllLibraries(libNames ...string) []LibraryMatch {
return nil
}
// FindLibraryPathSequential is a stub for non-Linux platforms.
func FindLibraryPathSequential(libName string) (string, error) {
return "", &LibraryNotFoundError{Name: libName}
}

View file

@ -0,0 +1,28 @@
//go:build linux
package libpath
import "os"
// getNixLibPaths returns cached library paths for Nix/NixOS installations.
func getNixLibPaths() []string {
return cache.getNix()
}
// discoverNixLibPaths scans for Nix library paths.
func discoverNixLibPaths() []string {
var paths []string
nixProfileLib := os.ExpandEnv("$HOME/.nix-profile/lib")
if _, err := os.Stat(nixProfileLib); err == nil {
paths = append(paths, nixProfileLib)
}
// System Nix store - packages expose libs through profiles
nixStoreLib := "/run/current-system/sw/lib"
if _, err := os.Stat(nixStoreLib); err == nil {
paths = append(paths, nixStoreLib)
}
return paths
}

View file

@ -0,0 +1,42 @@
//go:build linux
package libpath
import (
"os"
"path/filepath"
)
// getSnapLibPaths returns cached library paths from installed Snap packages.
func getSnapLibPaths() []string {
return cache.getSnap()
}
// discoverSnapLibPaths scans for Snap package library directories.
// Scans /snap/*/current/usr/lib* directories.
func discoverSnapLibPaths() []string {
var paths []string
snapDir := "/snap"
if _, err := os.Stat(snapDir); err != nil {
return paths
}
// Find all snap packages with lib directories
patterns := []string{
filepath.Join(snapDir, "*", "current", "usr", "lib"),
filepath.Join(snapDir, "*", "current", "usr", "lib64"),
filepath.Join(snapDir, "*", "current", "usr", "lib", "*-linux-gnu"),
filepath.Join(snapDir, "*", "current", "lib"),
filepath.Join(snapDir, "*", "current", "lib", "*-linux-gnu"),
}
for _, pattern := range patterns {
matches, err := filepath.Glob(pattern)
if err == nil {
paths = append(paths, matches...)
}
}
return paths
}

View file

@ -35,14 +35,38 @@ tasks:
cmds:
- npx esbuild@latest desktop/@wailsio/runtime/src/index.ts --inject:desktop/compiled/main.js --format=esm --target=safari11 --bundle --ignore-annotations --tree-shaking=true --minify --outfile=../assetserver/bundledassets/runtime.js --define:DEBUG=false --drop:console
build:all:
build:docs:
internal: true
dir: desktop/@wailsio/runtime
deps:
- install-deps
cmds:
- npm run build:docs
build:docs:md:
internal: true
dir: desktop/@wailsio/runtime
deps:
- install-deps
cmds:
- npm run build:docs:md
build:runtime:
internal: true
deps:
- build:debug
- build:production
cmds:
- cmd: echo "Build Complete."
- cmd: echo "Runtime build complete."
build:all:
internal: true
cmds:
- task: generate:events
- task: build:docs
- task: build:runtime
- echo "Build Complete."
build:
deps:
@ -50,8 +74,39 @@ tasks:
cmds:
- task: build:all
docs:
summary: Generate TypeDoc documentation for the runtime
dir: desktop/@wailsio/runtime
deps:
- install-deps
cmds:
- npm run build:docs
- echo "Documentation generated at desktop/@wailsio/runtime/docs/"
docs:md:
summary: Generate markdown documentation for the runtime
dir: desktop/@wailsio/runtime
deps:
- install-deps
cmds:
- npm run build:docs:md
- echo "Markdown documentation generated"
generate:events:
dir: ../../tasks/events
cmds:
- go run generate.go
- go fmt ../../pkg/events/events.go
clean:
summary: Clean built artifacts and documentation
dir: desktop/@wailsio/runtime
cmds:
- npm run clean
- echo "Cleaned runtime artifacts"
generate:
summary: Generate events only (use runtime:build to rebuild everything)
cmds:
- task: generate:events
- echo "Events generated. Run 'wails3 task runtime:build' to rebuild runtime with updated documentation"

View file

@ -1 +1 @@
window.hierarchyData = "eJylj8EKgzAQRP9lztGi2KC59tpD70Uk1RWDa4QkPYn/XrRUerCl0NMuO8zOmwluHIOHusosEUWalwKOWqY6mNF6qAkyS5Zh9UBQOGlbE7O+MV3cOBhPZ9MTBHpjG6j0KAXujqFgbCDX6pr8Yd8Ud2FgCNSsvYdC8E20fIk25yJ2hhtHdiGUWTkLSJl95dlYkjR/sawRuyAfIZ6HWaBI87e830tX6241V/G/pef5ASffjm8="
window.hierarchyData = "eJylj8EKwjAQRP9lzrFiSkKbq1cP3kVKbFca3CaQxJP03yWK4kFF8LTLDrPz5oIYQk4wO10r0Uq5F4h0ZOqzCz7BXKBrVYa3E8FgbX1PzPbAtI1hcok27kQQODk/wEilBc6RYeB8pni0PaXle1M15okh0LNNCQY5DYvyZfF0FnF0PETyhVA1+1lAq+Yrz5NlJZsHyy3iLchHiPthFmilfMn7vXR3273lrvq39DxfASprjnI="

View file

@ -1 +1 @@
window.navigationData = "eJylmW1v5CYQx7+LXydtk+bSNqoqJdk8Sdluur5qX5xOJ9ZmExoMFuBN9qp+9woDNrbx4N28SrTzn98MmGEw/vJvovC7Si6S3wlTWDBE/0iOkhKpl+QiKXheUSx//OZs3354UQVNjpJXwvLk4vQoyV4IzQVmycWXBrUiLOdvLSajSMouxki6tJPTX/87aiA3QnCxKBXhTLaomrBBWZfma3sZfjo/MvGTi0TJ/JjIY/xu/BIv2oPCAq0pjkVyusOiPHFJdJKxKE43iOKzBC+IxLcV3RBKcb7EsqIqSg56HTgaw3okr9Fp86QfirXE/+BM7TnYrtOH4q+IelliyekWi+iqDPkcFj0l36NTrDXQerl8Q0ThvMWoXdklWEUP8tNvv5x8Op2U5jViGaZU14cdu/0FCyAs4BVMBQx4846zSvH94jmn/cOZpbVnOOd0SDizjPYMZ5xi4Z6QUARRgG0Vhy8Qm1GKlQrsWMN4AfnhwZcY5ZzRHRDQST4SJOMCKjIjODxAupMKFzdbzNSfqMBApJ4y/Pi/+jtEWVKSoW6DckcAz9glnYXPAPck95LbVCyru3OHozVd2PmZl89fFVExhNYAiPTFP4eEEVozQPjzciX4m/SLzs2JNUyaj0WJ2d/Lx1AyDmMlYC7XiNJhIvrXSVksK6ZIgevz0vB8VmN8CXQ+0+LB8cwswprj2WM7z9XuYRaamJqjjcATvtp1y2DgPVz7Hf/uhPa8h/PafxyUlGuO/IpvnokzTXowKVaf9X+hTBqQFQGjiTOCAH9IM4Iof5bDAVnDpOFcVUqNHHMdxkigE8stofiWUOWXXgDUyiDYHEuJnrFxgl4rHDbkAAXQ1atTmR4h6AGFSNEW7xci6AGF6O0M7QpyxMC+0FmBD2zDIX9tB9zdnEAIpwFbB5bdPjbEOA3UPuz0QRinATArJBhhzxDFSsDKrFt5oDDN75PqcoUIlbV+uPtbTCsBd/9KKl70M/JWoKX5OmjdtVH1nrtG2Wu/qQzSc8JYd2k9ZkihKFaLpiNDZ7ABcvz45e3c2rUlbZEg+gzf0Gp7F/Kz739ThA9L1l2bobLbbADnxWYD+16Ge2jrfhnoox4hWKfOGyrQBcuCxdn4ZlBdLti8ooqU4QJvGE4EFuctRaGmWf88qTTvsNLiUCYGYgVgFg+LdJjDwyKdlMEMb4k/nb6/sU3CdDuAtyV4nGEX6G4HY12khzg+AWfjHpWKZIGnojHWOG1IRYkylaodHRS7j/JksXI30rEhdoGBMfqjTDOBMQuM0hqmvRjgLHyd5iBaAJ5Nah2IMH9ByNg9V4OIXHLdYTWyFzmCUQCbwh1W15UQnRYZxFgVjHoSpEBiF0FZFbiYzet84CnXv096yDdsSwRnBWZqtEgtricFD75pDGYUEOMalWhNKFHEb4LeXBmQLwPm3UsegHkqgHWPWE7xE0Vqw0VRn6UFLwFs2AGIQNiWvwY7kCUaAXTmlpfz2fkZgLAKmLGcw4TlPOYfy0ErQMYMidc5D98cNRgngkl4XQWbaYvRCpDxSFj1DjJqBciYowwkzFEG+psPZVBRNBpw+1jNH4d7x2r+OHHj6H4ba3PQBGMFRrHElKN8zN1YI7de+lJ79NaqMcL3Vf2L8TGUr5lA1DfT+kKfcAZmOBDul+3oZ7awFN5t+x7x71oRHyicvVH8LBCTJRfhc0ZfBAEzSnRf8lZU+7rkbMCrEl/rp6BfyoIvXJ4ZgPS/cbf+oU/bHddnrAJz0RaFbweqSkYwEsJ8/R+gZOex"
window.navigationData = "eJylmVFv2zYQx7+LnpNtydJsC4oBSeykAeI5s1r4oSgKWqZjLhQpkJQTd9h3HyiSEiVRR9l5SuD73++OFI9HUV//TRR+U8lV8pEwhQVD9M/kJCmQ2iZXSc7XJcXy5+/O9v2nrcppcpK8ELZOrs5PkmxL6Fpgllx9rVFLwtb8tcFkFEnZxhhJm3Z2/vt/JzVkKgQX80IRzmSDqggblLVpvraT4YfLExM/uUqUXJ8SeYrfjF/iRXtQWKAVxbFITndclCcuiU4yFsXpelF8luA5kfiupBtCKV4vsCypipKDXkeOxrAeyUt02jzpu2It8D84UwcOtu30rvhLorYLLDndYRFdlSGf46Kn5Ed0irUGWi/Xr4govG4wal+0CVbRgfzyx29nH85HpXmLWIYp1fVhx25/wQIIC3gFUwEDTt9wVip+WDzndHg4s7QODOecjglnltGB4YxTLNwTEoogCrCt4vgFYjNKsVKBHasfLyA/PvgCozVndA8EdJL3BMm4gIrMCI4PkO6lwvl0h5n6C+UYiNRRhh//N3+HKApKMtRuUO4I4BnbpIvwGeATWXvJbUqWVd25xdGaNuzywsvn75KoGEJrAES69c8hYYTW9BD+vNwI/ir9onNzYg2j5mNeYPZl8RhKxmGsBMzlFlHaT0T/OiqLRckUyXF1XuqfzyqML4HOZ1rcO56ZRVhxPHts57nZP0xCE1NxtBF4wjf7dhn0vPtrv+XfntCOd39eu4+DkmLFkV/x9TNxplEPJsXqs/4vlEkNsiJgNHFGEOAPaUIQ5c+yPyBrGDWcm1KpgWOuwxgJdGK5IxTfEar80guAGhkEm2Ep0TM2TtBrhcOGHKAAunp1KuMjBD2gECna4cNCBD2gEJ2doVlBjhjYF1or8IFtOOSv7YC7mxMI4TRg68Cy3cf6GKeB2oedPgjjNABmiQQj7BmiWAlYmVUrDxSm+X1UXS4RobLS93d/i2kk4O5fSsXzbkbeCrQ0Xwetuyaq3nNXKHvpNpVeek4Y6y6NxwQpFMVq0Xhk6AzWQw4fv7ydW7s2pB0SRJ/ha1plb0N+9f2nefiwZN21GSq7zQZwnm82sO91uIc27teBPuoRgnXqvKECnbMsWJy1bwbV5ZzNSqpIES7wmuFEYHHeURRqmtXPo0rzHistDmViIFYAZvEwT/s5PMzTURlM8I740+n7G9soTLsDeFuCx+l3gfZ2MNRFOojTM3A2PqFCkSzwVDTGGscNKS9QplK1p71i91GeLFbuRjo0xDYwMEZ/lGkmMGaBUVrDuBcDnIWv0xxEC8CzSaUDEeYvCBm656oRkUuue6wG9iJHMApgU7jH6rYUotUigxirglFPguRI7CMoqwIXs3mdDzzl6vdRD3nKdkRwlmOmBovU4jpS8OCbxmBGATFuUYFWhBJF/CbozZUB+TJg3r3kAZinAliE7fhLsD9YjBFAJ2J5PZtcXgAIq4AZixlMWMxi/rEctAJkTJB4mfHwvU6NcSKYhFdlsNU1GK0AGY+ElW8go1KAjBnKQMIMZaC/+YwFLdlaAxb3cvbYr+zl7HFkWbe/XDU5aIKxAqNYYMrResjdWCN3UvrKefBOqTbCt0nda+shlK8ZQdT3xvq6nXAGZtgTHpbt4EewsBTeC7se8a9OER8onL3v+ywQkwUX4VNAVwQBM0p01/BWVPMy42zAiwxf6aegX5mCr0OeGYB0v0A3/qEPzy3XZ6wCc9EUhW8HqkpGMBLCfPsftSXExw=="

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,7 @@
in case a <a href="CancellablePromise.html" class="tsd-kind-class">CancellablePromise</a> is cancelled successfully.</p>
<p>The value of the <a href="CancelError.html#name" class="tsd-kind-property">name</a> property is the string <code>&quot;CancelError&quot;</code>.
The value of the <a href="Call.RuntimeError.html#cause" class="tsd-kind-property">cause</a> property is the cause passed to the cancel method, if any.</p>
</div><div class="tsd-comment tsd-typography"></div></section><section class="tsd-panel tsd-hierarchy" data-refl="621"><h4>Hierarchy</h4><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error" class="tsd-signature-type external" target="_blank">Error</a><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><span class="tsd-hierarchy-target">CancelError</span></li></ul></li></ul></section><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts#L20">src/cancellable.ts:20</a></li></ul></aside><section class="tsd-panel-group tsd-index-group"><section class="tsd-panel tsd-index-panel"><details class="tsd-index-content tsd-accordion" open><summary class="tsd-accordion-summary tsd-index-summary"><h5 class="tsd-index-heading uppercase" role="button" aria-expanded="false" tabIndex="0"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronSmall"></use></svg> Index</h5></summary><div class="tsd-accordion-details"><section class="tsd-index-section"><h3 class="tsd-index-heading">Constructors</h3><div class="tsd-index-list"><a href="CancelError.html#constructor" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Constructor"><use href="../assets/icons.svg#icon-512"></use></svg><span>constructor</span></a>
</div><div class="tsd-comment tsd-typography"></div></section><section class="tsd-panel tsd-hierarchy" data-refl="615"><h4>Hierarchy</h4><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error" class="tsd-signature-type external" target="_blank">Error</a><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><span class="tsd-hierarchy-target">CancelError</span></li></ul></li></ul></section><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts#L20">src/cancellable.ts:20</a></li></ul></aside><section class="tsd-panel-group tsd-index-group"><section class="tsd-panel tsd-index-panel"><details class="tsd-index-content tsd-accordion" open><summary class="tsd-accordion-summary tsd-index-summary"><h5 class="tsd-index-heading uppercase" role="button" aria-expanded="false" tabIndex="0"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronSmall"></use></svg> Index</h5></summary><div class="tsd-accordion-details"><section class="tsd-index-section"><h3 class="tsd-index-heading">Constructors</h3><div class="tsd-index-list"><a href="CancelError.html#constructor" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Constructor"><use href="../assets/icons.svg#icon-512"></use></svg><span>constructor</span></a>
</div></section><section class="tsd-index-section"><h3 class="tsd-index-heading">Properties</h3><div class="tsd-index-list"><a href="CancelError.html#cause" class="tsd-index-link tsd-is-inherited tsd-is-external"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="../assets/icons.svg#icon-1024"></use></svg><span>cause?</span></a>
<a href="CancelError.html#message" class="tsd-index-link tsd-is-inherited tsd-is-external"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="../assets/icons.svg#icon-1024"></use></svg><span>message</span></a>
<a href="CancelError.html#name" class="tsd-index-link tsd-is-inherited tsd-is-external"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="../assets/icons.svg#icon-1024"></use></svg><span>name</span></a>

View file

@ -35,7 +35,7 @@ This might be reconsidered in case the proposal is retired.</p>
and is compliant with the <a href="https://promisesaplus.com/">Promises/A+ specification</a>
(it passes the <a href="https://github.com/promises-aplus/promises-tests">compliance suite</a>)
if so is the underlying implementation.</p>
</div><div class="tsd-comment tsd-typography"></div></section> <section class="tsd-panel"><h4>Type Parameters</h4><ul class="tsd-type-parameter-list"><li><span><a id="t" class="tsd-anchor"></a><span class="tsd-kind-type-parameter">T</span></span></li></ul></section> <section class="tsd-panel tsd-hierarchy" data-refl="664"><h4>Hierarchy</h4><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise" class="tsd-signature-type external" target="_blank">Promise</a><span class="tsd-signature-symbol">&lt;</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">&gt;</span><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><span class="tsd-hierarchy-target">CancellablePromise</span></li></ul></li></ul></section><section class="tsd-panel"><h4>Implements</h4><ul class="tsd-hierarchy"><li><a href="../interfaces/_internal_.PromiseLike.html" class="tsd-signature-type tsd-kind-interface">PromiseLike</a><span class="tsd-signature-symbol">&lt;</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">&gt;</span></li><li><a href="../interfaces/CancellablePromiseLike.html" class="tsd-signature-type tsd-kind-interface">CancellablePromiseLike</a><span class="tsd-signature-symbol">&lt;</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">&gt;</span></li></ul></section><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts#L147">src/cancellable.ts:147</a></li></ul></aside><section class="tsd-panel-group tsd-index-group"><section class="tsd-panel tsd-index-panel"><details class="tsd-index-content tsd-accordion" open><summary class="tsd-accordion-summary tsd-index-summary"><h5 class="tsd-index-heading uppercase" role="button" aria-expanded="false" tabIndex="0"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronSmall"></use></svg> Index</h5></summary><div class="tsd-accordion-details"><section class="tsd-index-section"><h3 class="tsd-index-heading">Constructors</h3><div class="tsd-index-list"><a href="CancellablePromise.html#constructor" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Constructor"><use href="../assets/icons.svg#icon-512"></use></svg><span>constructor</span></a>
</div><div class="tsd-comment tsd-typography"></div></section> <section class="tsd-panel"><h4>Type Parameters</h4><ul class="tsd-type-parameter-list"><li><span><a id="t" class="tsd-anchor"></a><span class="tsd-kind-type-parameter">T</span></span></li></ul></section> <section class="tsd-panel tsd-hierarchy" data-refl="658"><h4>Hierarchy</h4><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise" class="tsd-signature-type external" target="_blank">Promise</a><span class="tsd-signature-symbol">&lt;</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">&gt;</span><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><span class="tsd-hierarchy-target">CancellablePromise</span></li></ul></li></ul></section><section class="tsd-panel"><h4>Implements</h4><ul class="tsd-hierarchy"><li><a href="../interfaces/_internal_.PromiseLike.html" class="tsd-signature-type tsd-kind-interface">PromiseLike</a><span class="tsd-signature-symbol">&lt;</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">&gt;</span></li><li><a href="../interfaces/CancellablePromiseLike.html" class="tsd-signature-type tsd-kind-interface">CancellablePromiseLike</a><span class="tsd-signature-symbol">&lt;</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">&gt;</span></li></ul></section><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts#L147">src/cancellable.ts:147</a></li></ul></aside><section class="tsd-panel-group tsd-index-group"><section class="tsd-panel tsd-index-panel"><details class="tsd-index-content tsd-accordion" open><summary class="tsd-accordion-summary tsd-index-summary"><h5 class="tsd-index-heading uppercase" role="button" aria-expanded="false" tabIndex="0"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronSmall"></use></svg> Index</h5></summary><div class="tsd-accordion-details"><section class="tsd-index-section"><h3 class="tsd-index-heading">Constructors</h3><div class="tsd-index-list"><a href="CancellablePromise.html#constructor" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Constructor"><use href="../assets/icons.svg#icon-512"></use></svg><span>constructor</span></a>
</div></section><section class="tsd-index-section"><h3 class="tsd-index-heading">Properties</h3><div class="tsd-index-list"><a href="CancellablePromise.html#tostringtag" class="tsd-index-link tsd-is-inherited tsd-is-external"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="../assets/icons.svg#icon-1024"></use></svg><span>[to<wbr/>String<wbr/>Tag]</span></a>
</div></section><section class="tsd-index-section"><h3 class="tsd-index-heading">Methods</h3><div class="tsd-index-list"><a href="CancellablePromise.html#cancel" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Method"><use href="../assets/icons.svg#icon-2048"></use></svg><span>cancel</span></a>
<a href="CancellablePromise.html#cancelon" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Method"><use href="../assets/icons.svg#icon-2048"></use></svg><span>cancel<wbr/>On</span></a>

View file

@ -6,7 +6,7 @@ The value of the <a href="Call.RuntimeError.html#cause" class="tsd-kind-property
<p>Because the original promise was cancelled,
a wrapper promise will be passed to the unhandled rejection listener instead.
The <a href="CancelledRejectionError.html#promise" class="tsd-kind-property">promise</a> property holds a reference to the original promise.</p>
</div><div class="tsd-comment tsd-typography"></div></section><section class="tsd-panel tsd-hierarchy" data-refl="630"><h4>Hierarchy</h4><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error" class="tsd-signature-type external" target="_blank">Error</a><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><span class="tsd-hierarchy-target">CancelledRejectionError</span></li></ul></li></ul></section><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts#L44">src/cancellable.ts:44</a></li></ul></aside><section class="tsd-panel-group tsd-index-group"><section class="tsd-panel tsd-index-panel"><details class="tsd-index-content tsd-accordion" open><summary class="tsd-accordion-summary tsd-index-summary"><h5 class="tsd-index-heading uppercase" role="button" aria-expanded="false" tabIndex="0"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronSmall"></use></svg> Index</h5></summary><div class="tsd-accordion-details"><section class="tsd-index-section"><h3 class="tsd-index-heading">Constructors</h3><div class="tsd-index-list"><a href="CancelledRejectionError.html#constructor" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Constructor"><use href="../assets/icons.svg#icon-512"></use></svg><span>constructor</span></a>
</div><div class="tsd-comment tsd-typography"></div></section><section class="tsd-panel tsd-hierarchy" data-refl="624"><h4>Hierarchy</h4><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error" class="tsd-signature-type external" target="_blank">Error</a><ul class="tsd-hierarchy"><li class="tsd-hierarchy-item"><span class="tsd-hierarchy-target">CancelledRejectionError</span></li></ul></li></ul></section><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts#L44">src/cancellable.ts:44</a></li></ul></aside><section class="tsd-panel-group tsd-index-group"><section class="tsd-panel tsd-index-panel"><details class="tsd-index-content tsd-accordion" open><summary class="tsd-accordion-summary tsd-index-summary"><h5 class="tsd-index-heading uppercase" role="button" aria-expanded="false" tabIndex="0"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronSmall"></use></svg> Index</h5></summary><div class="tsd-accordion-details"><section class="tsd-index-section"><h3 class="tsd-index-heading">Constructors</h3><div class="tsd-index-list"><a href="CancelledRejectionError.html#constructor" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Constructor"><use href="../assets/icons.svg#icon-512"></use></svg><span>constructor</span></a>
</div></section><section class="tsd-index-section"><h3 class="tsd-index-heading">Properties</h3><div class="tsd-index-list"><a href="CancelledRejectionError.html#cause" class="tsd-index-link tsd-is-inherited tsd-is-external"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="../assets/icons.svg#icon-1024"></use></svg><span>cause?</span></a>
<a href="CancelledRejectionError.html#message" class="tsd-index-link tsd-is-inherited tsd-is-external"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="../assets/icons.svg#icon-1024"></use></svg><span>message</span></a>
<a href="CancelledRejectionError.html#name" class="tsd-index-link tsd-is-inherited tsd-is-external"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="../assets/icons.svg#icon-1024"></use></svg><span>name</span></a>

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,3 @@
<!DOCTYPE html><html class="default" lang="en" data-base=".."><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>Capabilities | @wailsio/runtime</title><meta name="description" content="Documentation for @wailsio/runtime"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script><script async src="../assets/hierarchy.js" id="tsd-hierarchy-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><div class="table-cell" id="tsd-search"><div class="field"><label for="tsd-search-field" class="tsd-widget tsd-toolbar-icon search no-caption"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></label><input type="text" id="tsd-search-field" aria-label="Search"/></div><div class="field"><div id="tsd-toolbar-links"></div></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@wailsio/runtime</a></div><div class="table-cell" id="tsd-widgets"><a href="#" class="tsd-widget tsd-toolbar-icon menu no-caption" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@wailsio/runtime</a></li><li><a href="../modules/System.html">System</a></li><li><a href="System.Capabilities.html">Capabilities</a></li></ul><h1>Function Capabilities</h1></div><section class="tsd-panel"><ul class="tsd-signatures"><li class=""><div class="tsd-signature tsd-anchor-link"><a id="capabilities" class="tsd-anchor"></a><span class="tsd-kind-call-signature">Capabilities</span><span class="tsd-signature-symbol">()</span><span class="tsd-signature-symbol">:</span> <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise" class="tsd-signature-type external" target="_blank">Promise</a><span class="tsd-signature-symbol">&lt;</span><a href="../types/_internal_.Record.html" class="tsd-signature-type tsd-kind-type-alias">Record</a><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">,</span> <span class="tsd-signature-type">any</span><span class="tsd-signature-symbol">&gt;</span><span class="tsd-signature-symbol">&gt;</span><a href="#capabilities" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../assets/icons.svg#icon-anchor"></use></svg></a></div><div class="tsd-description"><div class="tsd-comment tsd-typography"><p>Fetches the capabilities of the application from the server.</p>
</div><h4 class="tsd-returns-title">Returns <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise" class="tsd-signature-type external" target="_blank">Promise</a><span class="tsd-signature-symbol">&lt;</span><a href="../types/_internal_.Record.html" class="tsd-signature-type tsd-kind-type-alias">Record</a><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">,</span> <span class="tsd-signature-type">any</span><span class="tsd-signature-symbol">&gt;</span><span class="tsd-signature-symbol">&gt;</span></h4><p>A promise that resolves to an object containing the capabilities.</p>
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L61">src/system.ts:61</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L60">src/system.ts:60</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>

View file

@ -1,3 +1,3 @@
<!DOCTYPE html><html class="default" lang="en" data-base=".."><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>Environment | @wailsio/runtime</title><meta name="description" content="Documentation for @wailsio/runtime"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script><script async src="../assets/hierarchy.js" id="tsd-hierarchy-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><div class="table-cell" id="tsd-search"><div class="field"><label for="tsd-search-field" class="tsd-widget tsd-toolbar-icon search no-caption"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></label><input type="text" id="tsd-search-field" aria-label="Search"/></div><div class="field"><div id="tsd-toolbar-links"></div></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@wailsio/runtime</a></div><div class="table-cell" id="tsd-widgets"><a href="#" class="tsd-widget tsd-toolbar-icon menu no-caption" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@wailsio/runtime</a></li><li><a href="../modules/System.html">System</a></li><li><a href="System.Environment.html">Environment</a></li></ul><h1>Function Environment</h1></div><section class="tsd-panel"><ul class="tsd-signatures"><li class=""><div class="tsd-signature tsd-anchor-link"><a id="environment" class="tsd-anchor"></a><span class="tsd-kind-call-signature">Environment</span><span class="tsd-signature-symbol">()</span><span class="tsd-signature-symbol">:</span> <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise" class="tsd-signature-type external" target="_blank">Promise</a><span class="tsd-signature-symbol">&lt;</span><a href="../interfaces/System.EnvironmentInfo.html" class="tsd-signature-type tsd-kind-interface">EnvironmentInfo</a><span class="tsd-signature-symbol">&gt;</span><a href="#environment" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../assets/icons.svg#icon-anchor"></use></svg></a></div><div class="tsd-description"><div class="tsd-comment tsd-typography"><p>Retrieves environment details.</p>
</div><h4 class="tsd-returns-title">Returns <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise" class="tsd-signature-type external" target="_blank">Promise</a><span class="tsd-signature-symbol">&lt;</span><a href="../interfaces/System.EnvironmentInfo.html" class="tsd-signature-type tsd-kind-interface">EnvironmentInfo</a><span class="tsd-signature-symbol">&gt;</span></h4><p>A promise that resolves to an object containing OS and system architecture.</p>
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L94">src/system.ts:94</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L93">src/system.ts:93</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>

View file

@ -1,6 +0,0 @@
<!DOCTYPE html><html class="default" lang="en" data-base=".."><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>HandlePlatformFileDrop | @wailsio/runtime</title><meta name="description" content="Documentation for @wailsio/runtime"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script><script async src="../assets/hierarchy.js" id="tsd-hierarchy-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><div class="table-cell" id="tsd-search"><div class="field"><label for="tsd-search-field" class="tsd-widget tsd-toolbar-icon search no-caption"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></label><input type="text" id="tsd-search-field" aria-label="Search"/></div><div class="field"><div id="tsd-toolbar-links"></div></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@wailsio/runtime</a></div><div class="table-cell" id="tsd-widgets"><a href="#" class="tsd-widget tsd-toolbar-icon menu no-caption" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@wailsio/runtime</a></li><li><a href="../modules/System.html">System</a></li><li><a href="System.HandlePlatformFileDrop.html">HandlePlatformFileDrop</a></li></ul><h1>Function HandlePlatformFileDrop</h1></div><section class="tsd-panel"><ul class="tsd-signatures"><li class=""><div class="tsd-signature tsd-anchor-link"><a id="handleplatformfiledrop" class="tsd-anchor"></a><span class="tsd-kind-call-signature">HandlePlatformFileDrop</span><span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">filenames</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">[]</span><span class="tsd-signature-symbol">,</span> <span class="tsd-kind-parameter">x</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">number</span><span class="tsd-signature-symbol">,</span> <span class="tsd-kind-parameter">y</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">number</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">void</span><a href="#handleplatformfiledrop" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../assets/icons.svg#icon-anchor"></use></svg></a></div><div class="tsd-description"><div class="tsd-comment tsd-typography"><p>Handles file drops originating from platform-specific code (e.g., macOS native drag-and-drop).
Gathers information about the drop target element and sends it back to the Go backend.</p>
</div><div class="tsd-parameters"><h4 class="tsd-parameters-title">Parameters</h4><ul class="tsd-parameter-list"><li><span><span class="tsd-kind-parameter">filenames</span>: <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">[]</span></span><div class="tsd-comment tsd-typography"><p>An array of file paths (strings) that were dropped.</p>
</div><div class="tsd-comment tsd-typography"></div></li><li><span><span class="tsd-kind-parameter">x</span>: <span class="tsd-signature-type">number</span></span><div class="tsd-comment tsd-typography"><p>The x-coordinate of the drop event.</p>
</div><div class="tsd-comment tsd-typography"></div></li><li><span><span class="tsd-kind-parameter">y</span>: <span class="tsd-signature-type">number</span></span><div class="tsd-comment tsd-typography"><p>The y-coordinate of the drop event.</p>
</div><div class="tsd-comment tsd-typography"></div></li></ul></div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4><div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L169">src/system.ts:169</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>

View file

@ -1,3 +1,3 @@
<!DOCTYPE html><html class="default" lang="en" data-base=".."><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>IsAMD64 | @wailsio/runtime</title><meta name="description" content="Documentation for @wailsio/runtime"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script><script async src="../assets/hierarchy.js" id="tsd-hierarchy-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><div class="table-cell" id="tsd-search"><div class="field"><label for="tsd-search-field" class="tsd-widget tsd-toolbar-icon search no-caption"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></label><input type="text" id="tsd-search-field" aria-label="Search"/></div><div class="field"><div id="tsd-toolbar-links"></div></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@wailsio/runtime</a></div><div class="table-cell" id="tsd-widgets"><a href="#" class="tsd-widget tsd-toolbar-icon menu no-caption" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@wailsio/runtime</a></li><li><a href="../modules/System.html">System</a></li><li><a href="System.IsAMD64.html">IsAMD64</a></li></ul><h1>Function IsAMD64</h1></div><section class="tsd-panel"><ul class="tsd-signatures"><li class=""><div class="tsd-signature tsd-anchor-link"><a id="isamd64" class="tsd-anchor"></a><span class="tsd-kind-call-signature">IsAMD64</span><span class="tsd-signature-symbol">()</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">boolean</span><a href="#isamd64" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../assets/icons.svg#icon-anchor"></use></svg></a></div><div class="tsd-description"><div class="tsd-comment tsd-typography"><p>Checks if the current environment architecture is AMD64.</p>
</div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">boolean</span></h4><p>True if the current environment architecture is AMD64, false otherwise.</p>
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L130">src/system.ts:130</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L129">src/system.ts:129</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>

View file

@ -1,3 +1,3 @@
<!DOCTYPE html><html class="default" lang="en" data-base=".."><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>IsARM | @wailsio/runtime</title><meta name="description" content="Documentation for @wailsio/runtime"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script><script async src="../assets/hierarchy.js" id="tsd-hierarchy-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><div class="table-cell" id="tsd-search"><div class="field"><label for="tsd-search-field" class="tsd-widget tsd-toolbar-icon search no-caption"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></label><input type="text" id="tsd-search-field" aria-label="Search"/></div><div class="field"><div id="tsd-toolbar-links"></div></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@wailsio/runtime</a></div><div class="table-cell" id="tsd-widgets"><a href="#" class="tsd-widget tsd-toolbar-icon menu no-caption" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@wailsio/runtime</a></li><li><a href="../modules/System.html">System</a></li><li><a href="System.IsARM.html">IsARM</a></li></ul><h1>Function IsARM</h1></div><section class="tsd-panel"><ul class="tsd-signatures"><li class=""><div class="tsd-signature tsd-anchor-link"><a id="isarm" class="tsd-anchor"></a><span class="tsd-kind-call-signature">IsARM</span><span class="tsd-signature-symbol">()</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">boolean</span><a href="#isarm" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../assets/icons.svg#icon-anchor"></use></svg></a></div><div class="tsd-description"><div class="tsd-comment tsd-typography"><p>Checks if the current architecture is ARM.</p>
</div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">boolean</span></h4><p>True if the current architecture is ARM, false otherwise.</p>
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L139">src/system.ts:139</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L138">src/system.ts:138</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>

View file

@ -1,3 +1,3 @@
<!DOCTYPE html><html class="default" lang="en" data-base=".."><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>IsARM64 | @wailsio/runtime</title><meta name="description" content="Documentation for @wailsio/runtime"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script><script async src="../assets/hierarchy.js" id="tsd-hierarchy-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><div class="table-cell" id="tsd-search"><div class="field"><label for="tsd-search-field" class="tsd-widget tsd-toolbar-icon search no-caption"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></label><input type="text" id="tsd-search-field" aria-label="Search"/></div><div class="field"><div id="tsd-toolbar-links"></div></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@wailsio/runtime</a></div><div class="table-cell" id="tsd-widgets"><a href="#" class="tsd-widget tsd-toolbar-icon menu no-caption" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@wailsio/runtime</a></li><li><a href="../modules/System.html">System</a></li><li><a href="System.IsARM64.html">IsARM64</a></li></ul><h1>Function IsARM64</h1></div><section class="tsd-panel"><ul class="tsd-signatures"><li class=""><div class="tsd-signature tsd-anchor-link"><a id="isarm64" class="tsd-anchor"></a><span class="tsd-kind-call-signature">IsARM64</span><span class="tsd-signature-symbol">()</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">boolean</span><a href="#isarm64" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="../assets/icons.svg#icon-anchor"></use></svg></a></div><div class="tsd-description"><div class="tsd-comment tsd-typography"><p>Checks if the current environment is ARM64 architecture.</p>
</div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">boolean</span></h4><p>Returns true if the environment is ARM64 architecture, otherwise returns false.</p>
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L148">src/system.ts:148</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/wailsapp/wails/blob/v3-alpha/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts#L147">src/system.ts:147</a></li></ul></aside></div></li></ul></section></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@wailsio/runtime</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>

Some files were not shown because too many files have changed in this diff Show more