mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 22:55:48 +01:00
Merge branch 'v3-alpha' into fix/systemtray-menu-realtime-update
This commit is contained in:
commit
c44f5df9d4
186 changed files with 13799 additions and 3120 deletions
1
.github/workflows/automated-releases.yml
vendored
1
.github/workflows/automated-releases.yml
vendored
|
|
@ -23,6 +23,7 @@ env:
|
|||
jobs:
|
||||
check-permissions:
|
||||
name: Check Release Permissions
|
||||
permissions: {}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
authorized: ${{ steps.check.outputs.authorized }}
|
||||
|
|
|
|||
2
.github/workflows/test-nightly-releases.yml
vendored
2
.github/workflows/test-nightly-releases.yml
vendored
|
|
@ -1,4 +1,6 @@
|
|||
name: Test Nightly Releases (Dry Run)
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ func main() {
|
|||
},
|
||||
})
|
||||
|
||||
app.NewWebviewWindow()
|
||||
app.Window.New()
|
||||
app.Run()
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -628,7 +628,7 @@ func main() {
|
|||
},
|
||||
})
|
||||
|
||||
app.NewWebviewWindow()
|
||||
app.Window.New()
|
||||
app.Run()
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
212
docs/src/content/docs/features/drag-and-drop/files.mdx
Normal file
212
docs/src/content/docs/features/drag-and-drop/files.mdx
Normal 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
|
||||
232
docs/src/content/docs/features/drag-and-drop/html.mdx
Normal file
232
docs/src/content/docs/features/drag-and-drop/html.mdx
Normal 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
|
||||
|
|
@ -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
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
:::
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -482,7 +482,7 @@ func main() {
|
|||
textMenu := createTextMenu(app)
|
||||
app.RegisterContextMenu("text-menu", textMenu)
|
||||
|
||||
app.NewWebviewWindow()
|
||||
app.Window.New()
|
||||
app.Run()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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").
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 | |
|
||||
|
|
@ -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>
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
66
v3/examples/spotlight/README.md
Normal file
66
v3/examples/spotlight/README.md
Normal 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) |
|
||||
142
v3/examples/spotlight/main.go
Normal file
142
v3/examples/spotlight/main.go
Normal 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>`
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
242
v3/internal/assetserver/assetserver_bench_test.go
Normal file
242
v3/internal/assetserver/assetserver_bench_test.go
Normal 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
|
|
@ -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...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
277
v3/internal/assetserver/mimetype_stdlib_test.go
Normal file
277
v3/internal/assetserver/mimetype_stdlib_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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>`
|
||||
|
|
|
|||
|
|
@ -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>`
|
||||
|
|
|
|||
|
|
@ -68,9 +68,10 @@ static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCod
|
|||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"unsafe"
|
||||
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
var _ ResponseWriter = &responseWriter{}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
96
v3/internal/fileexplorer/desktopfile.go
Normal file
96
v3/internal/fileexplorer/desktopfile.go
Normal 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
|
||||
}
|
||||
532
v3/internal/fileexplorer/desktopfile_test.go
Normal file
532
v3/internal/fileexplorer/desktopfile_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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": {},
|
||||
}
|
||||
|
|
|
|||
79
v3/internal/libpath/cache_linux.go
Normal file
79
v3/internal/libpath/cache_linux.go
Normal 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()
|
||||
}
|
||||
53
v3/internal/libpath/flatpak_linux.go
Normal file
53
v3/internal/libpath/flatpak_linux.go
Normal 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
|
||||
}
|
||||
104
v3/internal/libpath/libpath.go
Normal file
104
v3/internal/libpath/libpath.go
Normal 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
|
||||
551
v3/internal/libpath/libpath_linux.go
Normal file
551
v3/internal/libpath/libpath_linux.go
Normal 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}
|
||||
}
|
||||
769
v3/internal/libpath/libpath_linux_test.go
Normal file
769
v3/internal/libpath/libpath_linux_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
73
v3/internal/libpath/libpath_other.go
Normal file
73
v3/internal/libpath/libpath_other.go
Normal 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}
|
||||
}
|
||||
28
v3/internal/libpath/nix_linux.go
Normal file
28
v3/internal/libpath/nix_linux.go
Normal 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
|
||||
}
|
||||
42
v3/internal/libpath/snap_linux.go
Normal file
42
v3/internal/libpath/snap_linux.go
Normal 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
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
window.hierarchyData = "eJylj8EKgzAQRP9lztGi2KC59tpD70Uk1RWDa4QkPYn/XrRUerCl0NMuO8zOmwluHIOHusosEUWalwKOWqY6mNF6qAkyS5Zh9UBQOGlbE7O+MV3cOBhPZ9MTBHpjG6j0KAXujqFgbCDX6pr8Yd8Ud2FgCNSsvYdC8E20fIk25yJ2hhtHdiGUWTkLSJl95dlYkjR/sawRuyAfIZ6HWaBI87e830tX6241V/G/pef5ASffjm8="
|
||||
window.hierarchyData = "eJylj8EKwjAQRP9lzrFiSkKbq1cP3kVKbFca3CaQxJP03yWK4kFF8LTLDrPz5oIYQk4wO10r0Uq5F4h0ZOqzCz7BXKBrVYa3E8FgbX1PzPbAtI1hcok27kQQODk/wEilBc6RYeB8pni0PaXle1M15okh0LNNCQY5DYvyZfF0FnF0PETyhVA1+1lAq+Yrz5NlJZsHyy3iLchHiPthFmilfMn7vXR3273lrvq39DxfASprjnI="
|
||||
|
|
@ -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
|
|
@ -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>"CancelError"</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>
|
||||
|
|
|
|||
|
|
@ -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"><</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">></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"><</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">></span></li><li><a href="../interfaces/CancellablePromiseLike.html" class="tsd-signature-type tsd-kind-interface">CancellablePromiseLike</a><span class="tsd-signature-symbol"><</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">></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"><</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">></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"><</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">></span></li><li><a href="../interfaces/CancellablePromiseLike.html" class="tsd-signature-type tsd-kind-interface">CancellablePromiseLike</a><span class="tsd-signature-symbol"><</span><a class="tsd-signature-type tsd-kind-type-parameter" href="CancellablePromise.html#constructorcancellablepromiset">T</a><span class="tsd-signature-symbol">></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>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"><</span><a href="../types/_internal_.Record.html" class="tsd-signature-type tsd-kind-type-alias">Record</a><span class="tsd-signature-symbol"><</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">></span><span class="tsd-signature-symbol">></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"><</span><a href="../types/_internal_.Record.html" class="tsd-signature-type tsd-kind-type-alias">Record</a><span class="tsd-signature-symbol"><</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">></span><span class="tsd-signature-symbol">></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>
|
||||
|
|
|
|||
|
|
@ -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"><</span><a href="../interfaces/System.EnvironmentInfo.html" class="tsd-signature-type tsd-kind-interface">EnvironmentInfo</a><span class="tsd-signature-symbol">></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"><</span><a href="../interfaces/System.EnvironmentInfo.html" class="tsd-signature-type tsd-kind-interface">EnvironmentInfo</a><span class="tsd-signature-symbol">></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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue