fix: add involves edge from task to agent:nomos at creation
Plus sync vendor directory for Docker build compatibility.
This commit is contained in:
10
vendor/github.com/wailsapp/wails/v3/pkg/application/TODO.md
generated
vendored
Normal file
10
vendor/github.com/wailsapp/wails/v3/pkg/application/TODO.md
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
Features
|
||||
- [ ] AssetServer
|
||||
- [ ] Offline page if navigating to external URL
|
||||
- [x] Application menu
|
||||
|
||||
Bugs
|
||||
- [ ] Resize Window
|
||||
- [ ] Fullscreen/Maximise/Minimise/Restore
|
||||
|
||||
7
vendor/github.com/wailsapp/wails/v3/pkg/application/android_logging_debug.go
generated
vendored
Normal file
7
vendor/github.com/wailsapp/wails/v3/pkg/application/android_logging_debug.go
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build android && !production
|
||||
|
||||
package application
|
||||
|
||||
// androidVerboseLogging enables the framework's internal diagnostic logging
|
||||
// in debug builds. See android_logging_production.go for the release value.
|
||||
const androidVerboseLogging = true
|
||||
7
vendor/github.com/wailsapp/wails/v3/pkg/application/android_logging_production.go
generated
vendored
Normal file
7
vendor/github.com/wailsapp/wails/v3/pkg/application/android_logging_production.go
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build android && production
|
||||
|
||||
package application
|
||||
|
||||
// androidVerboseLogging is disabled in production builds: the framework's
|
||||
// internal diagnostic logging compiles to a no-op.
|
||||
const androidVerboseLogging = false
|
||||
948
vendor/github.com/wailsapp/wails/v3/pkg/application/application.go
generated
vendored
Normal file
948
vendor/github.com/wailsapp/wails/v3/pkg/application/application.go
generated
vendored
Normal file
@@ -0,0 +1,948 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/assetserver"
|
||||
"github.com/wailsapp/wails/v3/internal/assetserver/bundledassets"
|
||||
"github.com/wailsapp/wails/v3/internal/assetserver/webview"
|
||||
"github.com/wailsapp/wails/v3/internal/capabilities"
|
||||
"github.com/wailsapp/wails/v3/pkg/updater"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
var alphaAssets embed.FS
|
||||
|
||||
var globalApplication *App
|
||||
|
||||
// AlphaAssets is the default assets for the alpha application
|
||||
var AlphaAssets = AssetOptions{
|
||||
Handler: BundledAssetFileServer(alphaAssets),
|
||||
}
|
||||
|
||||
type EventListener struct {
|
||||
callback func(app *ApplicationEvent)
|
||||
}
|
||||
|
||||
func Get() *App {
|
||||
return globalApplication
|
||||
}
|
||||
|
||||
func New(appOptions Options) *App {
|
||||
// If we were spawned as an updater helper the process must perform the
|
||||
// swap and exit before any application machinery touches the disk. This
|
||||
// is a no-op when the sentinel env vars are absent, so normal startup is
|
||||
// unaffected.
|
||||
updater.HandleHelperMode()
|
||||
|
||||
if globalApplication != nil {
|
||||
return globalApplication
|
||||
}
|
||||
|
||||
mergeApplicationDefaults(&appOptions)
|
||||
|
||||
result := newApplication(appOptions)
|
||||
globalApplication = result
|
||||
fatalHandler(result.handleFatalError)
|
||||
|
||||
if result.Logger == nil {
|
||||
if result.isDebugMode {
|
||||
result.Logger = DefaultLogger(result.options.LogLevel)
|
||||
} else {
|
||||
result.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
}
|
||||
|
||||
// Set up signal handling (platform-specific)
|
||||
result.setupSignalHandler(appOptions)
|
||||
|
||||
result.logStartup()
|
||||
result.logPlatformInfo()
|
||||
|
||||
result.customEventProcessor = NewWailsEventProcessor(result.Event.dispatch)
|
||||
|
||||
messageProc := NewMessageProcessor(result.Logger)
|
||||
result.messageProcessor = messageProc
|
||||
|
||||
// Initialize transport (default to HTTP if not specified)
|
||||
transport := appOptions.Transport
|
||||
if transport == nil {
|
||||
transport = NewHTTPTransport(HTTPTransportWithLogger(result.Logger))
|
||||
}
|
||||
|
||||
err := transport.Start(result.ctx, messageProc)
|
||||
if err != nil {
|
||||
result.fatal("failed to start custom transport: %w", err)
|
||||
}
|
||||
// Register shutdown task to stop transport
|
||||
result.OnShutdown(func() {
|
||||
if err := transport.Stop(); err != nil {
|
||||
result.error("failed to stop custom transport: %w", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Auto-wire events if transport supports event delivery
|
||||
if eventTransport, ok := transport.(WailsEventListener); ok {
|
||||
result.wailsEventListeners = append(result.wailsEventListeners, eventTransport)
|
||||
} else {
|
||||
// otherwise fallback to IPC
|
||||
result.wailsEventListeners = append(result.wailsEventListeners, &EventIPCTransport{
|
||||
app: result,
|
||||
})
|
||||
}
|
||||
|
||||
middlewares := []assetserver.Middleware{
|
||||
func(next http.Handler) http.Handler {
|
||||
if m := appOptions.Assets.Middleware; m != nil {
|
||||
return m(next)
|
||||
}
|
||||
return next
|
||||
},
|
||||
func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
switch path {
|
||||
case "/wails/runtime.js":
|
||||
err := assetserver.ServeFile(rw, path, bundledassets.RuntimeJS)
|
||||
if err != nil {
|
||||
result.fatal("unable to serve runtime.js: %w", err)
|
||||
}
|
||||
case "/wails/transport.js":
|
||||
err := assetserver.ServeFile(rw, path, transport.JSClient())
|
||||
if err != nil {
|
||||
result.fatal("unable to serve transport.js: %w", err)
|
||||
}
|
||||
case "/wails/custom.js":
|
||||
// custom.js is only served in server mode.
|
||||
// Return 404 so the runtime's loadOptionalScript skips it.
|
||||
http.NotFound(rw, req)
|
||||
default:
|
||||
next.ServeHTTP(rw, req)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
if handler, ok := transport.(TransportHTTPHandler); ok {
|
||||
middlewares = append(middlewares, handler.Handler())
|
||||
}
|
||||
|
||||
opts := &assetserver.Options{
|
||||
Handler: appOptions.Assets.Handler,
|
||||
Middleware: assetserver.ChainMiddleware(middlewares...),
|
||||
Logger: result.Logger,
|
||||
}
|
||||
|
||||
if appOptions.Assets.DisableLogging {
|
||||
opts.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
|
||||
srv, err := assetserver.NewAssetServer(opts)
|
||||
if err != nil {
|
||||
result.fatal("application initialisation failed: %w", err)
|
||||
}
|
||||
|
||||
result.assets = srv
|
||||
result.assets.LogDetails()
|
||||
|
||||
// If transport implements AssetServerTransport, configure it to serve assets
|
||||
if assetTransport, ok := transport.(AssetServerTransport); ok {
|
||||
err := assetTransport.ServeAssets(srv)
|
||||
if err != nil {
|
||||
result.fatal("failed to configure transport for serving assets: %w", err)
|
||||
}
|
||||
result.debug("Transport configured to serve assets")
|
||||
}
|
||||
|
||||
result.bindings = NewBindings(appOptions.MarshalError, appOptions.BindAliases)
|
||||
result.options.Services = slices.Clone(appOptions.Services)
|
||||
|
||||
// Process keybindings
|
||||
if result.options.KeyBindings != nil {
|
||||
result.keyBindings = processKeyBindingOptions(result.options.KeyBindings)
|
||||
}
|
||||
|
||||
if appOptions.OnShutdown != nil {
|
||||
result.OnShutdown(appOptions.OnShutdown)
|
||||
}
|
||||
|
||||
// Initialize single instance manager if enabled
|
||||
if appOptions.SingleInstance != nil {
|
||||
manager, err := newSingleInstanceManager(result, appOptions.SingleInstance)
|
||||
if err != nil {
|
||||
if errors.Is(err, alreadyRunningError) && manager != nil {
|
||||
err = manager.notifyFirstInstance()
|
||||
if err != nil {
|
||||
globalApplication.error("failed to notify first instance: %w", err)
|
||||
}
|
||||
os.Exit(appOptions.SingleInstance.ExitCode)
|
||||
}
|
||||
result.fatal("failed to initialize single instance manager: %w", err)
|
||||
} else {
|
||||
result.singleInstanceManager = manager
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeApplicationDefaults(o *Options) {
|
||||
if o.Name == "" {
|
||||
o.Name = "My Wails Application"
|
||||
}
|
||||
if o.Description == "" {
|
||||
o.Description = "An application written using Wails"
|
||||
}
|
||||
if o.Windows.WndClass == "" {
|
||||
o.Windows.WndClass = "WailsWebviewWindow"
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
platformApp interface {
|
||||
run() error
|
||||
destroy()
|
||||
setApplicationMenu(menu *Menu)
|
||||
name() string
|
||||
getCurrentWindowID() uint
|
||||
showAboutDialog(name string, description string, icon []byte)
|
||||
setIcon(icon []byte)
|
||||
on(id uint)
|
||||
dispatchOnMainThread(id uint)
|
||||
hide()
|
||||
show()
|
||||
getPrimaryScreen() (*Screen, error)
|
||||
getScreens() ([]*Screen, error)
|
||||
GetFlags(options Options) map[string]any
|
||||
isOnMainThread() bool
|
||||
isDarkMode() bool
|
||||
getAccentColor() string
|
||||
}
|
||||
|
||||
runnable interface {
|
||||
Run()
|
||||
}
|
||||
)
|
||||
|
||||
// Messages sent from javascript get routed here
|
||||
type windowMessage struct {
|
||||
windowId uint
|
||||
message string
|
||||
originInfo *OriginInfo
|
||||
}
|
||||
|
||||
type OriginInfo struct {
|
||||
Origin string
|
||||
TopOrigin string
|
||||
IsMainFrame bool
|
||||
}
|
||||
|
||||
var windowMessageBuffer = make(chan *windowMessage, 64)
|
||||
|
||||
// DropTargetDetails contains information about the HTML element
|
||||
// where files were dropped (the element with data-file-drop-target attribute).
|
||||
type DropTargetDetails struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
ElementID string `json:"id"`
|
||||
ClassList []string `json:"classList"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
type dragAndDropMessage struct {
|
||||
windowId uint
|
||||
filenames []string
|
||||
X int
|
||||
Y int
|
||||
DropTarget *DropTargetDetails
|
||||
}
|
||||
|
||||
var windowDragAndDropBuffer = make(chan *dragAndDropMessage, 5)
|
||||
|
||||
func addDragAndDropMessage(windowId uint, filenames []string, dropTarget *DropTargetDetails) {
|
||||
windowDragAndDropBuffer <- &dragAndDropMessage{
|
||||
windowId: windowId,
|
||||
filenames: filenames,
|
||||
DropTarget: dropTarget,
|
||||
}
|
||||
}
|
||||
|
||||
var _ webview.Request = &webViewAssetRequest{}
|
||||
|
||||
const webViewRequestHeaderWindowId = "x-wails-window-id"
|
||||
const webViewRequestHeaderWindowName = "x-wails-window-name"
|
||||
|
||||
type webViewAssetRequest struct {
|
||||
Request webview.Request
|
||||
windowId uint
|
||||
windowName string
|
||||
}
|
||||
|
||||
var windowKeyEvents = make(chan *windowKeyEvent, 5)
|
||||
|
||||
type windowKeyEvent struct {
|
||||
windowId uint
|
||||
acceleratorString string
|
||||
}
|
||||
|
||||
func (r *webViewAssetRequest) URL() (string, error) {
|
||||
return r.Request.URL()
|
||||
}
|
||||
|
||||
func (r *webViewAssetRequest) Method() (string, error) {
|
||||
return r.Request.Method()
|
||||
}
|
||||
|
||||
func (r *webViewAssetRequest) Header() (http.Header, error) {
|
||||
h, err := r.Request.Header()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hh := h.Clone()
|
||||
hh.Set(webViewRequestHeaderWindowId, strconv.FormatUint(uint64(r.windowId), 10))
|
||||
if r.windowName != "" {
|
||||
hh.Set(webViewRequestHeaderWindowName, r.windowName)
|
||||
}
|
||||
return hh, nil
|
||||
}
|
||||
|
||||
func (r *webViewAssetRequest) Body() (io.ReadCloser, error) {
|
||||
return r.Request.Body()
|
||||
}
|
||||
|
||||
func (r *webViewAssetRequest) Response() webview.ResponseWriter {
|
||||
return r.Request.Response()
|
||||
}
|
||||
|
||||
func (r *webViewAssetRequest) Close() error {
|
||||
return r.Request.Close()
|
||||
}
|
||||
|
||||
var webviewRequests = make(chan *webViewAssetRequest, 256)
|
||||
|
||||
type eventHook struct {
|
||||
callback func(event *ApplicationEvent)
|
||||
}
|
||||
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
options Options
|
||||
applicationEventListeners map[uint][]*EventListener
|
||||
applicationEventListenersLock sync.RWMutex
|
||||
applicationEventHooks map[uint][]*eventHook
|
||||
applicationEventHooksLock sync.RWMutex
|
||||
|
||||
// Manager pattern for organized API
|
||||
Window *WindowManager
|
||||
ContextMenu *ContextMenuManager
|
||||
KeyBinding *KeyBindingManager
|
||||
Browser *BrowserManager
|
||||
Env *EnvironmentManager
|
||||
Dialog *DialogManager
|
||||
Event *EventManager
|
||||
Menu *MenuManager
|
||||
Screen *ScreenManager
|
||||
Clipboard *ClipboardManager
|
||||
SystemTray *SystemTrayManager
|
||||
Autostart *AutostartManager
|
||||
GlobalShortcut *GlobalShortcutManager
|
||||
Updater *updater.Updater
|
||||
|
||||
// Windows
|
||||
windows map[uint]Window
|
||||
windowsLock sync.RWMutex
|
||||
|
||||
// System Trays
|
||||
systemTrays map[uint]*SystemTray
|
||||
systemTraysLock sync.Mutex
|
||||
systemTrayID uint
|
||||
systemTrayIDLock sync.RWMutex
|
||||
|
||||
// MenuItems
|
||||
menuItems map[uint]*MenuItem
|
||||
menuItemsLock sync.Mutex
|
||||
|
||||
// Starting and running
|
||||
starting bool
|
||||
running bool
|
||||
runLock sync.Mutex
|
||||
pendingRun []runnable
|
||||
|
||||
bindings *Bindings
|
||||
|
||||
// platform app
|
||||
impl platformApp
|
||||
|
||||
// The main application menu (private - use app.Menu.GetApplicationMenu/SetApplicationMenu)
|
||||
applicationMenu *Menu
|
||||
|
||||
clipboard *Clipboard
|
||||
customEventProcessor *EventProcessor
|
||||
Logger *slog.Logger
|
||||
|
||||
contextMenus map[string]*ContextMenu
|
||||
contextMenusLock sync.RWMutex
|
||||
|
||||
assets *assetserver.AssetServer
|
||||
startURL string
|
||||
|
||||
// Hooks
|
||||
windowCreatedCallbacks []func(window Window)
|
||||
pid int
|
||||
|
||||
// Capabilities
|
||||
capabilities capabilities.Capabilities
|
||||
isDebugMode bool
|
||||
|
||||
// Keybindings
|
||||
keyBindings map[string]func(window Window)
|
||||
keyBindingsLock sync.RWMutex
|
||||
|
||||
// Shutdown
|
||||
performingShutdown bool
|
||||
shutdownLock sync.Mutex
|
||||
serviceShutdownLock sync.Mutex
|
||||
|
||||
// Shutdown tasks are run when the application is shutting down.
|
||||
// They are run in the order they are added and run on the main thread.
|
||||
// The application option `OnShutdown` is run first.
|
||||
shutdownTasks []func()
|
||||
|
||||
// Platform-specific fields (includes signal handler on desktop)
|
||||
platformSignalHandler
|
||||
|
||||
// Wails ApplicationEvent Listener related
|
||||
wailsEventListenerLock sync.Mutex
|
||||
wailsEventListeners []WailsEventListener
|
||||
|
||||
// singleInstanceManager handles single instance functionality
|
||||
singleInstanceManager *singleInstanceManager
|
||||
|
||||
// messageProcessor handles runtime messages
|
||||
messageProcessor *MessageProcessor
|
||||
}
|
||||
|
||||
func (a *App) Config() Options {
|
||||
return a.options
|
||||
}
|
||||
|
||||
// Context returns the application context that is canceled when the application shuts down.
|
||||
// This context should be used for graceful shutdown of goroutines and long-running operations.
|
||||
func (a *App) Context() context.Context {
|
||||
return a.ctx
|
||||
}
|
||||
|
||||
func (a *App) handleWarning(msg string) {
|
||||
if a.options.WarningHandler != nil {
|
||||
a.options.WarningHandler(msg)
|
||||
} else {
|
||||
a.Logger.Warn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) handleError(err error) {
|
||||
if a.options.ErrorHandler != nil {
|
||||
a.options.ErrorHandler(err)
|
||||
} else {
|
||||
a.Logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterService appends the given service to the list of bound services.
|
||||
// Registered services will be bound and initialised
|
||||
// in registration order upon calling [App.Run].
|
||||
//
|
||||
// RegisterService will log an error message
|
||||
// and discard the given service
|
||||
// if called after [App.Run].
|
||||
func (a *App) RegisterService(service Service) {
|
||||
a.runLock.Lock()
|
||||
defer a.runLock.Unlock()
|
||||
|
||||
if a.starting || a.running {
|
||||
a.error(
|
||||
"services must be registered before running the application. Service '%s' will not be registered.",
|
||||
getServiceName(service),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
a.options.Services = append(a.options.Services, service)
|
||||
}
|
||||
|
||||
func (a *App) handleFatalError(err error) {
|
||||
a.handleError(&FatalError{err: err})
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (a *App) init() {
|
||||
a.ctx, a.cancel = context.WithCancel(context.Background())
|
||||
a.applicationEventHooks = make(map[uint][]*eventHook)
|
||||
a.applicationEventListeners = make(map[uint][]*EventListener)
|
||||
a.windows = make(map[uint]Window)
|
||||
a.systemTrays = make(map[uint]*SystemTray)
|
||||
a.contextMenus = make(map[string]*ContextMenu)
|
||||
a.keyBindings = make(map[string]func(window Window))
|
||||
a.Logger = a.options.Logger
|
||||
a.pid = os.Getpid()
|
||||
a.wailsEventListeners = make([]WailsEventListener, 0)
|
||||
|
||||
// Initialize managers
|
||||
a.Window = newWindowManager(a)
|
||||
a.ContextMenu = newContextMenuManager(a)
|
||||
a.KeyBinding = newKeyBindingManager(a)
|
||||
a.Browser = newBrowserManager(a)
|
||||
a.Env = newEnvironmentManager(a)
|
||||
a.Dialog = newDialogManager(a)
|
||||
a.Event = newEventManager(a)
|
||||
a.Menu = newMenuManager(a)
|
||||
a.Screen = newScreenManager(a)
|
||||
a.Clipboard = newClipboardManager(a)
|
||||
a.SystemTray = newSystemTrayManager(a)
|
||||
a.Autostart = newAutostartManager(a)
|
||||
a.GlobalShortcut = newGlobalShortcutManager(a)
|
||||
a.Updater = updater.New(newUpdaterHost(a))
|
||||
}
|
||||
|
||||
func (a *App) Capabilities() capabilities.Capabilities {
|
||||
return a.capabilities
|
||||
}
|
||||
|
||||
func (a *App) GetPID() int {
|
||||
return a.pid
|
||||
}
|
||||
|
||||
func (a *App) info(message string, args ...any) {
|
||||
if a.Logger != nil {
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
a.Logger.Info(message, args...)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) debug(message string, args ...any) {
|
||||
if a.Logger != nil {
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
a.Logger.Debug(message, args...)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) fatal(message string, args ...any) {
|
||||
err := fmt.Errorf(message, args...)
|
||||
a.handleFatalError(err)
|
||||
}
|
||||
func (a *App) warning(message string, args ...any) {
|
||||
msg := fmt.Sprintf(message, args...)
|
||||
a.handleWarning(msg)
|
||||
}
|
||||
|
||||
func (a *App) error(message string, args ...any) {
|
||||
a.handleError(fmt.Errorf(message, args...))
|
||||
}
|
||||
|
||||
func (a *App) Run() error {
|
||||
a.runLock.Lock()
|
||||
// Prevent double invocations.
|
||||
if a.starting || a.running {
|
||||
a.runLock.Unlock()
|
||||
return errors.New("application is running or a previous run has failed")
|
||||
}
|
||||
// Block further service registrations.
|
||||
a.starting = true
|
||||
a.runLock.Unlock()
|
||||
|
||||
// Ensure application context is cancelled in case of failures.
|
||||
defer a.cancel()
|
||||
|
||||
// Call post-create hooks
|
||||
err := a.preRun()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.impl = newPlatformApp(a)
|
||||
|
||||
// Ensure services are shut down in case of failures.
|
||||
defer a.shutdownServices()
|
||||
|
||||
// Ensure application context is canceled before service shutdown (duplicate calls don't hurt).
|
||||
defer a.cancel()
|
||||
|
||||
// startup performs the remaining startup sequence: start services, spawn the
|
||||
// event-handling reader goroutines, run any pending windows, and apply the
|
||||
// menu/icon. On desktop this runs inline on the main goroutine. On iOS it is
|
||||
// deferred to a background goroutine (see below).
|
||||
startup := func() error {
|
||||
// Startup services before dispatching any events.
|
||||
// No need to hold the lock here because a.options.Services may only change when a.running is false.
|
||||
services := a.options.Services
|
||||
a.options.Services = nil
|
||||
for i, service := range services {
|
||||
if err := a.startupService(service); err != nil {
|
||||
return fmt.Errorf("error starting service '%s': %w", getServiceName(service), err)
|
||||
}
|
||||
// Schedule started services for shutdown.
|
||||
a.options.Services = services[:i+1]
|
||||
}
|
||||
|
||||
|
||||
// Start the MCP server when the application is built with -tags mcp.
|
||||
// All configuration is read from environment variables (WAILS_MCP_HOST,
|
||||
// WAILS_MCP_PORT, WAILS_MCP_TIMEOUT, WAILS_MCP_HIDE_CURSOR).
|
||||
if err := startMCPServer(a); err != nil {
|
||||
return fmt.Errorf("mcp: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
event := <-applicationEvents
|
||||
go a.Event.handleApplicationEvent(event)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
event := <-windowEvents
|
||||
go a.handleWindowEvent(event)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
request := <-webviewRequests
|
||||
go a.handleWebViewRequest(request)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
event := <-windowMessageBuffer
|
||||
go a.handleWindowMessage(event)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
event := <-windowKeyEvents
|
||||
go a.handleWindowKeyEvent(event)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
dragAndDropMessage := <-windowDragAndDropBuffer
|
||||
go a.handleDragAndDropMessage(dragAndDropMessage)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
menuItemID := <-menuItemClicked
|
||||
go a.Menu.handleMenuItemClicked(menuItemID)
|
||||
}
|
||||
}()
|
||||
|
||||
a.runLock.Lock()
|
||||
a.running = true
|
||||
a.runLock.Unlock()
|
||||
|
||||
// Bind any global shortcuts that were registered before the app started.
|
||||
a.GlobalShortcut.flushPending()
|
||||
|
||||
// No need to hold the lock here because
|
||||
// - a.pendingRun may only change while a.running is false.
|
||||
// - runnables are scheduled asynchronously anyway.
|
||||
for _, pending := range a.pendingRun {
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
pending.Run()
|
||||
}()
|
||||
}
|
||||
a.pendingRun = nil
|
||||
|
||||
// set the application menu
|
||||
if runtime.GOOS == "darwin" {
|
||||
a.impl.setApplicationMenu(a.applicationMenu)
|
||||
}
|
||||
if a.options.Icon != nil {
|
||||
a.impl.setIcon(a.options.Icon)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := startup(); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.impl.run()
|
||||
}
|
||||
|
||||
func (a *App) startupService(service Service) error {
|
||||
err := a.bindings.Add(service)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot bind service methods: %w", err)
|
||||
}
|
||||
|
||||
if service.options.Route != "" {
|
||||
handler, ok := service.Instance().(http.Handler)
|
||||
if !ok {
|
||||
handler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
http.Error(
|
||||
rw,
|
||||
fmt.Sprintf(
|
||||
"Service '%s' does not handle HTTP requests",
|
||||
getServiceName(service),
|
||||
),
|
||||
http.StatusServiceUnavailable,
|
||||
)
|
||||
})
|
||||
}
|
||||
a.assets.AttachServiceHandler(service.options.Route, handler)
|
||||
}
|
||||
|
||||
if s, ok := service.instance.(ServiceStartup); ok {
|
||||
a.debug("Starting up service:", "name", getServiceName(service))
|
||||
return s.ServiceStartup(a.ctx, service.options)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) shutdownServices() {
|
||||
// Acquire lock to prevent double calls (defer in Run() + OnShutdown)
|
||||
a.serviceShutdownLock.Lock()
|
||||
defer a.serviceShutdownLock.Unlock()
|
||||
|
||||
// Ensure app context is cancelled first (duplicate calls don't hurt).
|
||||
a.cancel()
|
||||
|
||||
for len(a.options.Services) > 0 {
|
||||
last := len(a.options.Services) - 1
|
||||
service := a.options.Services[last]
|
||||
a.options.Services = a.options.Services[:last] // Prevent double shutdowns
|
||||
|
||||
if s, ok := service.instance.(ServiceShutdown); ok {
|
||||
a.debug("Shutting down service:", "name", getServiceName(service))
|
||||
if err := s.ServiceShutdown(); err != nil {
|
||||
a.error("error shutting down service '%s': %w", getServiceName(service), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) {
|
||||
defer handlePanic()
|
||||
a.windowsLock.Lock()
|
||||
window, ok := a.windows[event.windowId]
|
||||
a.windowsLock.Unlock()
|
||||
if !ok {
|
||||
a.warning("WebviewWindow #%d not found", event.windowId)
|
||||
return
|
||||
}
|
||||
window.handleDragAndDropMessage(event.filenames, event.DropTarget)
|
||||
}
|
||||
|
||||
func (a *App) handleWindowMessage(event *windowMessage) {
|
||||
defer handlePanic()
|
||||
// Get window from window map
|
||||
a.windowsLock.RLock()
|
||||
window, ok := a.windows[event.windowId]
|
||||
// Debug: log all window IDs
|
||||
var ids []uint
|
||||
for id := range a.windows {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
a.windowsLock.RUnlock()
|
||||
|
||||
a.debug("handleWindowMessage: Looking for window", "windowId", event.windowId, "availableIDs", ids)
|
||||
|
||||
if !ok {
|
||||
a.warning("WebviewWindow #%d not found", event.windowId)
|
||||
return
|
||||
}
|
||||
// Check if the message starts with "wails:"
|
||||
if strings.HasPrefix(event.message, "wails:") {
|
||||
a.debug("handleWindowMessage: Processing wails message", "message", event.message)
|
||||
window.HandleMessage(event.message)
|
||||
} else {
|
||||
if a.options.RawMessageHandler != nil {
|
||||
a.options.RawMessageHandler(window, event.message, event.originInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) handleWebViewRequest(request *webViewAssetRequest) {
|
||||
defer handlePanic()
|
||||
// Log that we're processing the request
|
||||
url, _ := request.Request.URL()
|
||||
a.debug("handleWebViewRequest: Processing request", "url", url)
|
||||
// IMPORTANT: pass the wrapper request so our injected headers (x-wails-window-id/name) are used
|
||||
a.assets.ServeWebViewRequest(request)
|
||||
a.debug("handleWebViewRequest: Request processing complete", "url", url)
|
||||
}
|
||||
|
||||
func (a *App) handleWindowEvent(event *windowEvent) {
|
||||
defer handlePanic()
|
||||
// Get window from window map
|
||||
a.windowsLock.RLock()
|
||||
window, ok := a.windows[event.WindowID]
|
||||
a.windowsLock.RUnlock()
|
||||
if !ok {
|
||||
a.warning("Window #%d not found", event.WindowID)
|
||||
return
|
||||
}
|
||||
window.HandleWindowEvent(event.EventID)
|
||||
}
|
||||
|
||||
// OnShutdown adds a function to be run when the application is shutting down.
|
||||
func (a *App) OnShutdown(f func()) {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.shutdownLock.Lock()
|
||||
|
||||
if !a.performingShutdown {
|
||||
defer a.shutdownLock.Unlock()
|
||||
a.shutdownTasks = append(a.shutdownTasks, f)
|
||||
return
|
||||
}
|
||||
|
||||
a.shutdownLock.Unlock()
|
||||
InvokeAsync(f)
|
||||
}
|
||||
|
||||
func (a *App) cleanup() {
|
||||
a.shutdownLock.Lock()
|
||||
if a.performingShutdown {
|
||||
a.shutdownLock.Unlock()
|
||||
return
|
||||
}
|
||||
a.cancel() // Cancel app context before running shutdown hooks.
|
||||
a.performingShutdown = true
|
||||
a.shutdownLock.Unlock()
|
||||
|
||||
// No need to hold the lock here because a.shutdownTasks
|
||||
// may only change while a.performingShutdown is false.
|
||||
for _, shutdownTask := range a.shutdownTasks {
|
||||
InvokeSync(shutdownTask)
|
||||
}
|
||||
// Release any global shortcuts the application registered with the OS.
|
||||
if a.GlobalShortcut != nil {
|
||||
if err := a.GlobalShortcut.UnregisterAll(); err != nil {
|
||||
a.handleError(err)
|
||||
}
|
||||
}
|
||||
InvokeSync(func() {
|
||||
a.shutdownServices()
|
||||
a.windowsLock.Lock()
|
||||
for _, window := range a.windows {
|
||||
window.Close()
|
||||
}
|
||||
a.windows = nil
|
||||
a.windowsLock.Unlock()
|
||||
a.systemTraysLock.Lock()
|
||||
for _, systray := range a.systemTrays {
|
||||
systray.destroy()
|
||||
}
|
||||
a.systemTrays = nil
|
||||
a.systemTraysLock.Unlock()
|
||||
|
||||
// Cleanup single instance manager
|
||||
if a.singleInstanceManager != nil {
|
||||
a.singleInstanceManager.cleanup()
|
||||
}
|
||||
|
||||
a.postQuit()
|
||||
|
||||
if a.options.PostShutdown != nil {
|
||||
a.options.PostShutdown()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) Quit() {
|
||||
if a.impl != nil {
|
||||
InvokeSync(a.impl.destroy)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) SetIcon(icon []byte) {
|
||||
if a.impl != nil {
|
||||
a.impl.setIcon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) dispatchOnMainThread(fn func()) {
|
||||
// If we are on the main thread, just call the function
|
||||
if a.impl.isOnMainThread() {
|
||||
fn()
|
||||
return
|
||||
}
|
||||
|
||||
mainThreadFunctionStoreLock.Lock()
|
||||
id := generateFunctionStoreID()
|
||||
mainThreadFunctionStore[id] = fn
|
||||
mainThreadFunctionStoreLock.Unlock()
|
||||
// Call platform specific dispatch function
|
||||
a.impl.dispatchOnMainThread(id)
|
||||
}
|
||||
|
||||
func (a *App) Hide() {
|
||||
if a.impl != nil {
|
||||
a.impl.hide()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Show() {
|
||||
if a.impl != nil {
|
||||
a.impl.show()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) runOrDeferToAppRun(r runnable) {
|
||||
a.runLock.Lock()
|
||||
|
||||
if !a.running {
|
||||
defer a.runLock.Unlock() // Defer unlocking for panic tolerance.
|
||||
a.pendingRun = append(a.pendingRun, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Unlock immediately to prevent deadlocks.
|
||||
// No TOC/TOU risk here because a.running can never switch back to false.
|
||||
a.runLock.Unlock()
|
||||
r.Run()
|
||||
}
|
||||
|
||||
func (a *App) handleWindowKeyEvent(event *windowKeyEvent) {
|
||||
defer handlePanic()
|
||||
// Get window from window map
|
||||
a.windowsLock.RLock()
|
||||
window, ok := a.windows[event.windowId]
|
||||
a.windowsLock.RUnlock()
|
||||
if !ok {
|
||||
a.warning("WebviewWindow #%d not found", event.windowId)
|
||||
return
|
||||
}
|
||||
// Get callback from window
|
||||
window.HandleKeyEvent(event.acceleratorString)
|
||||
}
|
||||
|
||||
func (a *App) shouldQuit() bool {
|
||||
if a.options.ShouldQuit != nil {
|
||||
return a.options.ShouldQuit()
|
||||
}
|
||||
return true
|
||||
}
|
||||
1164
vendor/github.com/wailsapp/wails/v3/pkg/application/application_android.go
generated
vendored
Normal file
1164
vendor/github.com/wailsapp/wails/v3/pkg/application/application_android.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
190
vendor/github.com/wailsapp/wails/v3/pkg/application/application_android_nocgo.go
generated
vendored
Normal file
190
vendor/github.com/wailsapp/wails/v3/pkg/application/application_android_nocgo.go
generated
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
//go:build android && !cgo && !server
|
||||
|
||||
package application
|
||||
|
||||
// This file keeps GOOS=android builds compiling without cgo (used by
|
||||
// tooling such as `wails3 generate bindings`). A real Android app is always
|
||||
// built with CGO_ENABLED=1 — see application_android.go for the JNI bridge.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
var (
|
||||
globalApp *App
|
||||
globalAppLock sync.RWMutex
|
||||
|
||||
androidMainFunc func()
|
||||
androidMainLock sync.Mutex
|
||||
)
|
||||
|
||||
func androidLogf(level string, format string, a ...interface{}) {
|
||||
println(fmt.Sprintf("[Android/%s] %s", level, fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
func androidDebugLogf(format string, a ...interface{}) {
|
||||
if androidVerboseLogging {
|
||||
androidLogf("debug", format, a...)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAndroidMain registers the main function to be called when the
|
||||
// Android app starts. Call it from init() in your main package.
|
||||
func RegisterAndroidMain(mainFunc func()) {
|
||||
androidMainLock.Lock()
|
||||
defer androidMainLock.Unlock()
|
||||
androidMainFunc = mainFunc
|
||||
}
|
||||
|
||||
// Go-level bridge call API stubs (no JNI without cgo)
|
||||
|
||||
func androidBridgeString(method string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func androidBridgeVoidString(method string, arg string) {}
|
||||
|
||||
func androidBridgeVoidInt(method string, v int) {}
|
||||
|
||||
func androidBridgeVoidIntString(method string, id int, arg string) {}
|
||||
|
||||
func androidBridgeBool(method string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func executeJavaScript(js string) {
|
||||
androidLogf("warn", "executeJavaScript called but cgo is not enabled")
|
||||
}
|
||||
|
||||
func (a *App) platformRun() {
|
||||
globalAppLock.Lock()
|
||||
globalApp = a
|
||||
globalAppLock.Unlock()
|
||||
|
||||
applicationEvents <- newApplicationEvent(events.Android.ActivityCreated)
|
||||
|
||||
// Block forever - Android manages the app lifecycle via JNI callbacks
|
||||
select {}
|
||||
}
|
||||
|
||||
func (a *App) platformQuit() {
|
||||
}
|
||||
|
||||
func (a *App) isDarkMode() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) isWindows() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Platform-specific app implementation for Android
|
||||
type androidApp struct {
|
||||
parent *App
|
||||
}
|
||||
|
||||
func newPlatformApp(app *App) *androidApp {
|
||||
return &androidApp{
|
||||
parent: app,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *androidApp) run() error {
|
||||
a.setupCommonEvents()
|
||||
a.parent.platformRun()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *androidApp) destroy() {
|
||||
}
|
||||
|
||||
func (a *androidApp) setIcon(_ []byte) {
|
||||
}
|
||||
|
||||
func (a *androidApp) name() string {
|
||||
return a.parent.options.Name
|
||||
}
|
||||
|
||||
func (a *androidApp) GetFlags(options Options) map[string]any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *androidApp) getAccentColor() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *androidApp) getCurrentWindowID() uint {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (a *androidApp) hide() {
|
||||
}
|
||||
|
||||
func (a *androidApp) isDarkMode() bool {
|
||||
return a.parent.isDarkMode()
|
||||
}
|
||||
|
||||
func (a *androidApp) on(eventID uint) {
|
||||
registerAndroidListener(eventID)
|
||||
}
|
||||
|
||||
func (a *androidApp) setApplicationMenu(_ *Menu) {
|
||||
}
|
||||
|
||||
func (a *androidApp) show() {
|
||||
}
|
||||
|
||||
func (a *androidApp) showAboutDialog(_ string, _ string, _ []byte) {
|
||||
}
|
||||
|
||||
func (a *androidApp) getPrimaryScreen() (*Screen, error) {
|
||||
if a.parent.Screen.GetPrimary() == nil {
|
||||
screens, err := getScreens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.parent.Screen.LayoutScreens(screens); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return a.parent.Screen.GetPrimary(), nil
|
||||
}
|
||||
|
||||
func (a *androidApp) getScreens() ([]*Screen, error) {
|
||||
if len(a.parent.Screen.GetAll()) == 0 {
|
||||
screens, err := getScreens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.parent.Screen.LayoutScreens(screens); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return a.parent.Screen.GetAll(), nil
|
||||
}
|
||||
|
||||
func (a *App) logPlatformInfo() {
|
||||
}
|
||||
|
||||
func (a *App) platformEnvironment() map[string]any {
|
||||
return map[string]any{
|
||||
"platform": "android",
|
||||
}
|
||||
}
|
||||
|
||||
func fatalHandler(errFunc func(error)) {
|
||||
}
|
||||
|
||||
var (
|
||||
androidEventListeners = make(map[uint]bool)
|
||||
androidEventListenersLock sync.RWMutex
|
||||
)
|
||||
|
||||
func registerAndroidListener(eventID uint) {
|
||||
androidEventListenersLock.Lock()
|
||||
defer androidEventListenersLock.Unlock()
|
||||
androidEventListeners[eventID] = true
|
||||
}
|
||||
774
vendor/github.com/wailsapp/wails/v3/pkg/application/application_darwin.go
generated
vendored
Normal file
774
vendor/github.com/wailsapp/wails/v3/pkg/application/application_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,774 @@
|
||||
//go:build darwin && !ios && !server
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
|
||||
#cgo CFLAGS: -mmacosx-version-min=10.13 -x objective-c
|
||||
#cgo LDFLAGS: -framework Cocoa -mmacosx-version-min=10.13
|
||||
|
||||
#include "application_darwin.h"
|
||||
#include "application_darwin_delegate.h"
|
||||
#include "webview_window_darwin.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
extern void registerListener(unsigned int event);
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
static AppDelegate *appDelegate = nil;
|
||||
|
||||
static void init(void) {
|
||||
[NSApplication sharedApplication];
|
||||
appDelegate = [[AppDelegate alloc] init];
|
||||
[NSApp setDelegate:appDelegate];
|
||||
|
||||
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
|
||||
NSWindow* eventWindow = [event window];
|
||||
if (eventWindow == nil ) {
|
||||
return event;
|
||||
}
|
||||
WebviewWindowDelegate* windowDelegate = (WebviewWindowDelegate*)[eventWindow delegate];
|
||||
if (windowDelegate == nil) {
|
||||
return event;
|
||||
}
|
||||
if ([windowDelegate respondsToSelector:@selector(handleLeftMouseDown:)]) {
|
||||
[windowDelegate handleLeftMouseDown:event];
|
||||
}
|
||||
return event;
|
||||
}];
|
||||
|
||||
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseUp handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
|
||||
NSWindow* eventWindow = [event window];
|
||||
if (eventWindow == nil ) {
|
||||
return event;
|
||||
}
|
||||
WebviewWindowDelegate* windowDelegate = (WebviewWindowDelegate*)[eventWindow delegate];
|
||||
if (windowDelegate == nil) {
|
||||
return event;
|
||||
}
|
||||
if ([windowDelegate respondsToSelector:@selector(handleLeftMouseUp:)]) {
|
||||
[windowDelegate handleLeftMouseUp:eventWindow];
|
||||
}
|
||||
return event;
|
||||
}];
|
||||
|
||||
NSDistributedNotificationCenter *center = [NSDistributedNotificationCenter defaultCenter];
|
||||
[center addObserver:appDelegate selector:@selector(themeChanged:) name:@"AppleInterfaceThemeChangedNotification" object:nil];
|
||||
|
||||
// Workspace power notifications are posted on a separate notification
|
||||
// center from the default one — apps must observe NSWorkspace's centre
|
||||
// to receive sleep/wake events. Mirrors WM_POWERBROADCAST on Windows.
|
||||
NSNotificationCenter *workspaceCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
[workspaceCenter addObserver:appDelegate selector:@selector(workspaceWillSleep:) name:NSWorkspaceWillSleepNotification object:nil];
|
||||
[workspaceCenter addObserver:appDelegate selector:@selector(workspaceDidWake:) name:NSWorkspaceDidWakeNotification object:nil];
|
||||
[workspaceCenter addObserver:appDelegate selector:@selector(workspaceScreensDidSleep:) name:NSWorkspaceScreensDidSleepNotification object:nil];
|
||||
[workspaceCenter addObserver:appDelegate selector:@selector(workspaceScreensDidWake:) name:NSWorkspaceScreensDidWakeNotification object:nil];
|
||||
|
||||
// Register the custom URL scheme handler
|
||||
StartCustomProtocolHandler();
|
||||
}
|
||||
|
||||
static bool isDarkMode(void) {
|
||||
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
|
||||
if (userDefaults == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NSString *interfaceStyle = [userDefaults stringForKey:@"AppleInterfaceStyle"];
|
||||
if (interfaceStyle == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [interfaceStyle isEqualToString:@"Dark"];
|
||||
}
|
||||
|
||||
static char* getAccentColor(void) {
|
||||
@autoreleasepool {
|
||||
NSColor *accentColor;
|
||||
if (@available(macOS 10.14, *)) {
|
||||
accentColor = [NSColor controlAccentColor];
|
||||
} else {
|
||||
// Fallback to system blue for older macOS versions
|
||||
accentColor = [NSColor systemBlueColor];
|
||||
}
|
||||
// Convert to RGB color space
|
||||
NSColor *rgbColor = [accentColor colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
|
||||
if (rgbColor == nil) {
|
||||
rgbColor = accentColor;
|
||||
}
|
||||
// Get RGB components
|
||||
CGFloat red, green, blue, alpha;
|
||||
[rgbColor getRed:&red green:&green blue:&blue alpha:&alpha];
|
||||
// Convert to 0-255 range and format as rgb() string
|
||||
int r = (int)(red * 255);
|
||||
int g = (int)(green * 255);
|
||||
int b = (int)(blue * 255);
|
||||
NSString *colorString = [NSString stringWithFormat:@"rgb(%d,%d,%d)", r, g, b];
|
||||
return strdup([colorString UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
static void setApplicationShouldTerminateAfterLastWindowClosed(bool shouldTerminate) {
|
||||
// Get the NSApp delegate
|
||||
AppDelegate *appDelegate = (AppDelegate*)[NSApp delegate];
|
||||
// Set the applicationShouldTerminateAfterLastWindowClosed boolean
|
||||
appDelegate.shouldTerminateWhenLastWindowClosed = shouldTerminate;
|
||||
}
|
||||
|
||||
static void setActivationPolicy(int policy) {
|
||||
[NSApp setActivationPolicy:policy];
|
||||
}
|
||||
|
||||
static void activateIgnoringOtherApps() {
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
}
|
||||
|
||||
static void run(void) {
|
||||
@autoreleasepool {
|
||||
[NSApp run];
|
||||
[appDelegate release];
|
||||
[NSApp abortModal];
|
||||
}
|
||||
}
|
||||
|
||||
// destroyApp destroys the application
|
||||
static void destroyApp(void) {
|
||||
[NSApp terminate:nil];
|
||||
}
|
||||
|
||||
// Set the application menu
|
||||
static void setApplicationMenu(void *menu) {
|
||||
NSMenu *nsMenu = (__bridge NSMenu *)menu;
|
||||
[NSApp setMainMenu:menu];
|
||||
}
|
||||
|
||||
// Get the application name
|
||||
static char* getAppName(void) {
|
||||
NSString *appName = [NSRunningApplication currentApplication].localizedName;
|
||||
if( appName == nil ) {
|
||||
appName = [[NSProcessInfo processInfo] processName];
|
||||
}
|
||||
return strdup([appName UTF8String]);
|
||||
}
|
||||
|
||||
// get the current window ID
|
||||
static unsigned int getCurrentWindowID(void) {
|
||||
// AppKit must be accessed on the main thread. This function may be called
|
||||
// from arbitrary Go goroutines, so we hop to the main queue when needed.
|
||||
__block unsigned int result = 0;
|
||||
if (NSApp == nil) {
|
||||
return result;
|
||||
}
|
||||
void (^resolve)(void) = ^{
|
||||
NSWindow *window = [NSApp keyWindow];
|
||||
if (window == nil) {
|
||||
window = [NSApp mainWindow];
|
||||
}
|
||||
if (window == nil) {
|
||||
return;
|
||||
}
|
||||
// System panels (e.g. PMPrintPanelController) can become the key window;
|
||||
// their delegates are not WebviewWindowDelegate and would crash on windowId.
|
||||
id delegateObj = [window delegate];
|
||||
if (![delegateObj isKindOfClass:[WebviewWindowDelegate class]]) {
|
||||
return;
|
||||
}
|
||||
WebviewWindowDelegate *delegate = (WebviewWindowDelegate*)delegateObj;
|
||||
if (delegate != nil) {
|
||||
result = delegate.windowId;
|
||||
}
|
||||
};
|
||||
if ([NSThread isMainThread]) {
|
||||
resolve();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), resolve);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Set the application icon
|
||||
static void setApplicationIcon(void *icon, int length) {
|
||||
// On main thread
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSImage *image = [[NSImage alloc] initWithData:[NSData dataWithBytes:icon length:length]];
|
||||
[NSApp setApplicationIconImage:image];
|
||||
});
|
||||
}
|
||||
|
||||
// Hide the application
|
||||
static void hide(void) {
|
||||
[NSApp hide:nil];
|
||||
}
|
||||
|
||||
// Show the application
|
||||
static void show(void) {
|
||||
[NSApp unhide:nil];
|
||||
}
|
||||
|
||||
static const char* serializationNSDictionary(void *dict) {
|
||||
@autoreleasepool {
|
||||
NSDictionary *nsDict = (__bridge NSDictionary *)dict;
|
||||
|
||||
if ([NSJSONSerialization isValidJSONObject:nsDict]) {
|
||||
NSError *error;
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:nsDict options:kNilOptions error:&error];
|
||||
NSString *result = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
|
||||
|
||||
return strdup([result UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
static void startSingleInstanceListener(const char *uniqueID) {
|
||||
// Convert to NSString
|
||||
NSString *uid = [NSString stringWithUTF8String:uniqueID];
|
||||
[[NSDistributedNotificationCenter defaultCenter] addObserver:appDelegate
|
||||
selector:@selector(handleSecondInstanceNotification:) name:uid object:nil];
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/assetserver/webview"
|
||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
type macosApp struct {
|
||||
applicationMenu unsafe.Pointer
|
||||
parent *App
|
||||
}
|
||||
|
||||
func (m *macosApp) isDarkMode() bool {
|
||||
return bool(C.isDarkMode())
|
||||
}
|
||||
|
||||
func (m *macosApp) getAccentColor() string {
|
||||
accentColorC := C.getAccentColor()
|
||||
defer C.free(unsafe.Pointer(accentColorC))
|
||||
return C.GoString(accentColorC)
|
||||
}
|
||||
|
||||
func getNativeApplication() *macosApp {
|
||||
return globalApplication.impl.(*macosApp)
|
||||
}
|
||||
|
||||
func (m *macosApp) hide() {
|
||||
C.hide()
|
||||
}
|
||||
|
||||
func (m *macosApp) show() {
|
||||
C.show()
|
||||
}
|
||||
|
||||
func (m *macosApp) on(eventID uint) {
|
||||
C.registerListener(C.uint(eventID))
|
||||
}
|
||||
|
||||
func (m *macosApp) setIcon(icon []byte) {
|
||||
C.setApplicationIcon(unsafe.Pointer(&icon[0]), C.int(len(icon)))
|
||||
}
|
||||
|
||||
func (m *macosApp) name() string {
|
||||
appName := C.getAppName()
|
||||
defer C.free(unsafe.Pointer(appName))
|
||||
return C.GoString(appName)
|
||||
}
|
||||
|
||||
func (m *macosApp) getCurrentWindowID() uint {
|
||||
return uint(C.getCurrentWindowID())
|
||||
}
|
||||
|
||||
func (m *macosApp) setApplicationMenu(menu *Menu) {
|
||||
if menu == nil {
|
||||
// Create a default menu for mac
|
||||
menu = DefaultApplicationMenu()
|
||||
}
|
||||
menu.Update()
|
||||
|
||||
// Convert impl to macosMenu object
|
||||
m.applicationMenu = (menu.impl).(*macosMenu).nsMenu
|
||||
C.setApplicationMenu(m.applicationMenu)
|
||||
}
|
||||
|
||||
func (m *macosApp) run() error {
|
||||
if m.parent.options.SingleInstance != nil {
|
||||
cUniqueID := C.CString(m.parent.options.SingleInstance.UniqueID)
|
||||
defer C.free(unsafe.Pointer(cUniqueID))
|
||||
C.startSingleInstanceListener(cUniqueID)
|
||||
}
|
||||
// Add a hook to the ApplicationDidFinishLaunching event
|
||||
m.parent.Event.OnApplicationEvent(
|
||||
events.Mac.ApplicationDidFinishLaunching,
|
||||
func(*ApplicationEvent) {
|
||||
C.setApplicationShouldTerminateAfterLastWindowClosed(
|
||||
C.bool(m.parent.options.Mac.ApplicationShouldTerminateAfterLastWindowClosed),
|
||||
)
|
||||
C.setActivationPolicy(C.int(m.parent.options.Mac.ActivationPolicy))
|
||||
C.activateIgnoringOtherApps()
|
||||
if err := m.processAndCacheScreens(); err != nil {
|
||||
m.parent.handleError(err)
|
||||
}
|
||||
},
|
||||
)
|
||||
// Refresh screen cache when display configuration changes
|
||||
m.parent.Event.OnApplicationEvent(
|
||||
events.Mac.ApplicationDidChangeScreenParameters,
|
||||
func(*ApplicationEvent) {
|
||||
if err := m.processAndCacheScreens(); err != nil {
|
||||
m.parent.handleError(err)
|
||||
}
|
||||
},
|
||||
)
|
||||
m.setupCommonEvents()
|
||||
// setup event listeners
|
||||
for eventID := range m.parent.applicationEventListeners {
|
||||
m.on(eventID)
|
||||
}
|
||||
C.run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *macosApp) destroy() {
|
||||
C.destroyApp()
|
||||
}
|
||||
|
||||
func (m *macosApp) GetFlags(options Options) map[string]any {
|
||||
if options.Flags == nil {
|
||||
options.Flags = make(map[string]any)
|
||||
}
|
||||
return options.Flags
|
||||
}
|
||||
|
||||
func newPlatformApp(app *App) *macosApp {
|
||||
C.init()
|
||||
return &macosApp{
|
||||
parent: app,
|
||||
}
|
||||
}
|
||||
|
||||
//export processApplicationEvent
|
||||
func processApplicationEvent(eventID C.uint, data unsafe.Pointer) {
|
||||
event := newApplicationEvent(events.ApplicationEventType(eventID))
|
||||
|
||||
if data != nil {
|
||||
dataCStrJSON := C.serializationNSDictionary(data)
|
||||
if dataCStrJSON != nil {
|
||||
defer C.free(unsafe.Pointer(dataCStrJSON))
|
||||
|
||||
dataJSON := C.GoString(dataCStrJSON)
|
||||
var result map[string]any
|
||||
err := json.Unmarshal([]byte(dataJSON), &result)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
event.Context().setData(result)
|
||||
}
|
||||
}
|
||||
|
||||
switch event.Id {
|
||||
case uint(events.Mac.ApplicationDidChangeTheme):
|
||||
isDark := globalApplication.Env.IsDarkMode()
|
||||
event.Context().setIsDarkMode(isDark)
|
||||
}
|
||||
applicationEvents <- event
|
||||
}
|
||||
|
||||
//export processWindowEvent
|
||||
func processWindowEvent(windowID C.uint, eventID C.uint) {
|
||||
windowEvents <- &windowEvent{
|
||||
WindowID: uint(windowID),
|
||||
EventID: uint(eventID),
|
||||
}
|
||||
}
|
||||
|
||||
//export processMessage
|
||||
func processMessage(windowID C.uint, message *C.char, origin *C.char, isMainFrame bool) {
|
||||
o := ""
|
||||
if origin != nil {
|
||||
o = C.GoString(origin)
|
||||
}
|
||||
windowMessageBuffer <- &windowMessage{
|
||||
windowId: uint(windowID),
|
||||
message: C.GoString(message),
|
||||
originInfo: &OriginInfo{
|
||||
Origin: o,
|
||||
IsMainFrame: isMainFrame,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//export processURLRequest
|
||||
func processURLRequest(windowID C.uint, wkUrlSchemeTask unsafe.Pointer) {
|
||||
window, ok := globalApplication.Window.GetByID(uint(windowID))
|
||||
if !ok || window == nil {
|
||||
globalApplication.debug("could not find window with id", "windowID", windowID)
|
||||
return
|
||||
}
|
||||
|
||||
webviewRequests <- &webViewAssetRequest{
|
||||
Request: webview.NewRequest(wkUrlSchemeTask),
|
||||
windowId: uint(windowID),
|
||||
windowName: window.Name(),
|
||||
}
|
||||
}
|
||||
|
||||
//export processWindowKeyDownEvent
|
||||
func processWindowKeyDownEvent(windowID C.uint, acceleratorString *C.char) {
|
||||
windowKeyEvents <- &windowKeyEvent{
|
||||
windowId: uint(windowID),
|
||||
acceleratorString: C.GoString(acceleratorString),
|
||||
}
|
||||
}
|
||||
|
||||
//export processDragItems
|
||||
func processDragItems(windowID C.uint, arr **C.char, length C.int, x C.int, y C.int) {
|
||||
var filenames []string
|
||||
// Convert the C array to a Go slice
|
||||
goSlice := (*[1 << 30]*C.char)(unsafe.Pointer(arr))[:length:length]
|
||||
for _, str := range goSlice {
|
||||
filenames = append(filenames, C.GoString(str))
|
||||
}
|
||||
|
||||
globalApplication.debug(
|
||||
"[DragDropDebug] processDragItems called",
|
||||
"windowID",
|
||||
windowID,
|
||||
"fileCount",
|
||||
len(filenames),
|
||||
"x",
|
||||
x,
|
||||
"y",
|
||||
y,
|
||||
)
|
||||
targetWindow, ok := globalApplication.Window.GetByID(uint(windowID))
|
||||
if !ok || targetWindow == nil {
|
||||
println("Error: processDragItems could not find window with ID:", uint(windowID))
|
||||
return
|
||||
}
|
||||
|
||||
globalApplication.debug(
|
||||
"[DragDropDebug] processDragItems: Calling targetWindow.InitiateFrontendDropProcessing",
|
||||
)
|
||||
targetWindow.InitiateFrontendDropProcessing(filenames, int(x), int(y))
|
||||
}
|
||||
|
||||
//export macosOnDragEnter
|
||||
func macosOnDragEnter(windowID C.uint) {
|
||||
window, ok := globalApplication.Window.GetByID(uint(windowID))
|
||||
if !ok || window == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Call JavaScript to show drag entered state
|
||||
window.ExecJS("window._wails.handleDragEnter();")
|
||||
}
|
||||
|
||||
//export macosOnDragExit
|
||||
func macosOnDragExit(windowID C.uint) {
|
||||
window, ok := globalApplication.Window.GetByID(uint(windowID))
|
||||
if !ok || window == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Call JavaScript to clean up drag state
|
||||
window.ExecJS("window._wails.handleDragLeave();")
|
||||
}
|
||||
|
||||
var (
|
||||
// Pre-allocated buffer for drag JS calls to avoid allocations
|
||||
dragOverJSBuffer = make([]byte, 128) // Increased for safety
|
||||
dragOverJSMutex sync.Mutex // Protects dragOverJSBuffer
|
||||
dragOverJSPrefix = []byte("window._wails.handleDragOver(")
|
||||
|
||||
// Cache window references to avoid repeated lookups
|
||||
windowImplCache sync.Map // windowID -> *macosWebviewWindow
|
||||
|
||||
// Per-window drag throttle state
|
||||
dragThrottle sync.Map // windowID -> *dragThrottleState
|
||||
)
|
||||
|
||||
type dragThrottleState struct {
|
||||
mu sync.Mutex // Protects all fields below
|
||||
lastX, lastY int
|
||||
timer *time.Timer
|
||||
pendingX int
|
||||
pendingY int
|
||||
hasPending bool
|
||||
}
|
||||
|
||||
// clearWindowDragCache removes cached references for a window
|
||||
func clearWindowDragCache(windowID uint) {
|
||||
windowImplCache.Delete(windowID)
|
||||
|
||||
// Cancel any pending timer
|
||||
if throttleVal, ok := dragThrottle.Load(windowID); ok {
|
||||
if throttle, ok := throttleVal.(*dragThrottleState); ok {
|
||||
throttle.mu.Lock()
|
||||
if throttle.timer != nil {
|
||||
throttle.timer.Stop()
|
||||
}
|
||||
throttle.mu.Unlock()
|
||||
}
|
||||
}
|
||||
dragThrottle.Delete(windowID)
|
||||
}
|
||||
|
||||
// writeInt writes an integer to a byte slice and returns the number of bytes written
|
||||
func writeInt(buf []byte, n int) int {
|
||||
if n < 0 {
|
||||
if len(buf) == 0 {
|
||||
return 0
|
||||
}
|
||||
buf[0] = '-'
|
||||
return 1 + writeInt(buf[1:], -n)
|
||||
}
|
||||
if n == 0 {
|
||||
if len(buf) == 0 {
|
||||
return 0
|
||||
}
|
||||
buf[0] = '0'
|
||||
return 1
|
||||
}
|
||||
|
||||
// Count digits
|
||||
tmp := n
|
||||
digits := 0
|
||||
for tmp > 0 {
|
||||
digits++
|
||||
tmp /= 10
|
||||
}
|
||||
|
||||
// Bounds check
|
||||
if digits > len(buf) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Write digits in reverse
|
||||
for i := digits - 1; i >= 0; i-- {
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return digits
|
||||
}
|
||||
|
||||
//export macosOnDragOver
|
||||
func macosOnDragOver(windowID C.uint, x C.int, y C.int) {
|
||||
winID := uint(windowID)
|
||||
intX, intY := int(x), int(y)
|
||||
|
||||
// Get or create throttle state
|
||||
throttleKey := winID
|
||||
throttleVal, _ := dragThrottle.LoadOrStore(throttleKey, &dragThrottleState{
|
||||
lastX: intX,
|
||||
lastY: intY,
|
||||
})
|
||||
throttle := throttleVal.(*dragThrottleState)
|
||||
|
||||
throttle.mu.Lock()
|
||||
|
||||
// Update pending position
|
||||
throttle.pendingX = intX
|
||||
throttle.pendingY = intY
|
||||
throttle.hasPending = true
|
||||
|
||||
// If timer is already running, just update the pending position
|
||||
if throttle.timer != nil {
|
||||
throttle.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Apply 5-pixel threshold for immediate update
|
||||
dx := intX - throttle.lastX
|
||||
dy := intY - throttle.lastY
|
||||
if dx < 0 {
|
||||
dx = -dx
|
||||
}
|
||||
if dy < 0 {
|
||||
dy = -dy
|
||||
}
|
||||
|
||||
// Check if we should send an immediate update
|
||||
shouldSendNow := dx >= 5 || dy >= 5
|
||||
|
||||
if shouldSendNow {
|
||||
// Update last position
|
||||
throttle.lastX = intX
|
||||
throttle.lastY = intY
|
||||
throttle.hasPending = false
|
||||
|
||||
// Send this update immediately (unlock before JS call to avoid deadlock)
|
||||
throttle.mu.Unlock()
|
||||
sendDragUpdate(winID, intX, intY)
|
||||
throttle.mu.Lock()
|
||||
}
|
||||
|
||||
// Start 50ms timer for next update (whether we sent now or not)
|
||||
throttle.timer = time.AfterFunc(50*time.Millisecond, func() {
|
||||
// Execute on main thread to ensure UI updates
|
||||
InvokeSync(func() {
|
||||
throttle.mu.Lock()
|
||||
// Clear timer reference
|
||||
throttle.timer = nil
|
||||
|
||||
// Send pending update if any
|
||||
if throttle.hasPending {
|
||||
pendingX, pendingY := throttle.pendingX, throttle.pendingY
|
||||
throttle.lastX = pendingX
|
||||
throttle.lastY = pendingY
|
||||
throttle.hasPending = false
|
||||
throttle.mu.Unlock()
|
||||
sendDragUpdate(winID, pendingX, pendingY)
|
||||
} else {
|
||||
throttle.mu.Unlock()
|
||||
}
|
||||
})
|
||||
})
|
||||
throttle.mu.Unlock()
|
||||
}
|
||||
|
||||
// sendDragUpdate sends the actual drag update to JavaScript
|
||||
func sendDragUpdate(winID uint, x, y int) {
|
||||
// Try cached implementation first
|
||||
var darwinImpl *macosWebviewWindow
|
||||
var needsExecJS bool
|
||||
|
||||
if cached, found := windowImplCache.Load(winID); found {
|
||||
darwinImpl = cached.(*macosWebviewWindow)
|
||||
if darwinImpl != nil && darwinImpl.nsWindow != nil {
|
||||
needsExecJS = true
|
||||
} else {
|
||||
// Invalid cache entry, remove it
|
||||
windowImplCache.Delete(winID)
|
||||
}
|
||||
}
|
||||
|
||||
if !needsExecJS {
|
||||
// Fallback to full lookup
|
||||
window, ok := globalApplication.Window.GetByID(winID)
|
||||
if !ok || window == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Type assert to WebviewWindow
|
||||
webviewWindow, ok := window.(*WebviewWindow)
|
||||
if !ok || webviewWindow == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get implementation
|
||||
darwinImpl, ok = webviewWindow.impl.(*macosWebviewWindow)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Cache for next time
|
||||
windowImplCache.Store(winID, darwinImpl)
|
||||
needsExecJS = true
|
||||
}
|
||||
|
||||
if !needsExecJS || darwinImpl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Protect shared buffer access
|
||||
dragOverJSMutex.Lock()
|
||||
|
||||
// Build JS string with zero allocations
|
||||
// Format: "window._wails.handleDragOver(X,Y)"
|
||||
// Max length with int32 coords: 30 + 11 + 1 + 11 + 1 + 1 = 55 bytes
|
||||
n := copy(dragOverJSBuffer[:], dragOverJSPrefix)
|
||||
n += writeInt(dragOverJSBuffer[n:], x)
|
||||
if n < len(dragOverJSBuffer) {
|
||||
dragOverJSBuffer[n] = ','
|
||||
n++
|
||||
}
|
||||
n += writeInt(dragOverJSBuffer[n:], y)
|
||||
if n < len(dragOverJSBuffer) {
|
||||
dragOverJSBuffer[n] = ')'
|
||||
n++
|
||||
}
|
||||
if n < len(dragOverJSBuffer) {
|
||||
dragOverJSBuffer[n] = 0 // null terminate for C
|
||||
} else {
|
||||
// Buffer overflow - this should not happen with 128 byte buffer
|
||||
dragOverJSMutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Call JavaScript with zero allocations
|
||||
darwinImpl.execJSDragOver(dragOverJSBuffer[:n+1]) // Include null terminator
|
||||
dragOverJSMutex.Unlock()
|
||||
}
|
||||
|
||||
//export processMenuItemClick
|
||||
func processMenuItemClick(menuID C.uint) {
|
||||
menuItemClicked <- uint(menuID)
|
||||
}
|
||||
|
||||
//export shouldQuitApplication
|
||||
func shouldQuitApplication() C.bool {
|
||||
// TODO: This should be configurable
|
||||
return C.bool(globalApplication.shouldQuit())
|
||||
}
|
||||
|
||||
//export cleanup
|
||||
func cleanup() {
|
||||
globalApplication.cleanup()
|
||||
}
|
||||
|
||||
func (a *App) logPlatformInfo() {
|
||||
info, err := operatingsystem.Info()
|
||||
if err != nil {
|
||||
a.error("error getting OS info: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.info("Platform Info:", info.AsLogSlice()...)
|
||||
|
||||
}
|
||||
|
||||
func (a *App) platformEnvironment() map[string]any {
|
||||
return map[string]any{}
|
||||
}
|
||||
|
||||
func fatalHandler(errFunc func(error)) {
|
||||
return
|
||||
}
|
||||
|
||||
//export HandleOpenFile
|
||||
func HandleOpenFile(filePath *C.char) {
|
||||
goFilepath := C.GoString(filePath)
|
||||
// Create new application event context
|
||||
eventContext := newApplicationEventContext()
|
||||
eventContext.setOpenedWithFile(goFilepath)
|
||||
// EmitEvent application started event
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(events.Common.ApplicationOpenedWithFile),
|
||||
ctx: eventContext,
|
||||
}
|
||||
}
|
||||
|
||||
//export HandleOpenURL
|
||||
func HandleOpenURL(urlCString *C.char) {
|
||||
urlString := C.GoString(urlCString)
|
||||
eventContext := newApplicationEventContext()
|
||||
eventContext.setURL(urlString)
|
||||
|
||||
// Emit the standard event with the URL string as data
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(events.Common.ApplicationLaunchedWithUrl),
|
||||
ctx: eventContext,
|
||||
}
|
||||
}
|
||||
11
vendor/github.com/wailsapp/wails/v3/pkg/application/application_darwin.h
generated
vendored
Normal file
11
vendor/github.com/wailsapp/wails/v3/pkg/application/application_darwin.h
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
#ifndef application_h
|
||||
#define application_h
|
||||
|
||||
static void init(void);
|
||||
static void run(void);
|
||||
static void setActivationPolicy(int policy);
|
||||
static char *getAppName(void);
|
||||
|
||||
#endif
|
||||
25
vendor/github.com/wailsapp/wails/v3/pkg/application/application_darwin_delegate.h
generated
vendored
Normal file
25
vendor/github.com/wailsapp/wails/v3/pkg/application/application_darwin_delegate.h
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
#ifndef appdelegate_h
|
||||
#define appdelegate_h
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface AppDelegate : NSResponder <NSApplicationDelegate>
|
||||
@property bool shouldTerminateWhenLastWindowClosed;
|
||||
@property bool shuttingDown;
|
||||
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app;
|
||||
@end
|
||||
|
||||
extern void HandleOpenFile(char *);
|
||||
|
||||
// Declarations for Apple Event based custom URL handling and universal link
|
||||
extern void HandleOpenURL(char*);
|
||||
|
||||
@interface CustomProtocolSchemeHandler : NSObject
|
||||
+ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
|
||||
@end
|
||||
|
||||
void StartCustomProtocolHandler(void);
|
||||
|
||||
#endif /* appdelegate_h */
|
||||
227
vendor/github.com/wailsapp/wails/v3/pkg/application/application_darwin_delegate.m
generated
vendored
Normal file
227
vendor/github.com/wailsapp/wails/v3/pkg/application/application_darwin_delegate.m
generated
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
//go:build darwin && !ios && !server
|
||||
#import "application_darwin_delegate.h"
|
||||
#import "../events/events_darwin.h"
|
||||
#import <CoreServices/CoreServices.h> // For Apple Event constants
|
||||
extern bool hasListeners(unsigned int);
|
||||
extern bool shouldQuitApplication();
|
||||
extern void cleanup();
|
||||
extern void handleSecondInstanceData(char * message);
|
||||
@implementation AppDelegate
|
||||
- (void)dealloc
|
||||
{
|
||||
[super dealloc];
|
||||
}
|
||||
-(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename
|
||||
{
|
||||
const char* utf8FileName = filename.UTF8String;
|
||||
HandleOpenFile((char*)utf8FileName);
|
||||
return YES;
|
||||
}
|
||||
- (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<NSUserActivityRestoring>> * _Nullable))restorationHandler {
|
||||
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
|
||||
NSURL *url = userActivity.webpageURL;
|
||||
if (url) {
|
||||
HandleOpenURL((char*)[[url absoluteString] UTF8String]);
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
// Create the applicationShouldTerminateAfterLastWindowClosed: method
|
||||
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
|
||||
{
|
||||
return self.shouldTerminateWhenLastWindowClosed;
|
||||
}
|
||||
- (void)themeChanged:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidChangeTheme) ) {
|
||||
processApplicationEvent(EventApplicationDidChangeTheme, NULL);
|
||||
}
|
||||
}
|
||||
- (void)workspaceWillSleep:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationWillSleep) ) {
|
||||
processApplicationEvent(EventApplicationWillSleep, NULL);
|
||||
}
|
||||
}
|
||||
- (void)workspaceDidWake:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidWake) ) {
|
||||
processApplicationEvent(EventApplicationDidWake, NULL);
|
||||
}
|
||||
}
|
||||
- (void)workspaceScreensDidSleep:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationScreensDidSleep) ) {
|
||||
processApplicationEvent(EventApplicationScreensDidSleep, NULL);
|
||||
}
|
||||
}
|
||||
- (void)workspaceScreensDidWake:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationScreensDidWake) ) {
|
||||
processApplicationEvent(EventApplicationScreensDidWake, NULL);
|
||||
}
|
||||
}
|
||||
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
|
||||
if( ! shouldQuitApplication() ) {
|
||||
return NSTerminateCancel;
|
||||
}
|
||||
if( !self.shuttingDown ) {
|
||||
self.shuttingDown = true;
|
||||
cleanup();
|
||||
}
|
||||
return NSTerminateNow;
|
||||
}
|
||||
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
- (BOOL)applicationShouldHandleReopen:(NSNotification *)notification
|
||||
hasVisibleWindows:(BOOL)flag { // Changed from NSApplication to NSNotification
|
||||
if( hasListeners(EventApplicationShouldHandleReopen) ) {
|
||||
processApplicationEvent(EventApplicationShouldHandleReopen, @{@"hasVisibleWindows": @(flag)});
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
- (void)handleSecondInstanceNotification:(NSNotification *)note;
|
||||
{
|
||||
if (note.object != nil) {
|
||||
NSString *message = (NSString *)note.object;
|
||||
const char* utf8Message = message.UTF8String;
|
||||
handleSecondInstanceData((char*)utf8Message);
|
||||
}
|
||||
}
|
||||
// GENERATED EVENTS START
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidBecomeActive) ) {
|
||||
processApplicationEvent(EventApplicationDidBecomeActive, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidChangeBackingProperties:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidChangeBackingProperties) ) {
|
||||
processApplicationEvent(EventApplicationDidChangeBackingProperties, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidChangeEffectiveAppearance:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidChangeEffectiveAppearance) ) {
|
||||
processApplicationEvent(EventApplicationDidChangeEffectiveAppearance, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidChangeIcon:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidChangeIcon) ) {
|
||||
processApplicationEvent(EventApplicationDidChangeIcon, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidChangeOcclusionState:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidChangeOcclusionState) ) {
|
||||
processApplicationEvent(EventApplicationDidChangeOcclusionState, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidChangeScreenParameters:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidChangeScreenParameters) ) {
|
||||
processApplicationEvent(EventApplicationDidChangeScreenParameters, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidChangeStatusBarFrame:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidChangeStatusBarFrame) ) {
|
||||
processApplicationEvent(EventApplicationDidChangeStatusBarFrame, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidChangeStatusBarOrientation:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidChangeStatusBarOrientation) ) {
|
||||
processApplicationEvent(EventApplicationDidChangeStatusBarOrientation, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidFinishLaunching) ) {
|
||||
processApplicationEvent(EventApplicationDidFinishLaunching, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidHide:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidHide) ) {
|
||||
processApplicationEvent(EventApplicationDidHide, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidResignActive:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidResignActive) ) {
|
||||
processApplicationEvent(EventApplicationDidResignActive, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidUnhide:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidUnhide) ) {
|
||||
processApplicationEvent(EventApplicationDidUnhide, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidUpdate:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidUpdate) ) {
|
||||
processApplicationEvent(EventApplicationDidUpdate, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillBecomeActive:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationWillBecomeActive) ) {
|
||||
processApplicationEvent(EventApplicationWillBecomeActive, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationWillFinishLaunching) ) {
|
||||
processApplicationEvent(EventApplicationWillFinishLaunching, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillHide:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationWillHide) ) {
|
||||
processApplicationEvent(EventApplicationWillHide, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationWillResignActive) ) {
|
||||
processApplicationEvent(EventApplicationWillResignActive, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationWillTerminate) ) {
|
||||
processApplicationEvent(EventApplicationWillTerminate, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillUnhide:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationWillUnhide) ) {
|
||||
processApplicationEvent(EventApplicationWillUnhide, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillUpdate:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationWillUpdate) ) {
|
||||
processApplicationEvent(EventApplicationWillUpdate, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// GENERATED EVENTS END
|
||||
@end
|
||||
// Implementation for Apple Event based custom URL handling
|
||||
@implementation CustomProtocolSchemeHandler
|
||||
+ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
|
||||
NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
|
||||
if (urlStr) {
|
||||
HandleOpenURL((char*)[urlStr UTF8String]);
|
||||
}
|
||||
}
|
||||
@end
|
||||
void StartCustomProtocolHandler(void) {
|
||||
NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager];
|
||||
[appleEventManager setEventHandler:[CustomProtocolSchemeHandler class]
|
||||
andSelector:@selector(handleGetURLEvent:withReplyEvent:)
|
||||
forEventClass:kInternetEventClass
|
||||
andEventID: kAEGetURL];
|
||||
}
|
||||
67
vendor/github.com/wailsapp/wails/v3/pkg/application/application_debug.go
generated
vendored
Normal file
67
vendor/github.com/wailsapp/wails/v3/pkg/application/application_debug.go
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
//go:build !production
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/internal/git"
|
||||
"github.com/wailsapp/wails/v3/internal/lo"
|
||||
"github.com/wailsapp/wails/v3/internal/version"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// BuildSettings contains the build settings for the application
|
||||
var BuildSettings map[string]string
|
||||
|
||||
// BuildInfo contains the build info for the application
|
||||
var BuildInfo *debug.BuildInfo
|
||||
|
||||
func init() {
|
||||
var ok bool
|
||||
BuildInfo, ok = debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
BuildSettings = lo.Associate(BuildInfo.Settings, func(setting debug.BuildSetting) (string, string) {
|
||||
return setting.Key, setting.Value
|
||||
})
|
||||
}
|
||||
|
||||
// We use this to patch the application to production mode.
|
||||
func newApplication(options Options) *App {
|
||||
result := &App{
|
||||
isDebugMode: true,
|
||||
options: options,
|
||||
}
|
||||
result.init()
|
||||
return result
|
||||
}
|
||||
|
||||
func (a *App) logStartup() {
|
||||
var args []any
|
||||
|
||||
// BuildInfo is nil when build with garble
|
||||
if BuildInfo == nil {
|
||||
return
|
||||
}
|
||||
|
||||
wailsPackage, _ := lo.Find(BuildInfo.Deps, func(dep *debug.Module) bool {
|
||||
return dep.Path == "github.com/wailsapp/wails/v3"
|
||||
})
|
||||
|
||||
wailsVersion := version.String()
|
||||
if wailsPackage != nil && wailsPackage.Replace != nil {
|
||||
wailsVersion = "(local) => " + filepath.ToSlash(wailsPackage.Replace.Path)
|
||||
// Get the latest commit hash
|
||||
if hash, err := git.HeadHash(filepath.Join(wailsPackage.Replace.Path, "..")); err == nil {
|
||||
wailsVersion += " (" + hash + ")"
|
||||
}
|
||||
}
|
||||
args = append(args, "Wails", wailsVersion)
|
||||
args = append(args, "Compiler", BuildInfo.GoVersion)
|
||||
for key, value := range BuildSettings {
|
||||
args = append(args, key, value)
|
||||
}
|
||||
|
||||
a.info("Build Info:", args...)
|
||||
}
|
||||
51
vendor/github.com/wailsapp/wails/v3/pkg/application/application_dev.go
generated
vendored
Normal file
51
vendor/github.com/wailsapp/wails/v3/pkg/application/application_dev.go
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
//go:build !production
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/assetserver"
|
||||
)
|
||||
|
||||
var devMode = false
|
||||
|
||||
func (a *App) preRun() error {
|
||||
// Check for frontend server url
|
||||
frontendURL := assetserver.GetDevServerURL()
|
||||
if frontendURL != "" {
|
||||
devMode = true
|
||||
// We want to check if the frontend server is running by trying to http get the url
|
||||
// and if it is not, we wait 500ms and try again for a maximum of 10 times. If it is
|
||||
// still not available, we return an error.
|
||||
// This is to allow the frontend server to start up before the backend server.
|
||||
client := http.Client{}
|
||||
a.Logger.Info("Waiting for frontend dev server to start...", "url", frontendURL)
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err := client.Get(frontendURL)
|
||||
if err == nil {
|
||||
a.Logger.Info("Connected to frontend dev server!")
|
||||
return nil
|
||||
}
|
||||
// Wait 500ms
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if i%2 == 0 {
|
||||
a.Logger.Info("Retrying...")
|
||||
}
|
||||
}
|
||||
a.fatal("unable to connect to frontend server. Please check it is running - FRONTEND_DEVSERVER_URL='%s'", frontendURL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) postQuit() {
|
||||
if devMode {
|
||||
a.Logger.Info("The application has terminated, but the watcher is still running.")
|
||||
a.Logger.Info("To terminate the watcher, press CTRL+C")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) enableDevTools() {
|
||||
|
||||
}
|
||||
451
vendor/github.com/wailsapp/wails/v3/pkg/application/application_ios.go
generated
vendored
Normal file
451
vendor/github.com/wailsapp/wails/v3/pkg/application/application_ios.go
generated
vendored
Normal file
@@ -0,0 +1,451 @@
|
||||
//go:build ios && !server
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c -fobjc-arc
|
||||
#cgo LDFLAGS: -framework Foundation -framework UIKit -framework WebKit -framework UniformTypeIdentifiers -framework Network
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "application_ios.h"
|
||||
#include "webview_window_ios.h"
|
||||
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/assetserver/webview"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
func iosConsoleLogf(level string, format string, a ...interface{}) {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
clevel := C.CString(level)
|
||||
cmsg := C.CString(msg)
|
||||
defer C.free(unsafe.Pointer(clevel))
|
||||
defer C.free(unsafe.Pointer(cmsg))
|
||||
C.ios_console_log(clevel, cmsg)
|
||||
}
|
||||
|
||||
// iosDebugLogf is for the framework's internal diagnostics. It compiles to a
|
||||
// no-op in production builds (see ios_logging_production.go).
|
||||
func iosDebugLogf(format string, a ...interface{}) {
|
||||
if iosVerboseLogging {
|
||||
iosConsoleLogf("debug", format, a...)
|
||||
}
|
||||
}
|
||||
|
||||
//export init_go
|
||||
func init_go() {
|
||||
// Called from the iOS main function to initialize the Go runtime.
|
||||
}
|
||||
|
||||
// iosLaunched is closed when UIApplicationDelegate's
|
||||
// didFinishLaunchingWithOptions fires, signalling that UIKit is ready.
|
||||
var (
|
||||
iosLaunched = make(chan struct{})
|
||||
iosLaunchedOnce sync.Once
|
||||
)
|
||||
|
||||
//export iosApplicationDidLaunch
|
||||
func iosApplicationDidLaunch() {
|
||||
iosLaunchedOnce.Do(func() {
|
||||
close(iosLaunched)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) platformRun() {
|
||||
iosDebugLogf("[application_ios.go] platformRun: initialising")
|
||||
|
||||
// Propagate the logging verbosity to the native layer.
|
||||
C.ios_set_verbose_logging(C.bool(iosVerboseLogging))
|
||||
|
||||
C.ios_app_init()
|
||||
|
||||
// The Go runtime is started by the UIApplication delegate's
|
||||
// didFinishLaunchingWithOptions (see application_ios_delegate.m), so by the
|
||||
// time we get here UIKit has already launched and appDelegate/window exist.
|
||||
// Release the window-creation waiter and keep the runtime alive. The OS main
|
||||
// thread is owned by UIApplicationMain (in main.m); this runs on the
|
||||
// background goroutine the delegate started.
|
||||
iosLaunchedOnce.Do(func() {
|
||||
close(iosLaunched)
|
||||
})
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func (a *App) platformQuit() {
|
||||
C.ios_app_quit()
|
||||
}
|
||||
|
||||
func (a *App) isDarkMode() bool {
|
||||
return bool(C.ios_is_dark_mode())
|
||||
}
|
||||
|
||||
func (a *App) isWindows() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//export LogInfo
|
||||
func LogInfo(source *C.char, message *C.char) {
|
||||
goSource := C.GoString(source)
|
||||
goMessage := C.GoString(message)
|
||||
|
||||
if globalApplication != nil && globalApplication.Logger != nil {
|
||||
globalApplication.info("iOS log", "source", goSource, "message", goMessage)
|
||||
} else {
|
||||
iosDebugLogf("[iOS-%s] %s", goSource, goMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-specific app implementation for iOS
|
||||
type iosApp struct {
|
||||
parent *App
|
||||
}
|
||||
|
||||
// newPlatformApp creates an iosApp for the provided App and applies iOS-specific
|
||||
// configuration derived from app.options. It sets input accessory visibility,
|
||||
// scrolling/bounce/indicator behavior, navigation gestures, link preview,
|
||||
// media playback, inspector, user agent strings, app background color, and
|
||||
// native tabs (marshaling items to JSON when enabled). The function invokes
|
||||
// platform bindings to apply these settings and returns the configured *iosApp.
|
||||
func newPlatformApp(app *App) *iosApp {
|
||||
result := &iosApp{
|
||||
parent: app,
|
||||
}
|
||||
// Configure input accessory visibility according to options
|
||||
// Default: false (show accessory) when not explicitly set to true
|
||||
disable := false
|
||||
if app != nil {
|
||||
disable = app.options.IOS.DisableInputAccessoryView
|
||||
}
|
||||
C.ios_set_disable_input_accessory(C.bool(disable))
|
||||
|
||||
// Scrolling / Bounce / Indicators (defaults enabled; using Disable* flags)
|
||||
C.ios_set_disable_scroll(C.bool(app.options.IOS.DisableScroll))
|
||||
C.ios_set_disable_bounce(C.bool(app.options.IOS.DisableBounce))
|
||||
C.ios_set_disable_scroll_indicators(C.bool(app.options.IOS.DisableScrollIndicators))
|
||||
|
||||
// Navigation gestures (Enable*)
|
||||
C.ios_set_enable_back_forward_gestures(C.bool(app.options.IOS.EnableBackForwardNavigationGestures))
|
||||
|
||||
// Link preview (Disable*)
|
||||
C.ios_set_disable_link_preview(C.bool(app.options.IOS.DisableLinkPreview))
|
||||
|
||||
// Media playback
|
||||
C.ios_set_enable_inline_media_playback(C.bool(app.options.IOS.EnableInlineMediaPlayback))
|
||||
C.ios_set_enable_autoplay_without_user_action(C.bool(app.options.IOS.EnableAutoplayWithoutUserAction))
|
||||
|
||||
// Inspector (Disable*)
|
||||
C.ios_set_disable_inspectable(C.bool(app.options.IOS.DisableInspectable))
|
||||
|
||||
// User agent strings
|
||||
if ua := strings.TrimSpace(app.options.IOS.UserAgent); ua != "" {
|
||||
cua := C.CString(ua)
|
||||
C.ios_set_user_agent(cua)
|
||||
C.free(unsafe.Pointer(cua))
|
||||
}
|
||||
if appName := strings.TrimSpace(app.options.IOS.ApplicationNameForUserAgent); appName != "" {
|
||||
cname := C.CString(appName)
|
||||
C.ios_set_app_name_for_user_agent(cname)
|
||||
C.free(unsafe.Pointer(cname))
|
||||
}
|
||||
// App-wide background colour for the iOS window (shown before the WebView
|
||||
// paints). A non-zero BackgroundColour is applied; the zero value (RGBA{})
|
||||
// means "unset" and the delegate falls back to white.
|
||||
if app.options.IOS.BackgroundColour != (RGBA{}) {
|
||||
rgba := app.options.IOS.BackgroundColour
|
||||
C.ios_set_app_background_color(
|
||||
C.uchar(rgba.Red), C.uchar(rgba.Green), C.uchar(rgba.Blue), C.uchar(rgba.Alpha), C.bool(true),
|
||||
)
|
||||
} else {
|
||||
// Not set: the delegate falls back to white.
|
||||
C.ios_set_app_background_color(255, 255, 255, 255, C.bool(false))
|
||||
}
|
||||
// Native tabs option: only enable when explicitly requested
|
||||
if app.options.IOS.EnableNativeTabs {
|
||||
if len(app.options.IOS.NativeTabsItems) > 0 {
|
||||
if data, err := json.Marshal(app.options.IOS.NativeTabsItems); err == nil {
|
||||
cjson := C.CString(string(data))
|
||||
C.ios_native_tabs_set_items_json(cjson)
|
||||
C.free(unsafe.Pointer(cjson))
|
||||
} else if globalApplication != nil {
|
||||
globalApplication.error("Failed to marshal IOS.NativeTabsItems: %v", err)
|
||||
}
|
||||
}
|
||||
C.ios_native_tabs_set_enabled(C.bool(true))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (a *iosApp) run() error {
|
||||
// CRITICAL (gomobile/Gio model): nothing may run on the main thread before
|
||||
// UIApplicationMain, or UIKit never delivers the launch to the delegate on a
|
||||
// physical device (blank screen). So defer ALL startup work — including the
|
||||
// pure-Go common-event wiring — to a goroutine. The main goroutine does
|
||||
// nothing but call UIApplicationMain (via platformRun) below.
|
||||
go func() {
|
||||
// Wire common events (maps ApplicationDidFinishLaunching ->
|
||||
// Common.ApplicationStarted) before the launch event is emitted.
|
||||
a.setupCommonEvents()
|
||||
|
||||
<-iosLaunched
|
||||
|
||||
// Populate the ScreenManager so Screens.* runtime calls return data.
|
||||
if screens, err := getScreens(); err == nil && len(screens) > 0 {
|
||||
if err := a.parent.Screen.LayoutScreens(screens); err != nil {
|
||||
iosConsoleLogf("error", "[application_ios.go] LayoutScreens failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the native system-event monitors (battery, network, lock, theme,
|
||||
// app lifecycle, memory). They emit "system:*" custom events to JS.
|
||||
C.ios_start_system_event_monitors()
|
||||
|
||||
// Emit the launch event now that listeners are wired and UIKit is up.
|
||||
applicationEvents <- newApplicationEvent(events.IOS.ApplicationDidFinishLaunching)
|
||||
}()
|
||||
|
||||
// Hand the main thread to UIKit. platformRun calls UIApplicationMain and does
|
||||
// not return for the lifetime of the app.
|
||||
a.parent.platformRun()
|
||||
|
||||
iosConsoleLogf("error", "[application_ios.go] ERROR: platformRun() returned unexpectedly")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *iosApp) destroy() {
|
||||
// Cleanup iOS resources
|
||||
}
|
||||
|
||||
func (a *iosApp) setIcon(_ []byte) {
|
||||
// iOS app icon is set through Info.plist
|
||||
}
|
||||
|
||||
func (a *iosApp) name() string {
|
||||
return a.parent.options.Name
|
||||
}
|
||||
|
||||
func (a *iosApp) GetFlags(options Options) map[string]any {
|
||||
return nil
|
||||
}
|
||||
|
||||
// dispatchOnMainThread is implemented in mainthread_ios.go
|
||||
|
||||
func (a *iosApp) getAccentColor() string {
|
||||
// iOS accent color
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *iosApp) getCurrentWindowID() uint {
|
||||
// iOS current window ID
|
||||
return 0
|
||||
}
|
||||
|
||||
func (a *iosApp) hide() {
|
||||
// iOS hide application - minimize to background
|
||||
}
|
||||
|
||||
func (a *iosApp) isDarkMode() bool {
|
||||
return a.parent.isDarkMode()
|
||||
}
|
||||
|
||||
// isOnMainThread is implemented in mainthread_ios.go
|
||||
|
||||
func (a *iosApp) on(eventID uint) {
|
||||
registerIOSListener(eventID)
|
||||
}
|
||||
|
||||
func (a *iosApp) setApplicationMenu(_ *Menu) {
|
||||
// iOS doesn't have application menus
|
||||
}
|
||||
|
||||
func (a *iosApp) show() {
|
||||
// iOS show application
|
||||
}
|
||||
|
||||
func (a *iosApp) showAboutDialog(_ string, _ string, _ []byte) {
|
||||
// iOS about dialog
|
||||
}
|
||||
|
||||
func (a *iosApp) getPrimaryScreen() (*Screen, error) {
|
||||
screens, err := getScreens()
|
||||
if err != nil || len(screens) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return screens[0], nil
|
||||
}
|
||||
|
||||
func (a *iosApp) getScreens() ([]*Screen, error) {
|
||||
return getScreens()
|
||||
}
|
||||
|
||||
func (a *App) logPlatformInfo() {
|
||||
// Log iOS platform info
|
||||
}
|
||||
|
||||
func (a *App) platformEnvironment() map[string]any {
|
||||
return map[string]any{
|
||||
"platform": "ios",
|
||||
}
|
||||
}
|
||||
|
||||
func fatalHandler(errFunc func(error)) {
|
||||
// iOS fatal handler
|
||||
}
|
||||
|
||||
// ExecuteJavaScript runs JavaScript code in the WebView
|
||||
func (a *App) ExecuteJavaScript(windowID uint, js string) {
|
||||
cjs := C.CString(js)
|
||||
defer C.free(unsafe.Pointer(cjs))
|
||||
C.ios_execute_javascript(C.uint(windowID), cjs)
|
||||
}
|
||||
|
||||
// iosRuntimeReadyWindows tracks windows for which a synthetic
|
||||
// "wails:runtime:ready" has been injected (see ServeAssetRequest).
|
||||
var iosRuntimeReadyWindows sync.Map
|
||||
|
||||
// ServeAssetRequest handles requests from the WebView
|
||||
//
|
||||
//export ServeAssetRequest
|
||||
func ServeAssetRequest(windowID C.uint, urlSchemeTask unsafe.Pointer) {
|
||||
// Run synchronously on the calling (UIKit) thread and hand the request off to
|
||||
// the long-lived reader goroutine via the buffered webviewRequests channel.
|
||||
//
|
||||
// We deliberately do NOT spawn a goroutine here. A goroutine started from
|
||||
// within a cgo //export call is not reliably scheduled during cold launch:
|
||||
// it could be created but never run, leaving asset requests unserved and the
|
||||
// WebView blank (an intermittent white/blank screen on relaunch). Waking the
|
||||
// already-running reader via a channel send is reliable, and webviewRequests
|
||||
// is buffered generously so this send does not block the UIKit thread.
|
||||
req := webview.NewRequest(urlSchemeTask)
|
||||
url, _ := req.URL()
|
||||
|
||||
// The JS runtime announces itself via a "wails:runtime:ready" postMessage,
|
||||
// but that can be dropped during the initial load; a /wails/runtime call
|
||||
// proves the runtime is mounted, so treat the first one as an implicit ready
|
||||
// signal (duplicate ready messages are handled gracefully).
|
||||
if strings.Contains(url, "/wails/runtime") {
|
||||
if _, alreadyReady := iosRuntimeReadyWindows.LoadOrStore(uint(windowID), true); !alreadyReady {
|
||||
windowMessageBuffer <- &windowMessage{
|
||||
windowId: uint(windowID),
|
||||
message: "wails:runtime:ready",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the window name so the AssetServer receives both x-wails-window-id
|
||||
// and x-wails-window-name headers.
|
||||
winName := ""
|
||||
if globalApplication != nil {
|
||||
if window, ok := globalApplication.Window.GetByID(uint(windowID)); ok && window != nil {
|
||||
winName = window.Name()
|
||||
}
|
||||
}
|
||||
|
||||
webviewRequests <- &webViewAssetRequest{
|
||||
Request: req,
|
||||
windowId: uint(windowID),
|
||||
windowName: winName,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleJSMessage handles messages from JavaScript
|
||||
//
|
||||
//export HandleJSMessage
|
||||
func HandleJSMessage(windowID C.uint, message *C.char) {
|
||||
msg := C.GoString(message)
|
||||
if msg == "" {
|
||||
return
|
||||
}
|
||||
|
||||
iosDebugLogf("[iOS-message] window %d: %s", windowID, msg)
|
||||
|
||||
// Structured payloads carry the message in a "name" or "message" field;
|
||||
// plain strings (e.g. "wails:runtime:ready") are forwarded as-is.
|
||||
var msgData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(msg), &msgData); err == nil && msgData != nil {
|
||||
if name, ok := msgData["name"].(string); ok && name != "" {
|
||||
msg = name
|
||||
} else if name, ok := msgData["message"].(string); ok && name != "" {
|
||||
msg = name
|
||||
}
|
||||
}
|
||||
|
||||
windowMessageBuffer <- &windowMessage{
|
||||
windowId: uint(windowID),
|
||||
message: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// Note: applicationEvents and windowEvents are already defined in events.go
|
||||
// We'll use those existing channels
|
||||
|
||||
type iosWindowEvent struct {
|
||||
WindowID uint
|
||||
EventID uint
|
||||
}
|
||||
|
||||
//export processApplicationEvent
|
||||
func processApplicationEvent(eventID C.uint, data unsafe.Pointer) {
|
||||
iosDebugLogf("[application_ios.go] application event: %d", eventID)
|
||||
|
||||
// Create and send the application event
|
||||
event := newApplicationEvent(events.ApplicationEventType(eventID))
|
||||
|
||||
// Mobile system events (battery, network, theme, …) pass a JSON object
|
||||
// string as data; attach it to the event context so Go listeners can read
|
||||
// it via event.Context().Data() / IsDarkMode(). Lifecycle events pass NULL.
|
||||
if data != nil {
|
||||
if jsonStr := C.GoString((*C.char)(data)); jsonStr != "" {
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(jsonStr), &m); err == nil && m != nil {
|
||||
event.Context().setData(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send to the applicationEvents channel for processing
|
||||
applicationEvents <- event
|
||||
}
|
||||
|
||||
//export processWindowEvent
|
||||
func processWindowEvent(windowID C.uint, eventID C.uint) {
|
||||
iosDebugLogf("[application_ios.go] window event: window %d, event %d", windowID, eventID)
|
||||
windowEvents <- &windowEvent{
|
||||
WindowID: uint(windowID),
|
||||
EventID: uint(eventID),
|
||||
}
|
||||
}
|
||||
|
||||
// iosEventListeners records which native event IDs have at least one Go-side
|
||||
// listener. Registration happens via iosApp.on / iosWebviewWindow.on, which
|
||||
// the cross-platform layer invokes whenever a listener is added. Listeners
|
||||
// are never unregistered natively (same behaviour as macOS).
|
||||
var (
|
||||
iosEventListeners = make(map[uint]bool)
|
||||
iosEventListenersLock sync.RWMutex
|
||||
)
|
||||
|
||||
func registerIOSListener(eventID uint) {
|
||||
iosEventListenersLock.Lock()
|
||||
defer iosEventListenersLock.Unlock()
|
||||
iosEventListeners[eventID] = true
|
||||
}
|
||||
|
||||
//export hasListeners
|
||||
func hasListeners(eventID C.uint) C.bool {
|
||||
iosEventListenersLock.RLock()
|
||||
defer iosEventListenersLock.RUnlock()
|
||||
return C.bool(iosEventListeners[uint(eventID)])
|
||||
}
|
||||
167
vendor/github.com/wailsapp/wails/v3/pkg/application/application_ios.h
generated
vendored
Normal file
167
vendor/github.com/wailsapp/wails/v3/pkg/application/application_ios.h
generated
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
//go:build ios
|
||||
|
||||
#ifndef APPLICATION_IOS_H
|
||||
#define APPLICATION_IOS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
// Forward declarations
|
||||
@class WailsViewController;
|
||||
@class WailsAppDelegate;
|
||||
|
||||
// Global references
|
||||
extern WailsAppDelegate *appDelegate;
|
||||
extern unsigned int nextWindowID;
|
||||
|
||||
// Initialize the iOS application
|
||||
void ios_app_init(void);
|
||||
|
||||
// Run the iOS application main loop
|
||||
void ios_app_run(void);
|
||||
|
||||
// Quit the iOS application
|
||||
void ios_app_quit(void);
|
||||
|
||||
// Check if dark mode is enabled
|
||||
bool ios_is_dark_mode(void);
|
||||
|
||||
// Configure/show state for iOS WKWebView input accessory view (keyboard toolbar)
|
||||
// If disabled, the accessory view will be hidden.
|
||||
void ios_set_disable_input_accessory(bool disabled);
|
||||
bool ios_is_input_accessory_disabled(void);
|
||||
|
||||
// Scrolling & bounce & indicators
|
||||
void ios_set_disable_scroll(bool disabled);
|
||||
bool ios_is_scroll_disabled(void);
|
||||
void ios_set_disable_bounce(bool disabled);
|
||||
bool ios_is_bounce_disabled(void);
|
||||
void ios_set_disable_scroll_indicators(bool disabled);
|
||||
bool ios_is_scroll_indicators_disabled(void);
|
||||
|
||||
// Navigation gestures
|
||||
void ios_set_enable_back_forward_gestures(bool enabled);
|
||||
bool ios_is_back_forward_gestures_enabled(void);
|
||||
|
||||
// Link previews
|
||||
void ios_set_disable_link_preview(bool disabled);
|
||||
bool ios_is_link_preview_disabled(void);
|
||||
|
||||
// Media playback
|
||||
void ios_set_enable_inline_media_playback(bool enabled);
|
||||
bool ios_is_inline_media_playback_enabled(void);
|
||||
void ios_set_enable_autoplay_without_user_action(bool enabled);
|
||||
bool ios_is_autoplay_without_user_action_enabled(void);
|
||||
|
||||
// Inspector
|
||||
void ios_set_disable_inspectable(bool disabled);
|
||||
bool ios_is_inspectable_disabled(void);
|
||||
|
||||
// User agent customization
|
||||
void ios_set_user_agent(const char* ua);
|
||||
const char* ios_get_user_agent(void);
|
||||
void ios_set_app_name_for_user_agent(const char* name);
|
||||
const char* ios_get_app_name_for_user_agent(void);
|
||||
|
||||
// Live runtime mutations (apply to existing WKWebView instances)
|
||||
// These functions iterate current view controllers and update the active webviews on the main thread.
|
||||
void ios_runtime_set_scroll_enabled(bool enabled);
|
||||
void ios_runtime_set_bounce_enabled(bool enabled);
|
||||
void ios_runtime_set_scroll_indicators_enabled(bool enabled);
|
||||
void ios_runtime_set_back_forward_gestures_enabled(bool enabled);
|
||||
void ios_runtime_set_link_preview_enabled(bool enabled);
|
||||
void ios_runtime_set_inspectable_enabled(bool enabled);
|
||||
void ios_runtime_set_custom_user_agent(const char* ua);
|
||||
|
||||
// Native bottom tab bar (UITabBar) controls
|
||||
void ios_native_tabs_set_enabled(bool enabled);
|
||||
bool ios_native_tabs_is_enabled(void);
|
||||
void ios_native_tabs_select_index(int index);
|
||||
// Configure native tabs items as a JSON array: [{"Title":"...","SystemImage":"..."}]
|
||||
void ios_native_tabs_set_items_json(const char* json);
|
||||
const char* ios_native_tabs_get_items_json(void);
|
||||
|
||||
// App-wide background colour control
|
||||
// Setter accepts RGBA (0-255) and a flag indicating whether the colour is intentionally set by the app.
|
||||
void ios_set_app_background_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a, bool isSet);
|
||||
// Getter returns true if a colour was set; outputs RGBA components via pointers when non-null.
|
||||
bool ios_get_app_background_color(unsigned char* r, unsigned char* g, unsigned char* b, unsigned char* a);
|
||||
|
||||
// Create a WebView window and return its ID
|
||||
unsigned int ios_create_webview(void);
|
||||
|
||||
// Create a WebView window with specified Wails ID and return its native handle
|
||||
void* ios_create_webview_with_id(unsigned int wailsID);
|
||||
|
||||
// Execute JavaScript in a WebView by ID (legacy)
|
||||
void ios_execute_javascript(unsigned int windowID, const char* js);
|
||||
|
||||
// Direct JavaScript execution on a specific window handle
|
||||
void ios_window_exec_js(void* viewController, const char* js);
|
||||
|
||||
// Loaders
|
||||
void ios_window_load_url(void* viewController, const char* url);
|
||||
void ios_window_set_html(void* viewController, const char* html);
|
||||
|
||||
// Get the window ID from a native handle
|
||||
unsigned int ios_window_get_id(void* viewController);
|
||||
|
||||
// Release a native handle when window is destroyed
|
||||
void ios_window_release_handle(void* viewController);
|
||||
|
||||
// Go callbacks
|
||||
extern void ServeAssetRequest(unsigned int windowID, void* urlSchemeTask);
|
||||
extern void HandleJSMessage(unsigned int windowID, char* message);
|
||||
extern bool hasListeners(unsigned int eventId);
|
||||
|
||||
// iOS Runtime bridges
|
||||
// Trigger haptic impact with a style: "light"|"medium"|"heavy"|"soft"|"rigid"
|
||||
void ios_haptics_impact(const char* style);
|
||||
// Returns a JSON string with basic device info. Caller is responsible for freeing with free().
|
||||
const char* ios_device_info_json(void);
|
||||
|
||||
// Framework diagnostic logging. Disabled by default; Go enables it for debug
|
||||
// (non-production) builds. WailsVLog gates native NSLog diagnostics.
|
||||
void ios_set_verbose_logging(bool enabled);
|
||||
bool ios_is_verbose_logging(void);
|
||||
#define WailsVLog(...) do { if (ios_is_verbose_logging()) NSLog(__VA_ARGS__); } while (0)
|
||||
|
||||
// Screen metrics. Returns a JSON object for the main screen:
|
||||
// {"pointWidth":..,"pointHeight":..,"pixelWidth":..,"pixelHeight":..,"scale":..,
|
||||
// "safeTop":..,"safeBottom":..,"safeLeft":..,"safeRight":..}
|
||||
// Caller is responsible for freeing with free().
|
||||
const char* ios_screen_info_json(void);
|
||||
|
||||
// Clipboard (UIPasteboard). Getter result must be freed with free(); returns
|
||||
// NULL when the pasteboard has no string.
|
||||
void ios_clipboard_set_text(const char* text);
|
||||
const char* ios_clipboard_get_text(void);
|
||||
|
||||
// Message dialog (UIAlertController). buttonsJSON is a JSON array of
|
||||
// {"label":string,"isCancel":bool,"isDefault":bool}. When the user picks a
|
||||
// button, iosDialogCallback(callbackID, buttonIndex) fires (index into the
|
||||
// array, or -1 for the implicit OK of a button-less dialog).
|
||||
void ios_show_message_dialog(const char* title, const char* message, const char* buttonsJSON, unsigned int callbackID);
|
||||
|
||||
// Document picker (UIDocumentPickerViewController). Selected paths are
|
||||
// delivered via iosOpenFileCallback (one call per path) followed by
|
||||
// iosOpenFileCallbackEnd. Cancellation delivers only the End callback.
|
||||
// Files are imported as copies into the app sandbox; directories are opened
|
||||
// in place with security-scoped access.
|
||||
void ios_show_document_picker(unsigned int callbackID, bool directories, bool multiple);
|
||||
|
||||
// Go callbacks for the above
|
||||
extern void iosDialogCallback(unsigned int callbackID, int buttonIndex);
|
||||
extern void iosOpenFileCallback(unsigned int callbackID, char* path);
|
||||
extern void iosOpenFileCallbackEnd(unsigned int callbackID);
|
||||
extern void iosApplicationDidLaunch(void);
|
||||
|
||||
// Start the native system-event monitors (battery, network, screen lock).
|
||||
// Each fires processApplicationEvent(EventXxx, json) so the Go side delivers a
|
||||
// typed ios: application event (mapped to a common: event) with its payload on
|
||||
// the event context. Safe to call once after launch; the observer setup is
|
||||
// dispatched to the main thread. (Theme is handled in the view controller's
|
||||
// traitCollectionDidChange; lifecycle/memory by the generated delegate events.)
|
||||
void ios_start_system_event_monitors(void);
|
||||
|
||||
#endif // APPLICATION_IOS_H
|
||||
483
vendor/github.com/wailsapp/wails/v3/pkg/application/application_ios.m
generated
vendored
Normal file
483
vendor/github.com/wailsapp/wails/v3/pkg/application/application_ios.m
generated
vendored
Normal file
@@ -0,0 +1,483 @@
|
||||
//go:build ios
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
#import "application_ios.h"
|
||||
#import "application_ios_delegate.h"
|
||||
#import "webview_window_ios.h"
|
||||
#import <sys/utsname.h>
|
||||
#import <stdlib.h>
|
||||
#import <string.h>
|
||||
#import <os/log.h>
|
||||
|
||||
// Forward declarations for Go callbacks
|
||||
void ServeAssetRequest(unsigned int windowID, void* urlSchemeTask);
|
||||
void HandleJSMessage(unsigned int windowID, char* message);
|
||||
|
||||
|
||||
// Global references - declare after interface
|
||||
WailsAppDelegate *appDelegate = nil;
|
||||
unsigned int nextWindowID = 1;
|
||||
static bool g_disableInputAccessory = false; // default: enabled (shown)
|
||||
// New global flags with sensible iOS defaults
|
||||
static bool g_disableScroll = false; // default: scrolling enabled
|
||||
static bool g_disableBounce = false; // default: bounce enabled
|
||||
static bool g_disableScrollIndicators = false; // default: indicators shown
|
||||
static bool g_enableBackForwardGestures = false; // default: gestures disabled
|
||||
static bool g_disableLinkPreview = false; // default: link preview enabled
|
||||
static bool g_enableInlineMediaPlayback = false; // default: inline playback disabled
|
||||
static bool g_enableAutoplayNoUserAction = false; // default: autoplay requires user action
|
||||
static bool g_disableInspectable = false; // default: inspector enabled
|
||||
static NSString* g_userAgent = nil;
|
||||
static NSString* g_appNameForUA = nil; // default applied in code when nil
|
||||
static bool g_enableNativeTabs = false; // default: off
|
||||
static NSString* g_nativeTabsItemsJSON = nil; // JSON array of items
|
||||
|
||||
// App-wide background colour storage (RGBA 0-255)
|
||||
static bool g_appBGSet = false;
|
||||
static unsigned char g_appBG_R = 255;
|
||||
static unsigned char g_appBG_G = 255;
|
||||
static unsigned char g_appBG_B = 255;
|
||||
static unsigned char g_appBG_A = 255;
|
||||
|
||||
// Framework diagnostic logging (off unless enabled from Go in debug builds)
|
||||
static bool g_verboseLogging = false;
|
||||
void ios_set_verbose_logging(bool enabled) { g_verboseLogging = enabled; }
|
||||
bool ios_is_verbose_logging(void) { return g_verboseLogging; }
|
||||
|
||||
// Note: The WailsAppDelegate implementation resides in application_ios_delegate.m
|
||||
|
||||
// C interface implementation
|
||||
void ios_app_init(void) {
|
||||
// This will be called from Go's init
|
||||
// Explicitly reference WailsAppDelegate to ensure the class is linked and registered.
|
||||
(void)[WailsAppDelegate class];
|
||||
// The actual UI startup happens via UIApplicationMain in main.m
|
||||
}
|
||||
|
||||
void ios_app_run(void) {
|
||||
// No-op: UIApplicationMain is invoked from main.m (the C entry point) on the
|
||||
// real OS main thread. The Go runtime is started by the app delegate's
|
||||
// didFinishLaunchingWithOptions, i.e. only after UIKit has launched.
|
||||
}
|
||||
|
||||
void ios_app_quit(void) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
bool ios_is_dark_mode(void) {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIUserInterfaceStyle style = [[UITraitCollection currentTraitCollection] userInterfaceStyle];
|
||||
return style == UIUserInterfaceStyleDark;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ios_set_disable_input_accessory(bool disabled) {
|
||||
g_disableInputAccessory = disabled;
|
||||
}
|
||||
|
||||
bool ios_is_input_accessory_disabled(void) {
|
||||
return g_disableInputAccessory;
|
||||
}
|
||||
|
||||
// Scrolling & bounce & indicators
|
||||
void ios_set_disable_scroll(bool disabled) { g_disableScroll = disabled; }
|
||||
bool ios_is_scroll_disabled(void) { return g_disableScroll; }
|
||||
void ios_set_disable_bounce(bool disabled) { g_disableBounce = disabled; }
|
||||
bool ios_is_bounce_disabled(void) { return g_disableBounce; }
|
||||
void ios_set_disable_scroll_indicators(bool disabled) { g_disableScrollIndicators = disabled; }
|
||||
bool ios_is_scroll_indicators_disabled(void) { return g_disableScrollIndicators; }
|
||||
|
||||
// Navigation gestures
|
||||
void ios_set_enable_back_forward_gestures(bool enabled) { g_enableBackForwardGestures = enabled; }
|
||||
bool ios_is_back_forward_gestures_enabled(void) { return g_enableBackForwardGestures; }
|
||||
|
||||
// Link previews
|
||||
void ios_set_disable_link_preview(bool disabled) { g_disableLinkPreview = disabled; }
|
||||
bool ios_is_link_preview_disabled(void) { return g_disableLinkPreview; }
|
||||
|
||||
// Media playback
|
||||
void ios_set_enable_inline_media_playback(bool enabled) { g_enableInlineMediaPlayback = enabled; }
|
||||
bool ios_is_inline_media_playback_enabled(void) { return g_enableInlineMediaPlayback; }
|
||||
void ios_set_enable_autoplay_without_user_action(bool enabled) { g_enableAutoplayNoUserAction = enabled; }
|
||||
bool ios_is_autoplay_without_user_action_enabled(void) { return g_enableAutoplayNoUserAction; }
|
||||
|
||||
// Inspector
|
||||
void ios_set_disable_inspectable(bool disabled) { g_disableInspectable = disabled; }
|
||||
bool ios_is_inspectable_disabled(void) { return g_disableInspectable; }
|
||||
|
||||
// User agent customization
|
||||
void ios_set_user_agent(const char* ua) {
|
||||
if (ua == NULL) { g_userAgent = nil; return; }
|
||||
g_userAgent = [NSString stringWithUTF8String:ua];
|
||||
}
|
||||
const char* ios_get_user_agent(void) {
|
||||
if (g_userAgent == nil) return NULL;
|
||||
return [g_userAgent UTF8String];
|
||||
}
|
||||
void ios_set_app_name_for_user_agent(const char* name) {
|
||||
if (name == NULL) { g_appNameForUA = nil; return; }
|
||||
g_appNameForUA = [NSString stringWithUTF8String:name];
|
||||
}
|
||||
const char* ios_get_app_name_for_user_agent(void) {
|
||||
if (g_appNameForUA == nil) return NULL;
|
||||
return [g_appNameForUA UTF8String];
|
||||
}
|
||||
|
||||
// Live runtime mutations (apply to existing WKWebView instances)
|
||||
static void forEachViewController(void (^block)(WailsViewController *vc)) {
|
||||
if (!appDelegate || !appDelegate.viewControllers) return;
|
||||
void (^applyBlock)(void) = ^{
|
||||
for (WailsViewController *vc in appDelegate.viewControllers) {
|
||||
if (!vc || !vc.webView) continue;
|
||||
block(vc);
|
||||
}
|
||||
};
|
||||
if ([NSThread isMainThread]) {
|
||||
applyBlock();
|
||||
} else {
|
||||
dispatch_async(dispatch_get_main_queue(), applyBlock);
|
||||
}
|
||||
}
|
||||
|
||||
void ios_runtime_set_scroll_enabled(bool enabled) {
|
||||
g_disableScroll = !enabled;
|
||||
forEachViewController(^(WailsViewController *vc){
|
||||
vc.webView.scrollView.scrollEnabled = enabled ? YES : NO;
|
||||
});
|
||||
}
|
||||
|
||||
void ios_runtime_set_bounce_enabled(bool enabled) {
|
||||
g_disableBounce = !enabled;
|
||||
forEachViewController(^(WailsViewController *vc){
|
||||
UIScrollView *sv = vc.webView.scrollView;
|
||||
sv.bounces = enabled ? YES : NO;
|
||||
sv.alwaysBounceVertical = enabled ? YES : NO;
|
||||
sv.alwaysBounceHorizontal = enabled ? YES : NO;
|
||||
});
|
||||
}
|
||||
|
||||
void ios_runtime_set_scroll_indicators_enabled(bool enabled) {
|
||||
g_disableScrollIndicators = !enabled;
|
||||
forEachViewController(^(WailsViewController *vc){
|
||||
UIScrollView *sv = vc.webView.scrollView;
|
||||
sv.showsVerticalScrollIndicator = enabled ? YES : NO;
|
||||
sv.showsHorizontalScrollIndicator = enabled ? YES : NO;
|
||||
});
|
||||
}
|
||||
|
||||
void ios_runtime_set_back_forward_gestures_enabled(bool enabled) {
|
||||
g_enableBackForwardGestures = enabled;
|
||||
forEachViewController(^(WailsViewController *vc){
|
||||
vc.webView.allowsBackForwardNavigationGestures = enabled ? YES : NO;
|
||||
});
|
||||
}
|
||||
|
||||
void ios_runtime_set_link_preview_enabled(bool enabled) {
|
||||
g_disableLinkPreview = !enabled;
|
||||
forEachViewController(^(WailsViewController *vc){
|
||||
vc.webView.allowsLinkPreview = enabled ? YES : NO;
|
||||
});
|
||||
}
|
||||
|
||||
void ios_runtime_set_inspectable_enabled(bool enabled) {
|
||||
g_disableInspectable = !enabled;
|
||||
forEachViewController(^(WailsViewController *vc){
|
||||
BOOL inspectorOn = enabled ? YES : NO;
|
||||
if (@available(iOS 16.4, *)) {
|
||||
vc.webView.inspectable = inspectorOn;
|
||||
} else {
|
||||
@try { [vc.webView setValue:@(inspectorOn) forKey:@"inspectable"]; } @catch (__unused NSException *e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ios_runtime_set_custom_user_agent(const char* ua) {
|
||||
ios_set_user_agent(ua);
|
||||
NSString *uaStr = (ua ? [NSString stringWithUTF8String:ua] : nil);
|
||||
forEachViewController(^(WailsViewController *vc){
|
||||
vc.webView.customUserAgent = uaStr;
|
||||
});
|
||||
}
|
||||
|
||||
// Forward declaration used by getters
|
||||
static const char* dupCString(NSString *str);
|
||||
|
||||
// Native bottom tab bar controls
|
||||
void ios_native_tabs_set_enabled(bool enabled) {
|
||||
g_enableNativeTabs = enabled;
|
||||
forEachViewController(^(WailsViewController *vc){
|
||||
[vc enableNativeTabs:(enabled ? YES : NO)];
|
||||
});
|
||||
}
|
||||
|
||||
bool ios_native_tabs_is_enabled(void) {
|
||||
return g_enableNativeTabs;
|
||||
}
|
||||
|
||||
void ios_native_tabs_select_index(int index) {
|
||||
forEachViewController(^(WailsViewController *vc){
|
||||
[vc selectNativeTabIndex:(NSInteger)index];
|
||||
});
|
||||
}
|
||||
|
||||
void ios_native_tabs_set_items_json(const char* json) {
|
||||
if (json == NULL) {
|
||||
g_nativeTabsItemsJSON = nil;
|
||||
return;
|
||||
}
|
||||
g_nativeTabsItemsJSON = [NSString stringWithUTF8String:json];
|
||||
// Apply to existing controllers if visible
|
||||
forEachViewController(^(WailsViewController *vc){
|
||||
if (vc.tabBar && !vc.tabBar.isHidden) {
|
||||
// Re-enable to rebuild items from JSON
|
||||
[vc enableNativeTabs:YES];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const char* ios_native_tabs_get_items_json(void) {
|
||||
return dupCString(g_nativeTabsItemsJSON);
|
||||
}
|
||||
|
||||
// App-wide background colour control
|
||||
void ios_set_app_background_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a, bool isSet) {
|
||||
g_appBGSet = isSet;
|
||||
if (isSet) {
|
||||
g_appBG_R = r; g_appBG_G = g; g_appBG_B = b; g_appBG_A = a;
|
||||
}
|
||||
}
|
||||
|
||||
bool ios_get_app_background_color(unsigned char* r, unsigned char* g, unsigned char* b, unsigned char* a) {
|
||||
if (!g_appBGSet) return false;
|
||||
if (r) *r = g_appBG_R;
|
||||
if (g) *g = g_appBG_G;
|
||||
if (b) *b = g_appBG_B;
|
||||
if (a) *a = g_appBG_A;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// iOS Runtime bridges
|
||||
void ios_haptics_impact(const char* cstyle) {
|
||||
if (cstyle == NULL) return;
|
||||
NSString *style = [NSString stringWithUTF8String:cstyle];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
WailsVLog(@"[ios_haptics_impact] requested style=%@", style);
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImpactFeedbackStyle feedbackStyle = UIImpactFeedbackStyleMedium;
|
||||
if ([style isEqualToString:@"light"]) feedbackStyle = UIImpactFeedbackStyleLight;
|
||||
else if ([style isEqualToString:@"medium"]) feedbackStyle = UIImpactFeedbackStyleMedium;
|
||||
else if ([style isEqualToString:@"heavy"]) feedbackStyle = UIImpactFeedbackStyleHeavy;
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
|
||||
else if ([style isEqualToString:@"soft"]) feedbackStyle = UIImpactFeedbackStyleSoft;
|
||||
else if ([style isEqualToString:@"rigid"]) feedbackStyle = UIImpactFeedbackStyleRigid;
|
||||
#endif
|
||||
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:feedbackStyle];
|
||||
[generator prepare];
|
||||
[generator impactOccurred];
|
||||
#if TARGET_OS_SIMULATOR
|
||||
WailsVLog(@"[ios_haptics_impact] Simulator detected: no physical haptic feedback will be felt.");
|
||||
#endif
|
||||
} else {
|
||||
WailsVLog(@"[ios_haptics_impact] iOS version < 13.0: no haptic API available.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static const char* dupCString(NSString *str) {
|
||||
if (str == nil) return NULL;
|
||||
const char* utf8 = [str UTF8String];
|
||||
if (utf8 == NULL) return NULL;
|
||||
size_t len = strlen(utf8) + 1;
|
||||
char* out = (char*)malloc(len);
|
||||
if (out) memcpy(out, utf8, len);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Run a block on the main thread, synchronously, without deadlocking when
|
||||
// already on the main thread.
|
||||
static void runOnMainSync(void (^block)(void)) {
|
||||
if ([NSThread isMainThread]) {
|
||||
block();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), block);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the view controller that should present modal UI (alerts, pickers).
|
||||
static UIViewController* wailsTopViewController(void) {
|
||||
UIViewController *vc = appDelegate.window.rootViewController;
|
||||
while (vc.presentedViewController != nil && !vc.presentedViewController.isBeingDismissed) {
|
||||
vc = vc.presentedViewController;
|
||||
}
|
||||
return vc;
|
||||
}
|
||||
|
||||
// MARK: - Screen metrics
|
||||
|
||||
const char* ios_screen_info_json(void) {
|
||||
__block NSString *json = nil;
|
||||
runOnMainSync(^{
|
||||
UIScreen *screen = [UIScreen mainScreen];
|
||||
CGRect bounds = screen.bounds; // points, current orientation
|
||||
CGRect native = screen.nativeBounds; // pixels, portrait-up
|
||||
CGFloat scale = screen.scale;
|
||||
UIEdgeInsets safe = UIEdgeInsetsZero;
|
||||
if (appDelegate && appDelegate.window) {
|
||||
safe = appDelegate.window.safeAreaInsets;
|
||||
}
|
||||
json = [NSString stringWithFormat:
|
||||
@"{\"pointWidth\":%d,\"pointHeight\":%d,\"pixelWidth\":%d,\"pixelHeight\":%d,\"scale\":%.2f,"
|
||||
"\"safeTop\":%d,\"safeBottom\":%d,\"safeLeft\":%d,\"safeRight\":%d}",
|
||||
(int)bounds.size.width, (int)bounds.size.height,
|
||||
(int)native.size.width, (int)native.size.height,
|
||||
(double)scale,
|
||||
(int)safe.top, (int)safe.bottom, (int)safe.left, (int)safe.right];
|
||||
});
|
||||
return dupCString(json);
|
||||
}
|
||||
|
||||
// MARK: - Clipboard
|
||||
|
||||
void ios_clipboard_set_text(const char* text) {
|
||||
NSString *str = text ? [NSString stringWithUTF8String:text] : @"";
|
||||
runOnMainSync(^{
|
||||
[UIPasteboard generalPasteboard].string = str;
|
||||
});
|
||||
}
|
||||
|
||||
const char* ios_clipboard_get_text(void) {
|
||||
__block NSString *str = nil;
|
||||
runOnMainSync(^{
|
||||
str = [UIPasteboard generalPasteboard].string;
|
||||
});
|
||||
return dupCString(str);
|
||||
}
|
||||
|
||||
// MARK: - Message dialogs
|
||||
|
||||
void ios_show_message_dialog(const char* ctitle, const char* cmessage, const char* cbuttonsJSON, unsigned int callbackID) {
|
||||
NSString *title = ctitle ? [NSString stringWithUTF8String:ctitle] : nil;
|
||||
NSString *message = cmessage ? [NSString stringWithUTF8String:cmessage] : nil;
|
||||
NSString *buttonsJSON = cbuttonsJSON ? [NSString stringWithUTF8String:cbuttonsJSON] : nil;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
NSArray *buttons = nil;
|
||||
if (buttonsJSON.length) {
|
||||
NSData *data = [buttonsJSON dataUsingEncoding:NSUTF8StringEncoding];
|
||||
id obj = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
if ([obj isKindOfClass:[NSArray class]]) {
|
||||
buttons = (NSArray *)obj;
|
||||
}
|
||||
}
|
||||
if (buttons.count == 0) {
|
||||
// No buttons configured: show a plain OK
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
iosDialogCallback(callbackID, -1);
|
||||
}]];
|
||||
} else {
|
||||
NSInteger idx = 0;
|
||||
for (id entry in buttons) {
|
||||
if (![entry isKindOfClass:[NSDictionary class]]) { idx++; continue; }
|
||||
NSDictionary *d = (NSDictionary *)entry;
|
||||
NSString *label = [d[@"label"] isKindOfClass:[NSString class]] ? d[@"label"] : @"";
|
||||
BOOL isCancel = [d[@"isCancel"] boolValue];
|
||||
BOOL isDefault = [d[@"isDefault"] boolValue];
|
||||
NSInteger buttonIndex = idx;
|
||||
UIAlertActionStyle style = isCancel ? UIAlertActionStyleCancel : UIAlertActionStyleDefault;
|
||||
UIAlertAction *action = [UIAlertAction actionWithTitle:label style:style
|
||||
handler:^(UIAlertAction *a) {
|
||||
iosDialogCallback(callbackID, (int)buttonIndex);
|
||||
}];
|
||||
[alert addAction:action];
|
||||
if (isDefault) {
|
||||
alert.preferredAction = action;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
[wailsTopViewController() presentViewController:alert animated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
// MARK: - Document picker
|
||||
|
||||
@interface WailsDocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
|
||||
@property (nonatomic, assign) unsigned int callbackID;
|
||||
@property (nonatomic, assign) BOOL opensInPlace;
|
||||
@end
|
||||
|
||||
// Keep delegates alive until the picker completes
|
||||
static NSMutableDictionary<NSNumber*, WailsDocumentPickerDelegate*> *g_pickerDelegates;
|
||||
|
||||
@implementation WailsDocumentPickerDelegate
|
||||
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
|
||||
for (NSURL *url in urls) {
|
||||
// Files are imported with asCopy:YES, so they are sandbox copies that
|
||||
// need no security-scoped access. Only directories are opened in place
|
||||
// and require it; that access is held for the app session (bookmark
|
||||
// persistence is not implemented).
|
||||
if (self.opensInPlace) {
|
||||
[url startAccessingSecurityScopedResource];
|
||||
}
|
||||
iosOpenFileCallback(self.callbackID, (char *)[url.path UTF8String]);
|
||||
}
|
||||
iosOpenFileCallbackEnd(self.callbackID);
|
||||
[g_pickerDelegates removeObjectForKey:@(self.callbackID)];
|
||||
}
|
||||
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
|
||||
iosOpenFileCallbackEnd(self.callbackID);
|
||||
[g_pickerDelegates removeObjectForKey:@(self.callbackID)];
|
||||
}
|
||||
@end
|
||||
|
||||
void ios_show_document_picker(unsigned int callbackID, bool directories, bool multiple) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIDocumentPickerViewController *picker;
|
||||
if (directories) {
|
||||
// Folders open in place (asCopy is not supported for folders)
|
||||
picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[UTTypeFolder]];
|
||||
} else {
|
||||
// Import file copies into the app sandbox so the app can read
|
||||
// them without managing security-scoped access.
|
||||
picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[UTTypeItem] asCopy:YES];
|
||||
}
|
||||
picker.allowsMultipleSelection = multiple ? YES : NO;
|
||||
WailsDocumentPickerDelegate *delegate = [[WailsDocumentPickerDelegate alloc] init];
|
||||
delegate.callbackID = callbackID;
|
||||
delegate.opensInPlace = directories ? YES : NO;
|
||||
if (!g_pickerDelegates) {
|
||||
g_pickerDelegates = [NSMutableDictionary dictionary];
|
||||
}
|
||||
g_pickerDelegates[@(callbackID)] = delegate;
|
||||
picker.delegate = delegate;
|
||||
[wailsTopViewController() presentViewController:picker animated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
const char* ios_device_info_json(void) {
|
||||
UIDevice *device = [UIDevice currentDevice];
|
||||
struct utsname systemInfo;
|
||||
uname(&systemInfo);
|
||||
NSString *model = [NSString stringWithUTF8String:systemInfo.machine];
|
||||
NSString *systemName = device.systemName ?: @"iOS";
|
||||
NSString *systemVersion = device.systemVersion ?: @"";
|
||||
#if TARGET_OS_SIMULATOR
|
||||
BOOL isSimulator = YES;
|
||||
#else
|
||||
BOOL isSimulator = NO;
|
||||
#endif
|
||||
NSString *json = [NSString stringWithFormat:
|
||||
@"{\"model\":\"%@\",\"systemName\":\"%@\",\"systemVersion\":\"%@\",\"isSimulator\":%@}",
|
||||
model, systemName, systemVersion, isSimulator ? @"true" : @"false"
|
||||
];
|
||||
return dupCString(json);
|
||||
}
|
||||
15
vendor/github.com/wailsapp/wails/v3/pkg/application/application_ios_delegate.h
generated
vendored
Normal file
15
vendor/github.com/wailsapp/wails/v3/pkg/application/application_ios_delegate.h
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build ios
|
||||
|
||||
#ifndef application_ios_delegate_h
|
||||
#define application_ios_delegate_h
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class WailsViewController;
|
||||
|
||||
@interface WailsAppDelegate : UIResponder <UIApplicationDelegate>
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
@property (nonatomic, strong) NSMutableArray<WailsViewController *> *viewControllers;
|
||||
@end
|
||||
|
||||
#endif /* application_ios_delegate_h */
|
||||
114
vendor/github.com/wailsapp/wails/v3/pkg/application/application_ios_delegate.m
generated
vendored
Normal file
114
vendor/github.com/wailsapp/wails/v3/pkg/application/application_ios_delegate.m
generated
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
//go:build ios
|
||||
#import "application_ios_delegate.h"
|
||||
#import "../events/events_ios.h"
|
||||
#import "application_ios.h"
|
||||
extern void processApplicationEvent(unsigned int, void* data);
|
||||
extern void processWindowEvent(unsigned int, unsigned int);
|
||||
extern bool hasListeners(unsigned int);
|
||||
extern void iosApplicationDidLaunch(void);
|
||||
// WailsIOSMain (app's generated main_ios.go) runs the user's main()/app.Run().
|
||||
// The delegate starts it AFTER UIKit has launched (see below).
|
||||
extern void WailsIOSMain(void);
|
||||
// Registers the UNUserNotificationCenter delegate so local notifications are
|
||||
// shown while the app is in the foreground (and taps are handled). Apple
|
||||
// requires this be set before launch finishes, hence the call below.
|
||||
extern void ios_notifications_init(void);
|
||||
|
||||
@implementation WailsAppDelegate
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
// Set global appDelegate reference and bring up a window if needed
|
||||
appDelegate = self;
|
||||
if (self.window == nil) {
|
||||
// Start the window with the launch-screen colour (a "LaunchBackground"
|
||||
// colour asset, also referenced by UILaunchScreen) so there's no white
|
||||
// flash between the launch screen and the first WebView paint. The Go
|
||||
// options set the colour too, but that happens after this delegate runs,
|
||||
// so it can't colour the initial window. Falls back to white if the
|
||||
// asset isn't present.
|
||||
UIColor *launchBG = [UIColor colorNamed:@"LaunchBackground"] ?: [UIColor whiteColor];
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
self.window.backgroundColor = launchBG;
|
||||
UIViewController *rootVC = [[UIViewController alloc] init];
|
||||
rootVC.view.backgroundColor = launchBG;
|
||||
self.window.rootViewController = rootVC;
|
||||
[self.window makeKeyAndVisible];
|
||||
}
|
||||
// Apply app-wide background colour if configured
|
||||
unsigned char r = 255, g = 255, b = 255, a = 255;
|
||||
if (ios_get_app_background_color(&r, &g, &b, &a)) {
|
||||
CGFloat fr = ((CGFloat)r) / 255.0;
|
||||
CGFloat fg = ((CGFloat)g) / 255.0;
|
||||
CGFloat fb = ((CGFloat)b) / 255.0;
|
||||
CGFloat fa = ((CGFloat)a) / 255.0;
|
||||
UIColor *color = [UIColor colorWithRed:fr green:fg blue:fb alpha:fa];
|
||||
self.window.backgroundColor = color;
|
||||
self.window.rootViewController.view.backgroundColor = color;
|
||||
}
|
||||
if (!self.viewControllers) {
|
||||
self.viewControllers = [NSMutableArray array];
|
||||
}
|
||||
// Register the notification-center delegate before launch finishes so local
|
||||
// notifications appear while the app is foregrounded (otherwise iOS delivers
|
||||
// them silently and no banner is shown).
|
||||
ios_notifications_init();
|
||||
// Unconditional launch signal for the Go runtime. platformRun waits on
|
||||
// this and emits ApplicationDidFinishLaunching from the Go side once the
|
||||
// event listeners are wired up - emitting it from here would race the Go
|
||||
// runtime's startup and the event could be dropped.
|
||||
// Start the Go runtime NOW — only after UIKit has delivered the launch and
|
||||
// the window exists. Starting Go earlier (concurrently with UIApplicationMain)
|
||||
// intermittently corrupts the FrontBoard launch handshake on a physical
|
||||
// device, so this method never fires (blank cold launch / 0x8BADF00D). Run it
|
||||
// on a background thread so app.Run()'s blocking loop never touches the main
|
||||
// thread. WailsIOSMain -> user main() -> app.Run(); the window's run() then
|
||||
// creates the WebView (appDelegate/window are already set above).
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
||||
WailsIOSMain();
|
||||
});
|
||||
return YES;
|
||||
}
|
||||
// GENERATED EVENTS START
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
||||
if( hasListeners(EventApplicationDidBecomeActive) ) {
|
||||
processApplicationEvent(EventApplicationDidBecomeActive, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
if( hasListeners(EventApplicationDidEnterBackground) ) {
|
||||
processApplicationEvent(EventApplicationDidEnterBackground, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(UIApplication *)application {
|
||||
if( hasListeners(EventApplicationDidFinishLaunching) ) {
|
||||
processApplicationEvent(EventApplicationDidFinishLaunching, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
|
||||
if( hasListeners(EventApplicationDidReceiveMemoryWarning) ) {
|
||||
processApplicationEvent(EventApplicationDidReceiveMemoryWarning, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillEnterForeground:(UIApplication *)application {
|
||||
if( hasListeners(EventApplicationWillEnterForeground) ) {
|
||||
processApplicationEvent(EventApplicationWillEnterForeground, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(UIApplication *)application {
|
||||
if( hasListeners(EventApplicationWillResignActive) ) {
|
||||
processApplicationEvent(EventApplicationWillResignActive, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(UIApplication *)application {
|
||||
if( hasListeners(EventApplicationWillTerminate) ) {
|
||||
processApplicationEvent(EventApplicationWillTerminate, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// GENERATED EVENTS END
|
||||
@end
|
||||
388
vendor/github.com/wailsapp/wails/v3/pkg/application/application_linux.go
generated
vendored
Normal file
388
vendor/github.com/wailsapp/wails/v3/pkg/application/application_linux.go
generated
vendored
Normal file
@@ -0,0 +1,388 @@
|
||||
//go:build linux && cgo && !gtk3 && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#include <gtk/gtk.h>
|
||||
#include <webkit/webkit.h>
|
||||
static guint get_compiled_gtk_major_version() { return GTK_MAJOR_VERSION; }
|
||||
static guint get_compiled_gtk_minor_version() { return GTK_MINOR_VERSION; }
|
||||
static guint get_compiled_gtk_micro_version() { return GTK_MICRO_VERSION; }
|
||||
static guint get_compiled_webkit_major_version() { return WEBKIT_MAJOR_VERSION; }
|
||||
static guint get_compiled_webkit_minor_version() { return WEBKIT_MINOR_VERSION; }
|
||||
static guint get_compiled_webkit_micro_version() { return WEBKIT_MICRO_VERSION; }
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
var invalidAppNameChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
|
||||
var leadingDigits = regexp.MustCompile(`^[0-9]+`)
|
||||
|
||||
func sanitizeAppName(name string) string {
|
||||
name = invalidAppNameChars.ReplaceAllString(name, "_")
|
||||
name = leadingDigits.ReplaceAllString(name, "_$0")
|
||||
for strings.Contains(name, "__") {
|
||||
name = strings.ReplaceAll(name, "__", "_")
|
||||
}
|
||||
name = strings.Trim(name, "_")
|
||||
if name == "" {
|
||||
name = "wailsapp"
|
||||
}
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Disable DMA-BUF renderer on any session type with NVIDIA to prevent blank windows and
|
||||
// "Error 71 (Protocol error)" crashes. NVIDIA proprietary drivers fail gbm_bo_map() when
|
||||
// importing DMA-BUF, causing blank/white screens on both X11 and Wayland.
|
||||
// See: https://bugs.webkit.org/show_bug.cgi?id=262607
|
||||
// See: https://github.com/wailsapp/wails/issues/4985
|
||||
if os.Getenv("WEBKIT_DISABLE_DMABUF_RENDERER") == "" && isNVIDIAGPU() {
|
||||
_ = os.Setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
|
||||
}
|
||||
}
|
||||
|
||||
func isNVIDIAGPU() bool {
|
||||
if _, err := os.Stat("/sys/module/nvidia"); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type linuxApp struct {
|
||||
application pointer
|
||||
parent *App
|
||||
|
||||
activated chan struct{}
|
||||
activatedOnce sync.Once
|
||||
|
||||
windowMap map[windowPointer]uint
|
||||
windowMapLock sync.Mutex
|
||||
|
||||
theme string
|
||||
|
||||
icon pointer
|
||||
}
|
||||
|
||||
func (a *linuxApp) GetFlags(options Options) map[string]any {
|
||||
if options.Flags == nil {
|
||||
options.Flags = make(map[string]any)
|
||||
}
|
||||
return options.Flags
|
||||
}
|
||||
|
||||
func (a *linuxApp) name() string {
|
||||
return appName()
|
||||
}
|
||||
|
||||
func (a *linuxApp) run() error {
|
||||
a.parent.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(evt *ApplicationEvent) {
|
||||
if err := a.processAndCacheScreens(); err != nil {
|
||||
a.parent.handleError(err)
|
||||
}
|
||||
})
|
||||
a.setupCommonEvents()
|
||||
// Theme changes are already monitored by listenForSystemThemeChanges via init();
|
||||
// it uses the portal-standard org.freedesktop.appearance namespace.
|
||||
a.monitorPowerEvents()
|
||||
return appRun(a.application)
|
||||
}
|
||||
|
||||
func (a *linuxApp) destroy() {
|
||||
if !globalApplication.shouldQuit() {
|
||||
return
|
||||
}
|
||||
globalApplication.cleanup()
|
||||
appDestroy(a.application)
|
||||
}
|
||||
|
||||
func (a *linuxApp) getApplicationMenu() *Menu {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *linuxApp) setApplicationMenu(menu *Menu) {}
|
||||
|
||||
func (a *linuxApp) hide() {
|
||||
a.hideAllWindows()
|
||||
}
|
||||
|
||||
func (a *linuxApp) show() {
|
||||
a.showAllWindows()
|
||||
}
|
||||
|
||||
func (a *linuxApp) on(eventID uint) {
|
||||
}
|
||||
|
||||
func (a *linuxApp) isOnMainThread() bool {
|
||||
return isOnMainThread()
|
||||
}
|
||||
|
||||
func (a *linuxApp) appendGTKVersion(result map[string]string) {
|
||||
result["GTK"] = fmt.Sprintf("%d.%d.%d",
|
||||
C.get_compiled_gtk_major_version(),
|
||||
C.get_compiled_gtk_minor_version(),
|
||||
C.get_compiled_gtk_micro_version())
|
||||
result["WebKit"] = fmt.Sprintf("%d.%d.%d",
|
||||
C.get_compiled_webkit_major_version(),
|
||||
C.get_compiled_webkit_minor_version(),
|
||||
C.get_compiled_webkit_micro_version())
|
||||
}
|
||||
|
||||
func (a *linuxApp) init(_ *App, options Options) {
|
||||
osInfo, _ := operatingsystem.Info()
|
||||
a.parent.info("Compiled with GTK %d.%d.%d",
|
||||
C.get_compiled_gtk_major_version(),
|
||||
C.get_compiled_gtk_minor_version(),
|
||||
C.get_compiled_gtk_micro_version())
|
||||
a.parent.info("Compiled with WebKitGTK %d.%d.%d",
|
||||
C.get_compiled_webkit_major_version(),
|
||||
C.get_compiled_webkit_minor_version(),
|
||||
C.get_compiled_webkit_micro_version())
|
||||
a.parent.info("Using %s", osInfo.Name)
|
||||
|
||||
if options.Icon != nil {
|
||||
a.setIcon(options.Icon)
|
||||
}
|
||||
|
||||
go listenForSystemThemeChanges(a)
|
||||
}
|
||||
|
||||
func listenForSystemThemeChanges(a *linuxApp) {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
a.parent.error("failed to connect to session bus: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = conn.AddMatchSignal(
|
||||
dbus.WithMatchInterface("org.freedesktop.portal.Settings"),
|
||||
dbus.WithMatchMember("SettingChanged"),
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c := make(chan *dbus.Signal, 10)
|
||||
conn.Signal(c)
|
||||
|
||||
for s := range c {
|
||||
if len(s.Body) < 3 {
|
||||
continue
|
||||
}
|
||||
namespace, ok := s.Body[0].(string)
|
||||
if !ok || namespace != "org.freedesktop.appearance" {
|
||||
continue
|
||||
}
|
||||
key, ok := s.Body[1].(string)
|
||||
if !ok || key != "color-scheme" {
|
||||
continue
|
||||
}
|
||||
processApplicationEvent(C.uint(events.Linux.SystemThemeChanged), nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *linuxApp) registerWindow(window pointer, id uint) {
|
||||
a.windowMapLock.Lock()
|
||||
a.windowMap[windowPointer(window)] = id
|
||||
a.windowMapLock.Unlock()
|
||||
}
|
||||
|
||||
func (a *linuxApp) unregisterWindow(window windowPointer) {
|
||||
a.windowMapLock.Lock()
|
||||
delete(a.windowMap, window)
|
||||
remainingWindows := len(a.windowMap)
|
||||
a.windowMapLock.Unlock()
|
||||
|
||||
if remainingWindows == 0 && !a.parent.options.Linux.DisableQuitOnLastWindowClosed {
|
||||
a.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
func newPlatformApp(parent *App) *linuxApp {
|
||||
name := sanitizeAppName(parent.options.Name)
|
||||
app := &linuxApp{
|
||||
parent: parent,
|
||||
application: appNew(name),
|
||||
activated: make(chan struct{}),
|
||||
windowMap: map[windowPointer]uint{},
|
||||
}
|
||||
|
||||
if parent.options.Linux.ProgramName != "" {
|
||||
setProgramName(parent.options.Linux.ProgramName)
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func (a *linuxApp) markActivated() {
|
||||
a.activatedOnce.Do(func() {
|
||||
close(a.activated)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *linuxApp) waitForActivation() {
|
||||
<-a.activated
|
||||
}
|
||||
|
||||
func (a *linuxApp) getIconForFile(filename string) ([]byte, error) {
|
||||
if filename == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ext := filepath.Ext(filename)
|
||||
iconMap := map[string]string{
|
||||
".txt": "text-x-generic",
|
||||
".pdf": "application-pdf",
|
||||
".doc": "x-office-document",
|
||||
".docx": "x-office-document",
|
||||
".xls": "x-office-spreadsheet",
|
||||
".xlsx": "x-office-spreadsheet",
|
||||
".ppt": "x-office-presentation",
|
||||
".pptx": "x-office-presentation",
|
||||
".zip": "package-x-generic",
|
||||
".tar": "package-x-generic",
|
||||
".gz": "package-x-generic",
|
||||
".jpg": "image-x-generic",
|
||||
".jpeg": "image-x-generic",
|
||||
".png": "image-x-generic",
|
||||
".gif": "image-x-generic",
|
||||
".mp3": "audio-x-generic",
|
||||
".wav": "audio-x-generic",
|
||||
".mp4": "video-x-generic",
|
||||
".avi": "video-x-generic",
|
||||
".html": "text-html",
|
||||
".css": "text-css",
|
||||
".js": "text-javascript",
|
||||
".json": "text-json",
|
||||
".xml": "text-xml",
|
||||
}
|
||||
|
||||
iconName := "application-x-generic"
|
||||
if name, ok := iconMap[ext]; ok {
|
||||
iconName = name
|
||||
}
|
||||
|
||||
return getIconBytes(iconName)
|
||||
}
|
||||
|
||||
func getIconBytes(iconName string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("icon lookup is not currently implemented for the GTK4 build path; build with -tags gtk3 for the legacy implementation")
|
||||
}
|
||||
|
||||
func (a *linuxApp) isDarkMode() bool {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
obj := conn.Object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")
|
||||
call := obj.Call("org.freedesktop.portal.Settings.Read", 0, "org.freedesktop.appearance", "color-scheme")
|
||||
if call.Err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var result dbus.Variant
|
||||
if err := call.Store(&result); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
innerVariant, ok := result.Value().(dbus.Variant)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
colorScheme, ok := innerVariant.Value().(uint32)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return colorScheme == 1
|
||||
}
|
||||
|
||||
func (a *linuxApp) getAccentColor() string {
|
||||
return "rgb(0,122,255)"
|
||||
}
|
||||
|
||||
func (a *linuxApp) isVisible() bool {
|
||||
windows := a.getWindows()
|
||||
for _, window := range windows {
|
||||
if C.gtk_widget_is_visible((*C.GtkWidget)(window)) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getNativeApplication() *linuxApp {
|
||||
return globalApplication.impl.(*linuxApp)
|
||||
}
|
||||
|
||||
// logPlatformInfo logs the platform information to the console
|
||||
func (a *App) logPlatformInfo() {
|
||||
info, err := operatingsystem.Info()
|
||||
if err != nil {
|
||||
a.error("error getting OS info: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
platformInfo := info.AsLogSlice()
|
||||
platformInfo = append(platformInfo, "GTK", fmt.Sprintf("%d.%d.%d",
|
||||
C.get_compiled_gtk_major_version(),
|
||||
C.get_compiled_gtk_minor_version(),
|
||||
C.get_compiled_gtk_micro_version()))
|
||||
platformInfo = append(platformInfo, "WebKitGTK", fmt.Sprintf("%d.%d.%d",
|
||||
C.get_compiled_webkit_major_version(),
|
||||
C.get_compiled_webkit_minor_version(),
|
||||
C.get_compiled_webkit_micro_version()))
|
||||
|
||||
a.info("Platform Info:", platformInfo...)
|
||||
}
|
||||
|
||||
func buildVersionString(major, minor, micro C.guint) string {
|
||||
return fmt.Sprintf("%d.%d.%d", uint(major), uint(minor), uint(micro))
|
||||
}
|
||||
|
||||
func (a *App) platformEnvironment() map[string]any {
|
||||
result := map[string]any{}
|
||||
result["gtk4-compiled"] = buildVersionString(
|
||||
C.get_compiled_gtk_major_version(),
|
||||
C.get_compiled_gtk_minor_version(),
|
||||
C.get_compiled_gtk_micro_version(),
|
||||
)
|
||||
result["gtk4-runtime"] = buildVersionString(
|
||||
C.gtk_get_major_version(),
|
||||
C.gtk_get_minor_version(),
|
||||
C.gtk_get_micro_version(),
|
||||
)
|
||||
|
||||
result["webkitgtk6-compiled"] = buildVersionString(
|
||||
C.get_compiled_webkit_major_version(),
|
||||
C.get_compiled_webkit_minor_version(),
|
||||
C.get_compiled_webkit_micro_version(),
|
||||
)
|
||||
result["webkitgtk6-runtime"] = buildVersionString(
|
||||
C.webkit_get_major_version(),
|
||||
C.webkit_get_minor_version(),
|
||||
C.webkit_get_micro_version(),
|
||||
)
|
||||
|
||||
result["compositor"] = detectCompositor()
|
||||
result["wayland"] = isWayland()
|
||||
result["focusFollowsMouse"] = detectFocusFollowsMouse()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func fatalHandler(errFunc func(error)) {
|
||||
// Stub for windows function
|
||||
return
|
||||
}
|
||||
153
vendor/github.com/wailsapp/wails/v3/pkg/application/application_linux_dbus.go
generated
vendored
Normal file
153
vendor/github.com/wailsapp/wails/v3/pkg/application/application_linux_dbus.go
generated
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
//go:build linux && cgo && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
func (a *linuxApp) monitorThemeChanges() {
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
a.parent.warning(
|
||||
"[WARNING] Failed to connect to session bus; monitoring for theme changes will not function: %v",
|
||||
err,
|
||||
)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err = conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath("/org/freedesktop/portal/desktop"),
|
||||
); err != nil {
|
||||
a.parent.warning(
|
||||
"[WARNING] Failed to subscribe to portal SettingChanged; theme changes will not fire: %v",
|
||||
err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
c := make(chan *dbus.Signal, 10)
|
||||
conn.Signal(c)
|
||||
|
||||
getTheme := func(body []interface{}) (string, bool) {
|
||||
if len(body) < 3 {
|
||||
return "", false
|
||||
}
|
||||
if entry, ok := body[0].(string); !ok || entry != "org.gnome.desktop.interface" {
|
||||
return "", false
|
||||
}
|
||||
if entry, ok := body[1].(string); !ok || entry != "color-scheme" {
|
||||
return "", false
|
||||
}
|
||||
variant, ok := body[2].(dbus.Variant)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
value, ok := variant.Value().(string)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
for v := range c {
|
||||
theme, ok := getTheme(v.Body)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if theme != a.theme {
|
||||
a.theme = theme
|
||||
event := newApplicationEvent(events.Linux.SystemThemeChanged)
|
||||
event.Context().setIsDarkMode(a.isDarkMode())
|
||||
applicationEvents <- event
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// monitorPowerEvents subscribes to systemd-logind's PrepareForSleep signal on
|
||||
// the system bus and translates it into Linux.SystemWillSleep (arg=true, just
|
||||
// before suspend) and Linux.SystemDidWake (arg=false, immediately on resume).
|
||||
// Mirrors NSWorkspace willSleep/didWake on macOS and WM_POWERBROADCAST on
|
||||
// Windows.
|
||||
//
|
||||
// On systems without systemd or logind/elogind reachable on the system bus
|
||||
// (Alpine, Void, some Devuan setups), we log a warning and exit cleanly so
|
||||
// the rest of the app keeps working.
|
||||
func (a *linuxApp) monitorPowerEvents() {
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
conn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
a.parent.warning(
|
||||
"[WARNING] Failed to connect to system bus; sleep/wake events will not fire: %v",
|
||||
err,
|
||||
)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Probe for logind/elogind ownership of org.freedesktop.login1 on the
|
||||
// system bus. Without this check, AddMatchSignal would succeed on any
|
||||
// systemd-less distro and the goroutine would block forever on a
|
||||
// channel that never receives — silently masking the missing service.
|
||||
var hasOwner bool
|
||||
if err := conn.BusObject().Call(
|
||||
"org.freedesktop.DBus.NameHasOwner", 0, "org.freedesktop.login1",
|
||||
).Store(&hasOwner); err != nil {
|
||||
a.parent.warning(
|
||||
"[WARNING] Failed to probe org.freedesktop.login1; sleep/wake events will not fire: %v",
|
||||
err,
|
||||
)
|
||||
return
|
||||
}
|
||||
if !hasOwner {
|
||||
a.parent.warning(
|
||||
"[WARNING] systemd-logind/elogind not reachable on the system bus; sleep/wake events will not fire",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Constrain the sender to logind's well-known name so a hostile
|
||||
// connection on the system bus can't spoof PrepareForSleep signals.
|
||||
if err = conn.AddMatchSignal(
|
||||
dbus.WithMatchSender("org.freedesktop.login1"),
|
||||
dbus.WithMatchInterface("org.freedesktop.login1.Manager"),
|
||||
dbus.WithMatchMember("PrepareForSleep"),
|
||||
dbus.WithMatchObjectPath("/org/freedesktop/login1"),
|
||||
); err != nil {
|
||||
a.parent.warning(
|
||||
"[WARNING] Failed to subscribe to logind PrepareForSleep; sleep/wake events will not fire: %v",
|
||||
err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
c := make(chan *dbus.Signal, 4)
|
||||
conn.Signal(c)
|
||||
|
||||
for v := range c {
|
||||
if v.Name != "org.freedesktop.login1.Manager.PrepareForSleep" {
|
||||
continue
|
||||
}
|
||||
if len(v.Body) < 1 {
|
||||
continue
|
||||
}
|
||||
willSleep, ok := v.Body[0].(bool)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if willSleep {
|
||||
applicationEvents <- newApplicationEvent(events.Linux.SystemWillSleep)
|
||||
} else {
|
||||
applicationEvents <- newApplicationEvent(events.Linux.SystemDidWake)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
305
vendor/github.com/wailsapp/wails/v3/pkg/application/application_linux_gtk3.go
generated
vendored
Normal file
305
vendor/github.com/wailsapp/wails/v3/pkg/application/application_linux_gtk3.go
generated
vendored
Normal file
@@ -0,0 +1,305 @@
|
||||
//go:build linux && cgo && gtk3 && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#include "gtk/gtk.h"
|
||||
#include "webkit2/webkit2.h"
|
||||
static guint get_compiled_gtk_major_version() { return GTK_MAJOR_VERSION; }
|
||||
static guint get_compiled_gtk_minor_version() { return GTK_MINOR_VERSION; }
|
||||
static guint get_compiled_gtk_micro_version() { return GTK_MICRO_VERSION; }
|
||||
static guint get_compiled_webkit_major_version() { return WEBKIT_MAJOR_VERSION; }
|
||||
static guint get_compiled_webkit_minor_version() { return WEBKIT_MINOR_VERSION; }
|
||||
static guint get_compiled_webkit_micro_version() { return WEBKIT_MICRO_VERSION; }
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"path/filepath"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
// sanitizeAppName sanitizes the application name to be a valid GTK/D-Bus application ID.
|
||||
// Valid IDs contain only alphanumeric characters, hyphens, and underscores.
|
||||
// They must not start with a digit.
|
||||
var invalidAppNameChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
|
||||
var leadingDigits = regexp.MustCompile(`^[0-9]+`)
|
||||
|
||||
func sanitizeAppName(name string) string {
|
||||
// Replace invalid characters with underscores
|
||||
name = invalidAppNameChars.ReplaceAllString(name, "_")
|
||||
// Prefix with underscore if starts with digit
|
||||
name = leadingDigits.ReplaceAllString(name, "_$0")
|
||||
// Remove consecutive underscores
|
||||
for strings.Contains(name, "__") {
|
||||
name = strings.ReplaceAll(name, "__", "_")
|
||||
}
|
||||
// Trim leading/trailing underscores
|
||||
name = strings.Trim(name, "_")
|
||||
if name == "" {
|
||||
name = "wailsapp"
|
||||
}
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// FIXME: This should be handled appropriately in the individual files most likely.
|
||||
// Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings
|
||||
if os.Getenv("GDK_BACKEND") == "" &&
|
||||
(os.Getenv("XDG_SESSION_TYPE") == "" || os.Getenv("XDG_SESSION_TYPE") == "unspecified" || os.Getenv("XDG_SESSION_TYPE") == "x11") {
|
||||
_ = os.Setenv("GDK_BACKEND", "x11")
|
||||
}
|
||||
|
||||
// Disable DMA-BUF renderer on any session type with NVIDIA to prevent blank windows and
|
||||
// "Error 71 (Protocol error)" crashes. NVIDIA proprietary drivers fail gbm_bo_map() when
|
||||
// importing DMA-BUF, causing blank/white screens on both X11 and Wayland.
|
||||
// See: https://bugs.webkit.org/show_bug.cgi?id=262607
|
||||
// See: https://github.com/wailsapp/wails/issues/4985
|
||||
if os.Getenv("WEBKIT_DISABLE_DMABUF_RENDERER") == "" && isNVIDIAGPU() {
|
||||
_ = os.Setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
|
||||
}
|
||||
}
|
||||
|
||||
// isNVIDIAGPU checks if an NVIDIA GPU is present by looking for the nvidia kernel module.
|
||||
func isNVIDIAGPU() bool {
|
||||
// Check if nvidia module is loaded (most reliable for proprietary driver)
|
||||
if _, err := os.Stat("/sys/module/nvidia"); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type linuxApp struct {
|
||||
application pointer
|
||||
parent *App
|
||||
|
||||
startupActions []func()
|
||||
|
||||
// Native -> uint
|
||||
windowMap map[windowPointer]uint
|
||||
windowMapLock sync.Mutex
|
||||
|
||||
theme string
|
||||
|
||||
icon pointer
|
||||
}
|
||||
|
||||
func (a *linuxApp) GetFlags(options Options) map[string]any {
|
||||
if options.Flags == nil {
|
||||
options.Flags = make(map[string]any)
|
||||
}
|
||||
return options.Flags
|
||||
}
|
||||
|
||||
func getNativeApplication() *linuxApp {
|
||||
return globalApplication.impl.(*linuxApp)
|
||||
}
|
||||
|
||||
func (a *linuxApp) hide() {
|
||||
a.hideAllWindows()
|
||||
}
|
||||
|
||||
func (a *linuxApp) show() {
|
||||
a.showAllWindows()
|
||||
}
|
||||
|
||||
func (a *linuxApp) on(eventID uint) {
|
||||
// TODO: Test register/unregister events
|
||||
//C.registerApplicationEvent(l.application, C.uint(eventID))
|
||||
}
|
||||
|
||||
func (a *linuxApp) name() string {
|
||||
return appName()
|
||||
}
|
||||
|
||||
type rnr struct {
|
||||
f func()
|
||||
}
|
||||
|
||||
func (r rnr) run() {
|
||||
r.f()
|
||||
}
|
||||
|
||||
func (a *linuxApp) setApplicationMenu(menu *Menu) {
|
||||
// FIXME: How do we avoid putting a menu?
|
||||
if menu == nil {
|
||||
// Create a default menu
|
||||
menu = DefaultApplicationMenu()
|
||||
globalApplication.applicationMenu = menu
|
||||
}
|
||||
}
|
||||
|
||||
func (a *linuxApp) run() error {
|
||||
|
||||
if len(os.Args) == 2 { // Case: program + 1 argument
|
||||
arg1 := os.Args[1]
|
||||
// Check if the argument is likely a URL from a custom protocol invocation
|
||||
if strings.Contains(arg1, "://") {
|
||||
a.parent.debug("Application launched with argument, potentially a URL from custom protocol", "url", arg1)
|
||||
eventContext := newApplicationEventContext()
|
||||
eventContext.setURL(arg1)
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(events.Common.ApplicationLaunchedWithUrl),
|
||||
ctx: eventContext,
|
||||
}
|
||||
} else {
|
||||
// Check if the argument matches any file associations
|
||||
matched := false
|
||||
if a.parent.options.FileAssociations != nil {
|
||||
ext := filepath.Ext(arg1)
|
||||
if slices.Contains(a.parent.options.FileAssociations, ext) {
|
||||
a.parent.debug("File opened via file association", "file", arg1, "extension", ext)
|
||||
eventContext := newApplicationEventContext()
|
||||
eventContext.setOpenedWithFile(arg1)
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(events.Common.ApplicationOpenedWithFile),
|
||||
ctx: eventContext,
|
||||
}
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
a.parent.debug("Application launched with single argument (not a URL), potential file open?", "arg", arg1)
|
||||
}
|
||||
}
|
||||
} else if len(os.Args) > 2 {
|
||||
// Log if multiple arguments are passed
|
||||
a.parent.debug("Application launched with multiple arguments", "args", os.Args[1:])
|
||||
}
|
||||
|
||||
a.parent.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(evt *ApplicationEvent) {
|
||||
if err := a.processAndCacheScreens(); err != nil {
|
||||
a.parent.handleError(err)
|
||||
}
|
||||
})
|
||||
a.setupCommonEvents()
|
||||
a.monitorThemeChanges()
|
||||
a.monitorPowerEvents()
|
||||
return appRun(a.application)
|
||||
}
|
||||
|
||||
func (a *linuxApp) unregisterWindow(w windowPointer) {
|
||||
a.windowMapLock.Lock()
|
||||
delete(a.windowMap, w)
|
||||
a.windowMapLock.Unlock()
|
||||
|
||||
// If this was the last window...
|
||||
if len(a.windowMap) == 0 && !a.parent.options.Linux.DisableQuitOnLastWindowClosed {
|
||||
a.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *linuxApp) destroy() {
|
||||
if !globalApplication.shouldQuit() {
|
||||
return
|
||||
}
|
||||
globalApplication.cleanup()
|
||||
appDestroy(a.application)
|
||||
}
|
||||
|
||||
func (a *linuxApp) isOnMainThread() bool {
|
||||
return isOnMainThread()
|
||||
}
|
||||
|
||||
// register our window to our parent mapping
|
||||
func (a *linuxApp) registerWindow(window pointer, id uint) {
|
||||
a.windowMapLock.Lock()
|
||||
a.windowMap[windowPointer(window)] = id
|
||||
a.windowMapLock.Unlock()
|
||||
}
|
||||
|
||||
func (a *linuxApp) isDarkMode() bool {
|
||||
return strings.Contains(a.theme, "dark")
|
||||
}
|
||||
|
||||
func (a *linuxApp) getAccentColor() string {
|
||||
// Linux doesn't have a unified system accent color API
|
||||
// Return a default blue color
|
||||
return "rgb(0,122,255)"
|
||||
}
|
||||
|
||||
func newPlatformApp(parent *App) *linuxApp {
|
||||
name := sanitizeAppName(parent.options.Name)
|
||||
app := &linuxApp{
|
||||
parent: parent,
|
||||
application: appNew(name),
|
||||
windowMap: map[windowPointer]uint{},
|
||||
}
|
||||
|
||||
if parent.options.Linux.ProgramName != "" {
|
||||
setProgramName(parent.options.Linux.ProgramName)
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// logPlatformInfo logs the platform information to the console
|
||||
func (a *App) logPlatformInfo() {
|
||||
info, err := operatingsystem.Info()
|
||||
if err != nil {
|
||||
a.error("error getting OS info: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
wkVersion := operatingsystem.GetWebkitVersion()
|
||||
platformInfo := info.AsLogSlice()
|
||||
platformInfo = append(platformInfo, "Webkit2Gtk", wkVersion)
|
||||
|
||||
a.info("Platform Info:", platformInfo...)
|
||||
}
|
||||
|
||||
//export processWindowEvent
|
||||
func processWindowEvent(windowID C.uint, eventID C.uint) {
|
||||
windowEvents <- &windowEvent{
|
||||
WindowID: uint(windowID),
|
||||
EventID: uint(eventID),
|
||||
}
|
||||
}
|
||||
|
||||
func buildVersionString(major, minor, micro C.uint) string {
|
||||
return fmt.Sprintf("%d.%d.%d", uint(major), uint(minor), uint(micro))
|
||||
}
|
||||
|
||||
func (a *App) platformEnvironment() map[string]any {
|
||||
result := map[string]any{}
|
||||
result["gtk3-compiled"] = buildVersionString(
|
||||
C.get_compiled_gtk_major_version(),
|
||||
C.get_compiled_gtk_minor_version(),
|
||||
C.get_compiled_gtk_micro_version(),
|
||||
)
|
||||
result["gtk3-runtime"] = buildVersionString(
|
||||
C.gtk_get_major_version(),
|
||||
C.gtk_get_minor_version(),
|
||||
C.gtk_get_micro_version(),
|
||||
)
|
||||
|
||||
result["webkit2gtk-compiled"] = buildVersionString(
|
||||
C.get_compiled_webkit_major_version(),
|
||||
C.get_compiled_webkit_minor_version(),
|
||||
C.get_compiled_webkit_micro_version(),
|
||||
)
|
||||
result["webkit2gtk-runtime"] = buildVersionString(
|
||||
C.webkit_get_major_version(),
|
||||
C.webkit_get_minor_version(),
|
||||
C.webkit_get_micro_version(),
|
||||
)
|
||||
|
||||
result["compositor"] = detectCompositor()
|
||||
result["wayland"] = isWayland()
|
||||
result["focusFollowsMouse"] = detectFocusFollowsMouse()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func fatalHandler(errFunc func(error)) {
|
||||
// Stub for windows function
|
||||
return
|
||||
}
|
||||
435
vendor/github.com/wailsapp/wails/v3/pkg/application/application_options.go
generated
vendored
Normal file
435
vendor/github.com/wailsapp/wails/v3/pkg/application/application_options.go
generated
vendored
Normal file
@@ -0,0 +1,435 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/assetserver"
|
||||
)
|
||||
|
||||
// Options contains the options for the application
|
||||
type Options struct {
|
||||
// Name is the name of the application (used in the default about box)
|
||||
Name string
|
||||
|
||||
// Description is the description of the application (used in the default about box)
|
||||
Description string
|
||||
|
||||
// Icon is the icon of the application (used in the default about box)
|
||||
Icon []byte
|
||||
|
||||
// Mac is the Mac specific configuration for Mac builds
|
||||
Mac MacOptions
|
||||
|
||||
// Windows is the Windows specific configuration for Windows builds
|
||||
Windows WindowsOptions
|
||||
|
||||
// Linux is the Linux specific configuration for Linux builds
|
||||
Linux LinuxOptions
|
||||
|
||||
// IOS is the iOS specific configuration for iOS builds
|
||||
IOS IOSOptions
|
||||
|
||||
// Android is the Android specific configuration for Android builds
|
||||
Android AndroidOptions
|
||||
|
||||
// Services allows you to bind Go methods to the frontend.
|
||||
Services []Service
|
||||
|
||||
// MarshalError will be called if non-nil
|
||||
// to marshal to JSON the error values returned by service methods.
|
||||
//
|
||||
// MarshalError is not allowed to fail,
|
||||
// but it may return a nil slice to fall back
|
||||
// to the default error handling mechanism.
|
||||
//
|
||||
// If the returned slice is not nil, it must contain valid JSON.
|
||||
MarshalError func(error) []byte
|
||||
|
||||
// BindAliases allows you to specify alias IDs for your bound methods.
|
||||
// Example: `BindAliases: map[uint32]uint32{1: 1411160069}` states that alias ID 1 maps to the Go method with ID 1411160069.
|
||||
BindAliases map[uint32]uint32
|
||||
|
||||
// Logger is a slog.Logger instance used for logging Wails system messages (not application messages).
|
||||
// If not defined, a default logger is used.
|
||||
Logger *slog.Logger
|
||||
|
||||
// LogLevel defines the log level of the Wails system logger.
|
||||
LogLevel slog.Level
|
||||
|
||||
// Assets are the application assets to be used.
|
||||
Assets AssetOptions
|
||||
|
||||
// Flags are key value pairs that are available to the frontend.
|
||||
// This is also used by Wails to provide information to the frontend.
|
||||
Flags map[string]any
|
||||
|
||||
// PanicHandler is called when a panic occurs
|
||||
PanicHandler func(*PanicDetails)
|
||||
|
||||
// DisableDefaultSignalHandler disables the default signal handler
|
||||
DisableDefaultSignalHandler bool
|
||||
|
||||
// KeyBindings is a map of key bindings to functions
|
||||
KeyBindings map[string]func(window Window)
|
||||
|
||||
// OnShutdown is called when the application is about to terminate.
|
||||
// This is useful for cleanup tasks.
|
||||
// The shutdown process blocks until this function returns.
|
||||
OnShutdown func()
|
||||
|
||||
// PostShutdown is called after the application
|
||||
// has finished shutting down, just before process termination.
|
||||
// This is useful for testing and logging purposes
|
||||
// on platforms where the Run() method does not return.
|
||||
// When PostShutdown is called, the application instance is not usable anymore.
|
||||
// The shutdown process blocks until this function returns.
|
||||
PostShutdown func()
|
||||
|
||||
// ShouldQuit is a function that is called when the user tries to quit the application.
|
||||
// If the function returns true, the application will quit.
|
||||
// If the function returns false, the application will not quit.
|
||||
ShouldQuit func() bool
|
||||
|
||||
// RawMessageHandler is called when the frontend sends a raw message.
|
||||
// This is useful for implementing custom frontend-to-backend communication.
|
||||
RawMessageHandler func(window Window, message string, originInfo *OriginInfo)
|
||||
|
||||
// WarningHandler is called when a warning occurs
|
||||
WarningHandler func(string)
|
||||
|
||||
// ErrorHandler is called when an error occurs
|
||||
ErrorHandler func(err error)
|
||||
|
||||
// File extensions associated with the application
|
||||
// Example: [".txt", ".md"]
|
||||
// The '.' is required
|
||||
FileAssociations []string
|
||||
|
||||
// SingleInstance options for single instance functionality
|
||||
SingleInstance *SingleInstanceOptions
|
||||
|
||||
// Transport allows you to provide a custom IPC transport layer.
|
||||
// When set, Wails will use your transport instead of the default HTTP fetch-based transport.
|
||||
// This allows you to use WebSockets, custom protocols, or any other transport mechanism
|
||||
// while retaining all Wails generated bindings and event communication.
|
||||
//
|
||||
// The default transport uses HTTP fetch requests to /wails/runtime + events via js.Exec in webview.
|
||||
// If not specified, the default transport is used.
|
||||
//
|
||||
// Example use case: Implementing WebSocket-based or PostMessage IPC.
|
||||
Transport Transport
|
||||
|
||||
// Server configures the HTTP server for server mode.
|
||||
// Server mode is enabled by building with the "server" build tag:
|
||||
// go build -tags server
|
||||
//
|
||||
// In server mode, the application runs as an HTTP server without a native window.
|
||||
// This enables deploying the same Wails application as a web server for:
|
||||
// - Docker/container deployments
|
||||
// - Server-side rendering
|
||||
// - Web-only access without desktop dependencies
|
||||
Server ServerOptions
|
||||
}
|
||||
|
||||
// ServerOptions configures the HTTP server for headless mode.
|
||||
type ServerOptions struct {
|
||||
// Host is the address to bind to.
|
||||
// Default: "localhost" for security. Use "0.0.0.0" for all interfaces.
|
||||
Host string
|
||||
|
||||
// Port is the port to listen on.
|
||||
// Default: 8080
|
||||
Port int
|
||||
|
||||
// ReadTimeout is the maximum duration for reading the entire request.
|
||||
// Default: 30 seconds
|
||||
ReadTimeout time.Duration
|
||||
|
||||
// WriteTimeout is the maximum duration before timing out writes of the response.
|
||||
// Default: 30 seconds
|
||||
WriteTimeout time.Duration
|
||||
|
||||
// IdleTimeout is the maximum duration to wait for the next request.
|
||||
// Default: 120 seconds
|
||||
IdleTimeout time.Duration
|
||||
|
||||
// ShutdownTimeout is the maximum duration to wait for active connections to close.
|
||||
// Default: 30 seconds
|
||||
ShutdownTimeout time.Duration
|
||||
|
||||
// TLS configures HTTPS. If nil, HTTP is used.
|
||||
TLS *TLSOptions
|
||||
}
|
||||
|
||||
// TLSOptions configures HTTPS for the headless server.
|
||||
type TLSOptions struct {
|
||||
// CertFile is the path to the TLS certificate file.
|
||||
CertFile string
|
||||
|
||||
// KeyFile is the path to the TLS private key file.
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
// AssetOptions defines the configuration of the AssetServer.
|
||||
type AssetOptions struct {
|
||||
// Handler which serves all the content to the WebView.
|
||||
Handler http.Handler
|
||||
|
||||
// Middleware is a HTTP Middleware which allows to hook into the AssetServer request chain. It allows to skip the default
|
||||
// request handler dynamically, e.g. implement specialized Routing etc.
|
||||
// The Middleware is called to build a new `http.Handler` used by the AssetSever and it also receives the default
|
||||
// handler used by the AssetServer as an argument.
|
||||
//
|
||||
// This middleware injects itself before any of Wails internal middlewares.
|
||||
//
|
||||
// If not defined, the default AssetServer request chain is executed.
|
||||
//
|
||||
// Multiple Middlewares can be chained together with:
|
||||
// ChainMiddleware(middleware ...Middleware) Middleware
|
||||
Middleware Middleware
|
||||
|
||||
// DisableLogging disables logging of the AssetServer. By default, the AssetServer logs every request.
|
||||
DisableLogging bool
|
||||
}
|
||||
|
||||
// Middleware defines HTTP middleware that can be applied to the AssetServer.
|
||||
// The handler passed as next is the next handler in the chain. One can decide to call the next handler
|
||||
// or implement a specialized handling.
|
||||
type Middleware func(next http.Handler) http.Handler
|
||||
|
||||
// ChainMiddleware allows chaining multiple middlewares to one middleware.
|
||||
func ChainMiddleware(middleware ...Middleware) Middleware {
|
||||
return func(h http.Handler) http.Handler {
|
||||
for i := len(middleware) - 1; i >= 0; i-- {
|
||||
h = middleware[i](h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
}
|
||||
|
||||
// AssetFileServerFS returns a http handler which serves the assets from the fs.FS.
|
||||
// If an external devserver has been provided 'FRONTEND_DEVSERVER_URL' the files are being served
|
||||
// from the external server, ignoring the `assets`.
|
||||
func AssetFileServerFS(assets fs.FS) http.Handler {
|
||||
return assetserver.NewAssetFileServer(assets)
|
||||
}
|
||||
|
||||
// BundledAssetFileServer returns a http handler which serves the assets from the fs.FS.
|
||||
// If an external devserver has been provided 'FRONTEND_DEVSERVER_URL' the files are being served
|
||||
// from the external server, ignoring the `assets`.
|
||||
// It also serves the compiled runtime.js file at `/wails/runtime.js`.
|
||||
// It will provide the production runtime.js file from the embedded assets if the `production` tag is used.
|
||||
func BundledAssetFileServer(assets fs.FS) http.Handler {
|
||||
return assetserver.NewBundledAssetFileServer(assets)
|
||||
}
|
||||
|
||||
/******** Mac Options ********/
|
||||
|
||||
// ActivationPolicy is the activation policy for the application.
|
||||
type ActivationPolicy int
|
||||
|
||||
const (
|
||||
// ActivationPolicyRegular is used for applications that have a user interface,
|
||||
ActivationPolicyRegular ActivationPolicy = iota
|
||||
// ActivationPolicyAccessory is used for applications that do not have a main window,
|
||||
// such as system tray applications or background applications.
|
||||
ActivationPolicyAccessory
|
||||
ActivationPolicyProhibited
|
||||
)
|
||||
|
||||
// MacOptions contains options for macOS applications.
|
||||
type MacOptions struct {
|
||||
// ActivationPolicy is the activation policy for the application. Defaults to
|
||||
// applicationActivationPolicyRegular.
|
||||
ActivationPolicy ActivationPolicy
|
||||
// If set to true, the application will terminate when the last window is closed.
|
||||
ApplicationShouldTerminateAfterLastWindowClosed bool
|
||||
}
|
||||
|
||||
/****** Windows Options *******/
|
||||
|
||||
// WindowsOptions contains options for Windows applications.
|
||||
type WindowsOptions struct {
|
||||
|
||||
// Window class name
|
||||
// Default: WailsWebviewWindow
|
||||
WndClass string
|
||||
|
||||
// WndProcInterceptor is a function that will be called for every message sent in the application.
|
||||
// Use this to hook into the main message loop. This is useful for handling custom window messages.
|
||||
// If `shouldReturn` is `true` then `returnCode` will be returned by the main message loop.
|
||||
// If `shouldReturn` is `false` then returnCode will be ignored and the message will be processed by the main message loop.
|
||||
WndProcInterceptor func(hwnd uintptr, msg uint32, wParam, lParam uintptr) (returnCode uintptr, shouldReturn bool)
|
||||
|
||||
// DisableQuitOnLastWindowClosed disables the auto quit of the application if the last window has been closed.
|
||||
DisableQuitOnLastWindowClosed bool
|
||||
|
||||
// Path where the WebView2 stores the user data. If empty %APPDATA%\[BinaryName.exe] will be used.
|
||||
// If the path is not valid, a messagebox will be displayed with the error and the app will exit with error code.
|
||||
WebviewUserDataPath string
|
||||
|
||||
// Path to the directory with WebView2 executables. If empty WebView2 installed in the system will be used.
|
||||
WebviewBrowserPath string
|
||||
|
||||
// EnabledFeatures, DisabledFeatures and AdditionalBrowserArgs configure the WebView2 browser.
|
||||
// These apply globally to ALL windows because WebView2 shares a single browser environment.
|
||||
// See: https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/webview-features-flags
|
||||
// AdditionalBrowserArgs must include the "--" prefix, e.g. "--remote-debugging-port=9222"
|
||||
EnabledFeatures []string
|
||||
DisabledFeatures []string
|
||||
AdditionalBrowserArgs []string
|
||||
|
||||
// UseVisualHosting forces WebView2 to use IDCompositionVisual hosting
|
||||
// instead of the default windowed (HWND-child) hosting. Set this to
|
||||
// true if your app is used over RDP — particularly the Microsoft
|
||||
// Remote Desktop iOS client, which provisions a Retina-optimised
|
||||
// virtual monitor mid-session whose DPI context differs from the
|
||||
// session's. With windowed hosting that DPI mismatch forces a
|
||||
// synchronous DComp re-marshal on every WebView2 controller call
|
||||
// (PutIsVisible, MoveFocus, first-paint, surface release), each
|
||||
// blocking the UI thread for ~2 seconds and persisting until the
|
||||
// server is rebooted. Visual hosting eliminates that re-marshal.
|
||||
//
|
||||
// Implementation: sets the COREWEBVIEW2_FORCED_HOSTING_MODE env var
|
||||
// to COREWEBVIEW2_HOSTING_MODE_WINDOW_TO_VISUAL before WebView2 is
|
||||
// initialised. Must be set before app.Run().
|
||||
//
|
||||
// See: https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/windowed-vs-visual-hosting
|
||||
// See: https://github.com/MicrosoftEdge/WebView2Feedback/issues/5248
|
||||
// See: https://github.com/MicrosoftEdge/WebView2Feedback/issues/4485
|
||||
UseVisualHosting bool
|
||||
}
|
||||
|
||||
/********* Linux Options *********/
|
||||
|
||||
// LinuxOptions contains options for Linux applications.
|
||||
type LinuxOptions struct {
|
||||
// DisableQuitOnLastWindowClosed disables the auto quit of the application if the last window has been closed.
|
||||
DisableQuitOnLastWindowClosed bool
|
||||
|
||||
// ProgramName is used to set the program's name for the window manager via GTK's g_set_prgname().
|
||||
//This name should not be localized. [see the docs]
|
||||
//
|
||||
//When a .desktop file is created this value helps with window grouping and desktop icons when the .desktop file's Name
|
||||
//property differs form the executable's filename.
|
||||
//
|
||||
//[see the docs]: https://docs.gtk.org/glib/func.set_prgname.html
|
||||
ProgramName string
|
||||
}
|
||||
|
||||
/********* iOS Options *********/
|
||||
|
||||
// IOSOptions contains options for iOS applications.
|
||||
type IOSOptions struct {
|
||||
// DisableInputAccessoryView controls whether the iOS WKWebView shows the
|
||||
// input accessory toolbar (the bar with Next/Previous/Done) above the keyboard.
|
||||
// Default: false (accessory bar is shown).
|
||||
// true => accessory view is disabled/hidden
|
||||
// false => accessory view is enabled/shown
|
||||
DisableInputAccessoryView bool
|
||||
|
||||
// Scrolling & Bounce (defaults: scroll/bounce/indicators are enabled on iOS)
|
||||
// Use Disable* to keep default true behavior without surprising zero-values.
|
||||
DisableScroll bool
|
||||
DisableBounce bool
|
||||
DisableScrollIndicators bool
|
||||
|
||||
// Navigation gestures (default false)
|
||||
EnableBackForwardNavigationGestures bool
|
||||
|
||||
// Link previews (default true on iOS)
|
||||
// Use Disable* so default (false) means previews are enabled.
|
||||
DisableLinkPreview bool
|
||||
|
||||
// Media playback
|
||||
// Inline playback (default false) -> Enable*
|
||||
EnableInlineMediaPlayback bool
|
||||
// Autoplay without user action (default false) -> Enable*
|
||||
EnableAutoplayWithoutUserAction bool
|
||||
|
||||
// Inspector / Debug (default true in dev)
|
||||
// Use Disable* so default (false) keeps inspector enabled.
|
||||
DisableInspectable bool
|
||||
|
||||
// User agent customization
|
||||
// If empty, defaults apply. ApplicationNameForUserAgent defaults to "wails.io".
|
||||
UserAgent string
|
||||
ApplicationNameForUserAgent string
|
||||
|
||||
// BackgroundColour is the app-wide background colour for the main iOS window,
|
||||
// shown before the WebView paints. Set it to match your web background to
|
||||
// avoid a white flash on launch. Defaults to white when left at its zero
|
||||
// value.
|
||||
BackgroundColour RGBA
|
||||
|
||||
// EnableNativeTabs enables a native iOS UITabBar at the bottom of the screen.
|
||||
// When enabled, the native tab bar will dispatch a 'nativeTabSelected' CustomEvent
|
||||
// to the window with detail: { index: number }.
|
||||
// NOTE: If NativeTabsItems has one or more entries, native tabs are auto-enabled
|
||||
// regardless of this flag, and the provided items will be used.
|
||||
EnableNativeTabs bool
|
||||
|
||||
// NativeTabsItems configures the labels and optional SF Symbol icons for the
|
||||
// native UITabBar. If one or more items are provided, native tabs are automatically
|
||||
// enabled. If empty and EnableNativeTabs is true, default items are used.
|
||||
NativeTabsItems []NativeTabItem
|
||||
}
|
||||
|
||||
// NativeTabItem describes a single item in the iOS native UITabBar.
|
||||
// SystemImage is the SF Symbols name to use for the icon (iOS 13+). If empty or
|
||||
// unavailable on the current OS, no icon is shown.
|
||||
type NativeTabItem struct {
|
||||
Title string `json:"Title"`
|
||||
SystemImage NativeTabIcon `json:"SystemImage"`
|
||||
}
|
||||
|
||||
// NativeTabIcon is a string-based enum for SF Symbols.
|
||||
// It allows using predefined constants for common symbols while still accepting
|
||||
// any valid SF Symbols name as a plain string.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// NativeTabsItems: []NativeTabItem{
|
||||
// { Title: "Home", SystemImage: NativeTabIconHouse },
|
||||
// { Title: "Settings", SystemImage: "gearshape" }, // arbitrary string still allowed
|
||||
// }
|
||||
type NativeTabIcon string
|
||||
|
||||
const (
|
||||
// Common icons
|
||||
NativeTabIconNone NativeTabIcon = ""
|
||||
NativeTabIconHouse NativeTabIcon = "house"
|
||||
NativeTabIconGear NativeTabIcon = "gear"
|
||||
NativeTabIconStar NativeTabIcon = "star"
|
||||
NativeTabIconPerson NativeTabIcon = "person"
|
||||
NativeTabIconBell NativeTabIcon = "bell"
|
||||
NativeTabIconMagnify NativeTabIcon = "magnifyingglass"
|
||||
NativeTabIconList NativeTabIcon = "list.bullet"
|
||||
NativeTabIconFolder NativeTabIcon = "folder"
|
||||
)
|
||||
|
||||
/********* Android Options *********/
|
||||
|
||||
// AndroidOptions contains options for Android applications.
|
||||
type AndroidOptions struct {
|
||||
// DisableScroll disables scrolling in the WebView
|
||||
DisableScroll bool
|
||||
|
||||
// DisableBounce disables the overscroll bounce effect
|
||||
DisableOverscroll bool
|
||||
|
||||
// EnableZoom allows pinch-to-zoom in the WebView (default: false)
|
||||
EnableZoom bool
|
||||
|
||||
// UserAgent sets a custom user agent string
|
||||
UserAgent string
|
||||
|
||||
// BackgroundColour sets the background colour of the WebView
|
||||
BackgroundColour RGBA
|
||||
|
||||
// DisableHardwareAcceleration disables hardware acceleration for the WebView
|
||||
DisableHardwareAcceleration bool
|
||||
}
|
||||
18
vendor/github.com/wailsapp/wails/v3/pkg/application/application_production.go
generated
vendored
Normal file
18
vendor/github.com/wailsapp/wails/v3/pkg/application/application_production.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
//go:build production
|
||||
|
||||
package application
|
||||
|
||||
func newApplication(options Options) *App {
|
||||
result := &App{
|
||||
isDebugMode: false,
|
||||
options: options,
|
||||
}
|
||||
result.init()
|
||||
return result
|
||||
}
|
||||
|
||||
func (a *App) logStartup() {}
|
||||
|
||||
func (a *App) preRun() error { return nil }
|
||||
|
||||
func (a *App) postQuit() error { return nil }
|
||||
570
vendor/github.com/wailsapp/wails/v3/pkg/application/application_server.go
generated
vendored
Normal file
570
vendor/github.com/wailsapp/wails/v3/pkg/application/application_server.go
generated
vendored
Normal file
@@ -0,0 +1,570 @@
|
||||
//go:build server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// serverApp implements platformApp for server mode.
|
||||
// It provides a minimal implementation that runs an HTTP server
|
||||
// without any GUI components.
|
||||
//
|
||||
// Server mode is enabled by building with the "server" build tag:
|
||||
//
|
||||
// go build -tags server
|
||||
type serverApp struct {
|
||||
app *App
|
||||
server *http.Server
|
||||
listener net.Listener
|
||||
broadcaster *WebSocketBroadcaster
|
||||
}
|
||||
|
||||
// newPlatformApp creates a new server-mode platform app.
|
||||
// This function is only compiled when building with the "server" tag.
|
||||
func newPlatformApp(app *App) *serverApp {
|
||||
app.info("Server mode enabled (built with -tags server)")
|
||||
return &serverApp{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
// parsePort parses a port string into an integer.
|
||||
func parsePort(s string) (int, error) {
|
||||
p, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if p < 1 || p > 65535 {
|
||||
return 0, errors.New("port out of range")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// run starts the HTTP server and blocks until shutdown.
|
||||
func (h *serverApp) run() error {
|
||||
// Set up common events
|
||||
h.setupCommonEvents()
|
||||
|
||||
// Create WebSocket broadcaster for events
|
||||
h.broadcaster = NewWebSocketBroadcaster(h.app)
|
||||
globalBroadcaster = h.broadcaster // Set global reference for browser ID lookups
|
||||
h.app.wailsEventListenerLock.Lock()
|
||||
h.app.wailsEventListeners = append(h.app.wailsEventListeners, h.broadcaster)
|
||||
h.app.wailsEventListenerLock.Unlock()
|
||||
|
||||
opts := h.app.options.Server
|
||||
|
||||
// Environment variables override config (useful for Docker/containers)
|
||||
host := os.Getenv("WAILS_SERVER_HOST")
|
||||
if host == "" {
|
||||
host = opts.Host
|
||||
}
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
port := opts.Port
|
||||
if envPort := os.Getenv("WAILS_SERVER_PORT"); envPort != "" {
|
||||
if p, err := parsePort(envPort); err == nil {
|
||||
port = p
|
||||
}
|
||||
}
|
||||
if port == 0 {
|
||||
port = 8080
|
||||
}
|
||||
|
||||
readTimeout := opts.ReadTimeout
|
||||
if readTimeout == 0 {
|
||||
readTimeout = 30 * time.Second
|
||||
}
|
||||
|
||||
writeTimeout := opts.WriteTimeout
|
||||
if writeTimeout == 0 {
|
||||
writeTimeout = 30 * time.Second
|
||||
}
|
||||
|
||||
idleTimeout := opts.IdleTimeout
|
||||
if idleTimeout == 0 {
|
||||
idleTimeout = 120 * time.Second
|
||||
}
|
||||
|
||||
shutdownTimeout := opts.ShutdownTimeout
|
||||
if shutdownTimeout == 0 {
|
||||
shutdownTimeout = 30 * time.Second
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
// Create HTTP handler from asset server
|
||||
handler := h.createHandler()
|
||||
|
||||
h.server = &http.Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
IdleTimeout: idleTimeout,
|
||||
}
|
||||
|
||||
// Create listener
|
||||
var err error
|
||||
h.listener, err = net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on %s: %w", addr, err)
|
||||
}
|
||||
|
||||
h.app.info("Server mode starting", "address", addr)
|
||||
|
||||
// Start server in goroutine
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
if opts.TLS != nil {
|
||||
errCh <- h.server.ServeTLS(h.listener, opts.TLS.CertFile, opts.TLS.KeyFile)
|
||||
} else {
|
||||
errCh <- h.server.Serve(h.listener)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for shutdown signal or error
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
case <-quit:
|
||||
h.app.info("Shutdown signal received")
|
||||
case <-h.app.ctx.Done():
|
||||
h.app.info("Application context cancelled")
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := h.server.Shutdown(ctx); err != nil {
|
||||
return fmt.Errorf("server shutdown error: %w", err)
|
||||
}
|
||||
|
||||
h.app.info("Server stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// customJS is the JavaScript that sets up WebSocket event connection for server mode.
|
||||
// Events FROM frontend TO backend use the existing HTTP transport.
|
||||
// This WebSocket is only for receiving broadcast events FROM backend TO all frontends.
|
||||
const customJS = `(function() {
|
||||
var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
var clientId = window._wails && window._wails.clientId ? window._wails.clientId : '';
|
||||
var wsUrl = protocol + '//' + location.host + '/wails/events' + (clientId ? '?clientId=' + encodeURIComponent(clientId) : '');
|
||||
var ws;
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(wsUrl);
|
||||
ws.onopen = function() {
|
||||
console.log('[Wails] Event WebSocket connected');
|
||||
};
|
||||
ws.onmessage = function(e) {
|
||||
try {
|
||||
var event = JSON.parse(e.data);
|
||||
if (window._wails && window._wails.dispatchWailsEvent) {
|
||||
window._wails.dispatchWailsEvent(event);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Wails] Failed to parse event:', err);
|
||||
}
|
||||
};
|
||||
ws.onclose = function() {
|
||||
console.log('[Wails] Event WebSocket disconnected, reconnecting...');
|
||||
setTimeout(connect, 1000);
|
||||
};
|
||||
ws.onerror = function() {
|
||||
ws.close();
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
})();`
|
||||
|
||||
// createHandler creates the HTTP handler for server mode.
|
||||
func (h *serverApp) createHandler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Health check endpoint
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
|
||||
// Serve custom.js for server mode (WebSocket event connection)
|
||||
mux.HandleFunc("/wails/custom.js", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(customJS))
|
||||
})
|
||||
|
||||
// WebSocket endpoint for events
|
||||
mux.Handle("/wails/events", h.broadcaster)
|
||||
|
||||
// Serve all other requests through the asset server
|
||||
mux.Handle("/", h.app.assets)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
// destroy stops the server and cleans up.
|
||||
func (h *serverApp) destroy() {
|
||||
if h.server != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
h.server.Shutdown(ctx)
|
||||
}
|
||||
h.app.cleanup()
|
||||
}
|
||||
|
||||
// setApplicationMenu is a no-op in server mode.
|
||||
func (h *serverApp) setApplicationMenu(menu *Menu) {
|
||||
// No-op: server mode has no GUI
|
||||
}
|
||||
|
||||
// name returns the application name.
|
||||
func (h *serverApp) name() string {
|
||||
return h.app.options.Name
|
||||
}
|
||||
|
||||
// getCurrentWindowID returns 0 in server mode (no windows).
|
||||
func (h *serverApp) getCurrentWindowID() uint {
|
||||
return 0
|
||||
}
|
||||
|
||||
// showAboutDialog is a no-op in server mode.
|
||||
func (h *serverApp) showAboutDialog(name string, description string, icon []byte) {
|
||||
// No-op: server mode has no GUI
|
||||
h.app.warning("showAboutDialog called in server mode - operation ignored")
|
||||
}
|
||||
|
||||
// setIcon is a no-op in server mode.
|
||||
func (h *serverApp) setIcon(icon []byte) {
|
||||
// No-op: server mode has no GUI
|
||||
}
|
||||
|
||||
// on is a no-op in server mode.
|
||||
func (h *serverApp) on(id uint) {
|
||||
// No-op: server mode has no platform-specific event handling
|
||||
}
|
||||
|
||||
// dispatchOnMainThread executes the function directly in server mode.
|
||||
func (h *serverApp) dispatchOnMainThread(id uint) {
|
||||
// In server mode, there's no "main thread" concept from GUI frameworks
|
||||
// Execute the function directly
|
||||
mainThreadFunctionStoreLock.Lock()
|
||||
fn, ok := mainThreadFunctionStore[id]
|
||||
if ok {
|
||||
delete(mainThreadFunctionStore, id)
|
||||
}
|
||||
mainThreadFunctionStoreLock.Unlock()
|
||||
|
||||
if ok && fn != nil {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
|
||||
// hide is a no-op in server mode.
|
||||
func (h *serverApp) hide() {
|
||||
// No-op: server mode has no GUI
|
||||
}
|
||||
|
||||
// show is a no-op in server mode.
|
||||
func (h *serverApp) show() {
|
||||
// No-op: server mode has no GUI
|
||||
}
|
||||
|
||||
// getPrimaryScreen returns nil in server mode.
|
||||
func (h *serverApp) getPrimaryScreen() (*Screen, error) {
|
||||
return nil, errors.New("screen information not available in server mode")
|
||||
}
|
||||
|
||||
// getScreens returns an error in server mode (screen info unavailable).
|
||||
func (h *serverApp) getScreens() ([]*Screen, error) {
|
||||
return nil, errors.New("screen information not available in server mode")
|
||||
}
|
||||
|
||||
// GetFlags returns the application flags for server mode.
|
||||
func (h *serverApp) GetFlags(options Options) map[string]any {
|
||||
flags := make(map[string]any)
|
||||
flags["server"] = true
|
||||
if options.Flags != nil {
|
||||
for k, v := range options.Flags {
|
||||
flags[k] = v
|
||||
}
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// isOnMainThread always returns true in server mode.
|
||||
func (h *serverApp) isOnMainThread() bool {
|
||||
// In server mode, there's no main thread concept
|
||||
return true
|
||||
}
|
||||
|
||||
// isDarkMode returns false in server mode.
|
||||
func (h *serverApp) isDarkMode() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// getAccentColor returns empty string in server mode.
|
||||
func (h *serverApp) getAccentColor() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// logPlatformInfo logs platform info for server mode.
|
||||
func (a *App) logPlatformInfo() {
|
||||
a.info("Platform Info:", "mode", "server")
|
||||
}
|
||||
|
||||
// platformEnvironment returns environment info for server mode.
|
||||
func (a *App) platformEnvironment() map[string]any {
|
||||
return map[string]any{
|
||||
"mode": "server",
|
||||
}
|
||||
}
|
||||
|
||||
// fatalHandler sets up fatal error handling for server mode.
|
||||
func fatalHandler(errFunc func(error)) {
|
||||
// In server mode, fatal errors are handled via standard mechanisms
|
||||
}
|
||||
|
||||
// newClipboardImpl creates a clipboard implementation for server mode.
|
||||
func newClipboardImpl() clipboardImpl {
|
||||
return &serverClipboard{}
|
||||
}
|
||||
|
||||
// serverClipboard is a no-op clipboard for server mode.
|
||||
type serverClipboard struct{}
|
||||
|
||||
func (c *serverClipboard) setText(text string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *serverClipboard) text() (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// newDialogImpl creates a dialog implementation for server mode.
|
||||
func newDialogImpl(d *MessageDialog) messageDialogImpl {
|
||||
return &serverDialog{}
|
||||
}
|
||||
|
||||
// serverDialog is a no-op dialog for server mode.
|
||||
type serverDialog struct{}
|
||||
|
||||
func (d *serverDialog) show() {
|
||||
// No-op in server mode
|
||||
}
|
||||
|
||||
// newOpenFileDialogImpl creates an open file dialog implementation for server mode.
|
||||
func newOpenFileDialogImpl(d *OpenFileDialogStruct) openFileDialogImpl {
|
||||
return &serverOpenFileDialog{}
|
||||
}
|
||||
|
||||
// serverOpenFileDialog is a no-op open file dialog for server mode.
|
||||
type serverOpenFileDialog struct{}
|
||||
|
||||
func (d *serverOpenFileDialog) show() (chan string, error) {
|
||||
ch := make(chan string, 1)
|
||||
close(ch)
|
||||
return ch, errors.New("file dialogs not available in server mode")
|
||||
}
|
||||
|
||||
// newSaveFileDialogImpl creates a save file dialog implementation for server mode.
|
||||
func newSaveFileDialogImpl(d *SaveFileDialogStruct) saveFileDialogImpl {
|
||||
return &serverSaveFileDialog{}
|
||||
}
|
||||
|
||||
// serverSaveFileDialog is a no-op save file dialog for server mode.
|
||||
type serverSaveFileDialog struct{}
|
||||
|
||||
func (d *serverSaveFileDialog) show() (chan string, error) {
|
||||
ch := make(chan string, 1)
|
||||
close(ch)
|
||||
return ch, errors.New("file dialogs not available in server mode")
|
||||
}
|
||||
|
||||
// newMenuImpl creates a menu implementation for server mode.
|
||||
func newMenuImpl(menu *Menu) menuImpl {
|
||||
return &serverMenu{}
|
||||
}
|
||||
|
||||
// serverMenu is a no-op menu for server mode.
|
||||
type serverMenu struct{}
|
||||
|
||||
func (m *serverMenu) update() {
|
||||
// No-op in server mode
|
||||
}
|
||||
|
||||
// newPlatformLock creates a platform-specific single instance lock for server mode.
|
||||
func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
|
||||
return &serverLock{}, nil
|
||||
}
|
||||
|
||||
// serverLock is a basic lock for server mode.
|
||||
type serverLock struct{}
|
||||
|
||||
func (l *serverLock) acquire(uniqueID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *serverLock) release() {
|
||||
// No-op in server mode
|
||||
}
|
||||
|
||||
func (l *serverLock) notify(data string) error {
|
||||
return errors.New("single instance not supported in server mode")
|
||||
}
|
||||
|
||||
// newSystemTrayImpl creates a system tray implementation for server mode.
|
||||
func newSystemTrayImpl(s *SystemTray) systemTrayImpl {
|
||||
return &serverSystemTray{parent: s}
|
||||
}
|
||||
|
||||
// serverSystemTray is a no-op system tray for server mode.
|
||||
type serverSystemTray struct {
|
||||
parent *SystemTray
|
||||
}
|
||||
|
||||
func (t *serverSystemTray) setLabel(label string) {}
|
||||
func (t *serverSystemTray) setTooltip(tooltip string) {}
|
||||
func (t *serverSystemTray) run() {}
|
||||
func (t *serverSystemTray) setIcon(icon []byte) {}
|
||||
func (t *serverSystemTray) setMenu(menu *Menu) {}
|
||||
func (t *serverSystemTray) setIconPosition(pos IconPosition) {}
|
||||
func (t *serverSystemTray) setTemplateIcon(icon []byte) {}
|
||||
func (t *serverSystemTray) destroy() {}
|
||||
func (t *serverSystemTray) setDarkModeIcon(icon []byte) {}
|
||||
func (t *serverSystemTray) bounds() (*Rect, error) {
|
||||
return nil, errors.New("system tray not available in server mode")
|
||||
}
|
||||
func (t *serverSystemTray) getScreen() (*Screen, error) {
|
||||
return nil, errors.New("system tray not available in server mode")
|
||||
}
|
||||
func (t *serverSystemTray) positionWindow(w Window, o int) error {
|
||||
return errors.New("system tray not available in server mode")
|
||||
}
|
||||
func (t *serverSystemTray) openMenu() {}
|
||||
func (t *serverSystemTray) Show() {}
|
||||
func (t *serverSystemTray) Hide() {}
|
||||
|
||||
// newWindowImpl creates a webview window implementation for server mode.
|
||||
func newWindowImpl(parent *WebviewWindow) *serverWebviewWindow {
|
||||
return &serverWebviewWindow{parent: parent}
|
||||
}
|
||||
|
||||
// serverWebviewWindow is a no-op webview window for server mode.
|
||||
type serverWebviewWindow struct {
|
||||
parent *WebviewWindow
|
||||
}
|
||||
|
||||
// All webviewWindowImpl methods as no-ops for server mode
|
||||
func (w *serverWebviewWindow) setTitle(title string) {}
|
||||
func (w *serverWebviewWindow) setSize(width, height int) {}
|
||||
func (w *serverWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) {}
|
||||
func (w *serverWebviewWindow) setURL(url string) {}
|
||||
func (w *serverWebviewWindow) setResizable(resizable bool) {}
|
||||
func (w *serverWebviewWindow) setMinSize(width, height int) {}
|
||||
func (w *serverWebviewWindow) setMaxSize(width, height int) {}
|
||||
func (w *serverWebviewWindow) execJS(js string) {}
|
||||
func (w *serverWebviewWindow) setBackgroundColour(color RGBA) {}
|
||||
func (w *serverWebviewWindow) run() {}
|
||||
func (w *serverWebviewWindow) center() {}
|
||||
func (w *serverWebviewWindow) size() (int, int) { return 0, 0 }
|
||||
func (w *serverWebviewWindow) width() int { return 0 }
|
||||
func (w *serverWebviewWindow) height() int { return 0 }
|
||||
func (w *serverWebviewWindow) destroy() {}
|
||||
func (w *serverWebviewWindow) reload() {}
|
||||
func (w *serverWebviewWindow) forceReload() {}
|
||||
func (w *serverWebviewWindow) openDevTools() {}
|
||||
func (w *serverWebviewWindow) zoomReset() {}
|
||||
func (w *serverWebviewWindow) zoomIn() {}
|
||||
func (w *serverWebviewWindow) zoomOut() {}
|
||||
func (w *serverWebviewWindow) getZoom() float64 { return 1.0 }
|
||||
func (w *serverWebviewWindow) setZoom(zoom float64) {}
|
||||
func (w *serverWebviewWindow) close() {}
|
||||
func (w *serverWebviewWindow) zoom() {}
|
||||
func (w *serverWebviewWindow) setHTML(html string) {}
|
||||
func (w *serverWebviewWindow) on(eventID uint) {}
|
||||
func (w *serverWebviewWindow) minimise() {}
|
||||
func (w *serverWebviewWindow) unminimise() {}
|
||||
func (w *serverWebviewWindow) maximise() {}
|
||||
func (w *serverWebviewWindow) unmaximise() {}
|
||||
func (w *serverWebviewWindow) fullscreen() {}
|
||||
func (w *serverWebviewWindow) unfullscreen() {}
|
||||
func (w *serverWebviewWindow) isMinimised() bool { return false }
|
||||
func (w *serverWebviewWindow) isMaximised() bool { return false }
|
||||
func (w *serverWebviewWindow) isFullscreen() bool { return false }
|
||||
func (w *serverWebviewWindow) isNormal() bool { return true }
|
||||
func (w *serverWebviewWindow) isVisible() bool { return false }
|
||||
func (w *serverWebviewWindow) isFocused() bool { return false }
|
||||
func (w *serverWebviewWindow) focus() {}
|
||||
func (w *serverWebviewWindow) show() {}
|
||||
func (w *serverWebviewWindow) hide() {}
|
||||
func (w *serverWebviewWindow) getScreen() (*Screen, error) {
|
||||
return nil, errors.New("screens not available in server mode")
|
||||
}
|
||||
func (w *serverWebviewWindow) setFrameless(frameless bool) {}
|
||||
func (w *serverWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData) {}
|
||||
func (w *serverWebviewWindow) nativeWindow() unsafe.Pointer { return nil }
|
||||
func (w *serverWebviewWindow) startDrag() error {
|
||||
return errors.New("drag not available in server mode")
|
||||
}
|
||||
func (w *serverWebviewWindow) startResize(border string) error {
|
||||
return errors.New("resize not available in server mode")
|
||||
}
|
||||
func (w *serverWebviewWindow) print() error { return errors.New("print not available in server mode") }
|
||||
func (w *serverWebviewWindow) setEnabled(enabled bool) {}
|
||||
func (w *serverWebviewWindow) physicalBounds() Rect { return Rect{} }
|
||||
func (w *serverWebviewWindow) setPhysicalBounds(bounds Rect) {}
|
||||
func (w *serverWebviewWindow) bounds() Rect { return Rect{} }
|
||||
func (w *serverWebviewWindow) setBounds(bounds Rect) {}
|
||||
func (w *serverWebviewWindow) position() (int, int) { return 0, 0 }
|
||||
func (w *serverWebviewWindow) setPosition(x int, y int) {}
|
||||
func (w *serverWebviewWindow) centerOnScreen(_ *Screen) {}
|
||||
func (w *serverWebviewWindow) relativePosition() (int, int) { return 0, 0 }
|
||||
func (w *serverWebviewWindow) setRelativePosition(x int, y int) {}
|
||||
func (w *serverWebviewWindow) flash(enabled bool) {}
|
||||
func (w *serverWebviewWindow) handleKeyEvent(acceleratorString string) {}
|
||||
func (w *serverWebviewWindow) getBorderSizes() *LRTB { return &LRTB{} }
|
||||
func (w *serverWebviewWindow) setMinimiseButtonState(state ButtonState) {}
|
||||
func (w *serverWebviewWindow) setMaximiseButtonState(state ButtonState) {}
|
||||
func (w *serverWebviewWindow) setCloseButtonState(state ButtonState) {}
|
||||
func (w *serverWebviewWindow) setFullscreenButtonState(state ButtonState) {}
|
||||
func (w *serverWebviewWindow) isIgnoreMouseEvents() bool { return false }
|
||||
func (w *serverWebviewWindow) setIgnoreMouseEvents(ignore bool) {}
|
||||
func (w *serverWebviewWindow) cut() {}
|
||||
func (w *serverWebviewWindow) copy() {}
|
||||
func (w *serverWebviewWindow) paste() {}
|
||||
func (w *serverWebviewWindow) undo() {}
|
||||
func (w *serverWebviewWindow) delete() {}
|
||||
func (w *serverWebviewWindow) selectAll() {}
|
||||
func (w *serverWebviewWindow) redo() {}
|
||||
func (w *serverWebviewWindow) showMenuBar() {}
|
||||
func (w *serverWebviewWindow) hideMenuBar() {}
|
||||
func (w *serverWebviewWindow) toggleMenuBar() {}
|
||||
func (w *serverWebviewWindow) setMenu(menu *Menu) {}
|
||||
func (w *serverWebviewWindow) snapAssist() {}
|
||||
func (w *serverWebviewWindow) attachModal(modalWindow *WebviewWindow) {}
|
||||
func (w *serverWebviewWindow) setContentProtection(enabled bool) {}
|
||||
func (w *serverWebviewWindow) setNonClientHitTestRegions([]nonClientHitTestRegion) {}
|
||||
478
vendor/github.com/wailsapp/wails/v3/pkg/application/application_windows.go
generated
vendored
Normal file
478
vendor/github.com/wailsapp/wails/v3/pkg/application/application_windows.go
generated
vendored
Normal file
@@ -0,0 +1,478 @@
|
||||
//go:build windows && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/webview2/webviewloader"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
)
|
||||
|
||||
var (
|
||||
wmTaskbarCreated = w32.RegisterWindowMessage(w32.MustStringToUTF16Ptr("TaskbarCreated"))
|
||||
)
|
||||
|
||||
type windowsApp struct {
|
||||
parent *App
|
||||
|
||||
windowClass w32.WNDCLASSEX
|
||||
instance w32.HINSTANCE
|
||||
|
||||
windowMap map[w32.HWND]*windowsWebviewWindow
|
||||
windowMapLock sync.RWMutex
|
||||
|
||||
systrayMap map[w32.HMENU]*windowsSystemTray
|
||||
systrayMapLock sync.RWMutex
|
||||
|
||||
mainThreadID w32.HANDLE
|
||||
mainThreadWindowHWND w32.HWND
|
||||
|
||||
// Windows hidden by application.Hide()
|
||||
hiddenWindows []*windowsWebviewWindow
|
||||
focusedWindow w32.HWND
|
||||
|
||||
// system theme
|
||||
isCurrentlyDarkMode bool
|
||||
currentWindowID uint
|
||||
|
||||
// Restart taskbar flag
|
||||
restartingTaskbar atomic.Bool
|
||||
}
|
||||
|
||||
func (m *windowsApp) isDarkMode() bool {
|
||||
return w32.IsCurrentlyDarkMode()
|
||||
}
|
||||
|
||||
func (m *windowsApp) getAccentColor() string {
|
||||
accentColor, err := w32.GetAccentColor()
|
||||
if err != nil {
|
||||
m.parent.error("failed to get accent color: %w", err)
|
||||
return "rgb(0,122,255)"
|
||||
}
|
||||
|
||||
return accentColor
|
||||
}
|
||||
|
||||
func (m *windowsApp) isOnMainThread() bool {
|
||||
return m.mainThreadID == w32.GetCurrentThreadId()
|
||||
}
|
||||
|
||||
func (m *windowsApp) GetFlags(options Options) map[string]any {
|
||||
if options.Flags == nil {
|
||||
options.Flags = make(map[string]any)
|
||||
}
|
||||
options.Flags["system"] = map[string]any{
|
||||
"resizeHandleWidth": w32.GetSystemMetrics(w32.SM_CXSIZEFRAME),
|
||||
"resizeHandleHeight": w32.GetSystemMetrics(w32.SM_CYSIZEFRAME),
|
||||
}
|
||||
return options.Flags
|
||||
}
|
||||
|
||||
func (m *windowsApp) getWindowForHWND(hwnd w32.HWND) *windowsWebviewWindow {
|
||||
m.windowMapLock.RLock()
|
||||
defer m.windowMapLock.RUnlock()
|
||||
return m.windowMap[hwnd]
|
||||
}
|
||||
|
||||
func getNativeApplication() *windowsApp {
|
||||
return globalApplication.impl.(*windowsApp)
|
||||
}
|
||||
|
||||
func (m *windowsApp) hide() {
|
||||
// Get the current focussed window
|
||||
m.focusedWindow = w32.GetForegroundWindow()
|
||||
|
||||
// Iterate over all windows and hide them if they aren't already hidden
|
||||
for _, window := range m.windowMap {
|
||||
if window.isVisible() {
|
||||
// Add to hidden windows
|
||||
m.hiddenWindows = append(m.hiddenWindows, window)
|
||||
window.hide()
|
||||
}
|
||||
}
|
||||
// Switch focus to the next application
|
||||
hwndNext := w32.GetWindow(m.mainThreadWindowHWND, w32.GW_HWNDNEXT)
|
||||
w32.SetForegroundWindow(hwndNext)
|
||||
}
|
||||
|
||||
func (m *windowsApp) show() {
|
||||
// Iterate over all windows and show them if they were previously hidden
|
||||
for _, window := range m.hiddenWindows {
|
||||
window.show()
|
||||
}
|
||||
// Show the foreground window
|
||||
w32.SetForegroundWindow(m.focusedWindow)
|
||||
}
|
||||
|
||||
func (m *windowsApp) on(_ uint) {
|
||||
}
|
||||
|
||||
func (m *windowsApp) setIcon(_ []byte) {
|
||||
}
|
||||
|
||||
func (m *windowsApp) name() string {
|
||||
// appName := C.getAppName()
|
||||
// defer C.free(unsafe.Pointer(appName))
|
||||
// return C.GoString(appName)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *windowsApp) getCurrentWindowID() uint {
|
||||
return m.currentWindowID
|
||||
}
|
||||
|
||||
func (m *windowsApp) setApplicationMenu(menu *Menu) {
|
||||
if menu == nil {
|
||||
// Create a default menu for windows
|
||||
menu = DefaultApplicationMenu()
|
||||
}
|
||||
menu.Update()
|
||||
|
||||
m.parent.applicationMenu = menu
|
||||
}
|
||||
|
||||
func (m *windowsApp) run() error {
|
||||
m.setupCommonEvents()
|
||||
for eventID := range m.parent.applicationEventListeners {
|
||||
m.on(eventID)
|
||||
}
|
||||
// EmitEvent application started event
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(events.Windows.ApplicationStarted),
|
||||
ctx: blankApplicationEventContext,
|
||||
}
|
||||
|
||||
if len(os.Args) == 2 { // Case: program + 1 argument
|
||||
arg1 := os.Args[1]
|
||||
// Check if the argument is likely a URL from a custom protocol invocation
|
||||
if strings.Contains(arg1, "://") {
|
||||
m.parent.debug("Application launched with argument, potentially a URL from custom protocol", "url", arg1)
|
||||
eventContext := newApplicationEventContext()
|
||||
eventContext.setURL(arg1)
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(events.Common.ApplicationLaunchedWithUrl),
|
||||
ctx: eventContext,
|
||||
}
|
||||
} else {
|
||||
// If not a URL-like string, check for file association
|
||||
if m.parent.options.FileAssociations != nil {
|
||||
ext := filepath.Ext(arg1)
|
||||
if slices.Contains(m.parent.options.FileAssociations, ext) {
|
||||
m.parent.debug("Application launched with file via file association", "file", arg1)
|
||||
eventContext := newApplicationEventContext()
|
||||
eventContext.setOpenedWithFile(arg1)
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(events.Common.ApplicationOpenedWithFile),
|
||||
ctx: eventContext,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if len(os.Args) > 2 {
|
||||
// Log if multiple arguments are passed, though typical protocol/file launch is a single arg.
|
||||
m.parent.debug("Application launched with multiple arguments", "args", os.Args[1:])
|
||||
}
|
||||
|
||||
_ = m.runMainLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *windowsApp) destroy() {
|
||||
if !globalApplication.shouldQuit() {
|
||||
return
|
||||
}
|
||||
globalApplication.cleanup()
|
||||
// destroy the main thread window
|
||||
w32.DestroyWindow(m.mainThreadWindowHWND)
|
||||
// Post a quit message to the main thread
|
||||
w32.PostQuitMessage(0)
|
||||
}
|
||||
|
||||
func (m *windowsApp) init() {
|
||||
// Register the window class
|
||||
|
||||
icon := w32.LoadIconWithResourceID(m.instance, w32.IDI_APPLICATION)
|
||||
|
||||
m.windowClass.Size = uint32(unsafe.Sizeof(m.windowClass))
|
||||
m.windowClass.Style = w32.CS_HREDRAW | w32.CS_VREDRAW
|
||||
m.windowClass.WndProc = syscall.NewCallback(m.wndProc)
|
||||
m.windowClass.Instance = m.instance
|
||||
m.windowClass.Background = w32.COLOR_BTNFACE + 1
|
||||
m.windowClass.Icon = icon
|
||||
m.windowClass.Cursor = w32.LoadCursorWithResourceID(0, w32.IDC_ARROW)
|
||||
m.windowClass.ClassName = w32.MustStringToUTF16Ptr(m.parent.options.Windows.WndClass)
|
||||
m.windowClass.MenuName = nil
|
||||
m.windowClass.IconSm = icon
|
||||
|
||||
if ret := w32.RegisterClassEx(&m.windowClass); ret == 0 {
|
||||
panic(syscall.GetLastError())
|
||||
}
|
||||
m.isCurrentlyDarkMode = w32.IsCurrentlyDarkMode()
|
||||
}
|
||||
|
||||
func (m *windowsApp) wndProc(hwnd w32.HWND, msg uint32, wParam, lParam uintptr) uintptr {
|
||||
|
||||
// Handle the invoke callback
|
||||
if msg == wmInvokeCallback {
|
||||
m.invokeCallback(wParam, lParam)
|
||||
return 0
|
||||
}
|
||||
|
||||
// If the WndProcInterceptor is set in options, pass the message on
|
||||
if m.parent.options.Windows.WndProcInterceptor != nil {
|
||||
returnValue, shouldReturn := m.parent.options.Windows.WndProcInterceptor(hwnd, msg, wParam, lParam)
|
||||
if shouldReturn {
|
||||
return returnValue
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the main thread window
|
||||
// Quit the application if requested
|
||||
// Reprocess and cache screens when display settings change
|
||||
if hwnd == m.mainThreadWindowHWND {
|
||||
if msg == w32.WM_ENDSESSION || msg == w32.WM_DESTROY || msg == w32.WM_CLOSE {
|
||||
globalApplication.Quit()
|
||||
}
|
||||
if msg == w32.WM_DISPLAYCHANGE || (msg == w32.WM_SETTINGCHANGE && wParam == w32.SPI_SETWORKAREA) {
|
||||
err := m.processAndCacheScreens()
|
||||
if err != nil {
|
||||
m.parent.handleError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch msg {
|
||||
case w32.WM_HOTKEY:
|
||||
// A global shortcut fired. wParam holds the id we passed to
|
||||
// RegisterHotKey. Route it to the global shortcut manager.
|
||||
if app := globalApplication; app != nil && app.GlobalShortcut != nil {
|
||||
app.GlobalShortcut.dispatch(int(wParam))
|
||||
}
|
||||
return 0
|
||||
case wmTaskbarCreated:
|
||||
if m.restartingTaskbar.Load() {
|
||||
break
|
||||
}
|
||||
m.restartingTaskbar.Store(true)
|
||||
m.reshowSystrays()
|
||||
go func() {
|
||||
// 1 second debounce
|
||||
time.Sleep(1000)
|
||||
m.restartingTaskbar.Store(false)
|
||||
}()
|
||||
case w32.WM_SETTINGCHANGE:
|
||||
settingChanged := w32.UTF16PtrToString((*uint16)(unsafe.Pointer(lParam)))
|
||||
if settingChanged == "ImmersiveColorSet" {
|
||||
isDarkMode := w32.IsCurrentlyDarkMode()
|
||||
if isDarkMode != m.isCurrentlyDarkMode {
|
||||
eventContext := newApplicationEventContext()
|
||||
eventContext.setIsDarkMode(isDarkMode)
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(events.Windows.SystemThemeChanged),
|
||||
ctx: eventContext,
|
||||
}
|
||||
m.isCurrentlyDarkMode = isDarkMode
|
||||
}
|
||||
}
|
||||
return 0
|
||||
case w32.WM_POWERBROADCAST:
|
||||
switch wParam {
|
||||
case w32.PBT_APMPOWERSTATUSCHANGE:
|
||||
applicationEvents <- newApplicationEvent(events.Windows.APMPowerStatusChange)
|
||||
case w32.PBT_APMSUSPEND:
|
||||
applicationEvents <- newApplicationEvent(events.Windows.APMSuspend)
|
||||
case w32.PBT_APMRESUMEAUTOMATIC:
|
||||
applicationEvents <- newApplicationEvent(events.Windows.APMResumeAutomatic)
|
||||
case w32.PBT_APMRESUMESUSPEND:
|
||||
applicationEvents <- newApplicationEvent(events.Windows.APMResumeSuspend)
|
||||
case w32.PBT_POWERSETTINGCHANGE:
|
||||
applicationEvents <- newApplicationEvent(events.Windows.APMPowerSettingChange)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if window, ok := m.windowMap[hwnd]; ok {
|
||||
return window.WndProc(msg, wParam, lParam)
|
||||
}
|
||||
|
||||
m.systrayMapLock.Lock()
|
||||
systray, ok := m.systrayMap[hwnd]
|
||||
m.systrayMapLock.Unlock()
|
||||
if ok {
|
||||
return systray.wndProc(msg, wParam, lParam)
|
||||
}
|
||||
|
||||
// Dispatch the message to the appropriate window
|
||||
|
||||
return w32.DefWindowProc(hwnd, msg, wParam, lParam)
|
||||
}
|
||||
|
||||
func (m *windowsApp) registerWindow(result *windowsWebviewWindow) {
|
||||
m.windowMapLock.Lock()
|
||||
m.windowMap[result.hwnd] = result
|
||||
m.windowMapLock.Unlock()
|
||||
}
|
||||
|
||||
func (m *windowsApp) registerSystemTray(result *windowsSystemTray) {
|
||||
m.systrayMapLock.Lock()
|
||||
defer m.systrayMapLock.Unlock()
|
||||
m.systrayMap[result.hwnd] = result
|
||||
}
|
||||
|
||||
func (m *windowsApp) unregisterSystemTray(result *windowsSystemTray) {
|
||||
m.systrayMapLock.Lock()
|
||||
defer m.systrayMapLock.Unlock()
|
||||
delete(m.systrayMap, result.hwnd)
|
||||
}
|
||||
|
||||
func (m *windowsApp) unregisterWindow(w *windowsWebviewWindow) {
|
||||
m.windowMapLock.Lock()
|
||||
delete(m.windowMap, w.hwnd)
|
||||
m.windowMapLock.Unlock()
|
||||
|
||||
// If this was the last window...
|
||||
if len(m.windowMap) == 0 && !m.parent.options.Windows.DisableQuitOnLastWindowClosed {
|
||||
w32.PostQuitMessage(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *windowsApp) reshowSystrays() {
|
||||
m.systrayMapLock.Lock()
|
||||
defer m.systrayMapLock.Unlock()
|
||||
for _, systray := range m.systrayMap {
|
||||
if _, err := systray.show(); err != nil {
|
||||
globalApplication.warning("failed to re-add system tray icon: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupDPIAwareness() error {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process
|
||||
// https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
|
||||
|
||||
// Check if DPI awareness has already been set (e.g., via application manifest).
|
||||
// Windows only allows setting DPI awareness once per process - either via manifest
|
||||
// or API, not both. If already set, skip the API call to avoid "Access is denied" errors.
|
||||
// See: https://github.com/wailsapp/wails/issues/4803 and #4835
|
||||
|
||||
// Prefer the newer GetThreadDpiAwarenessContext (Windows 10 1607+) over the older
|
||||
// GetProcessDpiAwareness (SHCORE) because a manifest entry with permonitorv2 sets the
|
||||
// context via SetProcessDpiAwarenessContext, which GetProcessDpiAwareness may not reflect.
|
||||
if w32.HasGetThreadDpiAwarenessContextFunc() && w32.HasAreDpiAwarenessContextsEqualFunc() {
|
||||
ctx := w32.GetThreadDpiAwarenessContext()
|
||||
if !w32.AreDpiAwarenessContextsEqual(ctx, w32.DPI_AWARENESS_CONTEXT_UNAWARE) {
|
||||
// DPI awareness already set (likely via manifest), skip API call
|
||||
return nil
|
||||
}
|
||||
} else if w32.HasGetProcessDpiAwarenessFunc() {
|
||||
awareness, err := w32.GetProcessDpiAwareness()
|
||||
if err == nil && awareness != w32.PROCESS_DPI_UNAWARE {
|
||||
// DPI awareness already set (likely via manifest), skip API call
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if w32.HasSetProcessDpiAwarenessContextFunc() {
|
||||
// This is most recent version with the best results
|
||||
// supported beginning with Windows 10, version 1703
|
||||
return w32.SetProcessDpiAwarenessContext(w32.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
|
||||
}
|
||||
|
||||
if w32.HasSetProcessDpiAwarenessFunc() {
|
||||
// Supported beginning with Windows 8.1
|
||||
return w32.SetProcessDpiAwareness(w32.PROCESS_PER_MONITOR_DPI_AWARE)
|
||||
}
|
||||
|
||||
if w32.HasSetProcessDPIAwareFunc() {
|
||||
// If none of the above is supported, fallback to SetProcessDPIAware
|
||||
// which is supported beginning with Windows Vista
|
||||
return w32.SetProcessDPIAware()
|
||||
}
|
||||
|
||||
return errors.New("no DPI awareness method supported")
|
||||
}
|
||||
|
||||
func newPlatformApp(app *App) *windowsApp {
|
||||
|
||||
// Force WebView2 visual hosting before any WebView2 environment is
|
||||
// initialised. This is the documented Microsoft workaround for the
|
||||
// "DPI-context-change hang" — most commonly seen when the Microsoft
|
||||
// Remote Desktop iOS client provisions a Retina-optimised virtual
|
||||
// monitor mid-session and every subsequent WebView2 controller call
|
||||
// blocks the UI thread for ~2 s on synchronous DComp re-marshal.
|
||||
// See WindowsOptions.UseVisualHosting for the full rationale.
|
||||
if app.options.Windows.UseVisualHosting {
|
||||
_ = os.Setenv("COREWEBVIEW2_FORCED_HOSTING_MODE",
|
||||
"COREWEBVIEW2_HOSTING_MODE_WINDOW_TO_VISUAL")
|
||||
}
|
||||
|
||||
err := setupDPIAwareness()
|
||||
if err != nil {
|
||||
app.handleError(err)
|
||||
}
|
||||
|
||||
result := &windowsApp{
|
||||
parent: app,
|
||||
instance: w32.GetModuleHandle(""),
|
||||
windowMap: make(map[w32.HWND]*windowsWebviewWindow),
|
||||
systrayMap: make(map[w32.HWND]*windowsSystemTray),
|
||||
}
|
||||
|
||||
err = result.processAndCacheScreens()
|
||||
if err != nil {
|
||||
app.handleFatalError(err)
|
||||
}
|
||||
|
||||
result.init()
|
||||
result.initMainLoop()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (a *App) logPlatformInfo() {
|
||||
var args []any
|
||||
args = append(args, "Go-WebView2Loader", webviewloader.UsingGoWebview2Loader)
|
||||
webviewVersion, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(
|
||||
a.options.Windows.WebviewBrowserPath,
|
||||
)
|
||||
if err != nil {
|
||||
args = append(args, "WebView2", "Error: "+err.Error())
|
||||
} else {
|
||||
args = append(args, "WebView2", webviewVersion)
|
||||
}
|
||||
|
||||
osInfo, _ := operatingsystem.Info()
|
||||
args = append(args, osInfo.AsLogSlice()...)
|
||||
|
||||
a.info("Platform Info:", args...)
|
||||
}
|
||||
|
||||
func (a *App) platformEnvironment() map[string]any {
|
||||
result := map[string]any{}
|
||||
webviewVersion, _ := webviewloader.GetAvailableCoreWebView2BrowserVersionString(
|
||||
a.options.Windows.WebviewBrowserPath,
|
||||
)
|
||||
result["Go-WebView2Loader"] = webviewloader.UsingGoWebview2Loader
|
||||
result["WebView2"] = webviewVersion
|
||||
return result
|
||||
}
|
||||
|
||||
func fatalHandler(errFunc func(error)) {
|
||||
w32.Fatal = errFunc
|
||||
return
|
||||
}
|
||||
81
vendor/github.com/wailsapp/wails/v3/pkg/application/assets/alpha/index.html
generated
vendored
Normal file
81
vendor/github.com/wailsapp/wails/v3/pkg/application/assets/alpha/index.html
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
167
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart.go
generated
vendored
Normal file
167
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart.go
generated
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// resolvedExecutable returns os.Executable() after resolving symlinks so
|
||||
// registrations don't break when the binary is installed via a symlink farm
|
||||
// (Homebrew, Scoop). Falls back to the unresolved path if EvalSymlinks fails.
|
||||
func resolvedExecutable() (string, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("autostart: get executable path: %w", err)
|
||||
}
|
||||
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
|
||||
return resolved, nil
|
||||
}
|
||||
return exe, nil
|
||||
}
|
||||
|
||||
// writeFileAtomic writes data to path by way of a tempfile + rename in the
|
||||
// same directory, so a partial write never leaves a half-formed plist or
|
||||
// .desktop file in place.
|
||||
func writeFileAtomic(path string, data []byte, perm os.FileMode) error {
|
||||
dir := filepath.Dir(path)
|
||||
tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
cleanup := func() { _ = os.Remove(tmpName) }
|
||||
// os.File.Write is documented to return an error on short writes, but we
|
||||
// double-check n == len(data) so a future change of writer type can't
|
||||
// silently rename a truncated artefact into place.
|
||||
n, err := tmp.Write(data)
|
||||
if err == nil && n != len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if err != nil {
|
||||
_ = tmp.Close()
|
||||
cleanup()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Chmod(perm); err != nil {
|
||||
_ = tmp.Close()
|
||||
cleanup()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
cleanup()
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmpName, path); err != nil {
|
||||
cleanup()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAutostartIdentifier rejects identifiers that contain characters
|
||||
// that would be unsafe as a filename, registry value, or launchd Label.
|
||||
func validateAutostartIdentifier(id string) error {
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
if len(id) > 200 {
|
||||
return fmt.Errorf("autostart identifier too long (max 200): %q", id)
|
||||
}
|
||||
for _, r := range id {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z',
|
||||
r >= 'A' && r <= 'Z',
|
||||
r >= '0' && r <= '9',
|
||||
r == '.', r == '_', r == '-':
|
||||
default:
|
||||
return fmt.Errorf("autostart identifier contains invalid character %q (allowed: A-Za-z0-9._-)", r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// autostartSlug turns a free-form application name into something usable as
|
||||
// the basename of a registration artefact. Empty input is rejected by the
|
||||
// caller; this helper never returns an empty string for non-empty input.
|
||||
func autostartSlug(name string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(name))
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z',
|
||||
r >= '0' && r <= '9',
|
||||
r == '.', r == '_', r == '-':
|
||||
b.WriteRune(r)
|
||||
case r >= 'A' && r <= 'Z':
|
||||
b.WriteRune(r + ('a' - 'A'))
|
||||
case r == ' ', r == '\t':
|
||||
b.WriteByte('-')
|
||||
}
|
||||
}
|
||||
out := strings.Trim(b.String(), "-._")
|
||||
if out == "" {
|
||||
return "wails-app"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ErrAutostartNotSupported is returned when autostart is not available on
|
||||
// the current platform (mobile, server builds).
|
||||
var ErrAutostartNotSupported = errors.New("autostart is not supported on this platform")
|
||||
|
||||
// AutostartOptions configures how the application is registered to launch at login.
|
||||
type AutostartOptions struct {
|
||||
// Identifier overrides the auto-derived registration ID.
|
||||
//
|
||||
// macOS: launchd Label / SMAppService key (reverse-DNS recommended).
|
||||
// Windows: registry value name under HKCU\…\Run.
|
||||
// Linux: .desktop filename (without extension).
|
||||
//
|
||||
// If empty, a sensible default is derived: on macOS the application's
|
||||
// bundle identifier (when running from a bundle) or "wails.autostart.<slug>";
|
||||
// on Windows and Linux a slugified form of the application's Options.Name
|
||||
// (i.e. application.Options.Name from application.New).
|
||||
Identifier string
|
||||
|
||||
// Arguments are appended to the executable path when launched at login.
|
||||
Arguments []string
|
||||
}
|
||||
|
||||
// AutostartStrategy names the underlying mechanism a registration used.
|
||||
//
|
||||
// On macOS this distinguishes between SMAppService (bundled .app on macOS 13+)
|
||||
// and a LaunchAgent plist (the fallback path). On Windows it is always
|
||||
// AutostartStrategyRegistryRun and on Linux always AutostartStrategyXDGAutostart.
|
||||
// Empty when AutostartStatus.Enabled is false.
|
||||
type AutostartStrategy string
|
||||
|
||||
const (
|
||||
AutostartStrategyNone AutostartStrategy = ""
|
||||
AutostartStrategySMAppService AutostartStrategy = "smappservice"
|
||||
AutostartStrategyLaunchAgent AutostartStrategy = "launchagent"
|
||||
AutostartStrategyRegistryRun AutostartStrategy = "registry-run"
|
||||
AutostartStrategyXDGAutostart AutostartStrategy = "xdg-autostart"
|
||||
)
|
||||
|
||||
// AutostartStatus describes the current autostart registration.
|
||||
type AutostartStatus struct {
|
||||
// Enabled reports whether a registration exists.
|
||||
Enabled bool
|
||||
// Path is the on-disk location of the registration artefact, when
|
||||
// applicable (plist path, .desktop path, registry sub-key path). Empty if
|
||||
// Enabled is false.
|
||||
Path string
|
||||
// Strategy names the mechanism that registered the application. Empty if
|
||||
// Enabled is false or the platform has only one mechanism.
|
||||
Strategy AutostartStrategy
|
||||
}
|
||||
|
||||
type autostartImpl interface {
|
||||
enable(opts AutostartOptions) error
|
||||
disable() error
|
||||
status() (AutostartStatus, error)
|
||||
}
|
||||
13
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_android.go
generated
vendored
Normal file
13
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_android.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build android
|
||||
|
||||
package application
|
||||
|
||||
type unsupportedAutostart struct{}
|
||||
|
||||
func newAutostartImpl(_ *App) autostartImpl { return unsupportedAutostart{} }
|
||||
|
||||
func (unsupportedAutostart) enable(AutostartOptions) error { return ErrAutostartNotSupported }
|
||||
func (unsupportedAutostart) disable() error { return ErrAutostartNotSupported }
|
||||
func (unsupportedAutostart) status() (AutostartStatus, error) {
|
||||
return AutostartStatus{}, ErrAutostartNotSupported
|
||||
}
|
||||
327
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_darwin.go
generated
vendored
Normal file
327
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,327 @@
|
||||
//go:build darwin && !ios && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/mac"
|
||||
)
|
||||
|
||||
type darwinAutostart struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
func newAutostartImpl(app *App) autostartImpl {
|
||||
return &darwinAutostart{app: app}
|
||||
}
|
||||
|
||||
// strategy picks SMAppService when running from a bundled .app on macOS 13+,
|
||||
// otherwise the LaunchAgent plist path. Both paths support unbundled binaries
|
||||
// (LaunchAgent path) so xbar-style scripts in development still work.
|
||||
func (a *darwinAutostart) strategy() AutostartStrategy {
|
||||
if !runningFromAppBundle() {
|
||||
return AutostartStrategyLaunchAgent
|
||||
}
|
||||
if mac.GetBundleID() == "" {
|
||||
return AutostartStrategyLaunchAgent
|
||||
}
|
||||
major, _ := darwinMajorVersion()
|
||||
if major < 13 {
|
||||
return AutostartStrategyLaunchAgent
|
||||
}
|
||||
return AutostartStrategySMAppService
|
||||
}
|
||||
|
||||
func (a *darwinAutostart) enable(opts AutostartOptions) error {
|
||||
if err := validateAutostartIdentifier(opts.Identifier); err != nil {
|
||||
return err
|
||||
}
|
||||
switch a.strategy() {
|
||||
case AutostartStrategySMAppService:
|
||||
if err := smAppServiceRegister(); err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, errSMAppServiceUnavailable) {
|
||||
return fmt.Errorf("SMAppService register: %w", err)
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return a.enableLaunchAgent(opts)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *darwinAutostart) disable() error {
|
||||
// Try both paths and merge errors — a previous version may have used
|
||||
// the other strategy.
|
||||
var errs []error
|
||||
if a.strategy() == AutostartStrategySMAppService {
|
||||
if err := smAppServiceUnregister(); err != nil && !errors.Is(err, errSMAppServiceUnavailable) && !errors.Is(err, errSMAppServiceNotRegistered) {
|
||||
errs = append(errs, fmt.Errorf("SMAppService unregister: %w", err))
|
||||
}
|
||||
}
|
||||
if err := a.disableLaunchAgent(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (a *darwinAutostart) status() (AutostartStatus, error) {
|
||||
if a.strategy() == AutostartStrategySMAppService {
|
||||
enabled, err := smAppServiceIsEnabled()
|
||||
// errSMAppServiceRequiresApproval means the user disabled the
|
||||
// login item in System Settings — semantically that's "not
|
||||
// enabled", not a hard error. Treat it like Unavailable so the
|
||||
// LaunchAgent fallback still gets a chance to find a legacy
|
||||
// entry from before the app was bundled.
|
||||
if err != nil &&
|
||||
!errors.Is(err, errSMAppServiceUnavailable) &&
|
||||
!errors.Is(err, errSMAppServiceRequiresApproval) {
|
||||
return AutostartStatus{}, fmt.Errorf("SMAppService status: %w", err)
|
||||
}
|
||||
if enabled {
|
||||
return AutostartStatus{
|
||||
Enabled: true,
|
||||
Path: mac.GetBundleID(),
|
||||
Strategy: AutostartStrategySMAppService,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
// LaunchAgent path: also checked when SMAppService said no, so a
|
||||
// previously-registered LaunchAgent doesn't disappear from view after an
|
||||
// upgrade to a bundled build.
|
||||
path, ok, err := a.findLaunchAgent()
|
||||
if err != nil {
|
||||
return AutostartStatus{}, err
|
||||
}
|
||||
if ok {
|
||||
return AutostartStatus{
|
||||
Enabled: true,
|
||||
Path: path,
|
||||
Strategy: AutostartStrategyLaunchAgent,
|
||||
}, nil
|
||||
}
|
||||
return AutostartStatus{}, nil
|
||||
}
|
||||
|
||||
func (a *darwinAutostart) launchAgentsDir() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("autostart: %w", err)
|
||||
}
|
||||
return filepath.Join(home, "Library", "LaunchAgents"), nil
|
||||
}
|
||||
|
||||
func (a *darwinAutostart) defaultLabel() string {
|
||||
if id := mac.GetBundleID(); id != "" {
|
||||
return id
|
||||
}
|
||||
return "wails.autostart." + autostartSlug(a.app.options.Name)
|
||||
}
|
||||
|
||||
func (a *darwinAutostart) enableLaunchAgent(opts AutostartOptions) error {
|
||||
exe, err := resolvedExecutable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := a.launchAgentsDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("autostart: create LaunchAgents dir: %w", err)
|
||||
}
|
||||
label := opts.Identifier
|
||||
if label == "" {
|
||||
label = a.defaultLabel()
|
||||
}
|
||||
path := filepath.Join(dir, label+".plist")
|
||||
body, err := launchAgentPlist(label, exe, opts.Arguments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeFileAtomic(path, body, 0644); err != nil {
|
||||
return fmt.Errorf("autostart: write plist: %w", err)
|
||||
}
|
||||
// Best-effort: activate immediately for the current GUI session.
|
||||
_ = launchctlBootstrap(path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *darwinAutostart) disableLaunchAgent() error {
|
||||
path, ok, err := a.findLaunchAgent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
_ = launchctlBootout(path)
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("autostart: remove plist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findLaunchAgent looks for a plist in ~/Library/LaunchAgents whose
|
||||
// ProgramArguments first element equals the current executable.
|
||||
func (a *darwinAutostart) findLaunchAgent() (string, bool, error) {
|
||||
dir, err := a.launchAgentsDir()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
exe, err := resolvedExecutable()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", false, nil
|
||||
}
|
||||
return "", false, fmt.Errorf("autostart: read LaunchAgents dir: %w", err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".plist") {
|
||||
continue
|
||||
}
|
||||
full := filepath.Join(dir, e.Name())
|
||||
data, err := os.ReadFile(full)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if plistFirstProgramArg(data) == exe {
|
||||
return full, true, nil
|
||||
}
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
// runningFromAppBundle reports whether the current executable lives inside a
|
||||
// .app bundle (path ends with .app/Contents/MacOS/<name>).
|
||||
func runningFromAppBundle() bool {
|
||||
exe, err := resolvedExecutable()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
macOSDir := filepath.Dir(exe)
|
||||
contentsDir := filepath.Dir(macOSDir)
|
||||
appDir := filepath.Dir(contentsDir)
|
||||
return filepath.Base(macOSDir) == "MacOS" &&
|
||||
filepath.Base(contentsDir) == "Contents" &&
|
||||
strings.HasSuffix(appDir, ".app")
|
||||
}
|
||||
|
||||
func darwinMajorVersion() (int, error) {
|
||||
out, err := exec.Command("sw_vers", "-productVersion").Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ver := strings.TrimSpace(string(out))
|
||||
if i := strings.IndexByte(ver, '.'); i > 0 {
|
||||
ver = ver[:i]
|
||||
}
|
||||
return strconv.Atoi(ver)
|
||||
}
|
||||
|
||||
// launchctlBootstrap loads a plist into the current GUI session. Best effort —
|
||||
// errors are ignored (the plist will still be picked up at next login).
|
||||
//
|
||||
// Indirected through a package-level variable so unit tests can replace it
|
||||
// with a no-op: a test plist with RunAtLoad=true that successfully bootstraps
|
||||
// would respawn the test binary recursively.
|
||||
var launchctlBootstrap = func(plistPath string) error {
|
||||
target := fmt.Sprintf("gui/%d", os.Getuid())
|
||||
return exec.Command("launchctl", "bootstrap", target, plistPath).Run()
|
||||
}
|
||||
|
||||
var launchctlBootout = func(plistPath string) error {
|
||||
target := fmt.Sprintf("gui/%d", os.Getuid())
|
||||
return exec.Command("launchctl", "bootout", target, plistPath).Run()
|
||||
}
|
||||
|
||||
// --- plist marshalling ------------------------------------------------------
|
||||
|
||||
func launchAgentPlist(label, exe string, args []string) ([]byte, error) {
|
||||
progArgs := append([]string{exe}, args...)
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`<?xml version="1.0" encoding="UTF-8"?>` + "\n")
|
||||
sb.WriteString(`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">` + "\n")
|
||||
sb.WriteString(`<plist version="1.0">` + "\n")
|
||||
sb.WriteString(" <dict>\n")
|
||||
sb.WriteString(" <key>Label</key>\n")
|
||||
sb.WriteString(" <string>" + xmlEscape(label) + "</string>\n")
|
||||
sb.WriteString(" <key>ProgramArguments</key>\n")
|
||||
sb.WriteString(" <array>\n")
|
||||
for _, a := range progArgs {
|
||||
sb.WriteString(" <string>" + xmlEscape(a) + "</string>\n")
|
||||
}
|
||||
sb.WriteString(" </array>\n")
|
||||
sb.WriteString(" <key>RunAtLoad</key>\n")
|
||||
sb.WriteString(" <true/>\n")
|
||||
sb.WriteString(" <key>KeepAlive</key>\n")
|
||||
sb.WriteString(" <false/>\n")
|
||||
sb.WriteString(" </dict>\n")
|
||||
sb.WriteString("</plist>\n")
|
||||
return []byte(sb.String()), nil
|
||||
}
|
||||
|
||||
func xmlEscape(s string) string {
|
||||
var b strings.Builder
|
||||
_ = xml.EscapeText(&b, []byte(s))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// plistFirstProgramArg returns the first <string> element under the
|
||||
// ProgramArguments array in a LaunchAgent plist. Empty string on any parse
|
||||
// failure — we treat a malformed file as "not ours".
|
||||
func plistFirstProgramArg(data []byte) string {
|
||||
dec := xml.NewDecoder(strings.NewReader(string(data)))
|
||||
dec.Strict = false
|
||||
var inDict, inArray, captureKey bool
|
||||
var lastKey string
|
||||
for {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
switch t := tok.(type) {
|
||||
case xml.StartElement:
|
||||
switch t.Name.Local {
|
||||
case "dict":
|
||||
inDict = true
|
||||
case "key":
|
||||
if inDict {
|
||||
captureKey = true
|
||||
}
|
||||
case "array":
|
||||
if lastKey == "ProgramArguments" {
|
||||
inArray = true
|
||||
}
|
||||
case "string":
|
||||
if inArray {
|
||||
var s string
|
||||
if err := dec.DecodeElement(&s, &t); err == nil {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
case xml.CharData:
|
||||
if captureKey {
|
||||
lastKey = string(t)
|
||||
captureKey = false
|
||||
}
|
||||
case xml.EndElement:
|
||||
if t.Name.Local == "array" && inArray {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_darwin_smappservice.go
generated
vendored
Normal file
141
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_darwin_smappservice.go
generated
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
//go:build darwin && !ios && !server
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -mmacosx-version-min=10.15 -x objective-c -Wno-unguarded-availability-new
|
||||
#cgo LDFLAGS: -framework Foundation -framework ServiceManagement
|
||||
|
||||
#include <stdlib.h> // free
|
||||
#include <string.h> // strdup
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
|
||||
// Return codes shared with the Go side.
|
||||
enum {
|
||||
SMAS_OK = 0,
|
||||
SMAS_UNAVAILABLE = 1, // SMAppService class not present (pre macOS 13)
|
||||
SMAS_NOT_REGISTERED = 2, // unregister called when nothing was registered
|
||||
SMAS_REQUIRES_APPROVAL = 3, // user disabled it in System Settings
|
||||
SMAS_ERROR = 4, // generic failure; *outMsg populated
|
||||
};
|
||||
|
||||
static int smAppServiceRegister(char** outMsg) {
|
||||
if (@available(macOS 13.0, *)) {
|
||||
@autoreleasepool {
|
||||
SMAppService *svc = [SMAppService mainAppService];
|
||||
NSError *err = nil;
|
||||
if ([svc registerAndReturnError:&err]) {
|
||||
return SMAS_OK;
|
||||
}
|
||||
if (err != nil) {
|
||||
*outMsg = strdup([[err localizedDescription] UTF8String]);
|
||||
}
|
||||
return SMAS_ERROR;
|
||||
}
|
||||
}
|
||||
return SMAS_UNAVAILABLE;
|
||||
}
|
||||
|
||||
static int smAppServiceUnregister(char** outMsg) {
|
||||
if (@available(macOS 13.0, *)) {
|
||||
@autoreleasepool {
|
||||
SMAppService *svc = [SMAppService mainAppService];
|
||||
if (svc.status == SMAppServiceStatusNotRegistered ||
|
||||
svc.status == SMAppServiceStatusNotFound) {
|
||||
return SMAS_NOT_REGISTERED;
|
||||
}
|
||||
NSError *err = nil;
|
||||
if ([svc unregisterAndReturnError:&err]) {
|
||||
return SMAS_OK;
|
||||
}
|
||||
if (err != nil) {
|
||||
*outMsg = strdup([[err localizedDescription] UTF8String]);
|
||||
}
|
||||
return SMAS_ERROR;
|
||||
}
|
||||
}
|
||||
return SMAS_UNAVAILABLE;
|
||||
}
|
||||
|
||||
// smAppServiceStatus: 0 = unavailable, 1 = not registered / not found,
|
||||
// 2 = enabled, 3 = requires approval.
|
||||
static int smAppServiceStatus(void) {
|
||||
if (@available(macOS 13.0, *)) {
|
||||
@autoreleasepool {
|
||||
SMAppService *svc = [SMAppService mainAppService];
|
||||
switch (svc.status) {
|
||||
case SMAppServiceStatusEnabled: return 2;
|
||||
case SMAppServiceStatusRequiresApproval: return 3;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
errSMAppServiceUnavailable = errors.New("SMAppService unavailable on this macOS")
|
||||
errSMAppServiceNotRegistered = errors.New("SMAppService not registered")
|
||||
errSMAppServiceRequiresApproval = errors.New("SMAppService requires user approval in System Settings")
|
||||
)
|
||||
|
||||
func smAppServiceRegister() error {
|
||||
var cMsg *C.char
|
||||
rc := C.smAppServiceRegister(&cMsg)
|
||||
if cMsg != nil {
|
||||
defer C.free(unsafe.Pointer(cMsg))
|
||||
}
|
||||
switch rc {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return errSMAppServiceUnavailable
|
||||
default:
|
||||
if cMsg != nil {
|
||||
return errors.New(C.GoString(cMsg))
|
||||
}
|
||||
return errors.New("SMAppService register failed")
|
||||
}
|
||||
}
|
||||
|
||||
func smAppServiceUnregister() error {
|
||||
var cMsg *C.char
|
||||
rc := C.smAppServiceUnregister(&cMsg)
|
||||
if cMsg != nil {
|
||||
defer C.free(unsafe.Pointer(cMsg))
|
||||
}
|
||||
switch rc {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return errSMAppServiceUnavailable
|
||||
case 2:
|
||||
return errSMAppServiceNotRegistered
|
||||
default:
|
||||
if cMsg != nil {
|
||||
return errors.New(C.GoString(cMsg))
|
||||
}
|
||||
return errors.New("SMAppService unregister failed")
|
||||
}
|
||||
}
|
||||
|
||||
func smAppServiceIsEnabled() (bool, error) {
|
||||
switch C.smAppServiceStatus() {
|
||||
case 0:
|
||||
return false, errSMAppServiceUnavailable
|
||||
case 2:
|
||||
return true, nil
|
||||
case 3:
|
||||
return false, errSMAppServiceRequiresApproval
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
13
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_ios.go
generated
vendored
Normal file
13
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_ios.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build ios
|
||||
|
||||
package application
|
||||
|
||||
type unsupportedAutostart struct{}
|
||||
|
||||
func newAutostartImpl(_ *App) autostartImpl { return unsupportedAutostart{} }
|
||||
|
||||
func (unsupportedAutostart) enable(AutostartOptions) error { return ErrAutostartNotSupported }
|
||||
func (unsupportedAutostart) disable() error { return ErrAutostartNotSupported }
|
||||
func (unsupportedAutostart) status() (AutostartStatus, error) {
|
||||
return AutostartStatus{}, ErrAutostartNotSupported
|
||||
}
|
||||
256
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_linux.go
generated
vendored
Normal file
256
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_linux.go
generated
vendored
Normal file
@@ -0,0 +1,256 @@
|
||||
//go:build linux && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type linuxAutostart struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
func newAutostartImpl(app *App) autostartImpl {
|
||||
return &linuxAutostart{app: app}
|
||||
}
|
||||
|
||||
func (a *linuxAutostart) enable(opts AutostartOptions) error {
|
||||
if err := validateAutostartIdentifier(opts.Identifier); err != nil {
|
||||
return err
|
||||
}
|
||||
// A newline inside the executable path or any argument would break the
|
||||
// .desktop file format (line-based key=value) and could be used to inject
|
||||
// arbitrary Desktop Entry keys when Arguments are user-influenced.
|
||||
for i, arg := range opts.Arguments {
|
||||
if err := validateDesktopExecToken(arg); err != nil {
|
||||
return fmt.Errorf("autostart argument %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
exe, err := resolvedExecutable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDesktopExecToken(exe); err != nil {
|
||||
return fmt.Errorf("autostart executable path: %w", err)
|
||||
}
|
||||
|
||||
dir, err := a.autostartDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("create autostart dir: %w", err)
|
||||
}
|
||||
|
||||
id := opts.Identifier
|
||||
if id == "" {
|
||||
id = autostartSlug(a.app.options.Name)
|
||||
}
|
||||
path := filepath.Join(dir, id+".desktop")
|
||||
|
||||
// Remove any stale .desktop file pointing at this binary under a
|
||||
// different identifier so a previous Enable() with a different
|
||||
// Identifier (or a slug derived from a renamed Options.Name) doesn't
|
||||
// leave a second entry behind.
|
||||
if existing, ferr := a.findDesktopFile(dir); ferr == nil && existing != "" && existing != path {
|
||||
_ = os.Remove(existing)
|
||||
}
|
||||
|
||||
body := buildDesktopEntry(a.app.options.Name, exe, opts.Arguments)
|
||||
if err := writeFileAtomic(path, []byte(body), 0644); err != nil {
|
||||
return fmt.Errorf("write desktop file %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *linuxAutostart) disable() error {
|
||||
dir, err := a.autostartDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := a.findDesktopFile(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("remove desktop file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *linuxAutostart) status() (AutostartStatus, error) {
|
||||
dir, err := a.autostartDir()
|
||||
if err != nil {
|
||||
return AutostartStatus{}, err
|
||||
}
|
||||
path, err := a.findDesktopFile(dir)
|
||||
if err != nil {
|
||||
return AutostartStatus{}, err
|
||||
}
|
||||
if path == "" {
|
||||
return AutostartStatus{}, nil
|
||||
}
|
||||
return AutostartStatus{
|
||||
Enabled: true,
|
||||
Path: path,
|
||||
Strategy: AutostartStrategyXDGAutostart,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *linuxAutostart) autostartDir() (string, error) {
|
||||
cfg := os.Getenv("XDG_CONFIG_HOME")
|
||||
if cfg == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("autostart: %w", err)
|
||||
}
|
||||
cfg = filepath.Join(home, ".config")
|
||||
}
|
||||
return filepath.Join(cfg, "autostart"), nil
|
||||
}
|
||||
|
||||
// findDesktopFile looks for a .desktop file in dir whose Exec= entry points at
|
||||
// the current executable. Returns empty path with no error if none found.
|
||||
// This survives identifier changes between Enable() calls.
|
||||
func (a *linuxAutostart) findDesktopFile(dir string) (string, error) {
|
||||
exe, err := resolvedExecutable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("read autostart dir: %w", err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".desktop") {
|
||||
continue
|
||||
}
|
||||
full := filepath.Join(dir, e.Name())
|
||||
data, err := os.ReadFile(full)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if desktopExecPath(string(data)) == exe {
|
||||
return full, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func buildDesktopEntry(appName, exe string, args []string) string {
|
||||
if appName == "" {
|
||||
appName = filepath.Base(exe)
|
||||
}
|
||||
execLine := quoteExec(exe)
|
||||
for _, a := range args {
|
||||
execLine += " " + quoteExec(a)
|
||||
}
|
||||
return fmt.Sprintf(`[Desktop Entry]
|
||||
Type=Application
|
||||
Name=%s
|
||||
Exec=%s
|
||||
X-GNOME-Autostart-enabled=true
|
||||
Hidden=false
|
||||
NoDisplay=false
|
||||
Terminal=false
|
||||
`, escapeDesktopValue(appName), execLine)
|
||||
}
|
||||
|
||||
// validateDesktopExecToken rejects control characters that would break the
|
||||
// .desktop file format or allow Desktop Entry key injection when interpolated
|
||||
// into an Exec= line. Allowed: spaces and tabs (which quoteExec handles by
|
||||
// double-quoting); rejected: all other ASCII control characters including
|
||||
// CR/LF.
|
||||
func validateDesktopExecToken(s string) error {
|
||||
for _, r := range s {
|
||||
if r == '\t' || r == ' ' {
|
||||
continue
|
||||
}
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return fmt.Errorf("control character %U not allowed in Exec field", r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// quoteExec escapes a single Exec field token per the freedesktop.org spec:
|
||||
// reserved chars are " ` $ \ → escape with backslash; if the token contains
|
||||
// any reserved or whitespace, double-quote it.
|
||||
func quoteExec(s string) string {
|
||||
needQuote := false
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"', '`', '$', '\\':
|
||||
b.WriteByte('\\')
|
||||
b.WriteRune(r)
|
||||
needQuote = true
|
||||
case ' ', '\t':
|
||||
// Newlines are rejected by validateDesktopExecToken before we
|
||||
// get here, so any whitespace remaining is safely quotable.
|
||||
b.WriteRune(r)
|
||||
needQuote = true
|
||||
default:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if needQuote {
|
||||
return `"` + b.String() + `"`
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// escapeDesktopValue escapes characters that are not allowed in raw Desktop
|
||||
// Entry values (newlines, leading/trailing whitespace).
|
||||
func escapeDesktopValue(s string) string {
|
||||
s = strings.ReplaceAll(s, "\r", " ")
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func desktopExecPath(contents string) string {
|
||||
for _, line := range strings.Split(contents, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "Exec=") {
|
||||
continue
|
||||
}
|
||||
val := strings.TrimPrefix(line, "Exec=")
|
||||
// First token, possibly quoted.
|
||||
val = strings.TrimSpace(val)
|
||||
if strings.HasPrefix(val, `"`) {
|
||||
end := strings.Index(val[1:], `"`)
|
||||
if end < 0 {
|
||||
return ""
|
||||
}
|
||||
return unescapeDesktopToken(val[1 : 1+end])
|
||||
}
|
||||
if i := strings.IndexAny(val, " \t"); i >= 0 {
|
||||
return val[:i]
|
||||
}
|
||||
return val
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func unescapeDesktopToken(s string) string {
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\\' && i+1 < len(s) {
|
||||
b.WriteByte(s[i+1])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
b.WriteByte(s[i])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
64
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_manager.go
generated
vendored
Normal file
64
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_manager.go
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
package application
|
||||
|
||||
// AutostartManager provides cross-platform control over whether the
|
||||
// application launches when the user logs in.
|
||||
//
|
||||
// Registration takes effect on the next login, not immediately.
|
||||
//
|
||||
// Platform behaviour:
|
||||
//
|
||||
// - macOS 13+ (bundled .app): SMAppService.mainAppService — works for
|
||||
// sandboxed and Mac-App-Store apps, no TCC automation prompt.
|
||||
// - macOS (older or unbundled): a LaunchAgent plist is written to
|
||||
// ~/Library/LaunchAgents/.
|
||||
// - Windows: a value is added under
|
||||
// HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
|
||||
// - Linux: an .desktop file is written to $XDG_CONFIG_HOME/autostart/
|
||||
// (defaulting to ~/.config/autostart/).
|
||||
// - Android / iOS / server builds: ErrAutostartNotSupported.
|
||||
type AutostartManager struct {
|
||||
app *App
|
||||
impl autostartImpl
|
||||
}
|
||||
|
||||
func newAutostartManager(app *App) *AutostartManager {
|
||||
return &AutostartManager{
|
||||
app: app,
|
||||
impl: newAutostartImpl(app),
|
||||
}
|
||||
}
|
||||
|
||||
// Enable registers the application to launch at user login using default
|
||||
// options. Calling Enable repeatedly is safe; the registration is overwritten.
|
||||
func (am *AutostartManager) Enable() error {
|
||||
return am.impl.enable(AutostartOptions{})
|
||||
}
|
||||
|
||||
// EnableWithOptions registers the application with the given options.
|
||||
// See AutostartOptions for the meaning of each field.
|
||||
func (am *AutostartManager) EnableWithOptions(opts AutostartOptions) error {
|
||||
return am.impl.enable(opts)
|
||||
}
|
||||
|
||||
// Disable removes the autostart registration. Returns nil if the application
|
||||
// was not registered.
|
||||
func (am *AutostartManager) Disable() error {
|
||||
return am.impl.disable()
|
||||
}
|
||||
|
||||
// IsEnabled reports whether the application is currently registered to launch
|
||||
// at login. It does not verify that the registered executable path still
|
||||
// points at the running binary; use Status for that.
|
||||
func (am *AutostartManager) IsEnabled() (bool, error) {
|
||||
st, err := am.impl.status()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return st.Enabled, nil
|
||||
}
|
||||
|
||||
// Status returns the full registration state, including the path of the
|
||||
// on-disk artefact and the platform mechanism used.
|
||||
func (am *AutostartManager) Status() (AutostartStatus, error) {
|
||||
return am.impl.status()
|
||||
}
|
||||
13
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_server.go
generated
vendored
Normal file
13
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_server.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build server
|
||||
|
||||
package application
|
||||
|
||||
type unsupportedAutostart struct{}
|
||||
|
||||
func newAutostartImpl(_ *App) autostartImpl { return unsupportedAutostart{} }
|
||||
|
||||
func (unsupportedAutostart) enable(AutostartOptions) error { return ErrAutostartNotSupported }
|
||||
func (unsupportedAutostart) disable() error { return ErrAutostartNotSupported }
|
||||
func (unsupportedAutostart) status() (AutostartStatus, error) {
|
||||
return AutostartStatus{}, ErrAutostartNotSupported
|
||||
}
|
||||
199
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_windows.go
generated
vendored
Normal file
199
vendor/github.com/wailsapp/wails/v3/pkg/application/autostart_windows.go
generated
vendored
Normal file
@@ -0,0 +1,199 @@
|
||||
//go:build windows && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
const defaultAutostartRegistrySubKey = `Software\Microsoft\Windows\CurrentVersion\Run`
|
||||
|
||||
type windowsAutostart struct {
|
||||
app *App
|
||||
|
||||
// registrySubKey is overridable for tests; production code reads/writes
|
||||
// HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
|
||||
registrySubKey string
|
||||
}
|
||||
|
||||
func newAutostartImpl(app *App) autostartImpl {
|
||||
return &windowsAutostart{
|
||||
app: app,
|
||||
registrySubKey: defaultAutostartRegistrySubKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *windowsAutostart) enable(opts AutostartOptions) error {
|
||||
if err := validateAutostartIdentifier(opts.Identifier); err != nil {
|
||||
return err
|
||||
}
|
||||
exe, err := resolvedExecutable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := opts.Identifier
|
||||
if id == "" {
|
||||
id = autostartSlug(a.app.options.Name)
|
||||
}
|
||||
|
||||
cmd := quoteWindowsArg(exe)
|
||||
for _, arg := range opts.Arguments {
|
||||
cmd += " " + quoteWindowsArg(arg)
|
||||
}
|
||||
|
||||
key, _, err := registry.CreateKey(registry.CURRENT_USER, a.registrySubKey, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("autostart: open registry key: %w", err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
// Remove any stale entry pointing at this binary under a different value
|
||||
// name, so a previous Enable() with a different Identifier (or a slug
|
||||
// derived from a renamed Options.Name) doesn't leave behind a second
|
||||
// autostart entry.
|
||||
if existing, _, ferr := a.find(); ferr == nil && existing != "" && existing != id {
|
||||
_ = key.DeleteValue(existing)
|
||||
}
|
||||
|
||||
if err := key.SetStringValue(id, cmd); err != nil {
|
||||
return fmt.Errorf("autostart: write registry value: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *windowsAutostart) disable() error {
|
||||
id, _, err := a.find()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
key, err := registry.OpenKey(registry.CURRENT_USER, a.registrySubKey, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
if errors.Is(err, registry.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("autostart: open registry key: %w", err)
|
||||
}
|
||||
defer key.Close()
|
||||
if err := key.DeleteValue(id); err != nil && !errors.Is(err, registry.ErrNotExist) {
|
||||
return fmt.Errorf("autostart: delete registry value: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *windowsAutostart) status() (AutostartStatus, error) {
|
||||
id, _, err := a.find()
|
||||
if err != nil {
|
||||
return AutostartStatus{}, err
|
||||
}
|
||||
if id == "" {
|
||||
return AutostartStatus{}, nil
|
||||
}
|
||||
return AutostartStatus{
|
||||
Enabled: true,
|
||||
Path: `HKCU\` + a.registrySubKey + `\` + id,
|
||||
Strategy: AutostartStrategyRegistryRun,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// find returns the value name and command of the registry entry whose first
|
||||
// token equals our current executable. Empty name means not registered.
|
||||
func (a *windowsAutostart) find() (string, string, error) {
|
||||
exe, err := resolvedExecutable()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
key, err := registry.OpenKey(registry.CURRENT_USER, a.registrySubKey, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
if errors.Is(err, registry.ErrNotExist) {
|
||||
return "", "", nil
|
||||
}
|
||||
return "", "", fmt.Errorf("autostart: open registry key: %w", err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
names, err := key.ReadValueNames(-1)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("autostart: list registry values: %w", err)
|
||||
}
|
||||
exeLower := strings.ToLower(exe)
|
||||
for _, name := range names {
|
||||
val, _, err := key.GetStringValue(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(parseWindowsCommandExe(val), exeLower) {
|
||||
return name, val, nil
|
||||
}
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// parseWindowsCommandExe returns the first token of a Windows command line,
|
||||
// honouring surrounding double quotes for paths with spaces. Returned in
|
||||
// lowercase for case-insensitive comparison.
|
||||
func parseWindowsCommandExe(cmd string) string {
|
||||
cmd = strings.TrimSpace(cmd)
|
||||
if cmd == "" {
|
||||
return ""
|
||||
}
|
||||
if cmd[0] == '"' {
|
||||
end := strings.IndexByte(cmd[1:], '"')
|
||||
if end < 0 {
|
||||
return strings.ToLower(cmd[1:])
|
||||
}
|
||||
return strings.ToLower(cmd[1 : 1+end])
|
||||
}
|
||||
if i := strings.IndexAny(cmd, " \t"); i >= 0 {
|
||||
return strings.ToLower(cmd[:i])
|
||||
}
|
||||
return strings.ToLower(cmd)
|
||||
}
|
||||
|
||||
// quoteWindowsArg wraps an argument in double quotes when it contains
|
||||
// whitespace or quotes, and escapes embedded quotes. Backslashes preceding a
|
||||
// quote are doubled per CommandLineToArgvW rules.
|
||||
func quoteWindowsArg(s string) string {
|
||||
if s != "" && !strings.ContainsAny(s, ` " `) {
|
||||
return s
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteByte('"')
|
||||
backslashes := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
switch c {
|
||||
case '\\':
|
||||
backslashes++
|
||||
case '"':
|
||||
// Per CommandLineToArgvW: a literal quote preceded by N
|
||||
// backslashes must be encoded as (2N+1) backslashes + quote.
|
||||
// Earlier versions emitted only (N+1), which made the parser
|
||||
// lose the quote (the 2 backslashes toggled quoted state).
|
||||
for j := 0; j < 2*backslashes; j++ {
|
||||
b.WriteByte('\\')
|
||||
}
|
||||
b.WriteByte('\\')
|
||||
b.WriteByte('"')
|
||||
backslashes = 0
|
||||
default:
|
||||
for j := 0; j < backslashes; j++ {
|
||||
b.WriteByte('\\')
|
||||
}
|
||||
backslashes = 0
|
||||
b.WriteByte(c)
|
||||
}
|
||||
}
|
||||
for j := 0; j < backslashes; j++ {
|
||||
b.WriteByte('\\')
|
||||
b.WriteByte('\\')
|
||||
}
|
||||
b.WriteByte('"')
|
||||
return b.String()
|
||||
}
|
||||
494
vendor/github.com/wailsapp/wails/v3/pkg/application/bindings.go
generated
vendored
Normal file
494
vendor/github.com/wailsapp/wails/v3/pkg/application/bindings.go
generated
vendored
Normal file
@@ -0,0 +1,494 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/hash"
|
||||
"github.com/wailsapp/wails/v3/internal/sliceutil"
|
||||
)
|
||||
|
||||
// CallOptions defines the options for a method call.
|
||||
// Field order is optimized to minimize struct padding.
|
||||
type CallOptions struct {
|
||||
MethodName string `json:"methodName"`
|
||||
Args []json.RawMessage `json:"args"`
|
||||
MethodID uint32 `json:"methodID"`
|
||||
}
|
||||
|
||||
type ErrorKind string
|
||||
|
||||
const (
|
||||
ReferenceError ErrorKind = "ReferenceError"
|
||||
TypeError ErrorKind = "TypeError"
|
||||
RuntimeError ErrorKind = "RuntimeError"
|
||||
)
|
||||
|
||||
// CallError represents an error that occurred during a method call.
|
||||
// Field order is optimized to minimize struct padding.
|
||||
type CallError struct {
|
||||
Message string `json:"message"`
|
||||
Cause any `json:"cause,omitempty"`
|
||||
Kind ErrorKind `json:"kind"`
|
||||
}
|
||||
|
||||
func (e *CallError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// Parameter defines a Go method parameter
|
||||
type Parameter struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
TypeName string `json:"type"`
|
||||
ReflectType reflect.Type
|
||||
}
|
||||
|
||||
func newParameter(Name string, Type reflect.Type) *Parameter {
|
||||
return &Parameter{
|
||||
Name: Name,
|
||||
TypeName: Type.String(),
|
||||
ReflectType: Type,
|
||||
}
|
||||
}
|
||||
|
||||
// IsType returns true if the given
|
||||
func (p *Parameter) IsType(typename string) bool {
|
||||
return p.TypeName == typename
|
||||
}
|
||||
|
||||
// IsError returns true if the parameter type is an error
|
||||
func (p *Parameter) IsError() bool {
|
||||
return p.IsType("error")
|
||||
}
|
||||
|
||||
// BoundMethod defines all the data related to a Go method that is
|
||||
// bound to the Wails application.
|
||||
// Field order is optimized to minimize struct padding (136 bytes vs 144 bytes).
|
||||
type BoundMethod struct {
|
||||
Method reflect.Value `json:"-"`
|
||||
Name string `json:"name"`
|
||||
FQN string `json:"-"`
|
||||
Comments string `json:"comments,omitempty"`
|
||||
Inputs []*Parameter `json:"inputs,omitempty"`
|
||||
Outputs []*Parameter `json:"outputs,omitempty"`
|
||||
marshalError func(error) []byte
|
||||
ID uint32 `json:"id"`
|
||||
needsContext bool
|
||||
isVariadic bool // cached at registration to avoid reflect call per invocation
|
||||
}
|
||||
|
||||
type Bindings struct {
|
||||
marshalError func(error) []byte
|
||||
boundMethods map[string]*BoundMethod
|
||||
boundByID map[uint32]*BoundMethod
|
||||
methodAliases map[uint32]uint32
|
||||
}
|
||||
|
||||
var registeredBindingMethodIDs sync.Map
|
||||
|
||||
// RegisterBindingMethodID registers a stable binding ID for a service method
|
||||
// expression so the runtime can resolve it without relying on reflection-visible
|
||||
// names (which may be obfuscated).
|
||||
func RegisterBindingMethodID(method any, id uint32) {
|
||||
value := reflect.ValueOf(method)
|
||||
if value.Kind() != reflect.Func {
|
||||
panic(fmt.Sprintf("binding method ID registration expects a function, got %s", value.Kind()))
|
||||
}
|
||||
registeredBindingMethodIDs.Store(value.Pointer(), id)
|
||||
}
|
||||
|
||||
// UnregisterBindingMethodID removes the stable binding ID for a service method.
|
||||
// Intended for use in tests to restore global state after calling RegisterBindingMethodID.
|
||||
func UnregisterBindingMethodID(method any) {
|
||||
value := reflect.ValueOf(method)
|
||||
if value.Kind() != reflect.Func {
|
||||
return
|
||||
}
|
||||
registeredBindingMethodIDs.Delete(value.Pointer())
|
||||
}
|
||||
|
||||
func getRegisteredBindingMethodID(method reflect.Method) (uint32, bool) {
|
||||
id, ok := registeredBindingMethodIDs.Load(method.Func.Pointer())
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return id.(uint32), true
|
||||
}
|
||||
|
||||
func NewBindings(marshalError func(error) []byte, aliases map[uint32]uint32) *Bindings {
|
||||
return &Bindings{
|
||||
marshalError: wrapErrorMarshaler(marshalError, defaultMarshalError),
|
||||
boundMethods: make(map[string]*BoundMethod),
|
||||
boundByID: make(map[uint32]*BoundMethod),
|
||||
methodAliases: aliases,
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds the given service to the bindings.
|
||||
func (b *Bindings) Add(service Service) error {
|
||||
methods, err := getMethods(service.Instance())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
marshalError := wrapErrorMarshaler(service.options.MarshalError, defaultMarshalError)
|
||||
|
||||
// Validate and log methods.
|
||||
for _, method := range methods {
|
||||
if _, ok := b.boundMethods[method.FQN]; ok {
|
||||
return fmt.Errorf("bound method '%s' is already registered. Please note that you can register at most one service of each type; additional instances must be wrapped in dedicated structs", method.FQN)
|
||||
}
|
||||
if boundMethod, ok := b.boundByID[method.ID]; ok {
|
||||
return fmt.Errorf("oh wow, we're sorry about this! Amazingly, a hash collision was detected for method '%s' (it generates the same hash as '%s'). To use this method, please rename it. Sorry :(", method.FQN, boundMethod.FQN)
|
||||
}
|
||||
|
||||
// Log
|
||||
attrs := []any{"fqn", method.FQN, "id", method.ID}
|
||||
if alias, ok := sliceutil.FindMapKey(b.methodAliases, method.ID); ok {
|
||||
attrs = append(attrs, "alias", alias)
|
||||
}
|
||||
globalApplication.debug("Registering bound method:", attrs...)
|
||||
}
|
||||
|
||||
for _, method := range methods {
|
||||
// Store composite error marshaler
|
||||
method.marshalError = marshalError
|
||||
|
||||
// Register method
|
||||
b.boundMethods[method.FQN] = method
|
||||
b.boundByID[method.ID] = method
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the bound method with the given name
|
||||
func (b *Bindings) Get(options *CallOptions) *BoundMethod {
|
||||
return b.boundMethods[options.MethodName]
|
||||
}
|
||||
|
||||
// GetByID returns the bound method with the given ID
|
||||
func (b *Bindings) GetByID(id uint32) *BoundMethod {
|
||||
// Check method aliases
|
||||
if b.methodAliases != nil {
|
||||
if alias, ok := b.methodAliases[id]; ok {
|
||||
id = alias
|
||||
}
|
||||
}
|
||||
|
||||
return b.boundByID[id]
|
||||
}
|
||||
|
||||
// internalServiceMethod is a set of methods
|
||||
// that are handled specially by the binding engine
|
||||
// and must not be exposed to the frontend.
|
||||
//
|
||||
// For simplicity we exclude these by name
|
||||
// without checking their signatures,
|
||||
// and so does the binding generator.
|
||||
var internalServiceMethods = map[string]bool{
|
||||
"ServiceName": true,
|
||||
"ServiceStartup": true,
|
||||
"ServiceShutdown": true,
|
||||
"ServeHTTP": true,
|
||||
}
|
||||
|
||||
var ctxType = reflect.TypeFor[context.Context]()
|
||||
|
||||
// getMethods returns the list of BoundMethod descriptors for the methods of the named pointer type provided by value.
|
||||
//
|
||||
// It returns an error if value is not a pointer to a named type, if a function value is supplied (binding functions is deprecated), or if a generic type is supplied.
|
||||
// The returned BoundMethod slice includes only exported methods that are not listed in internalServiceMethods. Each BoundMethod has its FQN, ID (computed from the FQN), Method reflect.Value, Inputs and Outputs populated, isVariadic cached from the method signature, and needsContext set when the first parameter is context.Context.
|
||||
func getMethods(value any) ([]*BoundMethod, error) {
|
||||
// Create result placeholder
|
||||
var result []*BoundMethod
|
||||
|
||||
// Check type
|
||||
if !isNamed(value) {
|
||||
if isFunction(value) {
|
||||
name := runtime.FuncForPC(reflect.ValueOf(value).Pointer()).Name()
|
||||
return nil, fmt.Errorf("%s is a function, not a pointer to named type. Wails v2 has deprecated the binding of functions. Please define your functions as methods on a struct and bind a pointer to that struct", name)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s is not a pointer to named type", reflect.ValueOf(value).Type().String())
|
||||
} else if !isPtr(value) {
|
||||
return nil, fmt.Errorf("%s is a named type, not a pointer to named type", reflect.ValueOf(value).Type().String())
|
||||
}
|
||||
|
||||
// Process Named Type
|
||||
namedValue := reflect.ValueOf(value)
|
||||
ptrType := namedValue.Type()
|
||||
namedType := ptrType.Elem()
|
||||
typeName := namedType.Name()
|
||||
packagePath := namedType.PkgPath()
|
||||
|
||||
if strings.Contains(namedType.String(), "[") {
|
||||
return nil, fmt.Errorf("%s.%s is a generic type. Generic bound types are not supported", packagePath, namedType.String())
|
||||
}
|
||||
|
||||
// Process Methods
|
||||
for i := range ptrType.NumMethod() {
|
||||
methodDef := ptrType.Method(i)
|
||||
methodName := methodDef.Name
|
||||
method := namedValue.Method(i)
|
||||
|
||||
if internalServiceMethods[methodName] {
|
||||
continue
|
||||
}
|
||||
|
||||
fqn := fmt.Sprintf("%s.%s.%s", packagePath, typeName, methodName)
|
||||
|
||||
// Iterate inputs
|
||||
methodType := method.Type()
|
||||
|
||||
// Create new method with cached flags
|
||||
methodID := hash.Fnv(fqn)
|
||||
if registeredID, ok := getRegisteredBindingMethodID(methodDef); ok {
|
||||
methodID = registeredID
|
||||
}
|
||||
|
||||
boundMethod := &BoundMethod{
|
||||
ID: methodID,
|
||||
FQN: fqn,
|
||||
Name: methodName,
|
||||
Inputs: nil,
|
||||
Outputs: nil,
|
||||
Comments: "",
|
||||
Method: method,
|
||||
isVariadic: methodType.IsVariadic(), // cache to avoid reflect call per invocation
|
||||
}
|
||||
inputParamCount := methodType.NumIn()
|
||||
var inputs []*Parameter
|
||||
for inputIndex := 0; inputIndex < inputParamCount; inputIndex++ {
|
||||
input := methodType.In(inputIndex)
|
||||
if inputIndex == 0 && input.AssignableTo(ctxType) {
|
||||
boundMethod.needsContext = true
|
||||
}
|
||||
thisParam := newParameter("", input)
|
||||
inputs = append(inputs, thisParam)
|
||||
}
|
||||
|
||||
boundMethod.Inputs = inputs
|
||||
|
||||
outputParamCount := methodType.NumOut()
|
||||
var outputs []*Parameter
|
||||
for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ {
|
||||
output := methodType.Out(outputIndex)
|
||||
thisParam := newParameter("", output)
|
||||
outputs = append(outputs, thisParam)
|
||||
}
|
||||
boundMethod.Outputs = outputs
|
||||
|
||||
// Save method in result
|
||||
result = append(result, boundMethod)
|
||||
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *BoundMethod) String() string {
|
||||
return b.FQN
|
||||
}
|
||||
|
||||
var errorType = reflect.TypeFor[error]()
|
||||
|
||||
// Call will attempt to call this bound method with the given args.
|
||||
// If the call succeeds, result will be either a non-error return value (if there is only one)
|
||||
// or a slice of non-error return values (if there are more than one).
|
||||
//
|
||||
// If the arguments are mistyped, the call returns one or more non-nil error values,
|
||||
// or the method panics, result is nil and err is an instance of *[CallError].
|
||||
func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (result any, err error) {
|
||||
// Convert panics raised by the bound method into a *CallError so the
|
||||
// frontend call rejects instead of the application dying: the default
|
||||
// panic handler is fatal, and a bug in one bound method must not take
|
||||
// down the whole application (#5037). A custom PanicHandler, when
|
||||
// registered, still observes the panic for logging/telemetry.
|
||||
defer func() {
|
||||
e := recover()
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
recoveredErr, ok := e.(error)
|
||||
if !ok {
|
||||
recoveredErr = fmt.Errorf("%v", e)
|
||||
}
|
||||
// The stack trace is only used for logging or the PanicHandler, so
|
||||
// only compute it when there is a globalApplication to consume it
|
||||
// (the CallError below carries the message, not the trace).
|
||||
if globalApplication != nil {
|
||||
stackTrace := getStackTrace(3, 5)
|
||||
if handler := globalApplication.options.PanicHandler; handler != nil {
|
||||
handler(newPanicDetails(recoveredErr, stackTrace))
|
||||
} else {
|
||||
globalApplication.error("panic in bound method %s: %s\n%s", b.FQN, recoveredErr, stackTrace)
|
||||
}
|
||||
}
|
||||
result = nil
|
||||
err = &CallError{
|
||||
Message: fmt.Sprintf("%s: panic: %s", b.FQN, recoveredErr),
|
||||
Kind: RuntimeError,
|
||||
}
|
||||
}()
|
||||
argCount := len(args)
|
||||
if b.needsContext {
|
||||
argCount++
|
||||
}
|
||||
|
||||
if argCount != len(b.Inputs) {
|
||||
err = &CallError{
|
||||
Message: fmt.Sprintf("%s expects %d arguments, got %d", b.FQN, len(b.Inputs), argCount),
|
||||
Kind: TypeError,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use stack-allocated buffer for common case (<=8 args), heap for larger
|
||||
var argBuffer [8]reflect.Value
|
||||
var callArgs []reflect.Value
|
||||
if argCount <= len(argBuffer) {
|
||||
callArgs = argBuffer[:argCount]
|
||||
} else {
|
||||
callArgs = make([]reflect.Value, argCount)
|
||||
}
|
||||
base := 0
|
||||
|
||||
if b.needsContext {
|
||||
callArgs[0] = reflect.ValueOf(ctx)
|
||||
base++
|
||||
}
|
||||
|
||||
// Iterate over given arguments
|
||||
for index, arg := range args {
|
||||
value := reflect.New(b.Inputs[base+index].ReflectType)
|
||||
err = json.Unmarshal(arg, value.Interface())
|
||||
if err != nil {
|
||||
err = &CallError{
|
||||
Message: fmt.Sprintf("could not parse argument #%d: %s", index, err),
|
||||
Cause: json.RawMessage(b.marshalError(err)),
|
||||
Kind: TypeError,
|
||||
}
|
||||
return
|
||||
}
|
||||
callArgs[base+index] = value.Elem()
|
||||
}
|
||||
|
||||
// Do the call using cached isVariadic flag
|
||||
var callResults []reflect.Value
|
||||
if b.isVariadic {
|
||||
callResults = b.Method.CallSlice(callArgs)
|
||||
} else {
|
||||
callResults = b.Method.Call(callArgs)
|
||||
}
|
||||
|
||||
// Process results - optimized for common case of 0-2 return values
|
||||
// to avoid slice allocation
|
||||
var firstResult any
|
||||
var hasFirstResult bool
|
||||
var nonErrorOutputs []any // only allocated if >1 non-error results
|
||||
var errorOutputs []error
|
||||
|
||||
for _, field := range callResults {
|
||||
if field.Type() == errorType {
|
||||
if field.IsNil() {
|
||||
continue
|
||||
}
|
||||
if errorOutputs == nil {
|
||||
errorOutputs = make([]error, 0, len(callResults))
|
||||
}
|
||||
errorOutputs = append(errorOutputs, field.Interface().(error))
|
||||
} else if errorOutputs == nil {
|
||||
// Only collect non-error outputs if no errors yet
|
||||
val := field.Interface()
|
||||
if !hasFirstResult {
|
||||
firstResult = val
|
||||
hasFirstResult = true
|
||||
} else if nonErrorOutputs == nil {
|
||||
// Second result - need to allocate slice
|
||||
nonErrorOutputs = make([]any, 0, len(callResults))
|
||||
nonErrorOutputs = append(nonErrorOutputs, firstResult, val)
|
||||
} else {
|
||||
nonErrorOutputs = append(nonErrorOutputs, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errorOutputs) > 0 {
|
||||
info := make([]json.RawMessage, len(errorOutputs))
|
||||
for i, err := range errorOutputs {
|
||||
info[i] = b.marshalError(err)
|
||||
}
|
||||
|
||||
cerr := &CallError{
|
||||
Message: errors.Join(errorOutputs...).Error(),
|
||||
Cause: info,
|
||||
Kind: RuntimeError,
|
||||
}
|
||||
if len(info) == 1 {
|
||||
cerr.Cause = info[0]
|
||||
}
|
||||
|
||||
err = cerr
|
||||
} else if nonErrorOutputs != nil {
|
||||
result = nonErrorOutputs
|
||||
} else if hasFirstResult {
|
||||
result = firstResult
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// wrapErrorMarshaler returns an error marshaling functions
|
||||
// that calls the primary marshaler first,
|
||||
// then falls back to the secondary one.
|
||||
func wrapErrorMarshaler(primary func(error) []byte, secondary func(error) []byte) func(error) []byte {
|
||||
if primary == nil {
|
||||
return secondary
|
||||
}
|
||||
|
||||
return func(err error) []byte {
|
||||
result := primary(err)
|
||||
if result == nil {
|
||||
result = secondary(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// defaultMarshalError implements the default error marshaling mechanism.
|
||||
func defaultMarshalError(err error) []byte {
|
||||
result, jsonErr := json.Marshal(&err)
|
||||
if jsonErr != nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// isPtr returns true if the value given is a pointer.
|
||||
func isPtr(value interface{}) bool {
|
||||
return reflect.ValueOf(value).Kind() == reflect.Ptr
|
||||
}
|
||||
|
||||
// isFunction returns true if the given value is a function
|
||||
func isFunction(value interface{}) bool {
|
||||
return reflect.ValueOf(value).Kind() == reflect.Func
|
||||
}
|
||||
|
||||
// isNamed returns true if the given value is of named type
|
||||
// or pointer to named type.
|
||||
func isNamed(value interface{}) bool {
|
||||
rv := reflect.ValueOf(value)
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
return rv.Type().Name() != ""
|
||||
}
|
||||
27
vendor/github.com/wailsapp/wails/v3/pkg/application/browser_manager.go
generated
vendored
Normal file
27
vendor/github.com/wailsapp/wails/v3/pkg/application/browser_manager.go
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/internal/browser"
|
||||
)
|
||||
|
||||
// BrowserManager manages browser-related operations
|
||||
type BrowserManager struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
// newBrowserManager creates a new BrowserManager instance
|
||||
func newBrowserManager(app *App) *BrowserManager {
|
||||
return &BrowserManager{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
// OpenURL opens a URL in the default browser
|
||||
func (bm *BrowserManager) OpenURL(url string) error {
|
||||
return browser.OpenURL(url)
|
||||
}
|
||||
|
||||
// OpenFile opens a file in the default browser
|
||||
func (bm *BrowserManager) OpenFile(path string) error {
|
||||
return browser.OpenFile(path)
|
||||
}
|
||||
152
vendor/github.com/wailsapp/wails/v3/pkg/application/browser_window.go
generated
vendored
Normal file
152
vendor/github.com/wailsapp/wails/v3/pkg/application/browser_window.go
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
//go:build server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
// BrowserWindow represents a browser client connection in server mode.
|
||||
// It implements the Window interface so browser clients can be treated
|
||||
// uniformly with native windows throughout the codebase.
|
||||
type BrowserWindow struct {
|
||||
id uint
|
||||
name string
|
||||
clientID string // The runtime's nanoid for this client
|
||||
}
|
||||
|
||||
// NewBrowserWindow creates a new browser window with the given ID.
|
||||
func NewBrowserWindow(id uint, clientID string) *BrowserWindow {
|
||||
return &BrowserWindow{
|
||||
id: id,
|
||||
name: fmt.Sprintf("browser-%d", id),
|
||||
clientID: clientID,
|
||||
}
|
||||
}
|
||||
|
||||
// Core identification methods
|
||||
|
||||
func (b *BrowserWindow) ID() uint { return b.id }
|
||||
func (b *BrowserWindow) Name() string { return b.name }
|
||||
func (b *BrowserWindow) ClientID() string { return b.clientID }
|
||||
|
||||
// Event methods - these are meaningful for browser windows
|
||||
|
||||
func (b *BrowserWindow) DispatchWailsEvent(event *CustomEvent) {
|
||||
// Events are dispatched via WebSocket broadcast, not per-window
|
||||
}
|
||||
|
||||
func (b *BrowserWindow) EmitEvent(name string, data ...any) bool {
|
||||
return globalApplication.Event.Emit(name, data...)
|
||||
}
|
||||
|
||||
// Logging methods
|
||||
|
||||
func (b *BrowserWindow) Error(message string, args ...any) {
|
||||
globalApplication.error(message, args...)
|
||||
}
|
||||
|
||||
func (b *BrowserWindow) Info(message string, args ...any) {
|
||||
globalApplication.info(message, args...)
|
||||
}
|
||||
|
||||
// No-op methods - these don't apply to browser windows
|
||||
|
||||
func (b *BrowserWindow) Center() {}
|
||||
func (b *BrowserWindow) Close() {}
|
||||
func (b *BrowserWindow) DisableSizeConstraints() {}
|
||||
func (b *BrowserWindow) EnableSizeConstraints() {}
|
||||
func (b *BrowserWindow) ExecJS(js string) {}
|
||||
func (b *BrowserWindow) Focus() {}
|
||||
func (b *BrowserWindow) ForceReload() {}
|
||||
func (b *BrowserWindow) Fullscreen() Window { return b }
|
||||
func (b *BrowserWindow) GetBorderSizes() *LRTB { return nil }
|
||||
func (b *BrowserWindow) GetScreen() (*Screen, error) { return nil, nil }
|
||||
func (b *BrowserWindow) SetScreen(screen *Screen) Window { return b }
|
||||
func (b *BrowserWindow) GetZoom() float64 { return 1.0 }
|
||||
func (b *BrowserWindow) handleDragAndDropMessage(filenames []string, dropTarget *DropTargetDetails) {}
|
||||
func (b *BrowserWindow) InitiateFrontendDropProcessing(filenames []string, x int, y int) {}
|
||||
func (b *BrowserWindow) HandleMessage(message string) {}
|
||||
func (b *BrowserWindow) HandleWindowEvent(id uint) {}
|
||||
func (b *BrowserWindow) Height() int { return 0 }
|
||||
func (b *BrowserWindow) Hide() Window { return b }
|
||||
func (b *BrowserWindow) HideMenuBar() {}
|
||||
func (b *BrowserWindow) IsFocused() bool { return false }
|
||||
func (b *BrowserWindow) IsFullscreen() bool { return false }
|
||||
func (b *BrowserWindow) IsIgnoreMouseEvents() bool { return false }
|
||||
func (b *BrowserWindow) IsMaximised() bool { return false }
|
||||
func (b *BrowserWindow) IsMinimised() bool { return false }
|
||||
func (b *BrowserWindow) HandleKeyEvent(acceleratorString string) {}
|
||||
func (b *BrowserWindow) Maximise() Window { return b }
|
||||
func (b *BrowserWindow) Minimise() Window { return b }
|
||||
func (b *BrowserWindow) OnWindowEvent(eventType events.WindowEventType, callback func(event *WindowEvent)) func() {
|
||||
return func() {}
|
||||
}
|
||||
func (b *BrowserWindow) OpenContextMenu(data *ContextMenuData) {}
|
||||
func (b *BrowserWindow) Position() (int, int) { return 0, 0 }
|
||||
func (b *BrowserWindow) RelativePosition() (int, int) { return 0, 0 }
|
||||
func (b *BrowserWindow) Reload() {}
|
||||
func (b *BrowserWindow) Resizable() bool { return false }
|
||||
func (b *BrowserWindow) Restore() {}
|
||||
func (b *BrowserWindow) Run() {}
|
||||
func (b *BrowserWindow) SetPosition(x, y int) {}
|
||||
func (b *BrowserWindow) SetAlwaysOnTop(b2 bool) Window { return b }
|
||||
func (b *BrowserWindow) SetBackgroundColour(colour RGBA) Window { return b }
|
||||
func (b *BrowserWindow) SetFrameless(frameless bool) Window { return b }
|
||||
func (b *BrowserWindow) SetHTML(html string) Window { return b }
|
||||
func (b *BrowserWindow) SetMinimiseButtonState(state ButtonState) Window { return b }
|
||||
func (b *BrowserWindow) SetMaximiseButtonState(state ButtonState) Window { return b }
|
||||
func (b *BrowserWindow) SetCloseButtonState(state ButtonState) Window { return b }
|
||||
func (b *BrowserWindow) SetFullscreenButtonState(state ButtonState) Window { return b }
|
||||
func (b *BrowserWindow) SetMaxSize(maxWidth, maxHeight int) Window { return b }
|
||||
func (b *BrowserWindow) SetMinSize(minWidth, minHeight int) Window { return b }
|
||||
func (b *BrowserWindow) SetRelativePosition(x, y int) Window { return b }
|
||||
func (b *BrowserWindow) SetResizable(b2 bool) Window { return b }
|
||||
func (b *BrowserWindow) SetIgnoreMouseEvents(ignore bool) Window { return b }
|
||||
func (b *BrowserWindow) SetSize(width, height int) Window { return b }
|
||||
func (b *BrowserWindow) SetTitle(title string) Window { return b }
|
||||
func (b *BrowserWindow) SetURL(s string) Window { return b }
|
||||
func (b *BrowserWindow) SetZoom(magnification float64) Window { return b }
|
||||
func (b *BrowserWindow) Show() Window { return b }
|
||||
func (b *BrowserWindow) ShowMenuBar() {}
|
||||
func (b *BrowserWindow) Size() (width int, height int) { return 0, 0 }
|
||||
func (b *BrowserWindow) OpenDevTools() {}
|
||||
func (b *BrowserWindow) ToggleFullscreen() {}
|
||||
func (b *BrowserWindow) ToggleMaximise() {}
|
||||
func (b *BrowserWindow) ToggleMenuBar() {}
|
||||
func (b *BrowserWindow) ToggleFrameless() {}
|
||||
func (b *BrowserWindow) UnFullscreen() {}
|
||||
func (b *BrowserWindow) UnMaximise() {}
|
||||
func (b *BrowserWindow) UnMinimise() {}
|
||||
func (b *BrowserWindow) Width() int { return 0 }
|
||||
func (b *BrowserWindow) IsVisible() bool { return true }
|
||||
func (b *BrowserWindow) Bounds() Rect { return Rect{} }
|
||||
func (b *BrowserWindow) SetBounds(bounds Rect) {}
|
||||
func (b *BrowserWindow) Zoom() {}
|
||||
func (b *BrowserWindow) ZoomIn() {}
|
||||
func (b *BrowserWindow) ZoomOut() {}
|
||||
func (b *BrowserWindow) ZoomReset() Window { return b }
|
||||
func (b *BrowserWindow) SetMenu(menu *Menu) {}
|
||||
func (b *BrowserWindow) SnapAssist() {}
|
||||
func (b *BrowserWindow) AttachModal(modalWindow Window) {}
|
||||
func (b *BrowserWindow) SetContentProtection(protection bool) Window { return b }
|
||||
func (b *BrowserWindow) NativeWindow() unsafe.Pointer { return nil }
|
||||
func (b *BrowserWindow) SetEnabled(enabled bool) {}
|
||||
func (b *BrowserWindow) Flash(enabled bool) {}
|
||||
func (b *BrowserWindow) Print() error { return nil }
|
||||
func (b *BrowserWindow) RegisterHook(eventType events.WindowEventType, callback func(event *WindowEvent)) func() {
|
||||
return func() {}
|
||||
}
|
||||
func (b *BrowserWindow) shouldUnconditionallyClose() bool { return true }
|
||||
|
||||
// Editing methods
|
||||
func (b *BrowserWindow) cut() {}
|
||||
func (b *BrowserWindow) copy() {}
|
||||
func (b *BrowserWindow) paste() {}
|
||||
func (b *BrowserWindow) undo() {}
|
||||
func (b *BrowserWindow) redo() {}
|
||||
func (b *BrowserWindow) delete() {}
|
||||
func (b *BrowserWindow) selectAll() {}
|
||||
26
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard.go
generated
vendored
Normal file
26
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard.go
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
package application
|
||||
|
||||
type clipboardImpl interface {
|
||||
setText(text string) bool
|
||||
text() (string, bool)
|
||||
}
|
||||
|
||||
type Clipboard struct {
|
||||
impl clipboardImpl
|
||||
}
|
||||
|
||||
func newClipboard() *Clipboard {
|
||||
return &Clipboard{
|
||||
impl: newClipboardImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Clipboard) SetText(text string) bool {
|
||||
return InvokeSyncWithResult(func() bool {
|
||||
return c.impl.setText(text)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Clipboard) Text() (string, bool) {
|
||||
return InvokeSyncWithResultAndOther(c.impl.text)
|
||||
}
|
||||
22
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_android.go
generated
vendored
Normal file
22
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_android.go
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
//go:build android
|
||||
|
||||
package application
|
||||
|
||||
// The clipboard is backed by Android's ClipboardManager via the WailsBridge.
|
||||
// Note: on Android 10+ reading the clipboard only succeeds while the app has
|
||||
// input focus.
|
||||
|
||||
type androidClipboardImpl struct{}
|
||||
|
||||
func newClipboardImpl() clipboardImpl {
|
||||
return &androidClipboardImpl{}
|
||||
}
|
||||
|
||||
func (c *androidClipboardImpl) setText(text string) bool {
|
||||
androidBridgeVoidString("setClipboardText", text)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *androidClipboardImpl) text() (string, bool) {
|
||||
return androidBridgeString("getClipboardText")
|
||||
}
|
||||
56
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_darwin.go
generated
vendored
Normal file
56
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
//go:build darwin && !ios && !server
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -mmacosx-version-min=10.13 -x objective-c
|
||||
#cgo LDFLAGS: -framework Cocoa -mmacosx-version-min=10.13
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <stdlib.h>
|
||||
|
||||
bool setClipboardText(const char* text) {
|
||||
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
|
||||
NSError *error = nil;
|
||||
NSString *string = [NSString stringWithUTF8String:text];
|
||||
[pasteBoard clearContents];
|
||||
return [pasteBoard setString:string forType:NSPasteboardTypeString];
|
||||
}
|
||||
|
||||
const char* getClipboardText() {
|
||||
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
|
||||
NSString *text = [pasteboard stringForType:NSPasteboardTypeString];
|
||||
return [text UTF8String];
|
||||
}
|
||||
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var clipboardLock sync.RWMutex
|
||||
|
||||
type macosClipboard struct{}
|
||||
|
||||
func (m macosClipboard) setText(text string) bool {
|
||||
clipboardLock.Lock()
|
||||
defer clipboardLock.Unlock()
|
||||
cText := C.CString(text)
|
||||
success := C.setClipboardText(cText)
|
||||
C.free(unsafe.Pointer(cText))
|
||||
return bool(success)
|
||||
}
|
||||
|
||||
func (m macosClipboard) text() (string, bool) {
|
||||
clipboardLock.RLock()
|
||||
defer clipboardLock.RUnlock()
|
||||
clipboardText := C.getClipboardText()
|
||||
result := C.GoString(clipboardText)
|
||||
return result, true
|
||||
}
|
||||
|
||||
func newClipboardImpl() *macosClipboard {
|
||||
return &macosClipboard{}
|
||||
}
|
||||
33
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_ios.go
generated
vendored
Normal file
33
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_ios.go
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
//go:build ios
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include "application_ios.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import "unsafe"
|
||||
|
||||
type iosClipboardImpl struct{}
|
||||
|
||||
func newClipboardImpl() clipboardImpl {
|
||||
return &iosClipboardImpl{}
|
||||
}
|
||||
|
||||
func (c *iosClipboardImpl) setText(text string) bool {
|
||||
ctext := C.CString(text)
|
||||
defer C.free(unsafe.Pointer(ctext))
|
||||
C.ios_clipboard_set_text(ctext)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *iosClipboardImpl) text() (string, bool) {
|
||||
ctext := C.ios_clipboard_get_text()
|
||||
if ctext == nil {
|
||||
return "", false
|
||||
}
|
||||
defer C.free(unsafe.Pointer(ctext))
|
||||
return C.GoString(ctext), true
|
||||
}
|
||||
28
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_linux.go
generated
vendored
Normal file
28
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_linux.go
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
//go:build linux && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var clipboardLock sync.RWMutex
|
||||
|
||||
type linuxClipboard struct{}
|
||||
|
||||
func (m linuxClipboard) setText(text string) bool {
|
||||
clipboardLock.Lock()
|
||||
defer clipboardLock.Unlock()
|
||||
clipboardSet(text)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m linuxClipboard) text() (string, bool) {
|
||||
clipboardLock.RLock()
|
||||
defer clipboardLock.RUnlock()
|
||||
return clipboardGet(), true
|
||||
}
|
||||
|
||||
func newClipboardImpl() *linuxClipboard {
|
||||
return &linuxClipboard{}
|
||||
}
|
||||
32
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_manager.go
generated
vendored
Normal file
32
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_manager.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package application
|
||||
|
||||
// ClipboardManager manages clipboard operations
|
||||
type ClipboardManager struct {
|
||||
app *App
|
||||
clipboard *Clipboard
|
||||
}
|
||||
|
||||
// newClipboardManager creates a new ClipboardManager instance
|
||||
func newClipboardManager(app *App) *ClipboardManager {
|
||||
return &ClipboardManager{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
// SetText sets text in the clipboard
|
||||
func (cm *ClipboardManager) SetText(text string) bool {
|
||||
return cm.getClipboard().SetText(text)
|
||||
}
|
||||
|
||||
// Text gets text from the clipboard
|
||||
func (cm *ClipboardManager) Text() (string, bool) {
|
||||
return cm.getClipboard().Text()
|
||||
}
|
||||
|
||||
// getClipboard returns the clipboard instance, creating it if needed (lazy initialization)
|
||||
func (cm *ClipboardManager) getClipboard() *Clipboard {
|
||||
if cm.clipboard == nil {
|
||||
cm.clipboard = newClipboard()
|
||||
}
|
||||
return cm.clipboard
|
||||
}
|
||||
29
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_windows.go
generated
vendored
Normal file
29
vendor/github.com/wailsapp/wails/v3/pkg/application/clipboard_windows.go
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build windows && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type windowsClipboard struct {
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *windowsClipboard) setText(text string) bool {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
return w32.SetClipboardText(text) == nil
|
||||
}
|
||||
|
||||
func (m *windowsClipboard) text() (string, bool) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
text, err := w32.GetClipboardText()
|
||||
return text, err == nil
|
||||
}
|
||||
|
||||
func newClipboardImpl() *windowsClipboard {
|
||||
return &windowsClipboard{}
|
||||
}
|
||||
62
vendor/github.com/wailsapp/wails/v3/pkg/application/context.go
generated
vendored
Normal file
62
vendor/github.com/wailsapp/wails/v3/pkg/application/context.go
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
package application
|
||||
|
||||
type Context struct {
|
||||
// contains filtered or unexported fields
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
func newContext() *Context {
|
||||
return &Context{
|
||||
data: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
clickedMenuItem string = "clickedMenuItem"
|
||||
menuItemIsChecked string = "menuItemIsChecked"
|
||||
contextMenuData string = "contextMenuData"
|
||||
)
|
||||
|
||||
func (c *Context) ClickedMenuItem() *MenuItem {
|
||||
result, exists := c.data[clickedMenuItem]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return result.(*MenuItem)
|
||||
}
|
||||
|
||||
func (c *Context) IsChecked() bool {
|
||||
result, exists := c.data[menuItemIsChecked]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
return result.(bool)
|
||||
}
|
||||
func (c *Context) ContextMenuData() string {
|
||||
result := c.data[contextMenuData]
|
||||
if result == nil {
|
||||
return ""
|
||||
}
|
||||
str, ok := result.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func (c *Context) withClickedMenuItem(menuItem *MenuItem) *Context {
|
||||
c.data[clickedMenuItem] = menuItem
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) withChecked(checked bool) {
|
||||
c.data[menuItemIsChecked] = checked
|
||||
}
|
||||
|
||||
func (c *Context) withContextMenuData(data *ContextMenuData) *Context {
|
||||
if data == nil {
|
||||
return c
|
||||
}
|
||||
c.data[contextMenuData] = data.Data
|
||||
return c
|
||||
}
|
||||
113
vendor/github.com/wailsapp/wails/v3/pkg/application/context_application_event.go
generated
vendored
Normal file
113
vendor/github.com/wailsapp/wails/v3/pkg/application/context_application_event.go
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
package application
|
||||
|
||||
import "log"
|
||||
|
||||
var blankApplicationEventContext = &ApplicationEventContext{}
|
||||
|
||||
const (
|
||||
CONTEXT_OPENED_FILES = "openedFiles"
|
||||
CONTEXT_FILENAME = "filename"
|
||||
CONTEXT_URL = "url"
|
||||
)
|
||||
|
||||
// ApplicationEventContext is the context of an application event
|
||||
type ApplicationEventContext struct {
|
||||
// contains filtered or unexported fields
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
// OpenedFiles returns the opened files from the event context if it was set
|
||||
func (c ApplicationEventContext) OpenedFiles() []string {
|
||||
files, ok := c.data[CONTEXT_OPENED_FILES]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
result, ok := files.([]string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c ApplicationEventContext) setOpenedFiles(files []string) {
|
||||
c.data[CONTEXT_OPENED_FILES] = files
|
||||
}
|
||||
|
||||
func (c ApplicationEventContext) setIsDarkMode(mode bool) {
|
||||
c.data["isDarkMode"] = mode
|
||||
}
|
||||
|
||||
func (c ApplicationEventContext) getBool(key string) bool {
|
||||
mode, ok := c.data[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
result, ok := mode.(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// IsDarkMode returns true if the event context has a dark mode
|
||||
func (c ApplicationEventContext) IsDarkMode() bool {
|
||||
return c.getBool("isDarkMode")
|
||||
}
|
||||
|
||||
// HasVisibleWindows returns true if the event context has a visible window
|
||||
func (c ApplicationEventContext) HasVisibleWindows() bool {
|
||||
return c.getBool("hasVisibleWindows")
|
||||
}
|
||||
|
||||
func (c *ApplicationEventContext) setData(data map[string]any) {
|
||||
c.data = data
|
||||
}
|
||||
|
||||
// Data returns the raw context data map. Mobile system events (battery,
|
||||
// network, …) attach their structured payload here; the typed getters above
|
||||
// (IsDarkMode, Filename, URL, …) cover the well-known keys.
|
||||
func (c ApplicationEventContext) Data() map[string]any {
|
||||
return c.data
|
||||
}
|
||||
|
||||
func (c *ApplicationEventContext) setOpenedWithFile(filepath string) {
|
||||
c.data[CONTEXT_FILENAME] = filepath
|
||||
}
|
||||
|
||||
func (c *ApplicationEventContext) setURL(openedWithURL string) {
|
||||
c.data[CONTEXT_URL] = openedWithURL
|
||||
}
|
||||
|
||||
// Filename returns the filename from the event context if it was set
|
||||
func (c ApplicationEventContext) Filename() string {
|
||||
filename, ok := c.data[CONTEXT_FILENAME]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
result, ok := filename.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// URL returns the URL from the event context if it was set
|
||||
func (c ApplicationEventContext) URL() string {
|
||||
url, ok := c.data[CONTEXT_URL]
|
||||
if !ok {
|
||||
log.Println("URL not found in event context")
|
||||
return ""
|
||||
}
|
||||
result, ok := url.(string)
|
||||
if !ok {
|
||||
log.Println("URL not a string in event context")
|
||||
return ""
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func newApplicationEventContext() *ApplicationEventContext {
|
||||
return &ApplicationEventContext{
|
||||
data: make(map[string]any),
|
||||
}
|
||||
}
|
||||
54
vendor/github.com/wailsapp/wails/v3/pkg/application/context_menu_manager.go
generated
vendored
Normal file
54
vendor/github.com/wailsapp/wails/v3/pkg/application/context_menu_manager.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
package application
|
||||
|
||||
// ContextMenuManager manages all context menu operations
|
||||
type ContextMenuManager struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
// newContextMenuManager creates a new ContextMenuManager instance
|
||||
func newContextMenuManager(app *App) *ContextMenuManager {
|
||||
return &ContextMenuManager{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new context menu
|
||||
func (cmm *ContextMenuManager) New() *ContextMenu {
|
||||
return &ContextMenu{
|
||||
Menu: NewMenu(),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a context menu (replaces Register for consistency)
|
||||
func (cmm *ContextMenuManager) Add(name string, menu *ContextMenu) {
|
||||
cmm.app.contextMenusLock.Lock()
|
||||
defer cmm.app.contextMenusLock.Unlock()
|
||||
cmm.app.contextMenus[name] = menu
|
||||
}
|
||||
|
||||
// Remove removes a context menu by name (replaces Unregister for consistency)
|
||||
func (cmm *ContextMenuManager) Remove(name string) {
|
||||
cmm.app.contextMenusLock.Lock()
|
||||
defer cmm.app.contextMenusLock.Unlock()
|
||||
delete(cmm.app.contextMenus, name)
|
||||
}
|
||||
|
||||
// Get retrieves a context menu by name
|
||||
func (cmm *ContextMenuManager) Get(name string) (*ContextMenu, bool) {
|
||||
cmm.app.contextMenusLock.RLock()
|
||||
defer cmm.app.contextMenusLock.RUnlock()
|
||||
menu, exists := cmm.app.contextMenus[name]
|
||||
return menu, exists
|
||||
}
|
||||
|
||||
// GetAll returns all registered context menus as a slice
|
||||
func (cmm *ContextMenuManager) GetAll() []*ContextMenu {
|
||||
cmm.app.contextMenusLock.RLock()
|
||||
defer cmm.app.contextMenusLock.RUnlock()
|
||||
|
||||
result := make([]*ContextMenu, 0, len(cmm.app.contextMenus))
|
||||
for _, menu := range cmm.app.contextMenus {
|
||||
result = append(result, menu)
|
||||
}
|
||||
return result
|
||||
}
|
||||
79
vendor/github.com/wailsapp/wails/v3/pkg/application/context_window_event.go
generated
vendored
Normal file
79
vendor/github.com/wailsapp/wails/v3/pkg/application/context_window_event.go
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
package application
|
||||
|
||||
var blankWindowEventContext = &WindowEventContext{}
|
||||
|
||||
const (
|
||||
droppedFiles = "droppedFiles"
|
||||
dropTargetDetailsKey = "dropTargetDetails"
|
||||
)
|
||||
|
||||
type WindowEventContext struct {
|
||||
// contains filtered or unexported fields
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
func (c WindowEventContext) DroppedFiles() []string {
|
||||
if c.data == nil {
|
||||
c.data = make(map[string]any)
|
||||
}
|
||||
files, ok := c.data[droppedFiles]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
result, ok := files.([]string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c WindowEventContext) setDroppedFiles(files []string) {
|
||||
if c.data == nil {
|
||||
c.data = make(map[string]any)
|
||||
}
|
||||
c.data[droppedFiles] = files
|
||||
}
|
||||
|
||||
func (c WindowEventContext) setCoordinates(x, y int) {
|
||||
if c.data == nil {
|
||||
c.data = make(map[string]any)
|
||||
}
|
||||
c.data["x"] = x
|
||||
c.data["y"] = y
|
||||
}
|
||||
|
||||
func (c WindowEventContext) setDropTargetDetails(details *DropTargetDetails) {
|
||||
if c.data == nil {
|
||||
c.data = make(map[string]any)
|
||||
}
|
||||
if details == nil {
|
||||
c.data[dropTargetDetailsKey] = nil
|
||||
return
|
||||
}
|
||||
c.data[dropTargetDetailsKey] = details
|
||||
}
|
||||
|
||||
// DropTargetDetails retrieves information about the drop target element.
|
||||
func (c WindowEventContext) DropTargetDetails() *DropTargetDetails {
|
||||
if c.data == nil {
|
||||
c.data = make(map[string]any)
|
||||
}
|
||||
details, ok := c.data[dropTargetDetailsKey]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if details == nil {
|
||||
return nil
|
||||
}
|
||||
result, ok := details.(*DropTargetDetails)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func newWindowEventContext() *WindowEventContext {
|
||||
return &WindowEventContext{
|
||||
data: make(map[string]any),
|
||||
}
|
||||
}
|
||||
57
vendor/github.com/wailsapp/wails/v3/pkg/application/dialog_manager.go
generated
vendored
Normal file
57
vendor/github.com/wailsapp/wails/v3/pkg/application/dialog_manager.go
generated
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
package application
|
||||
|
||||
// DialogManager manages dialog-related operations
|
||||
type DialogManager struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
// newDialogManager creates a new DialogManager instance
|
||||
func newDialogManager(app *App) *DialogManager {
|
||||
return &DialogManager{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
// OpenFile creates a file dialog for selecting files
|
||||
func (dm *DialogManager) OpenFile() *OpenFileDialogStruct {
|
||||
return newOpenFileDialog()
|
||||
}
|
||||
|
||||
// OpenFileWithOptions creates a file dialog with options
|
||||
func (dm *DialogManager) OpenFileWithOptions(options *OpenFileDialogOptions) *OpenFileDialogStruct {
|
||||
result := newOpenFileDialog()
|
||||
result.SetOptions(options)
|
||||
return result
|
||||
}
|
||||
|
||||
// SaveFile creates a save file dialog
|
||||
func (dm *DialogManager) SaveFile() *SaveFileDialogStruct {
|
||||
return newSaveFileDialog()
|
||||
}
|
||||
|
||||
// SaveFileWithOptions creates a save file dialog with options
|
||||
func (dm *DialogManager) SaveFileWithOptions(options *SaveFileDialogOptions) *SaveFileDialogStruct {
|
||||
result := newSaveFileDialog()
|
||||
result.SetOptions(options)
|
||||
return result
|
||||
}
|
||||
|
||||
// Info creates an information dialog
|
||||
func (dm *DialogManager) Info() *MessageDialog {
|
||||
return newMessageDialog(InfoDialogType)
|
||||
}
|
||||
|
||||
// Question creates a question dialog
|
||||
func (dm *DialogManager) Question() *MessageDialog {
|
||||
return newMessageDialog(QuestionDialogType)
|
||||
}
|
||||
|
||||
// Warning creates a warning dialog
|
||||
func (dm *DialogManager) Warning() *MessageDialog {
|
||||
return newMessageDialog(WarningDialogType)
|
||||
}
|
||||
|
||||
// Error creates an error dialog
|
||||
func (dm *DialogManager) Error() *MessageDialog {
|
||||
return newMessageDialog(ErrorDialogType)
|
||||
}
|
||||
497
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs.go
generated
vendored
Normal file
497
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs.go
generated
vendored
Normal file
@@ -0,0 +1,497 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type DialogType int
|
||||
|
||||
var dialogMapID = make(map[uint]struct{})
|
||||
var dialogIDLock sync.RWMutex
|
||||
|
||||
func getDialogID() uint {
|
||||
dialogIDLock.Lock()
|
||||
defer dialogIDLock.Unlock()
|
||||
var dialogID uint
|
||||
for {
|
||||
if _, ok := dialogMapID[dialogID]; !ok {
|
||||
dialogMapID[dialogID] = struct{}{}
|
||||
break
|
||||
}
|
||||
dialogID++
|
||||
if dialogID == 0 {
|
||||
panic("no more dialog IDs")
|
||||
}
|
||||
}
|
||||
return dialogID
|
||||
}
|
||||
|
||||
func freeDialogID(id uint) {
|
||||
dialogIDLock.Lock()
|
||||
defer dialogIDLock.Unlock()
|
||||
delete(dialogMapID, id)
|
||||
}
|
||||
|
||||
var openFileResponses = make(map[uint]chan string)
|
||||
var saveFileResponses = make(map[uint]chan string)
|
||||
|
||||
const (
|
||||
InfoDialogType DialogType = iota
|
||||
QuestionDialogType
|
||||
WarningDialogType
|
||||
ErrorDialogType
|
||||
)
|
||||
|
||||
type Button struct {
|
||||
Label string
|
||||
IsCancel bool
|
||||
IsDefault bool
|
||||
Callback func()
|
||||
}
|
||||
|
||||
func (b *Button) OnClick(callback func()) *Button {
|
||||
b.Callback = callback
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Button) SetAsDefault() *Button {
|
||||
b.IsDefault = true
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Button) SetAsCancel() *Button {
|
||||
b.IsCancel = true
|
||||
return b
|
||||
}
|
||||
|
||||
type messageDialogImpl interface {
|
||||
show()
|
||||
}
|
||||
|
||||
type MessageDialogOptions struct {
|
||||
DialogType DialogType
|
||||
Title string
|
||||
Message string
|
||||
Buttons []*Button
|
||||
Icon []byte
|
||||
window Window
|
||||
}
|
||||
|
||||
type MessageDialog struct {
|
||||
MessageDialogOptions
|
||||
|
||||
// platform independent
|
||||
impl messageDialogImpl
|
||||
}
|
||||
|
||||
var defaultTitles = map[DialogType]string{
|
||||
InfoDialogType: "Information",
|
||||
QuestionDialogType: "Question",
|
||||
WarningDialogType: "Warning",
|
||||
ErrorDialogType: "Error",
|
||||
}
|
||||
|
||||
func newMessageDialog(dialogType DialogType) *MessageDialog {
|
||||
return &MessageDialog{
|
||||
MessageDialogOptions: MessageDialogOptions{
|
||||
DialogType: dialogType,
|
||||
},
|
||||
impl: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MessageDialog) SetTitle(title string) *MessageDialog {
|
||||
d.Title = title
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *MessageDialog) Show() {
|
||||
if d.impl == nil {
|
||||
d.impl = newDialogImpl(d)
|
||||
}
|
||||
InvokeSync(d.impl.show)
|
||||
}
|
||||
|
||||
func (d *MessageDialog) SetIcon(icon []byte) *MessageDialog {
|
||||
d.Icon = icon
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *MessageDialog) AddButton(s string) *Button {
|
||||
result := &Button{
|
||||
Label: s,
|
||||
}
|
||||
d.Buttons = append(d.Buttons, result)
|
||||
return result
|
||||
}
|
||||
|
||||
func (d *MessageDialog) AddButtons(buttons []*Button) *MessageDialog {
|
||||
d.Buttons = buttons
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *MessageDialog) AttachToWindow(window Window) *MessageDialog {
|
||||
d.window = window
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *MessageDialog) SetDefaultButton(button *Button) *MessageDialog {
|
||||
for _, b := range d.Buttons {
|
||||
b.IsDefault = false
|
||||
}
|
||||
button.IsDefault = true
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *MessageDialog) SetCancelButton(button *Button) *MessageDialog {
|
||||
for _, b := range d.Buttons {
|
||||
b.IsCancel = false
|
||||
}
|
||||
button.IsCancel = true
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *MessageDialog) SetMessage(message string) *MessageDialog {
|
||||
d.Message = message
|
||||
return d
|
||||
}
|
||||
|
||||
type openFileDialogImpl interface {
|
||||
show() (chan string, error)
|
||||
}
|
||||
|
||||
type FileFilter struct {
|
||||
DisplayName string // Filter information EG: "Image Files (*.jpg, *.png)"
|
||||
Pattern string // semicolon separated list of extensions, EG: "*.jpg;*.png"
|
||||
}
|
||||
|
||||
type OpenFileDialogOptions struct {
|
||||
CanChooseDirectories bool
|
||||
CanChooseFiles bool
|
||||
CanCreateDirectories bool
|
||||
ShowHiddenFiles bool
|
||||
ResolvesAliases bool
|
||||
AllowsMultipleSelection bool
|
||||
HideExtension bool
|
||||
CanSelectHiddenExtension bool
|
||||
TreatsFilePackagesAsDirectories bool
|
||||
AllowsOtherFileTypes bool
|
||||
Filters []FileFilter
|
||||
Window Window
|
||||
|
||||
Title string
|
||||
Message string
|
||||
ButtonText string
|
||||
Directory string
|
||||
}
|
||||
|
||||
type OpenFileDialogStruct struct {
|
||||
id uint
|
||||
canChooseDirectories bool
|
||||
canChooseFiles bool
|
||||
canCreateDirectories bool
|
||||
showHiddenFiles bool
|
||||
resolvesAliases bool
|
||||
allowsMultipleSelection bool
|
||||
hideExtension bool
|
||||
canSelectHiddenExtension bool
|
||||
treatsFilePackagesAsDirectories bool
|
||||
allowsOtherFileTypes bool
|
||||
filters []FileFilter
|
||||
|
||||
title string
|
||||
message string
|
||||
buttonText string
|
||||
directory string
|
||||
window Window
|
||||
|
||||
impl openFileDialogImpl
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) CanChooseFiles(canChooseFiles bool) *OpenFileDialogStruct {
|
||||
d.canChooseFiles = canChooseFiles
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) CanChooseDirectories(canChooseDirectories bool) *OpenFileDialogStruct {
|
||||
d.canChooseDirectories = canChooseDirectories
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) CanCreateDirectories(canCreateDirectories bool) *OpenFileDialogStruct {
|
||||
d.canCreateDirectories = canCreateDirectories
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) AllowsOtherFileTypes(allowsOtherFileTypes bool) *OpenFileDialogStruct {
|
||||
d.allowsOtherFileTypes = allowsOtherFileTypes
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) ShowHiddenFiles(showHiddenFiles bool) *OpenFileDialogStruct {
|
||||
d.showHiddenFiles = showHiddenFiles
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) HideExtension(hideExtension bool) *OpenFileDialogStruct {
|
||||
d.hideExtension = hideExtension
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) TreatsFilePackagesAsDirectories(treatsFilePackagesAsDirectories bool) *OpenFileDialogStruct {
|
||||
d.treatsFilePackagesAsDirectories = treatsFilePackagesAsDirectories
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) AttachToWindow(window Window) *OpenFileDialogStruct {
|
||||
d.window = window
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) ResolvesAliases(resolvesAliases bool) *OpenFileDialogStruct {
|
||||
d.resolvesAliases = resolvesAliases
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) SetTitle(title string) *OpenFileDialogStruct {
|
||||
d.title = title
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) PromptForSingleSelection() (string, error) {
|
||||
d.allowsMultipleSelection = false
|
||||
if d.impl == nil {
|
||||
d.impl = newOpenFileDialogImpl(d)
|
||||
}
|
||||
|
||||
var result string
|
||||
selections, err := InvokeSyncWithResultAndError(d.impl.show)
|
||||
if err == nil {
|
||||
result = <-selections
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// AddFilter adds a filter to the dialog. The filter is a display name and a semicolon separated list of extensions.
|
||||
// EG: AddFilter("Image Files", "*.jpg;*.png")
|
||||
func (d *OpenFileDialogStruct) AddFilter(displayName, pattern string) *OpenFileDialogStruct {
|
||||
d.filters = append(d.filters, FileFilter{
|
||||
DisplayName: strings.TrimSpace(displayName),
|
||||
Pattern: strings.TrimSpace(pattern),
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) PromptForMultipleSelection() ([]string, error) {
|
||||
d.allowsMultipleSelection = true
|
||||
if d.impl == nil {
|
||||
d.impl = newOpenFileDialogImpl(d)
|
||||
}
|
||||
|
||||
selections, err := InvokeSyncWithResultAndError(d.impl.show)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []string
|
||||
for filename := range selections {
|
||||
result = append(result, filename)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) SetMessage(message string) *OpenFileDialogStruct {
|
||||
d.message = message
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) SetButtonText(text string) *OpenFileDialogStruct {
|
||||
d.buttonText = text
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) SetDirectory(directory string) *OpenFileDialogStruct {
|
||||
d.directory = directory
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) CanSelectHiddenExtension(canSelectHiddenExtension bool) *OpenFileDialogStruct {
|
||||
d.canSelectHiddenExtension = canSelectHiddenExtension
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *OpenFileDialogStruct) SetOptions(options *OpenFileDialogOptions) {
|
||||
d.title = options.Title
|
||||
d.message = options.Message
|
||||
d.buttonText = options.ButtonText
|
||||
d.directory = options.Directory
|
||||
d.canChooseDirectories = options.CanChooseDirectories
|
||||
d.canChooseFiles = options.CanChooseFiles
|
||||
if !options.CanChooseFiles && !options.CanChooseDirectories {
|
||||
d.canChooseFiles = true
|
||||
}
|
||||
d.canCreateDirectories = options.CanCreateDirectories
|
||||
d.showHiddenFiles = options.ShowHiddenFiles
|
||||
d.resolvesAliases = options.ResolvesAliases
|
||||
d.allowsMultipleSelection = options.AllowsMultipleSelection
|
||||
d.hideExtension = options.HideExtension
|
||||
d.canSelectHiddenExtension = options.CanSelectHiddenExtension
|
||||
d.treatsFilePackagesAsDirectories = options.TreatsFilePackagesAsDirectories
|
||||
d.allowsOtherFileTypes = options.AllowsOtherFileTypes
|
||||
d.filters = options.Filters
|
||||
d.window = options.Window
|
||||
}
|
||||
|
||||
func newOpenFileDialog() *OpenFileDialogStruct {
|
||||
return &OpenFileDialogStruct{
|
||||
id: getDialogID(),
|
||||
canChooseDirectories: false,
|
||||
canChooseFiles: true,
|
||||
canCreateDirectories: true,
|
||||
resolvesAliases: false,
|
||||
}
|
||||
}
|
||||
|
||||
func newSaveFileDialog() *SaveFileDialogStruct {
|
||||
return &SaveFileDialogStruct{
|
||||
id: getDialogID(),
|
||||
canCreateDirectories: true,
|
||||
}
|
||||
}
|
||||
|
||||
type SaveFileDialogOptions struct {
|
||||
CanCreateDirectories bool
|
||||
ShowHiddenFiles bool
|
||||
CanSelectHiddenExtension bool
|
||||
AllowOtherFileTypes bool
|
||||
HideExtension bool
|
||||
TreatsFilePackagesAsDirectories bool
|
||||
Title string
|
||||
Message string
|
||||
Directory string
|
||||
Filename string
|
||||
ButtonText string
|
||||
Filters []FileFilter
|
||||
Window Window
|
||||
}
|
||||
|
||||
type SaveFileDialogStruct struct {
|
||||
id uint
|
||||
canCreateDirectories bool
|
||||
showHiddenFiles bool
|
||||
canSelectHiddenExtension bool
|
||||
allowOtherFileTypes bool
|
||||
hideExtension bool
|
||||
treatsFilePackagesAsDirectories bool
|
||||
message string
|
||||
directory string
|
||||
filename string
|
||||
buttonText string
|
||||
filters []FileFilter
|
||||
|
||||
window Window
|
||||
|
||||
impl saveFileDialogImpl
|
||||
title string
|
||||
}
|
||||
|
||||
type saveFileDialogImpl interface {
|
||||
show() (chan string, error)
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) SetOptions(options *SaveFileDialogOptions) {
|
||||
d.title = options.Title
|
||||
d.canCreateDirectories = options.CanCreateDirectories
|
||||
d.showHiddenFiles = options.ShowHiddenFiles
|
||||
d.canSelectHiddenExtension = options.CanSelectHiddenExtension
|
||||
d.allowOtherFileTypes = options.AllowOtherFileTypes
|
||||
d.hideExtension = options.HideExtension
|
||||
d.treatsFilePackagesAsDirectories = options.TreatsFilePackagesAsDirectories
|
||||
d.message = options.Message
|
||||
d.directory = options.Directory
|
||||
d.filename = options.Filename
|
||||
d.buttonText = options.ButtonText
|
||||
d.filters = options.Filters
|
||||
d.window = options.Window
|
||||
}
|
||||
|
||||
// AddFilter adds a filter to the dialog. The filter is a display name and a semicolon separated list of extensions.
|
||||
// EG: AddFilter("Image Files", "*.jpg;*.png")
|
||||
func (d *SaveFileDialogStruct) AddFilter(displayName, pattern string) *SaveFileDialogStruct {
|
||||
d.filters = append(d.filters, FileFilter{
|
||||
DisplayName: strings.TrimSpace(displayName),
|
||||
Pattern: strings.TrimSpace(pattern),
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) CanCreateDirectories(canCreateDirectories bool) *SaveFileDialogStruct {
|
||||
d.canCreateDirectories = canCreateDirectories
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) CanSelectHiddenExtension(canSelectHiddenExtension bool) *SaveFileDialogStruct {
|
||||
d.canSelectHiddenExtension = canSelectHiddenExtension
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) ShowHiddenFiles(showHiddenFiles bool) *SaveFileDialogStruct {
|
||||
d.showHiddenFiles = showHiddenFiles
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) SetMessage(message string) *SaveFileDialogStruct {
|
||||
d.message = message
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) SetDirectory(directory string) *SaveFileDialogStruct {
|
||||
d.directory = directory
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) AttachToWindow(window Window) *SaveFileDialogStruct {
|
||||
d.window = window
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) PromptForSingleSelection() (string, error) {
|
||||
if d.impl == nil {
|
||||
d.impl = newSaveFileDialogImpl(d)
|
||||
}
|
||||
|
||||
var result string
|
||||
selections, err := InvokeSyncWithResultAndError(d.impl.show)
|
||||
if err == nil {
|
||||
result = <-selections
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) SetButtonText(text string) *SaveFileDialogStruct {
|
||||
d.buttonText = text
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) SetFilename(filename string) *SaveFileDialogStruct {
|
||||
d.filename = filename
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) AllowsOtherFileTypes(allowOtherFileTypes bool) *SaveFileDialogStruct {
|
||||
d.allowOtherFileTypes = allowOtherFileTypes
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) HideExtension(hideExtension bool) *SaveFileDialogStruct {
|
||||
d.hideExtension = hideExtension
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *SaveFileDialogStruct) TreatsFilePackagesAsDirectories(treatsFilePackagesAsDirectories bool) *SaveFileDialogStruct {
|
||||
d.treatsFilePackagesAsDirectories = treatsFilePackagesAsDirectories
|
||||
return d
|
||||
}
|
||||
174
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_android.go
generated
vendored
Normal file
174
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_android.go
generated
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
//go:build android
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Message dialogs are backed by AlertDialog, file dialogs by the Storage
|
||||
// Access Framework document picker (selected documents are copied into the
|
||||
// app's cache directory so callers receive real filesystem paths). Save
|
||||
// dialogs have no Android counterpart that yields a filesystem path (apps
|
||||
// write into their sandbox and share via intents), so they return an
|
||||
// explicit error instead of failing silently.
|
||||
|
||||
// Pending message dialogs keyed by callback ID
|
||||
var (
|
||||
androidDialogsLock sync.Mutex
|
||||
androidPendingDialogs = make(map[uint]*MessageDialog)
|
||||
androidNextDialogID uint = 1
|
||||
)
|
||||
|
||||
type androidDialogButton struct {
|
||||
Label string `json:"label"`
|
||||
IsCancel bool `json:"isCancel"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
}
|
||||
|
||||
type androidDialogOptions struct {
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Buttons []androidDialogButton `json:"buttons"`
|
||||
}
|
||||
|
||||
type androidDialog struct {
|
||||
dialog *MessageDialog
|
||||
}
|
||||
|
||||
func newDialogImpl(d *MessageDialog) *androidDialog {
|
||||
return &androidDialog{dialog: d}
|
||||
}
|
||||
|
||||
func (d *androidDialog) show() {
|
||||
androidDialogsLock.Lock()
|
||||
id := androidNextDialogID
|
||||
androidNextDialogID++
|
||||
androidPendingDialogs[id] = d.dialog
|
||||
androidDialogsLock.Unlock()
|
||||
|
||||
buttons := make([]androidDialogButton, 0, len(d.dialog.Buttons))
|
||||
for _, b := range d.dialog.Buttons {
|
||||
buttons = append(buttons, androidDialogButton{
|
||||
Label: b.Label,
|
||||
IsCancel: b.IsCancel,
|
||||
IsDefault: b.IsDefault,
|
||||
})
|
||||
}
|
||||
|
||||
title := d.dialog.Title
|
||||
if title == "" {
|
||||
title = defaultTitles[d.dialog.DialogType]
|
||||
}
|
||||
|
||||
optionsJSON, _ := json.Marshal(androidDialogOptions{
|
||||
Title: title,
|
||||
Message: d.dialog.Message,
|
||||
Buttons: buttons,
|
||||
})
|
||||
|
||||
androidBridgeVoidIntString("showMessageDialog", int(id), string(optionsJSON))
|
||||
}
|
||||
|
||||
// androidDialogCallback is invoked from JNI when a dialog button is pressed
|
||||
// (buttonIndex is the index into the dialog's button slice, or -1 for a
|
||||
// dismissal with no matching button).
|
||||
func androidDialogCallback(callbackID uint, buttonIndex int) {
|
||||
androidDialogsLock.Lock()
|
||||
dialog, ok := androidPendingDialogs[callbackID]
|
||||
delete(androidPendingDialogs, callbackID)
|
||||
androidDialogsLock.Unlock()
|
||||
if !ok || dialog == nil {
|
||||
return
|
||||
}
|
||||
if buttonIndex < 0 || buttonIndex >= len(dialog.Buttons) {
|
||||
return
|
||||
}
|
||||
button := dialog.Buttons[buttonIndex]
|
||||
if button.Callback != nil {
|
||||
// Run the callback off the JNI thread, mirroring desktop behaviour
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
button.Callback()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// File dialogs
|
||||
|
||||
// Pending file picker channels keyed by dialog ID
|
||||
var (
|
||||
androidFileDialogsLock sync.Mutex
|
||||
androidFileResponses = make(map[uint]chan string)
|
||||
)
|
||||
|
||||
type androidOpenFileDialog struct {
|
||||
dialog *OpenFileDialogStruct
|
||||
}
|
||||
|
||||
func newOpenFileDialogImpl(d *OpenFileDialogStruct) openFileDialogImpl {
|
||||
return &androidOpenFileDialog{dialog: d}
|
||||
}
|
||||
|
||||
type androidFilePickerOptions struct {
|
||||
Multiple bool `json:"multiple"`
|
||||
}
|
||||
|
||||
func (d *androidOpenFileDialog) show() (chan string, error) {
|
||||
if d.dialog.canChooseDirectories && !d.dialog.canChooseFiles {
|
||||
return nil, fmt.Errorf("directory selection is not supported on Android: the Storage Access Framework returns document-tree URIs, not filesystem paths")
|
||||
}
|
||||
|
||||
results := make(chan string, 16)
|
||||
|
||||
androidFileDialogsLock.Lock()
|
||||
id := d.dialog.id
|
||||
androidFileResponses[id] = results
|
||||
androidFileDialogsLock.Unlock()
|
||||
|
||||
optionsJSON, _ := json.Marshal(androidFilePickerOptions{
|
||||
Multiple: d.dialog.allowsMultipleSelection,
|
||||
})
|
||||
|
||||
androidBridgeVoidIntString("showFilePicker", int(id), string(optionsJSON))
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// androidFilePickerResult is invoked from JNI once per selected file.
|
||||
func androidFilePickerResult(callbackID uint, path string) {
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
androidFileDialogsLock.Lock()
|
||||
channel, ok := androidFileResponses[callbackID]
|
||||
androidFileDialogsLock.Unlock()
|
||||
if ok {
|
||||
channel <- path
|
||||
}
|
||||
}
|
||||
|
||||
// androidFilePickerDone is invoked from JNI when the picker finishes
|
||||
// (after all results, or immediately on cancellation).
|
||||
func androidFilePickerDone(callbackID uint) {
|
||||
androidFileDialogsLock.Lock()
|
||||
channel, ok := androidFileResponses[callbackID]
|
||||
delete(androidFileResponses, callbackID)
|
||||
androidFileDialogsLock.Unlock()
|
||||
if ok {
|
||||
close(channel)
|
||||
}
|
||||
}
|
||||
|
||||
// Save dialogs
|
||||
|
||||
type androidSaveFileDialog struct{}
|
||||
|
||||
func newSaveFileDialogImpl(_ *SaveFileDialogStruct) saveFileDialogImpl {
|
||||
return &androidSaveFileDialog{}
|
||||
}
|
||||
|
||||
func (d *androidSaveFileDialog) show() (chan string, error) {
|
||||
return nil, fmt.Errorf("save file dialogs are not supported on Android: write the file inside the app sandbox (e.g. the app's files directory) instead")
|
||||
}
|
||||
607
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_darwin.go
generated
vendored
Normal file
607
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,607 @@
|
||||
//go:build darwin && !ios && !server
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -mmacosx-version-min=10.13 -x objective-c
|
||||
#cgo LDFLAGS: -framework Cocoa -mmacosx-version-min=10.13 -framework UniformTypeIdentifiers
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import <UniformTypeIdentifiers/UTType.h>
|
||||
#import "dialogs_darwin_delegate.h"
|
||||
|
||||
extern void openFileDialogCallback(uint id, char* path);
|
||||
extern void openFileDialogCallbackEnd(uint id);
|
||||
extern void saveFileDialogCallback(uint id, char* path);
|
||||
extern void dialogCallback(int id, int buttonPressed);
|
||||
|
||||
static void showAboutBox(char* title, char *message, void *icon, int length) {
|
||||
|
||||
// run on main thread
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
if (title != NULL) {
|
||||
[alert setMessageText:[NSString stringWithUTF8String:title]];
|
||||
free(title);
|
||||
}
|
||||
if (message != NULL) {
|
||||
[alert setInformativeText:[NSString stringWithUTF8String:message]];
|
||||
free(message);
|
||||
}
|
||||
if (icon != NULL) {
|
||||
NSImage *image = [[NSImage alloc] initWithData:[NSData dataWithBytes:icon length:length]];
|
||||
[alert setIcon:image];
|
||||
// The alert retains its icon
|
||||
[image release];
|
||||
}
|
||||
[alert setAlertStyle:NSAlertStyleInformational];
|
||||
[alert runModal];
|
||||
[alert release];
|
||||
}
|
||||
|
||||
|
||||
// Create an NSAlert
|
||||
static void* createAlert(int alertType, char* title, char *message, void *icon, int length) {
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
[alert setAlertStyle:alertType];
|
||||
if (title != NULL) {
|
||||
[alert setMessageText:[NSString stringWithUTF8String:title]];
|
||||
free(title);
|
||||
}
|
||||
if (message != NULL) {
|
||||
[alert setInformativeText:[NSString stringWithUTF8String:message]];
|
||||
free(message);
|
||||
}
|
||||
if (icon != NULL) {
|
||||
NSImage *image = [[NSImage alloc] initWithData:[NSData dataWithBytes:icon length:length]];
|
||||
[alert setIcon:image];
|
||||
// The alert retains its icon
|
||||
[image release];
|
||||
} else {
|
||||
if(alertType == NSAlertStyleCritical || alertType == NSAlertStyleWarning) {
|
||||
NSImage *image = [NSImage imageNamed:NSImageNameCaution];
|
||||
[alert setIcon:image];
|
||||
} else {
|
||||
NSImage *image = [NSImage imageNamed:NSImageNameInfo];
|
||||
[alert setIcon:image];
|
||||
}
|
||||
}
|
||||
return alert;
|
||||
|
||||
}
|
||||
|
||||
static int getButtonNumber(NSModalResponse response) {
|
||||
int buttonNumber = 0;
|
||||
if( response == NSAlertFirstButtonReturn ) {
|
||||
buttonNumber = 0;
|
||||
}
|
||||
else if( response == NSAlertSecondButtonReturn ) {
|
||||
buttonNumber = 1;
|
||||
}
|
||||
else if( response == NSAlertThirdButtonReturn ) {
|
||||
buttonNumber = 2;
|
||||
} else {
|
||||
buttonNumber = 3;
|
||||
}
|
||||
return buttonNumber;
|
||||
}
|
||||
|
||||
// Run the dialog
|
||||
static void dialogRunModal(void *dialog, void *parent, int callBackID) {
|
||||
NSAlert *alert = (__bridge NSAlert *)dialog;
|
||||
|
||||
// If the parent is NULL, we are running a modal dialog, otherwise attach the alert to the parent
|
||||
if( parent == NULL ) {
|
||||
NSModalResponse response = [alert runModal];
|
||||
int returnCode = getButtonNumber(response);
|
||||
dialogCallback(callBackID, returnCode);
|
||||
} else {
|
||||
NSWindow *window = (__bridge NSWindow *)parent;
|
||||
[alert beginSheetModalForWindow:window completionHandler:^(NSModalResponse response) {
|
||||
int returnCode = getButtonNumber(response);
|
||||
dialogCallback(callBackID, returnCode);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
// Release the dialog
|
||||
static void releaseDialog(void *dialog) {
|
||||
NSAlert *alert = (__bridge NSAlert *)dialog;
|
||||
[alert release];
|
||||
}
|
||||
|
||||
// Add a button to the dialog
|
||||
static void alertAddButton(void *dialog, char *label, bool isDefault, bool isCancel) {
|
||||
NSAlert *alert = (__bridge NSAlert *)dialog;
|
||||
NSButton *button = [alert addButtonWithTitle:[NSString stringWithUTF8String:label]];
|
||||
free(label);
|
||||
if( isDefault ) {
|
||||
[button setKeyEquivalent:@"\r"];
|
||||
} else if( isCancel ) {
|
||||
[button setKeyEquivalent:@"\033"];
|
||||
} else {
|
||||
[button setKeyEquivalent:@""];
|
||||
}
|
||||
}
|
||||
|
||||
static void processOpenFileDialogResults(NSOpenPanel *panel, NSInteger result, uint dialogID) {
|
||||
const char *path = NULL;
|
||||
if (result == NSModalResponseOK) {
|
||||
NSArray *urls = [panel URLs];
|
||||
if ([urls count] > 0) {
|
||||
NSArray *urls = [panel URLs];
|
||||
for (NSURL *url in urls) {
|
||||
path = [[url path] UTF8String];
|
||||
openFileDialogCallback(dialogID, (char *)path);
|
||||
}
|
||||
} else {
|
||||
NSURL *url = [panel URL];
|
||||
path = [[url path] UTF8String];
|
||||
openFileDialogCallback(dialogID, (char *)path);
|
||||
}
|
||||
}
|
||||
openFileDialogCallbackEnd(dialogID);
|
||||
}
|
||||
|
||||
|
||||
static void showOpenFileDialog(unsigned int dialogID,
|
||||
bool canChooseFiles,
|
||||
bool canChooseDirectories,
|
||||
bool canCreateDirectories,
|
||||
bool showHiddenFiles,
|
||||
bool allowsMultipleSelection,
|
||||
bool resolvesAliases,
|
||||
bool hideExtension,
|
||||
bool treatsFilePackagesAsDirectories,
|
||||
bool allowsOtherFileTypes,
|
||||
char *filterPatterns,
|
||||
unsigned int filterPatternsCount,
|
||||
char* message,
|
||||
char* directory,
|
||||
char* buttonText,
|
||||
|
||||
void *window) {
|
||||
|
||||
// run on main thread
|
||||
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
||||
|
||||
// print out filterPatterns if length > 0
|
||||
if (filterPatternsCount > 0) {
|
||||
OpenPanelDelegate *delegate = [[OpenPanelDelegate alloc] init];
|
||||
[panel setDelegate:delegate];
|
||||
// Initialise NSString with bytes and UTF8 encoding
|
||||
NSString *filterPatternsString = [[NSString alloc] initWithBytes:filterPatterns length:filterPatternsCount encoding:NSUTF8StringEncoding];
|
||||
// Convert NSString to NSArray
|
||||
delegate.allowedExtensions = [filterPatternsString componentsSeparatedByString:@";"];
|
||||
// componentsSeparatedByString: returned a new (retained-by-property) array
|
||||
[filterPatternsString release];
|
||||
|
||||
// Use UTType if macOS 11 or higher to add file filters
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
|
||||
if (@available(macOS 11, *)) {
|
||||
NSMutableArray *filterTypes = [NSMutableArray array];
|
||||
// Iterate the filtertypes, create uti's that are limited to the file extensions then add
|
||||
for (NSString *filterType in delegate.allowedExtensions) {
|
||||
[filterTypes addObject:[UTType typeWithFilenameExtension:filterType]];
|
||||
}
|
||||
[panel setAllowedContentTypes:filterTypes];
|
||||
}
|
||||
#else
|
||||
[panel setAllowedFileTypes:delegate.allowedExtensions];
|
||||
#endif
|
||||
|
||||
// Free the memory
|
||||
free(filterPatterns);
|
||||
}
|
||||
|
||||
|
||||
if (message != NULL) {
|
||||
[panel setMessage:[NSString stringWithUTF8String:message]];
|
||||
free(message);
|
||||
}
|
||||
|
||||
if (directory != NULL) {
|
||||
[panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:directory]]];
|
||||
free(directory);
|
||||
}
|
||||
|
||||
if (buttonText != NULL) {
|
||||
[panel setPrompt:[NSString stringWithUTF8String:buttonText]];
|
||||
free(buttonText);
|
||||
}
|
||||
|
||||
[panel setCanChooseFiles:canChooseFiles];
|
||||
[panel setCanChooseDirectories:canChooseDirectories];
|
||||
[panel setCanCreateDirectories:canCreateDirectories];
|
||||
[panel setShowsHiddenFiles:showHiddenFiles];
|
||||
[panel setAllowsMultipleSelection:allowsMultipleSelection];
|
||||
[panel setResolvesAliases:resolvesAliases];
|
||||
[panel setExtensionHidden:hideExtension];
|
||||
[panel setTreatsFilePackagesAsDirectories:treatsFilePackagesAsDirectories];
|
||||
[panel setAllowsOtherFileTypes:allowsOtherFileTypes];
|
||||
|
||||
|
||||
|
||||
if (window != NULL) {
|
||||
[panel beginSheetModalForWindow:(__bridge NSWindow *)window completionHandler:^(NSInteger result) {
|
||||
processOpenFileDialogResults(panel, result, dialogID);
|
||||
// Release the OpenPanelDelegate created above (the panel's
|
||||
// delegate property does not own it)
|
||||
id delegate = panel.delegate;
|
||||
if (delegate != nil) {
|
||||
[panel setDelegate:nil];
|
||||
[delegate release];
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
[panel beginWithCompletionHandler:^(NSInteger result) {
|
||||
processOpenFileDialogResults(panel, result, dialogID);
|
||||
id delegate = panel.delegate;
|
||||
if (delegate != nil) {
|
||||
[panel setDelegate:nil];
|
||||
[delegate release];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
static void showSaveFileDialog(unsigned int dialogID,
|
||||
bool canCreateDirectories,
|
||||
bool showHiddenFiles,
|
||||
bool canSelectHiddenExtension,
|
||||
bool hideExtension,
|
||||
bool treatsFilePackagesAsDirectories,
|
||||
bool allowOtherFileTypes,
|
||||
char* message,
|
||||
char* directory,
|
||||
char* buttonText,
|
||||
char* filename,
|
||||
void *window) {
|
||||
|
||||
NSSavePanel *panel = [NSSavePanel savePanel];
|
||||
|
||||
if (message != NULL) {
|
||||
[panel setMessage:[NSString stringWithUTF8String:message]];
|
||||
free(message);
|
||||
}
|
||||
|
||||
if (directory != NULL) {
|
||||
[panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:directory]]];
|
||||
free(directory);
|
||||
}
|
||||
|
||||
if (filename != NULL) {
|
||||
[panel setNameFieldStringValue:[NSString stringWithUTF8String:filename]];
|
||||
free(filename);
|
||||
}
|
||||
|
||||
if (buttonText != NULL) {
|
||||
[panel setPrompt:[NSString stringWithUTF8String:buttonText]];
|
||||
free(buttonText);
|
||||
}
|
||||
|
||||
[panel setCanCreateDirectories:canCreateDirectories];
|
||||
[panel setShowsHiddenFiles:showHiddenFiles];
|
||||
[panel setCanSelectHiddenExtension:canSelectHiddenExtension];
|
||||
[panel setExtensionHidden:hideExtension];
|
||||
[panel setTreatsFilePackagesAsDirectories:treatsFilePackagesAsDirectories];
|
||||
[panel setAllowsOtherFileTypes:allowOtherFileTypes];
|
||||
|
||||
if (window != NULL) {
|
||||
[panel beginSheetModalForWindow:(__bridge NSWindow *)window completionHandler:^(NSInteger result) {
|
||||
const char *path = NULL;
|
||||
if (result == NSModalResponseOK) {
|
||||
NSURL *url = [panel URL];
|
||||
path = [[url path] UTF8String];
|
||||
}
|
||||
saveFileDialogCallback(dialogID, (char *)path);
|
||||
}];
|
||||
} else {
|
||||
[panel beginWithCompletionHandler:^(NSInteger result) {
|
||||
const char *path = NULL;
|
||||
if (result == NSModalResponseOK) {
|
||||
NSURL *url = [panel URL];
|
||||
path = [[url path] UTF8String];
|
||||
}
|
||||
saveFileDialogCallback(dialogID, (char *)path);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const NSAlertStyleWarning = C.int(0)
|
||||
const NSAlertStyleInformational = C.int(1)
|
||||
const NSAlertStyleCritical = C.int(2)
|
||||
|
||||
var alertTypeMap = map[DialogType]C.int{
|
||||
WarningDialogType: NSAlertStyleWarning,
|
||||
InfoDialogType: NSAlertStyleInformational,
|
||||
ErrorDialogType: NSAlertStyleCritical,
|
||||
QuestionDialogType: NSAlertStyleInformational,
|
||||
}
|
||||
|
||||
type dialogResultCallback func(int)
|
||||
|
||||
var (
|
||||
callbacks = make(map[int]dialogResultCallback)
|
||||
mutex = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func addDialogCallback(callback dialogResultCallback) int {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
// Find the first free integer key
|
||||
var id int
|
||||
for {
|
||||
if _, exists := callbacks[id]; !exists {
|
||||
break
|
||||
}
|
||||
id++
|
||||
}
|
||||
|
||||
// Save the function in the map using the integer key
|
||||
callbacks[id] = callback
|
||||
|
||||
// Return the key
|
||||
return id
|
||||
}
|
||||
|
||||
func removeDialogCallback(id int) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
delete(callbacks, id)
|
||||
}
|
||||
|
||||
//export dialogCallback
|
||||
func dialogCallback(id C.int, buttonPressed C.int) {
|
||||
mutex.Lock()
|
||||
callback, exists := callbacks[int(id)]
|
||||
mutex.Unlock()
|
||||
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Call the function with the button number
|
||||
callback(int(buttonPressed)) // Replace nil with the actual slice of buttons
|
||||
}
|
||||
|
||||
func (m *macosApp) showAboutDialog(title string, message string, icon []byte) {
|
||||
var iconData unsafe.Pointer
|
||||
if icon != nil {
|
||||
iconData = unsafe.Pointer(&icon[0])
|
||||
}
|
||||
InvokeAsync(func() {
|
||||
C.showAboutBox(C.CString(title), C.CString(message), iconData, C.int(len(icon)))
|
||||
})
|
||||
}
|
||||
|
||||
type macosDialog struct {
|
||||
dialog *MessageDialog
|
||||
|
||||
nsDialog unsafe.Pointer
|
||||
}
|
||||
|
||||
func (m *macosDialog) show() {
|
||||
InvokeAsync(func() {
|
||||
|
||||
// Mac can only have 4 Buttons on a dialog
|
||||
if len(m.dialog.Buttons) > 4 {
|
||||
m.dialog.Buttons = m.dialog.Buttons[:4]
|
||||
}
|
||||
|
||||
if m.nsDialog != nil {
|
||||
C.releaseDialog(m.nsDialog)
|
||||
}
|
||||
var title *C.char
|
||||
if m.dialog.Title != "" {
|
||||
title = C.CString(m.dialog.Title)
|
||||
}
|
||||
var message *C.char
|
||||
if m.dialog.Message != "" {
|
||||
message = C.CString(m.dialog.Message)
|
||||
}
|
||||
var iconData unsafe.Pointer
|
||||
var iconLength C.int
|
||||
if len(m.dialog.Icon) > 0 {
|
||||
iconData = unsafe.Pointer(&m.dialog.Icon[0])
|
||||
iconLength = C.int(len(m.dialog.Icon))
|
||||
} else {
|
||||
// if it's an error, use the application Icon
|
||||
if m.dialog.DialogType == ErrorDialogType {
|
||||
if len(globalApplication.options.Icon) > 0 {
|
||||
iconData = unsafe.Pointer(&globalApplication.options.Icon[0])
|
||||
iconLength = C.int(len(globalApplication.options.Icon))
|
||||
}
|
||||
}
|
||||
}
|
||||
var parent unsafe.Pointer
|
||||
if m.dialog.window != nil {
|
||||
// get NSWindow from window
|
||||
parent = m.dialog.window.NativeWindow()
|
||||
}
|
||||
|
||||
alertType, ok := alertTypeMap[m.dialog.DialogType]
|
||||
if !ok {
|
||||
alertType = C.NSAlertStyleInformational
|
||||
}
|
||||
|
||||
m.nsDialog = C.createAlert(alertType, title, message, iconData, iconLength)
|
||||
|
||||
// Reverse the Buttons so that the default is on the right
|
||||
reversedButtons := make([]*Button, len(m.dialog.Buttons))
|
||||
var count = 0
|
||||
for i := len(m.dialog.Buttons) - 1; i >= 0; i-- {
|
||||
button := m.dialog.Buttons[i]
|
||||
C.alertAddButton(m.nsDialog, C.CString(button.Label), C.bool(button.IsDefault), C.bool(button.IsCancel))
|
||||
reversedButtons[count] = m.dialog.Buttons[i]
|
||||
count++
|
||||
}
|
||||
|
||||
var callBackID int
|
||||
callBackID = addDialogCallback(func(buttonPressed int) {
|
||||
if len(m.dialog.Buttons) > buttonPressed {
|
||||
button := reversedButtons[buttonPressed]
|
||||
if button.Callback != nil {
|
||||
button.Callback()
|
||||
}
|
||||
}
|
||||
removeDialogCallback(callBackID)
|
||||
})
|
||||
|
||||
C.dialogRunModal(m.nsDialog, parent, C.int(callBackID))
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func newDialogImpl(d *MessageDialog) *macosDialog {
|
||||
return &macosDialog{
|
||||
dialog: d,
|
||||
}
|
||||
}
|
||||
|
||||
type macosOpenFileDialog struct {
|
||||
dialog *OpenFileDialogStruct
|
||||
}
|
||||
|
||||
func newOpenFileDialogImpl(d *OpenFileDialogStruct) *macosOpenFileDialog {
|
||||
return &macosOpenFileDialog{
|
||||
dialog: d,
|
||||
}
|
||||
}
|
||||
|
||||
func toCString(s string) *C.char {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return C.CString(s)
|
||||
}
|
||||
|
||||
func (m *macosOpenFileDialog) show() (chan string, error) {
|
||||
openFileResponses[m.dialog.id] = make(chan string)
|
||||
nsWindow := unsafe.Pointer(nil)
|
||||
if m.dialog.window != nil {
|
||||
// get NSWindow from window
|
||||
nsWindow = m.dialog.window.NativeWindow()
|
||||
}
|
||||
|
||||
// Massage filter patterns into macOS format
|
||||
// We iterate all filter patterns, tidy them up and then join them with a semicolon
|
||||
// This should produce a single string of extensions like "png;jpg;gif"
|
||||
var filterPatterns string
|
||||
if len(m.dialog.filters) > 0 {
|
||||
var allPatterns []string
|
||||
for _, filter := range m.dialog.filters {
|
||||
patternComponents := strings.Split(filter.Pattern, ";")
|
||||
for i, component := range patternComponents {
|
||||
filterPattern := strings.TrimSpace(component)
|
||||
filterPattern = strings.TrimPrefix(filterPattern, "*.")
|
||||
patternComponents[i] = filterPattern
|
||||
}
|
||||
allPatterns = append(allPatterns, strings.Join(patternComponents, ";"))
|
||||
}
|
||||
filterPatterns = strings.Join(allPatterns, ";")
|
||||
}
|
||||
C.showOpenFileDialog(C.uint(m.dialog.id),
|
||||
C.bool(m.dialog.canChooseFiles),
|
||||
C.bool(m.dialog.canChooseDirectories),
|
||||
C.bool(m.dialog.canCreateDirectories),
|
||||
C.bool(m.dialog.showHiddenFiles),
|
||||
C.bool(m.dialog.allowsMultipleSelection),
|
||||
C.bool(m.dialog.resolvesAliases),
|
||||
C.bool(m.dialog.hideExtension),
|
||||
C.bool(m.dialog.treatsFilePackagesAsDirectories),
|
||||
C.bool(m.dialog.allowsOtherFileTypes),
|
||||
toCString(filterPatterns),
|
||||
C.uint(len(filterPatterns)),
|
||||
toCString(m.dialog.message),
|
||||
toCString(m.dialog.directory),
|
||||
toCString(m.dialog.buttonText),
|
||||
nsWindow)
|
||||
|
||||
return openFileResponses[m.dialog.id], nil
|
||||
}
|
||||
|
||||
//export openFileDialogCallback
|
||||
func openFileDialogCallback(cid C.uint, cpath *C.char) {
|
||||
path := C.GoString(cpath)
|
||||
id := uint(cid)
|
||||
channel, ok := openFileResponses[id]
|
||||
if ok {
|
||||
channel <- path
|
||||
} else {
|
||||
panic("No channel found for open file dialog")
|
||||
}
|
||||
}
|
||||
|
||||
//export openFileDialogCallbackEnd
|
||||
func openFileDialogCallbackEnd(cid C.uint) {
|
||||
id := uint(cid)
|
||||
channel, ok := openFileResponses[id]
|
||||
if ok {
|
||||
close(channel)
|
||||
delete(openFileResponses, id)
|
||||
freeDialogID(id)
|
||||
} else {
|
||||
panic("No channel found for open file dialog")
|
||||
}
|
||||
}
|
||||
|
||||
type macosSaveFileDialog struct {
|
||||
dialog *SaveFileDialogStruct
|
||||
}
|
||||
|
||||
func newSaveFileDialogImpl(d *SaveFileDialogStruct) *macosSaveFileDialog {
|
||||
return &macosSaveFileDialog{
|
||||
dialog: d,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *macosSaveFileDialog) show() (chan string, error) {
|
||||
saveFileResponses[m.dialog.id] = make(chan string)
|
||||
nsWindow := unsafe.Pointer(nil)
|
||||
if m.dialog.window != nil {
|
||||
// get NSWindow from window
|
||||
nsWindow = m.dialog.window.NativeWindow()
|
||||
}
|
||||
C.showSaveFileDialog(C.uint(m.dialog.id),
|
||||
C.bool(m.dialog.canCreateDirectories),
|
||||
C.bool(m.dialog.showHiddenFiles),
|
||||
C.bool(m.dialog.canSelectHiddenExtension),
|
||||
C.bool(m.dialog.hideExtension),
|
||||
C.bool(m.dialog.treatsFilePackagesAsDirectories),
|
||||
C.bool(m.dialog.allowOtherFileTypes),
|
||||
toCString(m.dialog.message),
|
||||
toCString(m.dialog.directory),
|
||||
toCString(m.dialog.buttonText),
|
||||
toCString(m.dialog.filename),
|
||||
nsWindow)
|
||||
return saveFileResponses[m.dialog.id], nil
|
||||
}
|
||||
|
||||
//export saveFileDialogCallback
|
||||
func saveFileDialogCallback(cid C.uint, cpath *C.char) {
|
||||
// Covert the path to a string
|
||||
path := C.GoString(cpath)
|
||||
id := uint(cid)
|
||||
// put response on channel
|
||||
channel, ok := saveFileResponses[id]
|
||||
if ok {
|
||||
channel <- path
|
||||
close(channel)
|
||||
delete(saveFileResponses, id)
|
||||
freeDialogID(id)
|
||||
|
||||
} else {
|
||||
panic("No channel found for save file dialog")
|
||||
}
|
||||
}
|
||||
18
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_darwin_delegate.h
generated
vendored
Normal file
18
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_darwin_delegate.h
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
#ifndef _DIALOGS_DELEGATE_H_
|
||||
#define _DIALOGS_DELEGATE_H_
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
// Conditionally import UniformTypeIdentifiers based on OS version
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
|
||||
#import <UniformTypeIdentifiers/UTType.h>
|
||||
#endif
|
||||
|
||||
// OpenPanel delegate to handle file filtering
|
||||
@interface OpenPanelDelegate : NSObject <NSOpenSavePanelDelegate>
|
||||
@property (nonatomic, strong) NSArray *allowedExtensions;
|
||||
@end
|
||||
|
||||
#endif
|
||||
38
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_darwin_delegate.m
generated
vendored
Normal file
38
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_darwin_delegate.m
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
//go:build darwin && !ios && !server
|
||||
|
||||
#import "dialogs_darwin_delegate.h"
|
||||
|
||||
// Override shouldEnableURL
|
||||
@implementation OpenPanelDelegate
|
||||
- (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url {
|
||||
if (url == nil) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
BOOL isDirectory = NO;
|
||||
if ([fileManager fileExistsAtPath:url.path isDirectory:&isDirectory] && isDirectory) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// If no extensions specified, allow all files
|
||||
if (self.allowedExtensions == nil || [self.allowedExtensions count] == 0) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
NSString *extension = [url.pathExtension lowercaseString];
|
||||
if (extension == nil || [extension isEqualToString:@""]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Check if the extension is in our allowed list (case insensitive)
|
||||
for (NSString *allowedExt in self.allowedExtensions) {
|
||||
if ([[allowedExt lowercaseString] isEqualToString:extension]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
162
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_ios.go
generated
vendored
Normal file
162
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_ios.go
generated
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
//go:build ios
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include "application_ios.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Message dialogs are backed by UIAlertController, file dialogs by
|
||||
// UIDocumentPickerViewController. Save dialogs have no iOS counterpart
|
||||
// (apps write into their sandbox and share via the share sheet), so they
|
||||
// return an explicit error instead of failing silently.
|
||||
|
||||
// Pending message dialogs keyed by callback ID
|
||||
var (
|
||||
iosDialogsLock sync.Mutex
|
||||
iosPendingDialogs = make(map[uint]*MessageDialog)
|
||||
iosNextDialogID uint = 1
|
||||
)
|
||||
|
||||
type iosDialogButton struct {
|
||||
Label string `json:"label"`
|
||||
IsCancel bool `json:"isCancel"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
}
|
||||
|
||||
type iosDialog struct {
|
||||
dialog *MessageDialog
|
||||
}
|
||||
|
||||
func newDialogImpl(d *MessageDialog) *iosDialog {
|
||||
return &iosDialog{dialog: d}
|
||||
}
|
||||
|
||||
func (d *iosDialog) show() {
|
||||
iosDialogsLock.Lock()
|
||||
id := iosNextDialogID
|
||||
iosNextDialogID++
|
||||
iosPendingDialogs[id] = d.dialog
|
||||
iosDialogsLock.Unlock()
|
||||
|
||||
buttons := make([]iosDialogButton, 0, len(d.dialog.Buttons))
|
||||
for _, b := range d.dialog.Buttons {
|
||||
buttons = append(buttons, iosDialogButton{
|
||||
Label: b.Label,
|
||||
IsCancel: b.IsCancel,
|
||||
IsDefault: b.IsDefault,
|
||||
})
|
||||
}
|
||||
buttonsJSON, _ := json.Marshal(buttons)
|
||||
|
||||
title := d.dialog.Title
|
||||
if title == "" {
|
||||
title = defaultTitles[d.dialog.DialogType]
|
||||
}
|
||||
|
||||
ctitle := C.CString(title)
|
||||
cmessage := C.CString(d.dialog.Message)
|
||||
cbuttons := C.CString(string(buttonsJSON))
|
||||
defer C.free(unsafe.Pointer(ctitle))
|
||||
defer C.free(unsafe.Pointer(cmessage))
|
||||
defer C.free(unsafe.Pointer(cbuttons))
|
||||
C.ios_show_message_dialog(ctitle, cmessage, cbuttons, C.uint(id))
|
||||
}
|
||||
|
||||
//export iosDialogCallback
|
||||
func iosDialogCallback(callbackID C.uint, buttonIndex C.int) {
|
||||
iosDialogsLock.Lock()
|
||||
dialog, ok := iosPendingDialogs[uint(callbackID)]
|
||||
delete(iosPendingDialogs, uint(callbackID))
|
||||
iosDialogsLock.Unlock()
|
||||
if !ok || dialog == nil {
|
||||
return
|
||||
}
|
||||
idx := int(buttonIndex)
|
||||
if idx < 0 || idx >= len(dialog.Buttons) {
|
||||
return
|
||||
}
|
||||
button := dialog.Buttons[idx]
|
||||
if button.Callback != nil {
|
||||
// Run the callback off the main thread, mirroring desktop behaviour
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
button.Callback()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// File dialogs
|
||||
|
||||
// Pending file picker channels keyed by dialog ID
|
||||
var (
|
||||
iosFileDialogsLock sync.Mutex
|
||||
iosFileResponses = make(map[uint]chan string)
|
||||
)
|
||||
|
||||
type iosOpenFileDialog struct {
|
||||
dialog *OpenFileDialogStruct
|
||||
}
|
||||
|
||||
func newOpenFileDialogImpl(d *OpenFileDialogStruct) openFileDialogImpl {
|
||||
return &iosOpenFileDialog{dialog: d}
|
||||
}
|
||||
|
||||
func (d *iosOpenFileDialog) show() (chan string, error) {
|
||||
results := make(chan string, 16)
|
||||
|
||||
iosFileDialogsLock.Lock()
|
||||
id := d.dialog.id
|
||||
iosFileResponses[id] = results
|
||||
iosFileDialogsLock.Unlock()
|
||||
|
||||
directories := d.dialog.canChooseDirectories && !d.dialog.canChooseFiles
|
||||
C.ios_show_document_picker(C.uint(id), C.bool(directories), C.bool(d.dialog.allowsMultipleSelection))
|
||||
return results, nil
|
||||
}
|
||||
|
||||
//export iosOpenFileCallback
|
||||
func iosOpenFileCallback(callbackID C.uint, cpath *C.char) {
|
||||
if cpath == nil {
|
||||
return
|
||||
}
|
||||
path := C.GoString(cpath)
|
||||
iosFileDialogsLock.Lock()
|
||||
channel, ok := iosFileResponses[uint(callbackID)]
|
||||
iosFileDialogsLock.Unlock()
|
||||
if ok {
|
||||
channel <- path
|
||||
}
|
||||
}
|
||||
|
||||
//export iosOpenFileCallbackEnd
|
||||
func iosOpenFileCallbackEnd(callbackID C.uint) {
|
||||
iosFileDialogsLock.Lock()
|
||||
channel, ok := iosFileResponses[uint(callbackID)]
|
||||
delete(iosFileResponses, uint(callbackID))
|
||||
iosFileDialogsLock.Unlock()
|
||||
if ok {
|
||||
close(channel)
|
||||
}
|
||||
}
|
||||
|
||||
// Save dialogs
|
||||
|
||||
type iosSaveFileDialog struct{}
|
||||
|
||||
func newSaveFileDialogImpl(_ *SaveFileDialogStruct) saveFileDialogImpl {
|
||||
return &iosSaveFileDialog{}
|
||||
}
|
||||
|
||||
func (d *iosSaveFileDialog) show() (chan string, error) {
|
||||
return nil, fmt.Errorf("save file dialogs are not supported on iOS: write the file inside the app sandbox (e.g. the Documents directory) instead")
|
||||
}
|
||||
87
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_linux.go
generated
vendored
Normal file
87
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_linux.go
generated
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
//go:build linux && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
func (a *linuxApp) showAboutDialog(title string, message string, icon []byte) {
|
||||
window, _ := globalApplication.Window.GetByID(a.getCurrentWindowID())
|
||||
var parent uintptr
|
||||
if window != nil {
|
||||
nativeWindow := window.NativeWindow()
|
||||
if nativeWindow != nil {
|
||||
parent = uintptr(nativeWindow)
|
||||
}
|
||||
}
|
||||
about := newMessageDialog(InfoDialogType)
|
||||
about.SetTitle(title).
|
||||
SetMessage(message).
|
||||
SetIcon(icon)
|
||||
gtkDispatch(func() {
|
||||
runQuestionDialog(
|
||||
pointer(parent),
|
||||
about,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
type linuxDialog struct {
|
||||
dialog *MessageDialog
|
||||
}
|
||||
|
||||
func (m *linuxDialog) show() {
|
||||
windowId := getNativeApplication().getCurrentWindowID()
|
||||
window, _ := globalApplication.Window.GetByID(windowId)
|
||||
var parent uintptr
|
||||
if window != nil {
|
||||
nativeWindow := window.NativeWindow()
|
||||
if nativeWindow != nil {
|
||||
parent = uintptr(nativeWindow)
|
||||
}
|
||||
}
|
||||
|
||||
gtkDispatch(func() {
|
||||
response := runQuestionDialog(pointer(parent), m.dialog)
|
||||
if response >= 0 && response < len(m.dialog.Buttons) {
|
||||
button := m.dialog.Buttons[response]
|
||||
if button.Callback != nil {
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
button.Callback()
|
||||
}()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func newDialogImpl(d *MessageDialog) *linuxDialog {
|
||||
return &linuxDialog{
|
||||
dialog: d,
|
||||
}
|
||||
}
|
||||
|
||||
type linuxOpenFileDialog struct {
|
||||
dialog *OpenFileDialogStruct
|
||||
}
|
||||
|
||||
func newOpenFileDialogImpl(d *OpenFileDialogStruct) *linuxOpenFileDialog {
|
||||
return &linuxOpenFileDialog{
|
||||
dialog: d,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *linuxOpenFileDialog) show() (chan string, error) {
|
||||
return runOpenFileDialog(m.dialog)
|
||||
}
|
||||
|
||||
type linuxSaveFileDialog struct {
|
||||
dialog *SaveFileDialogStruct
|
||||
}
|
||||
|
||||
func newSaveFileDialogImpl(d *SaveFileDialogStruct) *linuxSaveFileDialog {
|
||||
return &linuxSaveFileDialog{
|
||||
dialog: d,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *linuxSaveFileDialog) show() (chan string, error) {
|
||||
return runSaveFileDialog(m.dialog)
|
||||
}
|
||||
296
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_windows.go
generated
vendored
Normal file
296
vendor/github.com/wailsapp/wails/v3/pkg/application/dialogs_windows.go
generated
vendored
Normal file
@@ -0,0 +1,296 @@
|
||||
//go:build windows && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/go-common-file-dialog/cfd"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func (m *windowsApp) showAboutDialog(title string, message string, _ []byte) {
|
||||
about := newDialogImpl(&MessageDialog{
|
||||
MessageDialogOptions: MessageDialogOptions{
|
||||
DialogType: InfoDialogType,
|
||||
Title: title,
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
about.UseAppIcon = true
|
||||
about.show()
|
||||
}
|
||||
|
||||
type windowsDialog struct {
|
||||
dialog *MessageDialog
|
||||
|
||||
//dialogImpl unsafe.Pointer
|
||||
UseAppIcon bool
|
||||
}
|
||||
|
||||
func (m *windowsDialog) show() {
|
||||
|
||||
title := w32.MustStringToUTF16Ptr(m.dialog.Title)
|
||||
message := w32.MustStringToUTF16Ptr(m.dialog.Message)
|
||||
flags := calculateMessageDialogFlags(m.dialog.MessageDialogOptions)
|
||||
var button int32
|
||||
var err error
|
||||
|
||||
var parentWindow uintptr
|
||||
if m.dialog.window != nil {
|
||||
nativeWindow := m.dialog.window.NativeWindow()
|
||||
if nativeWindow != nil {
|
||||
parentWindow = uintptr(nativeWindow)
|
||||
}
|
||||
}
|
||||
|
||||
if m.UseAppIcon || m.dialog.Icon != nil {
|
||||
// Use the application's embedded icon resource (ID 3). MessageBoxIndirect
|
||||
// cannot render arbitrary icon bytes, so a custom Icon also maps to the
|
||||
// app icon — the closest supported behaviour (full custom-icon support
|
||||
// needs a TaskDialog implementation).
|
||||
//
|
||||
button, err = w32.MessageBoxWithIcon(parentWindow, message, title, 3, messageDialogUserIconFlags(flags))
|
||||
if err != nil {
|
||||
// Dev binaries (`go run`, `wails3 dev`) have no embedded icon
|
||||
// resource, which makes MessageBoxIndirect fail outright — the
|
||||
// dialog never appeared and the app aborted via fatal error
|
||||
// (#4233). Fall back to a standard dialog instead.
|
||||
button, err = windows.MessageBox(windows.HWND(parentWindow), message, title, flags|windows.MB_SYSTEMMODAL)
|
||||
}
|
||||
if err != nil {
|
||||
globalApplication.handleFatalError(err)
|
||||
}
|
||||
} else {
|
||||
button, err = windows.MessageBox(windows.HWND(parentWindow), message, title, flags|windows.MB_SYSTEMMODAL)
|
||||
if err != nil {
|
||||
globalApplication.handleFatalError(err)
|
||||
}
|
||||
}
|
||||
// This maps MessageBox return values to strings
|
||||
responses := []string{"", "Ok", "Cancel", "Abort", "Retry", "Ignore", "Yes", "No", "", "", "Try Again", "Continue"}
|
||||
result := "Error"
|
||||
if int(button) < len(responses) {
|
||||
result = responses[button]
|
||||
}
|
||||
// Check if there's a callback for the button pressed
|
||||
for _, buttonInDialog := range m.dialog.Buttons {
|
||||
if buttonInDialog.Label == result {
|
||||
if buttonInDialog.Callback != nil {
|
||||
buttonInDialog.Callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newDialogImpl(d *MessageDialog) *windowsDialog {
|
||||
return &windowsDialog{
|
||||
dialog: d,
|
||||
}
|
||||
}
|
||||
|
||||
type windowOpenFileDialog struct {
|
||||
dialog *OpenFileDialogStruct
|
||||
}
|
||||
|
||||
func newOpenFileDialogImpl(d *OpenFileDialogStruct) *windowOpenFileDialog {
|
||||
return &windowOpenFileDialog{
|
||||
dialog: d,
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultFolder(folder string) (string, error) {
|
||||
if folder == "" {
|
||||
return "", nil
|
||||
}
|
||||
return filepath.Abs(folder)
|
||||
}
|
||||
|
||||
func (m *windowOpenFileDialog) show() (chan string, error) {
|
||||
|
||||
defaultFolder, err := getDefaultFolder(m.dialog.directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := cfd.DialogConfig{
|
||||
Title: m.dialog.title,
|
||||
Role: "PickFolder",
|
||||
FileFilters: convertFilters(m.dialog.filters),
|
||||
Folder: defaultFolder,
|
||||
}
|
||||
|
||||
var result []string
|
||||
if m.dialog.allowsMultipleSelection && !m.dialog.canChooseDirectories {
|
||||
temp, err := showCfdDialog(
|
||||
func() (cfd.Dialog, error) {
|
||||
return cfd.NewOpenMultipleFilesDialog(config)
|
||||
}, true, m.dialog.window)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = temp.([]string)
|
||||
} else {
|
||||
if m.dialog.canChooseDirectories {
|
||||
temp, err := showCfdDialog(
|
||||
func() (cfd.Dialog, error) {
|
||||
return cfd.NewSelectFolderDialog(config)
|
||||
}, false, m.dialog.window)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = []string{temp.(string)}
|
||||
} else {
|
||||
temp, err := showCfdDialog(
|
||||
func() (cfd.Dialog, error) {
|
||||
return cfd.NewOpenFileDialog(config)
|
||||
}, false, m.dialog.window)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = []string{temp.(string)}
|
||||
}
|
||||
}
|
||||
|
||||
files := make(chan string)
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
for _, file := range result {
|
||||
files <- file
|
||||
}
|
||||
close(files)
|
||||
}()
|
||||
return files, nil
|
||||
}
|
||||
|
||||
type windowSaveFileDialog struct {
|
||||
dialog *SaveFileDialogStruct
|
||||
}
|
||||
|
||||
func newSaveFileDialogImpl(d *SaveFileDialogStruct) *windowSaveFileDialog {
|
||||
return &windowSaveFileDialog{
|
||||
dialog: d,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *windowSaveFileDialog) show() (chan string, error) {
|
||||
files := make(chan string)
|
||||
defaultFolder, err := getDefaultFolder(m.dialog.directory)
|
||||
if err != nil {
|
||||
close(files)
|
||||
return files, err
|
||||
}
|
||||
|
||||
config := cfd.DialogConfig{
|
||||
Title: m.dialog.title,
|
||||
Role: "SaveFile",
|
||||
FileFilters: convertFilters(m.dialog.filters),
|
||||
FileName: m.dialog.filename,
|
||||
Folder: defaultFolder,
|
||||
}
|
||||
|
||||
// Original PR for v2 by @almas1992: https://github.com/wailsapp/wails/pull/3205
|
||||
if len(m.dialog.filters) > 0 {
|
||||
config.DefaultExtension = strings.TrimPrefix(strings.Split(m.dialog.filters[0].Pattern, ";")[0], "*")
|
||||
}
|
||||
|
||||
result, err := showCfdDialog(
|
||||
func() (cfd.Dialog, error) {
|
||||
return cfd.NewSaveFileDialog(config)
|
||||
}, false, m.dialog.window)
|
||||
if err != nil {
|
||||
close(files)
|
||||
return files, err
|
||||
}
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
f, ok := result.(string)
|
||||
if ok {
|
||||
files <- f
|
||||
}
|
||||
close(files)
|
||||
}()
|
||||
return files, err
|
||||
}
|
||||
|
||||
// messageDialogUserIconFlags converts standard message dialog flags for use
|
||||
// with MB_USERICON. The user icon replaces the standard one, so the MB_ICON*
|
||||
// bits are stripped — but the button configuration is preserved: forcing
|
||||
// MB_OK here (the old behaviour) silently destroyed Yes/No buttons on
|
||||
// question dialogs shown with an icon (#4233).
|
||||
func messageDialogUserIconFlags(flags uint32) uint32 {
|
||||
const mbIconMask = w32.MB_ICONHAND | w32.MB_ICONQUESTION | w32.MB_ICONASTERISK | w32.MB_USERICON
|
||||
return (flags &^ uint32(mbIconMask)) | windows.MB_USERICON | windows.MB_SYSTEMMODAL
|
||||
}
|
||||
|
||||
func calculateMessageDialogFlags(options MessageDialogOptions) uint32 {
|
||||
var flags uint32
|
||||
|
||||
switch options.DialogType {
|
||||
case InfoDialogType:
|
||||
flags = windows.MB_OK | windows.MB_ICONINFORMATION
|
||||
case ErrorDialogType:
|
||||
flags = windows.MB_ICONERROR | windows.MB_OK
|
||||
case QuestionDialogType:
|
||||
flags = windows.MB_YESNO
|
||||
for _, button := range options.Buttons {
|
||||
if strings.TrimSpace(strings.ToLower(button.Label)) == "no" && button.IsDefault {
|
||||
flags |= windows.MB_DEFBUTTON2
|
||||
}
|
||||
}
|
||||
case WarningDialogType:
|
||||
flags = windows.MB_OK | windows.MB_ICONWARNING
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func convertFilters(filters []FileFilter) []cfd.FileFilter {
|
||||
var result []cfd.FileFilter
|
||||
for _, filter := range filters {
|
||||
result = append(result, cfd.FileFilter(filter))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func showCfdDialog(newDlg func() (cfd.Dialog, error), isMultiSelect bool, parentWindow Window) (any, error) {
|
||||
dlg, err := newDlg()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set parent window if provided
|
||||
if parentWindow != nil {
|
||||
nativeWindow := parentWindow.NativeWindow()
|
||||
if nativeWindow != nil {
|
||||
dlg.SetParentWindowHandle(uintptr(nativeWindow))
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := dlg.Release()
|
||||
if err != nil {
|
||||
globalApplication.error("unable to release dialog: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if multi, _ := dlg.(cfd.OpenMultipleFilesDialog); multi != nil && isMultiSelect {
|
||||
paths, err := multi.ShowAndGetResults()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, path := range paths {
|
||||
paths[i] = filepath.Clean(path)
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
path, err := dlg.ShowAndGetResult()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filepath.Clean(path), nil
|
||||
}
|
||||
18
vendor/github.com/wailsapp/wails/v3/pkg/application/environment.go
generated
vendored
Normal file
18
vendor/github.com/wailsapp/wails/v3/pkg/application/environment.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package application
|
||||
|
||||
import "github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
|
||||
// EnvironmentInfo represents information about the current environment.
|
||||
//
|
||||
// Fields:
|
||||
// - OS: the operating system that the program is running on.
|
||||
// - Arch: the architecture of the operating system.
|
||||
// - Debug: indicates whether debug mode is enabled.
|
||||
// - OSInfo: information about the operating system.
|
||||
type EnvironmentInfo struct {
|
||||
OS string `json:"OS"`
|
||||
Arch string `json:"Arch"`
|
||||
Debug bool `json:"Debug"`
|
||||
OSInfo *operatingsystem.OS `json:"OSInfo"`
|
||||
PlatformInfo map[string]any `json:"PlatformInfo"`
|
||||
}
|
||||
210
vendor/github.com/wailsapp/wails/v3/pkg/application/environment_linux.go
generated
vendored
Normal file
210
vendor/github.com/wailsapp/wails/v3/pkg/application/environment_linux.go
generated
vendored
Normal file
@@ -0,0 +1,210 @@
|
||||
//go:build linux && !android
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func detectCompositor() string {
|
||||
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||
return "hyprland"
|
||||
}
|
||||
if os.Getenv("SWAYSOCK") != "" {
|
||||
return "sway"
|
||||
}
|
||||
if os.Getenv("I3SOCK") != "" {
|
||||
return "i3"
|
||||
}
|
||||
if desktop := os.Getenv("XDG_CURRENT_DESKTOP"); desktop != "" {
|
||||
return strings.ToLower(desktop)
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func detectFocusFollowsMouse() bool {
|
||||
compositor := detectCompositor()
|
||||
switch compositor {
|
||||
case "hyprland", "sway", "i3":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isWayland() bool {
|
||||
return os.Getenv("XDG_SESSION_TYPE") == "wayland" ||
|
||||
os.Getenv("WAYLAND_DISPLAY") != ""
|
||||
}
|
||||
|
||||
func isTilingWM() bool {
|
||||
switch detectCompositor() {
|
||||
case "hyprland", "sway", "i3":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getCursorPositionFromCompositor() (x, y int, ok bool) {
|
||||
switch detectCompositor() {
|
||||
case "hyprland":
|
||||
out, err := hyprlandIPC("cursorpos")
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return parseCursorPos(strings.TrimSpace(out))
|
||||
case "sway":
|
||||
out, err := swayIPC("get_seats")
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return parseSwayCursor(out)
|
||||
}
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
func parseCursorPos(s string) (x, y int, ok bool) {
|
||||
parts := strings.Split(s, ", ")
|
||||
if len(parts) != 2 {
|
||||
return 0, 0, false
|
||||
}
|
||||
var err error
|
||||
x, err = strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
y, err = strconv.Atoi(strings.TrimSpace(parts[1]))
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return x, y, true
|
||||
}
|
||||
|
||||
func parseSwayCursor(json string) (x, y int, ok bool) {
|
||||
cursorIdx := strings.Index(json, `"cursor"`)
|
||||
if cursorIdx == -1 {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
xIdx := strings.Index(json[cursorIdx:], `"x"`)
|
||||
if xIdx == -1 {
|
||||
return 0, 0, false
|
||||
}
|
||||
xStart := cursorIdx + xIdx + 4
|
||||
xEnd := strings.IndexAny(json[xStart:], ",}")
|
||||
if xEnd == -1 {
|
||||
return 0, 0, false
|
||||
}
|
||||
x, _ = strconv.Atoi(strings.TrimSpace(json[xStart : xStart+xEnd]))
|
||||
|
||||
yIdx := strings.Index(json[cursorIdx:], `"y"`)
|
||||
if yIdx == -1 {
|
||||
return 0, 0, false
|
||||
}
|
||||
yStart := cursorIdx + yIdx + 4
|
||||
yEnd := strings.IndexAny(json[yStart:], ",}")
|
||||
if yEnd == -1 {
|
||||
return 0, 0, false
|
||||
}
|
||||
y, _ = strconv.Atoi(strings.TrimSpace(json[yStart : yStart+yEnd]))
|
||||
|
||||
return x, y, true
|
||||
}
|
||||
|
||||
func hyprlandIPC(command string) (string, error) {
|
||||
sig := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE")
|
||||
if sig == "" {
|
||||
return "", fmt.Errorf("HYPRLAND_INSTANCE_SIGNATURE not set")
|
||||
}
|
||||
|
||||
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if runtimeDir == "" {
|
||||
runtimeDir = fmt.Sprintf("/run/user/%d", os.Getuid())
|
||||
}
|
||||
|
||||
socketPath := filepath.Join(runtimeDir, "hypr", sig, ".socket.sock")
|
||||
conn, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte(command))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if n > 0 {
|
||||
result.Write(buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if n < len(buf) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func swayIPC(command string) (string, error) {
|
||||
socketPath := os.Getenv("SWAYSOCK")
|
||||
if socketPath == "" {
|
||||
return "", fmt.Errorf("SWAYSOCK not set")
|
||||
}
|
||||
|
||||
conn, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
msgType := uint32(0)
|
||||
if strings.HasPrefix(command, "get_") {
|
||||
switch command {
|
||||
case "get_seats":
|
||||
msgType = 5
|
||||
}
|
||||
} else {
|
||||
msgType = 0
|
||||
}
|
||||
|
||||
payload := []byte(command)
|
||||
header := make([]byte, 14)
|
||||
copy(header[0:6], "i3-ipc")
|
||||
header[6] = byte(len(payload))
|
||||
header[7] = byte(len(payload) >> 8)
|
||||
header[8] = byte(len(payload) >> 16)
|
||||
header[9] = byte(len(payload) >> 24)
|
||||
header[10] = byte(msgType)
|
||||
header[11] = byte(msgType >> 8)
|
||||
header[12] = byte(msgType >> 16)
|
||||
header[13] = byte(msgType >> 24)
|
||||
|
||||
conn.Write(header)
|
||||
conn.Write(payload)
|
||||
|
||||
respHeader := make([]byte, 14)
|
||||
_, err = conn.Read(respHeader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
respLen := uint32(respHeader[6]) | uint32(respHeader[7])<<8 | uint32(respHeader[8])<<16 | uint32(respHeader[9])<<24
|
||||
respBody := make([]byte, respLen)
|
||||
_, err = conn.Read(respBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(respBody), nil
|
||||
}
|
||||
67
vendor/github.com/wailsapp/wails/v3/pkg/application/environment_manager.go
generated
vendored
Normal file
67
vendor/github.com/wailsapp/wails/v3/pkg/application/environment_manager.go
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/fileexplorer"
|
||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
)
|
||||
|
||||
// EnvironmentManager manages environment-related operations
|
||||
type EnvironmentManager struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
// newEnvironmentManager creates a new EnvironmentManager instance
|
||||
func newEnvironmentManager(app *App) *EnvironmentManager {
|
||||
return &EnvironmentManager{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
// Info returns environment information
|
||||
func (em *EnvironmentManager) Info() EnvironmentInfo {
|
||||
info, _ := operatingsystem.Info()
|
||||
result := EnvironmentInfo{
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Debug: em.app.isDebugMode,
|
||||
OSInfo: info,
|
||||
}
|
||||
result.PlatformInfo = em.app.platformEnvironment()
|
||||
return result
|
||||
}
|
||||
|
||||
// IsDarkMode returns true if the system is in dark mode
|
||||
func (em *EnvironmentManager) IsDarkMode() bool {
|
||||
if em.app.impl == nil {
|
||||
return false
|
||||
}
|
||||
return em.app.impl.isDarkMode()
|
||||
}
|
||||
|
||||
// GetAccentColor returns the system accent color
|
||||
func (em *EnvironmentManager) GetAccentColor() string {
|
||||
if em.app.impl == nil {
|
||||
return "rgb(0,122,255)"
|
||||
}
|
||||
return em.app.impl.getAccentColor()
|
||||
}
|
||||
|
||||
// OpenFileManager opens the file manager at the specified path, optionally selecting the file
|
||||
func (em *EnvironmentManager) OpenFileManager(path string, selectFile bool) error {
|
||||
return InvokeSyncWithError(func() error {
|
||||
return fileexplorer.OpenFileManager(path, selectFile)
|
||||
})
|
||||
}
|
||||
|
||||
func (em *EnvironmentManager) HasFocusFollowsMouse() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
info := em.app.platformEnvironment()
|
||||
if ffm, ok := info["focusFollowsMouse"].(bool); ok {
|
||||
return ffm
|
||||
}
|
||||
return false
|
||||
}
|
||||
55
vendor/github.com/wailsapp/wails/v3/pkg/application/errors.go
generated
vendored
Normal file
55
vendor/github.com/wailsapp/wails/v3/pkg/application/errors.go
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FatalError instances are passed to the registered error handler
|
||||
// in case of catastrophic, unrecoverable failures that require immediate termination.
|
||||
// FatalError wraps the original error value in an informative message.
|
||||
// The underlying error may be retrieved through the [FatalError.Unwrap] method.
|
||||
type FatalError struct {
|
||||
err error
|
||||
internal bool
|
||||
}
|
||||
|
||||
// Internal returns true when the error was triggered from wails' internal code.
|
||||
func (e *FatalError) Internal() bool {
|
||||
return e.internal
|
||||
}
|
||||
|
||||
// Unwrap returns the original cause of the fatal error,
|
||||
// for easy inspection using the [errors.As] API.
|
||||
func (e *FatalError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e *FatalError) Error() string {
|
||||
var buffer strings.Builder
|
||||
buffer.WriteString("\n\n******************************** FATAL *********************************\n")
|
||||
buffer.WriteString("* There has been a catastrophic failure in your application. *\n")
|
||||
if e.internal {
|
||||
buffer.WriteString("* Please report this error at https://github.com/wailsapp/wails/issues *\n")
|
||||
}
|
||||
buffer.WriteString("**************************** Error Details *****************************\n")
|
||||
buffer.WriteString(e.err.Error())
|
||||
buffer.WriteString("************************************************************************\n")
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func Fatal(message string, args ...any) {
|
||||
err := &FatalError{
|
||||
err: fmt.Errorf(message, args...),
|
||||
internal: true,
|
||||
}
|
||||
|
||||
if globalApplication != nil {
|
||||
globalApplication.handleError(err)
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
174
vendor/github.com/wailsapp/wails/v3/pkg/application/event_manager.go
generated
vendored
Normal file
174
vendor/github.com/wailsapp/wails/v3/pkg/application/event_manager.go
generated
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
// EventManager manages event-related operations
|
||||
type EventManager struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
// newEventManager creates a new EventManager instance
|
||||
func newEventManager(app *App) *EventManager {
|
||||
return &EventManager{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
// Emit emits a custom event with the specified name and associated data.
|
||||
// It returns a boolean indicating whether the event was cancelled by a hook.
|
||||
//
|
||||
// If no data argument is provided, Emit emits an event with nil data.
|
||||
// When there is exactly one data argument, it will be used as the custom event's data field.
|
||||
// When more than one argument is provided, the event's data field will be set to the argument slice.
|
||||
//
|
||||
// If the given event name is registered, Emit validates the data parameter
|
||||
// against the expected data type. In case of a mismatch, Emit reports an error
|
||||
// to the registered error handler for the application and cancels the event.
|
||||
func (em *EventManager) Emit(name string, data ...any) bool {
|
||||
event := &CustomEvent{Name: name}
|
||||
|
||||
if len(data) == 1 {
|
||||
event.Data = data[0]
|
||||
} else if len(data) > 1 {
|
||||
event.Data = data
|
||||
}
|
||||
|
||||
if err := em.app.customEventProcessor.Emit(event); err != nil {
|
||||
globalApplication.handleError(err)
|
||||
}
|
||||
|
||||
return event.IsCancelled()
|
||||
}
|
||||
|
||||
// EmitEvent emits a custom event object (internal use)
|
||||
// It returns a boolean indicating whether the event was cancelled by a hook.
|
||||
//
|
||||
// If the given event name is registered, emitEvent validates the data parameter
|
||||
// against the expected data type. In case of a mismatch, emitEvent reports an error
|
||||
// to the registered error handler for the application and cancels the event.
|
||||
func (em *EventManager) EmitEvent(event *CustomEvent) bool {
|
||||
if err := em.app.customEventProcessor.Emit(event); err != nil {
|
||||
globalApplication.handleError(err)
|
||||
}
|
||||
|
||||
return event.IsCancelled()
|
||||
}
|
||||
|
||||
// On registers a listener for custom events
|
||||
func (em *EventManager) On(name string, callback func(event *CustomEvent)) func() {
|
||||
return em.app.customEventProcessor.On(name, callback)
|
||||
}
|
||||
|
||||
// Off removes all listeners for a custom event
|
||||
func (em *EventManager) Off(name string) {
|
||||
em.app.customEventProcessor.Off(name)
|
||||
}
|
||||
|
||||
// OnMultiple registers a listener for custom events that will be called N times
|
||||
func (em *EventManager) OnMultiple(name string, callback func(event *CustomEvent), counter int) {
|
||||
em.app.customEventProcessor.OnMultiple(name, callback, counter)
|
||||
}
|
||||
|
||||
// Reset removes all custom event listeners
|
||||
func (em *EventManager) Reset() {
|
||||
em.app.customEventProcessor.OffAll()
|
||||
}
|
||||
|
||||
// OnApplicationEvent registers a listener for application events
|
||||
func (em *EventManager) OnApplicationEvent(eventType events.ApplicationEventType, callback func(event *ApplicationEvent)) func() {
|
||||
eventID := uint(eventType)
|
||||
em.app.applicationEventListenersLock.Lock()
|
||||
defer em.app.applicationEventListenersLock.Unlock()
|
||||
listener := &EventListener{
|
||||
callback: callback,
|
||||
}
|
||||
em.app.applicationEventListeners[eventID] = append(em.app.applicationEventListeners[eventID], listener)
|
||||
if em.app.impl != nil {
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
em.app.impl.on(eventID)
|
||||
}()
|
||||
}
|
||||
|
||||
return func() {
|
||||
// lock the map
|
||||
em.app.applicationEventListenersLock.Lock()
|
||||
defer em.app.applicationEventListenersLock.Unlock()
|
||||
// Remove listener
|
||||
em.app.applicationEventListeners[eventID] = slices.DeleteFunc(em.app.applicationEventListeners[eventID], func(l *EventListener) bool {
|
||||
return l == listener
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterApplicationEventHook registers an application event hook
|
||||
func (em *EventManager) RegisterApplicationEventHook(eventType events.ApplicationEventType, callback func(event *ApplicationEvent)) func() {
|
||||
eventID := uint(eventType)
|
||||
em.app.applicationEventHooksLock.Lock()
|
||||
defer em.app.applicationEventHooksLock.Unlock()
|
||||
thisHook := &eventHook{
|
||||
callback: callback,
|
||||
}
|
||||
em.app.applicationEventHooks[eventID] = append(em.app.applicationEventHooks[eventID], thisHook)
|
||||
|
||||
return func() {
|
||||
em.app.applicationEventHooksLock.Lock()
|
||||
em.app.applicationEventHooks[eventID] = slices.DeleteFunc(em.app.applicationEventHooks[eventID], func(h *eventHook) bool {
|
||||
return h == thisHook
|
||||
})
|
||||
em.app.applicationEventHooksLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch dispatches an event to listeners (internal use)
|
||||
func (em *EventManager) dispatch(event *CustomEvent) {
|
||||
// Snapshot listeners under Lock
|
||||
em.app.wailsEventListenerLock.Lock()
|
||||
listeners := slices.Clone(em.app.wailsEventListeners)
|
||||
em.app.wailsEventListenerLock.Unlock()
|
||||
|
||||
for _, listener := range listeners {
|
||||
if event.IsCancelled() {
|
||||
return
|
||||
}
|
||||
listener.DispatchWailsEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleApplicationEvent handles application events (internal use)
|
||||
func (em *EventManager) handleApplicationEvent(event *ApplicationEvent) {
|
||||
defer handlePanic()
|
||||
em.app.applicationEventListenersLock.RLock()
|
||||
listeners, ok := em.app.applicationEventListeners[event.Id]
|
||||
em.app.applicationEventListenersLock.RUnlock()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Process Hooks
|
||||
em.app.applicationEventHooksLock.RLock()
|
||||
hooks, ok := em.app.applicationEventHooks[event.Id]
|
||||
em.app.applicationEventHooksLock.RUnlock()
|
||||
if ok {
|
||||
for _, thisHook := range hooks {
|
||||
thisHook.callback(event)
|
||||
if event.IsCancelled() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, listener := range listeners {
|
||||
go func() {
|
||||
if event.IsCancelled() {
|
||||
return
|
||||
}
|
||||
defer handlePanic()
|
||||
listener.callback(event)
|
||||
}()
|
||||
}
|
||||
}
|
||||
366
vendor/github.com/wailsapp/wails/v3/pkg/application/events.go
generated
vendored
Normal file
366
vendor/github.com/wailsapp/wails/v3/pkg/application/events.go
generated
vendored
Normal file
@@ -0,0 +1,366 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
type ApplicationEvent struct {
|
||||
Id uint
|
||||
ctx *ApplicationEventContext
|
||||
cancelled atomic.Bool
|
||||
}
|
||||
|
||||
func (w *ApplicationEvent) Context() *ApplicationEventContext {
|
||||
return w.ctx
|
||||
}
|
||||
|
||||
func newApplicationEvent(id events.ApplicationEventType) *ApplicationEvent {
|
||||
return &ApplicationEvent{
|
||||
Id: uint(id),
|
||||
ctx: newApplicationEventContext(),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ApplicationEvent) Cancel() {
|
||||
w.cancelled.Store(true)
|
||||
}
|
||||
|
||||
func (w *ApplicationEvent) IsCancelled() bool {
|
||||
return w.cancelled.Load()
|
||||
}
|
||||
|
||||
var applicationEvents = make(chan *ApplicationEvent, 5)
|
||||
|
||||
type windowEvent struct {
|
||||
WindowID uint
|
||||
EventID uint
|
||||
}
|
||||
|
||||
var windowEvents = make(chan *windowEvent, 5)
|
||||
|
||||
var menuItemClicked = make(chan uint, 5)
|
||||
|
||||
type CustomEvent struct {
|
||||
Name string `json:"name"`
|
||||
Data any `json:"data"`
|
||||
|
||||
// Sender records the name of the window sending the event,
|
||||
// or "" if sent from application.
|
||||
Sender string `json:"sender,omitempty"`
|
||||
|
||||
cancelled atomic.Bool
|
||||
}
|
||||
|
||||
func (e *CustomEvent) Cancel() {
|
||||
e.cancelled.Store(true)
|
||||
}
|
||||
|
||||
func (e *CustomEvent) IsCancelled() bool {
|
||||
return e.cancelled.Load()
|
||||
}
|
||||
|
||||
func (e *CustomEvent) ToJSON() string {
|
||||
marshal, err := json.Marshal(&e)
|
||||
if err != nil {
|
||||
// TODO: Fatal error? log?
|
||||
return ""
|
||||
}
|
||||
return string(marshal)
|
||||
}
|
||||
|
||||
// WailsEventListener is an interface for receiving all emitted Wails events.
|
||||
// Used by transport layers (IPC, WebSocket) to broadcast events.
|
||||
type WailsEventListener interface {
|
||||
DispatchWailsEvent(event *CustomEvent)
|
||||
}
|
||||
|
||||
type hook struct {
|
||||
callback func(*CustomEvent)
|
||||
}
|
||||
|
||||
// eventListener holds a callback function which is invoked when
|
||||
// the event listened for is emitted. It has a counter which indicates
|
||||
// how the total number of events it is interested in. A value of zero
|
||||
// means it does not expire (default).
|
||||
type eventListener struct {
|
||||
callback func(*CustomEvent) // Function to call with emitted event data
|
||||
counter int // The number of times this callback may be called. -1 = infinite
|
||||
delete bool // Flag to indicate that this listener should be deleted
|
||||
}
|
||||
|
||||
// EventProcessor handles custom events
|
||||
type EventProcessor struct {
|
||||
// Go event listeners
|
||||
listeners map[string][]*eventListener
|
||||
notifyLock sync.RWMutex
|
||||
dispatchEventToWindows func(*CustomEvent)
|
||||
hooks map[string][]*hook
|
||||
hookLock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewWailsEventProcessor(dispatchEventToWindows func(*CustomEvent)) *EventProcessor {
|
||||
return &EventProcessor{
|
||||
listeners: make(map[string][]*eventListener),
|
||||
dispatchEventToWindows: dispatchEventToWindows,
|
||||
hooks: make(map[string][]*hook),
|
||||
}
|
||||
}
|
||||
|
||||
// On is the equivalent of Javascript's `addEventListener`
|
||||
func (e *EventProcessor) On(eventName string, callback func(event *CustomEvent)) func() {
|
||||
return e.registerListener(eventName, callback, -1)
|
||||
}
|
||||
|
||||
// OnMultiple is the same as `OnApplicationEvent` but will unregister after `count` events
|
||||
func (e *EventProcessor) OnMultiple(eventName string, callback func(event *CustomEvent), counter int) func() {
|
||||
return e.registerListener(eventName, callback, counter)
|
||||
}
|
||||
|
||||
// Once is the same as `OnApplicationEvent` but will unregister after the first event
|
||||
func (e *EventProcessor) Once(eventName string, callback func(event *CustomEvent)) func() {
|
||||
return e.registerListener(eventName, callback, 1)
|
||||
}
|
||||
|
||||
// Emit sends an event to all listeners.
|
||||
//
|
||||
// If the event is globally registered, it validates associated data
|
||||
// against the expected data type. In case of mismatches,
|
||||
// it cancels the event and returns an error.
|
||||
func (e *EventProcessor) Emit(thisEvent *CustomEvent) error {
|
||||
if thisEvent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate data type; in case of mismatches cancel and report error.
|
||||
if err := validateCustomEvent(thisEvent); err != nil {
|
||||
thisEvent.Cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
// If we have any hooks, run them first and check if the event was cancelled
|
||||
if e.hooks != nil {
|
||||
if hooks, ok := e.hooks[thisEvent.Name]; ok {
|
||||
for _, thisHook := range hooks {
|
||||
thisHook.callback(thisEvent)
|
||||
if thisEvent.IsCancelled() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
e.dispatchEventToListeners(thisEvent)
|
||||
}()
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
e.dispatchEventToWindows(thisEvent)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EventProcessor) Off(eventName string) {
|
||||
e.unRegisterListener(eventName)
|
||||
}
|
||||
|
||||
func (e *EventProcessor) OffAll() {
|
||||
e.notifyLock.Lock()
|
||||
defer e.notifyLock.Unlock()
|
||||
e.listeners = make(map[string][]*eventListener)
|
||||
}
|
||||
|
||||
// registerListener provides a means of subscribing to events of type "eventName"
|
||||
func (e *EventProcessor) registerListener(eventName string, callback func(*CustomEvent), counter int) func() {
|
||||
// Create new eventListener
|
||||
thisListener := &eventListener{
|
||||
callback: callback,
|
||||
counter: counter,
|
||||
delete: false,
|
||||
}
|
||||
e.notifyLock.Lock()
|
||||
// Append the new listener to the listeners slice
|
||||
e.listeners[eventName] = append(e.listeners[eventName], thisListener)
|
||||
e.notifyLock.Unlock()
|
||||
return func() {
|
||||
e.notifyLock.Lock()
|
||||
defer e.notifyLock.Unlock()
|
||||
|
||||
if _, ok := e.listeners[eventName]; !ok {
|
||||
return
|
||||
}
|
||||
e.listeners[eventName] = slices.DeleteFunc(e.listeners[eventName], func(l *eventListener) bool {
|
||||
return l == thisListener
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHook provides a means of registering methods to be called before emitting the event
|
||||
func (e *EventProcessor) RegisterHook(eventName string, callback func(*CustomEvent)) func() {
|
||||
// Create new hook
|
||||
thisHook := &hook{
|
||||
callback: callback,
|
||||
}
|
||||
e.hookLock.Lock()
|
||||
// Append the new listener to the listeners slice
|
||||
e.hooks[eventName] = append(e.hooks[eventName], thisHook)
|
||||
e.hookLock.Unlock()
|
||||
return func() {
|
||||
e.hookLock.Lock()
|
||||
defer e.hookLock.Unlock()
|
||||
|
||||
if _, ok := e.hooks[eventName]; !ok {
|
||||
return
|
||||
}
|
||||
e.hooks[eventName] = slices.DeleteFunc(e.hooks[eventName], func(h *hook) bool {
|
||||
return h == thisHook
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// unRegisterListener provides a means of unsubscribing to events of type "eventName"
|
||||
func (e *EventProcessor) unRegisterListener(eventName string) {
|
||||
e.notifyLock.Lock()
|
||||
defer e.notifyLock.Unlock()
|
||||
delete(e.listeners, eventName)
|
||||
}
|
||||
|
||||
// dispatchEventToListeners calls all registered listeners event name
|
||||
func (e *EventProcessor) dispatchEventToListeners(event *CustomEvent) {
|
||||
|
||||
e.notifyLock.Lock()
|
||||
defer e.notifyLock.Unlock()
|
||||
|
||||
listeners := e.listeners[event.Name]
|
||||
if listeners == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// We have a dirty flag to indicate that there are items to delete
|
||||
itemsToDelete := false
|
||||
|
||||
// Callback in goroutine
|
||||
for _, listener := range listeners {
|
||||
if listener.counter > 0 {
|
||||
listener.counter--
|
||||
}
|
||||
go func() {
|
||||
if event.IsCancelled() {
|
||||
return
|
||||
}
|
||||
defer handlePanic()
|
||||
listener.callback(event)
|
||||
}()
|
||||
|
||||
if listener.counter == 0 {
|
||||
listener.delete = true
|
||||
itemsToDelete = true
|
||||
}
|
||||
}
|
||||
|
||||
// Do we have items to delete?
|
||||
if itemsToDelete == true {
|
||||
e.listeners[event.Name] = slices.DeleteFunc(listeners, func(l *eventListener) bool {
|
||||
return l.delete == true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Void will be translated by the binding generator to the TypeScript type 'void'.
|
||||
// It can be used as an event data type to register events that must not have any associated data.
|
||||
type Void interface {
|
||||
sentinel()
|
||||
}
|
||||
|
||||
var registeredEvents sync.Map
|
||||
var voidType = reflect.TypeFor[Void]()
|
||||
|
||||
// RegisterEvent registers a custom event name and associated data type.
|
||||
// Events may be registered at most once.
|
||||
// Duplicate calls for the same event name trigger a panic.
|
||||
//
|
||||
// The binding generator emits typing information for all registered custom events.
|
||||
// [App.EmitEvent] and [Window.EmitEvent] check the data type for registered events.
|
||||
// Data types are matched exactly and no conversion is performed.
|
||||
//
|
||||
// It is recommended to call RegisterEvent directly,
|
||||
// with constant arguments, and only from init functions.
|
||||
// Indirect calls or instantiations are not discoverable by the binding generator.
|
||||
func RegisterEvent[Data any](name string) {
|
||||
if events.IsKnownEvent(name) {
|
||||
panic(fmt.Errorf("'%s' is a known system event name", name))
|
||||
}
|
||||
if typ, ok := registeredEvents.Load(name); ok {
|
||||
panic(fmt.Errorf("event '%s' is already registered with data type %s", name, typ))
|
||||
}
|
||||
|
||||
registeredEvents.Store(name, reflect.TypeFor[Data]())
|
||||
eventRegistered(name)
|
||||
}
|
||||
|
||||
func validateCustomEvent(event *CustomEvent) error {
|
||||
r, ok := registeredEvents.Load(event.Name)
|
||||
if !ok {
|
||||
warnAboutUnregisteredEvent(event.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
typ := r.(reflect.Type)
|
||||
|
||||
if typ == voidType {
|
||||
if event.Data == nil {
|
||||
return nil
|
||||
}
|
||||
} else if typ.Kind() == reflect.Interface {
|
||||
if reflect.TypeOf(event.Data).Implements(typ) {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if reflect.TypeOf(event.Data) == typ {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf(
|
||||
"data of type %s for event '%s' does not match registered data type %s",
|
||||
reflect.TypeOf(event.Data),
|
||||
event.Name,
|
||||
typ,
|
||||
)
|
||||
}
|
||||
|
||||
func decodeEventData(name string, data []byte) (result any, err error) {
|
||||
r, ok := registeredEvents.Load(name)
|
||||
if !ok {
|
||||
// Unregistered events unmarshal to any.
|
||||
err = json.Unmarshal(data, &result)
|
||||
return
|
||||
}
|
||||
|
||||
typ := r.(reflect.Type)
|
||||
|
||||
if typ == voidType {
|
||||
// When typ is voidType, perform a null check
|
||||
err = json.Unmarshal(data, &result)
|
||||
if err == nil && result != nil {
|
||||
err = fmt.Errorf("non-null data for event '%s' does not match registered data type %s", name, typ)
|
||||
}
|
||||
} else {
|
||||
value := reflect.New(typ.(reflect.Type))
|
||||
err = json.Unmarshal(data, value.Interface())
|
||||
if err == nil {
|
||||
result = value.Elem().Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
33
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_android.go
generated
vendored
Normal file
33
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_android.go
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
//go:build android
|
||||
|
||||
package application
|
||||
|
||||
import "github.com/wailsapp/wails/v3/pkg/events"
|
||||
|
||||
// Map platform events → common events (same pattern as macOS & others).
|
||||
// setupCommonEvents copies the source event's context, so any data attached on
|
||||
// the Android side (battery level, theme, …) rides along to the common event.
|
||||
var commonApplicationEventMap = map[events.ApplicationEventType]events.ApplicationEventType{
|
||||
events.Android.ActivityCreated: events.Common.ApplicationStarted,
|
||||
events.Android.ApplicationLowMemory: events.Common.LowMemory,
|
||||
events.Android.BatteryChanged: events.Common.BatteryChanged,
|
||||
events.Android.NetworkChanged: events.Common.NetworkChanged,
|
||||
events.Android.ThemeChanged: events.Common.ThemeChanged,
|
||||
events.Android.ScreenLocked: events.Common.ScreenLocked,
|
||||
events.Android.ScreenUnlocked: events.Common.ScreenUnlocked,
|
||||
}
|
||||
|
||||
// setupCommonEvents forwards Android platform events to their common counterparts
|
||||
func (a *androidApp) setupCommonEvents() {
|
||||
for sourceEvent, targetEvent := range commonApplicationEventMap {
|
||||
sourceEvent := sourceEvent
|
||||
targetEvent := targetEvent
|
||||
a.parent.Event.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) {
|
||||
androidDebugLogf("[events_common_android.go] forwarding android event %d → common %d", sourceEvent, targetEvent)
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(targetEvent),
|
||||
ctx: event.ctx,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
40
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_darwin.go
generated
vendored
Normal file
40
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
//go:build darwin && !ios && !server
|
||||
|
||||
package application
|
||||
|
||||
import "github.com/wailsapp/wails/v3/pkg/events"
|
||||
|
||||
var commonApplicationEventMap = map[events.ApplicationEventType]events.ApplicationEventType{
|
||||
events.Mac.ApplicationDidFinishLaunching: events.Common.ApplicationStarted,
|
||||
events.Mac.ApplicationDidChangeTheme: events.Common.ThemeChanged,
|
||||
events.Mac.ApplicationWillSleep: events.Common.SystemWillSleep,
|
||||
events.Mac.ApplicationDidWake: events.Common.SystemDidWake,
|
||||
}
|
||||
|
||||
func (m *macosApp) setupCommonEvents() {
|
||||
for sourceEvent, targetEvent := range commonApplicationEventMap {
|
||||
sourceEvent := sourceEvent
|
||||
targetEvent := targetEvent
|
||||
m.parent.Event.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) {
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(targetEvent),
|
||||
ctx: event.ctx,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Handle dock icon click (applicationShouldHandleReopen) to show windows
|
||||
// when there are no visible windows. This provides the expected macOS UX
|
||||
// where clicking the dock icon shows a hidden app's window.
|
||||
// Issue #4583: Apps with StartHidden: true should show when dock icon is clicked.
|
||||
m.parent.Event.OnApplicationEvent(events.Mac.ApplicationShouldHandleReopen, func(event *ApplicationEvent) {
|
||||
if !event.Context().HasVisibleWindows() {
|
||||
// Show all windows that are not visible
|
||||
for _, window := range m.parent.Window.GetAll() {
|
||||
if !window.IsVisible() {
|
||||
window.Show()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
34
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_ios.go
generated
vendored
Normal file
34
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_ios.go
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
//go:build ios
|
||||
|
||||
package application
|
||||
|
||||
import "github.com/wailsapp/wails/v3/pkg/events"
|
||||
|
||||
// Map platform events → common events (same pattern as macOS & others).
|
||||
// setupCommonEvents copies the source event's context, so any data attached on
|
||||
// the iOS side (battery level, theme, …) rides along to the common event.
|
||||
var commonApplicationEventMap = map[events.ApplicationEventType]events.ApplicationEventType{
|
||||
events.IOS.ApplicationDidFinishLaunching: events.Common.ApplicationStarted,
|
||||
events.IOS.ApplicationDidReceiveMemoryWarning: events.Common.LowMemory,
|
||||
events.IOS.BatteryChanged: events.Common.BatteryChanged,
|
||||
events.IOS.NetworkChanged: events.Common.NetworkChanged,
|
||||
events.IOS.ThemeChanged: events.Common.ThemeChanged,
|
||||
events.IOS.ScreenLocked: events.Common.ScreenLocked,
|
||||
events.IOS.ScreenUnlocked: events.Common.ScreenUnlocked,
|
||||
}
|
||||
|
||||
// setupCommonEvents forwards iOS platform events to their common counterparts
|
||||
func (i *iosApp) setupCommonEvents() {
|
||||
for sourceEvent, targetEvent := range commonApplicationEventMap {
|
||||
sourceEvent := sourceEvent
|
||||
targetEvent := targetEvent
|
||||
i.parent.Event.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) {
|
||||
// Log the forwarding so we can see every emitted event in iOS NSLog
|
||||
iosConsoleLogf("info", " [events_common_ios.go] Forwarding iOS event %d → common %d", sourceEvent, targetEvent)
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(targetEvent),
|
||||
ctx: event.ctx,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
25
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_linux.go
generated
vendored
Normal file
25
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_linux.go
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build linux && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
import "github.com/wailsapp/wails/v3/pkg/events"
|
||||
|
||||
var commonApplicationEventMap = map[events.ApplicationEventType]events.ApplicationEventType{
|
||||
events.Linux.ApplicationStartup: events.Common.ApplicationStarted,
|
||||
events.Linux.SystemThemeChanged: events.Common.ThemeChanged,
|
||||
events.Linux.SystemWillSleep: events.Common.SystemWillSleep,
|
||||
events.Linux.SystemDidWake: events.Common.SystemDidWake,
|
||||
}
|
||||
|
||||
func (a *linuxApp) setupCommonEvents() {
|
||||
for sourceEvent, targetEvent := range commonApplicationEventMap {
|
||||
sourceEvent := sourceEvent
|
||||
targetEvent := targetEvent
|
||||
a.parent.Event.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) {
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(targetEvent),
|
||||
ctx: event.ctx,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
10
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_server.go
generated
vendored
Normal file
10
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_server.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
//go:build server
|
||||
|
||||
package application
|
||||
|
||||
// setupCommonEvents sets up common application events for server mode.
|
||||
// In server mode, there are no platform-specific events to map,
|
||||
// so this is a no-op.
|
||||
func (h *serverApp) setupCommonEvents() {
|
||||
// No-op: server mode has no platform-specific events to map
|
||||
}
|
||||
25
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_windows.go
generated
vendored
Normal file
25
vendor/github.com/wailsapp/wails/v3/pkg/application/events_common_windows.go
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build windows && !server
|
||||
|
||||
package application
|
||||
|
||||
import "github.com/wailsapp/wails/v3/pkg/events"
|
||||
|
||||
var commonApplicationEventMap = map[events.ApplicationEventType]events.ApplicationEventType{
|
||||
events.Windows.SystemThemeChanged: events.Common.ThemeChanged,
|
||||
events.Windows.ApplicationStarted: events.Common.ApplicationStarted,
|
||||
events.Windows.APMSuspend: events.Common.SystemWillSleep,
|
||||
events.Windows.APMResumeAutomatic: events.Common.SystemDidWake,
|
||||
}
|
||||
|
||||
func (m *windowsApp) setupCommonEvents() {
|
||||
for sourceEvent, targetEvent := range commonApplicationEventMap {
|
||||
sourceEvent := sourceEvent
|
||||
targetEvent := targetEvent
|
||||
m.parent.Event.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) {
|
||||
applicationEvents <- &ApplicationEvent{
|
||||
Id: uint(targetEvent),
|
||||
ctx: event.ctx,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
7
vendor/github.com/wailsapp/wails/v3/pkg/application/events_loose.go
generated
vendored
Normal file
7
vendor/github.com/wailsapp/wails/v3/pkg/application/events_loose.go
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build production || !strictevents
|
||||
|
||||
package application
|
||||
|
||||
func eventRegistered(name string) {}
|
||||
|
||||
func warnAboutUnregisteredEvent(name string) {}
|
||||
27
vendor/github.com/wailsapp/wails/v3/pkg/application/events_strict.go
generated
vendored
Normal file
27
vendor/github.com/wailsapp/wails/v3/pkg/application/events_strict.go
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
//go:build !production && strictevents
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var knownUnregisteredEvents sync.Map
|
||||
|
||||
func eventRegistered(name string) {
|
||||
knownUnregisteredEvents.Delete(name)
|
||||
}
|
||||
|
||||
func warnAboutUnregisteredEvent(name string) {
|
||||
// Perform a load first to avoid thrashing the map with unnecessary swaps.
|
||||
if _, known := knownUnregisteredEvents.Load(name); known {
|
||||
return
|
||||
}
|
||||
|
||||
// Perform a swap to synchronize with concurrent emissions.
|
||||
if _, known := knownUnregisteredEvents.Swap(name, true); known {
|
||||
return
|
||||
}
|
||||
|
||||
globalApplication.warning("unregistered event name '%s'", name)
|
||||
}
|
||||
195
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_darwin.go
generated
vendored
Normal file
195
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
//go:build darwin && !ios && !server
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -mmacosx-version-min=10.13
|
||||
#cgo LDFLAGS: -framework Carbon
|
||||
|
||||
#include <Carbon/Carbon.h>
|
||||
|
||||
extern void globalShortcutCallback(int id);
|
||||
|
||||
static EventHandlerRef gHotKeyHandler = NULL;
|
||||
|
||||
// hotKeyHandlerProc is the single Carbon event handler that receives every
|
||||
// kEventHotKeyPressed event. It extracts the EventHotKeyID we set at
|
||||
// registration time and forwards the numeric id back into Go.
|
||||
static OSStatus hotKeyHandlerProc(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
|
||||
EventHotKeyID hkID;
|
||||
OSStatus status = GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID,
|
||||
NULL, sizeof(hkID), NULL, &hkID);
|
||||
if (status == noErr) {
|
||||
globalShortcutCallback((int)hkID.id);
|
||||
}
|
||||
return noErr;
|
||||
}
|
||||
|
||||
// installHotKeyHandler installs the shared handler exactly once.
|
||||
static void installHotKeyHandler(void) {
|
||||
if (gHotKeyHandler != NULL) {
|
||||
return;
|
||||
}
|
||||
EventTypeSpec evt;
|
||||
evt.eventClass = kEventClassKeyboard;
|
||||
evt.eventKind = kEventHotKeyPressed;
|
||||
InstallApplicationEventHandler(&hotKeyHandlerProc, 1, &evt, NULL, &gHotKeyHandler);
|
||||
}
|
||||
|
||||
// registerHotKey binds (keyCode, modifiers) to id and returns the OSStatus.
|
||||
// On success *outRef receives the hot key reference used to unregister later.
|
||||
static int registerHotKey(unsigned int keyCode, unsigned int modifiers, int id, EventHotKeyRef *outRef) {
|
||||
installHotKeyHandler();
|
||||
EventHotKeyID hkID;
|
||||
hkID.signature = 'WLgs'; // "Wails global shortcut"
|
||||
hkID.id = (unsigned int)id;
|
||||
OSStatus status = RegisterEventHotKey(keyCode, modifiers, hkID,
|
||||
GetApplicationEventTarget(), 0, outRef);
|
||||
return (int)status;
|
||||
}
|
||||
|
||||
static int unregisterHotKey(EventHotKeyRef ref) {
|
||||
if (ref == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return (int)UnregisterEventHotKey(ref);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Carbon classic modifier masks (Events.h). RegisterEventHotKey expects these,
|
||||
// not the Cocoa NSEventModifierFlag values.
|
||||
const (
|
||||
carbonCmdKey = 0x0100
|
||||
carbonShiftKey = 0x0200
|
||||
carbonOptionKey = 0x0800
|
||||
carbonControlKey = 0x1000
|
||||
)
|
||||
|
||||
// macosGlobalShortcuts implements globalShortcutImpl using the Carbon Event
|
||||
// Manager's RegisterEventHotKey API. Carbon's hot key API is the standard,
|
||||
// still-supported mechanism for system-wide hot keys on macOS and - unlike a
|
||||
// CGEventTap or an NSEvent global monitor - does not require Accessibility
|
||||
// permission.
|
||||
type macosGlobalShortcuts struct {
|
||||
manager *GlobalShortcutManager
|
||||
refs map[int]C.EventHotKeyRef
|
||||
}
|
||||
|
||||
func newGlobalShortcutImpl(manager *GlobalShortcutManager) globalShortcutImpl {
|
||||
return &macosGlobalShortcuts{
|
||||
manager: manager,
|
||||
refs: make(map[int]C.EventHotKeyRef),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *macosGlobalShortcuts) register(id int, accel *accelerator) error {
|
||||
keyCode, ok := macKeyCodes[accel.Key]
|
||||
if !ok {
|
||||
return fmt.Errorf("key %q is not supported as a global shortcut on macOS", accel.Key)
|
||||
}
|
||||
|
||||
var mods C.uint
|
||||
for _, m := range accel.Modifiers {
|
||||
switch m {
|
||||
case CmdOrCtrlKey, SuperKey:
|
||||
mods |= carbonCmdKey
|
||||
case ControlKey:
|
||||
mods |= carbonControlKey
|
||||
case OptionOrAltKey:
|
||||
mods |= carbonOptionKey
|
||||
case ShiftKey:
|
||||
mods |= carbonShiftKey
|
||||
}
|
||||
}
|
||||
|
||||
var ref C.EventHotKeyRef
|
||||
status := C.registerHotKey(C.uint(keyCode), mods, C.int(id), &ref)
|
||||
if status != 0 {
|
||||
// -9878 (eventHotKeyExistsErr) means the combination is already taken.
|
||||
if status == -9878 {
|
||||
return fmt.Errorf("the shortcut is already registered (possibly by another application) (OSStatus %d)", int(status))
|
||||
}
|
||||
return fmt.Errorf("RegisterEventHotKey failed (OSStatus %d)", int(status))
|
||||
}
|
||||
g.refs[id] = ref
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *macosGlobalShortcuts) unregister(id int) error {
|
||||
ref, ok := g.refs[id]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
delete(g.refs, id)
|
||||
if status := C.unregisterHotKey(ref); status != 0 {
|
||||
return fmt.Errorf("UnregisterEventHotKey failed (OSStatus %d)", int(status))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *macosGlobalShortcuts) unregisterAll() error {
|
||||
var firstErr error
|
||||
for id := range g.refs {
|
||||
if err := g.unregister(id); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
//export globalShortcutCallback
|
||||
func globalShortcutCallback(id C.int) {
|
||||
if globalApplication != nil && globalApplication.GlobalShortcut != nil {
|
||||
globalApplication.GlobalShortcut.dispatch(int(id))
|
||||
}
|
||||
}
|
||||
|
||||
// macKeyCodes maps Wails accelerator key names (already lower-cased by
|
||||
// parseAccelerator) to macOS hardware virtual key codes (kVK_* from Carbon's
|
||||
// HIToolbox/Events.h).
|
||||
//
|
||||
// NOTE: macOS hot keys are bound to *hardware* key codes, not characters, so
|
||||
// this table assumes a standard ANSI/QWERTY physical layout. On non-QWERTY
|
||||
// layouts the physical key in the QWERTY position is what triggers the
|
||||
// shortcut. This matches the behaviour of essentially every macOS global hot
|
||||
// key implementation; a layout-aware mapping (via UCKeyTranslate) could be
|
||||
// added later if required.
|
||||
var macKeyCodes = map[string]int{
|
||||
// Letters
|
||||
"a": 0, "s": 1, "d": 2, "f": 3, "h": 4, "g": 5, "z": 6, "x": 7,
|
||||
"c": 8, "v": 9, "b": 11, "q": 12, "w": 13, "e": 14, "r": 15, "y": 16,
|
||||
"t": 17, "o": 31, "u": 32, "i": 34, "p": 35, "l": 37, "j": 38, "k": 40,
|
||||
"n": 45, "m": 46,
|
||||
// Number row
|
||||
"1": 18, "2": 19, "3": 20, "4": 21, "6": 22, "5": 23, "9": 25, "7": 26,
|
||||
"8": 28, "0": 29,
|
||||
// Punctuation
|
||||
"=": 24, "-": 27, "]": 30, "[": 33, "'": 39, ";": 41, "\\": 42,
|
||||
",": 43, "/": 44, ".": 47, "`": 50, "+": 24,
|
||||
// Named keys
|
||||
"return": 36,
|
||||
"enter": 36,
|
||||
"tab": 48,
|
||||
"space": 49,
|
||||
"backspace": 51, // kVK_Delete (labelled "delete" on Mac keyboards)
|
||||
"delete": 117, // kVK_ForwardDelete
|
||||
"escape": 53,
|
||||
"home": 115,
|
||||
"page up": 116,
|
||||
"end": 119,
|
||||
"page down": 121,
|
||||
"left": 123,
|
||||
"right": 124,
|
||||
"down": 125,
|
||||
"up": 126,
|
||||
// Function keys
|
||||
"f1": 122, "f2": 120, "f3": 99, "f4": 118, "f5": 96, "f6": 97,
|
||||
"f7": 98, "f8": 100, "f9": 101, "f10": 109, "f11": 103, "f12": 111,
|
||||
"f13": 105, "f14": 107, "f15": 113, "f16": 106, "f17": 64, "f18": 79,
|
||||
"f19": 80, "f20": 90,
|
||||
}
|
||||
38
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_linux.go
generated
vendored
Normal file
38
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_linux.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
//go:build linux && cgo && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// newGlobalShortcutImpl selects the appropriate Linux backend.
|
||||
//
|
||||
// On X11 sessions the XGrabKey-based backend is used: it is self-contained,
|
||||
// requires no portal support and grabs the exact accelerator requested.
|
||||
//
|
||||
// On Wayland sessions there is, by design, no way for a client to grab keys
|
||||
// directly. The only sanctioned mechanism is the XDG Desktop Portal's
|
||||
// org.freedesktop.portal.GlobalShortcuts interface, so the portal backend is
|
||||
// used there. Note that under the portal the compositor (and ultimately the
|
||||
// user) decides the final key binding; see portalGlobalShortcuts.
|
||||
func newGlobalShortcutImpl(manager *GlobalShortcutManager) globalShortcutImpl {
|
||||
if isWaylandSession() {
|
||||
return newPortalGlobalShortcuts(manager)
|
||||
}
|
||||
return newX11GlobalShortcuts(manager)
|
||||
}
|
||||
|
||||
// isWaylandSession reports whether the process is running under a Wayland
|
||||
// session. XDG_SESSION_TYPE is authoritative when set; otherwise the presence
|
||||
// of WAYLAND_DISPLAY is used as a fallback.
|
||||
func isWaylandSession() bool {
|
||||
switch strings.ToLower(os.Getenv("XDG_SESSION_TYPE")) {
|
||||
case "wayland":
|
||||
return true
|
||||
case "x11":
|
||||
return false
|
||||
}
|
||||
return os.Getenv("WAYLAND_DISPLAY") != ""
|
||||
}
|
||||
404
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_linux_portal.go
generated
vendored
Normal file
404
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_linux_portal.go
generated
vendored
Normal file
@@ -0,0 +1,404 @@
|
||||
//go:build linux && cgo && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
portalService = "org.freedesktop.portal.Desktop"
|
||||
portalPath = "/org/freedesktop/portal/desktop"
|
||||
portalShortcutIf = "org.freedesktop.portal.GlobalShortcuts"
|
||||
portalRequestIf = "org.freedesktop.portal.Request"
|
||||
)
|
||||
|
||||
// portalShortcut is one shortcut as known to the portal backend.
|
||||
type portalShortcut struct {
|
||||
id int
|
||||
trigger string // preferred trigger, e.g. "CTRL+SHIFT+a"
|
||||
desc string
|
||||
}
|
||||
|
||||
// portalGlobalShortcuts implements globalShortcutImpl on Wayland using the XDG
|
||||
// Desktop Portal's org.freedesktop.portal.GlobalShortcuts interface.
|
||||
//
|
||||
// IMPORTANT semantic difference from the X11/macOS/Windows backends: the portal
|
||||
// only takes a *preferred* trigger. The compositor - and ultimately the user -
|
||||
// decides the final key combination, and may change or reject it. The callback
|
||||
// still fires when the (possibly remapped) shortcut is activated, but the exact
|
||||
// keys are not guaranteed to match what was requested. IsRegistered/GetAll
|
||||
// therefore report what the application asked for, not what the compositor
|
||||
// bound.
|
||||
//
|
||||
// All D-Bus work runs on a dedicated goroutine so that register/unregister
|
||||
// never block the UI thread on a (potentially interactive) portal call.
|
||||
type portalGlobalShortcuts struct {
|
||||
manager *GlobalShortcutManager
|
||||
|
||||
mu sync.Mutex
|
||||
desired map[int]portalShortcut // current desired set, keyed by numeric id
|
||||
cmds chan func() // serialized onto the worker goroutine
|
||||
tokenSeq int
|
||||
sessionWG sync.Once
|
||||
}
|
||||
|
||||
func newPortalGlobalShortcuts(manager *GlobalShortcutManager) globalShortcutImpl {
|
||||
return &portalGlobalShortcuts{
|
||||
manager: manager,
|
||||
desired: make(map[int]portalShortcut),
|
||||
cmds: make(chan func(), 32),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *portalGlobalShortcuts) ensureWorker() {
|
||||
p.sessionWG.Do(func() {
|
||||
go p.worker()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *portalGlobalShortcuts) register(id int, accel *accelerator) error {
|
||||
p.mu.Lock()
|
||||
p.desired[id] = portalShortcut{
|
||||
id: id,
|
||||
trigger: portalTrigger(accel),
|
||||
desc: accel.String(),
|
||||
}
|
||||
p.mu.Unlock()
|
||||
p.ensureWorker()
|
||||
p.requestRebind()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *portalGlobalShortcuts) unregister(id int) error {
|
||||
p.mu.Lock()
|
||||
_, ok := p.desired[id]
|
||||
delete(p.desired, id)
|
||||
p.mu.Unlock()
|
||||
if ok {
|
||||
p.requestRebind()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *portalGlobalShortcuts) unregisterAll() error {
|
||||
p.mu.Lock()
|
||||
p.desired = make(map[int]portalShortcut)
|
||||
p.mu.Unlock()
|
||||
p.requestRebind()
|
||||
return nil
|
||||
}
|
||||
|
||||
// requestRebind asks the worker to (re)bind the full desired set. Binds are
|
||||
// debounced so that the burst of register() calls applications make at startup
|
||||
// collapses into a single portal interaction.
|
||||
func (p *portalGlobalShortcuts) requestRebind() {
|
||||
select {
|
||||
case p.cmds <- p.rebind:
|
||||
default:
|
||||
// A rebind is already queued; the worker reads the latest desired set.
|
||||
}
|
||||
}
|
||||
|
||||
func (p *portalGlobalShortcuts) snapshot() []portalShortcut {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
out := make([]portalShortcut, 0, len(p.desired))
|
||||
for _, s := range p.desired {
|
||||
out = append(out, s)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].id < out[j].id })
|
||||
return out
|
||||
}
|
||||
|
||||
// worker owns the D-Bus connection and serializes all portal interaction.
|
||||
func (p *portalGlobalShortcuts) worker() {
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
p.manager.app.handleError(fmt.Errorf("global shortcuts: cannot connect to session bus: %w", err))
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
w := &portalWorker{
|
||||
p: p,
|
||||
conn: conn,
|
||||
pending: make(map[dbus.ObjectPath]chan portalResponse),
|
||||
shortcut: make(map[string]int),
|
||||
}
|
||||
|
||||
sender := conn.Names()[0]
|
||||
w.senderToken = strings.ReplaceAll(strings.TrimPrefix(sender, ":"), ".", "_")
|
||||
|
||||
// One signal channel for everything: Request.Response (method results) and
|
||||
// GlobalShortcuts.Activated (shortcut presses). A dedicated goroutine reads
|
||||
// it so that synchronous portal calls (which block waiting for their
|
||||
// Response) do not starve signal delivery.
|
||||
sigs := make(chan *dbus.Signal, 32)
|
||||
conn.Signal(sigs)
|
||||
_ = conn.AddMatchSignal(dbus.WithMatchInterface(portalRequestIf), dbus.WithMatchMember("Response"))
|
||||
_ = conn.AddMatchSignal(dbus.WithMatchInterface(portalShortcutIf), dbus.WithMatchMember("Activated"))
|
||||
go func() {
|
||||
for sig := range sigs {
|
||||
w.handleSignal(sig)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := w.createSession(); err != nil {
|
||||
p.manager.app.handleError(fmt.Errorf("global shortcuts: portal session failed: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Debounce timer for coalescing the startup burst of register() calls into
|
||||
// a single BindShortcuts (and thus a single portal interaction).
|
||||
var debounce *time.Timer
|
||||
var debounceC <-chan time.Time
|
||||
rebindPending := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-p.cmds:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rebindPending = true
|
||||
if debounce == nil {
|
||||
debounce = time.NewTimer(150 * time.Millisecond)
|
||||
debounceC = debounce.C
|
||||
} else {
|
||||
debounce.Reset(150 * time.Millisecond)
|
||||
}
|
||||
case <-debounceC:
|
||||
if rebindPending {
|
||||
rebindPending = false
|
||||
w.bindShortcuts()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rebind is the command pushed onto cmds; the actual work happens in the worker
|
||||
// loop via the debounce timer.
|
||||
func (p *portalGlobalShortcuts) rebind() {}
|
||||
|
||||
type portalResponse struct {
|
||||
code uint32
|
||||
results map[string]dbus.Variant
|
||||
}
|
||||
|
||||
type portalWorker struct {
|
||||
p *portalGlobalShortcuts
|
||||
conn *dbus.Conn
|
||||
senderToken string
|
||||
sessionPath dbus.ObjectPath
|
||||
|
||||
mu sync.Mutex
|
||||
pending map[dbus.ObjectPath]chan portalResponse
|
||||
shortcut map[string]int // portal shortcut id string -> numeric id
|
||||
}
|
||||
|
||||
func (w *portalWorker) nextToken(prefix string) string {
|
||||
w.p.mu.Lock()
|
||||
w.p.tokenSeq++
|
||||
seq := w.p.tokenSeq
|
||||
w.p.mu.Unlock()
|
||||
return fmt.Sprintf("wails_%s_%d", prefix, seq)
|
||||
}
|
||||
|
||||
func (w *portalWorker) requestPath(token string) dbus.ObjectPath {
|
||||
return dbus.ObjectPath("/org/freedesktop/portal/desktop/request/" + w.senderToken + "/" + token)
|
||||
}
|
||||
|
||||
// call invokes a portal method that follows the Request/Response pattern and
|
||||
// blocks (on the worker goroutine) until the Response signal arrives.
|
||||
func (w *portalWorker) call(method string, options map[string]dbus.Variant, args ...interface{}) (portalResponse, error) {
|
||||
token := w.nextToken("req")
|
||||
options["handle_token"] = dbus.MakeVariant(token)
|
||||
expected := w.requestPath(token)
|
||||
|
||||
respCh := make(chan portalResponse, 1)
|
||||
w.mu.Lock()
|
||||
w.pending[expected] = respCh
|
||||
w.mu.Unlock()
|
||||
defer func() {
|
||||
w.mu.Lock()
|
||||
delete(w.pending, expected)
|
||||
w.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Build the argument list: portal request methods take their own args
|
||||
// followed by the options dictionary last.
|
||||
callArgs := append(append([]interface{}{}, args...), options)
|
||||
obj := w.conn.Object(portalService, portalPath)
|
||||
var handle dbus.ObjectPath
|
||||
if err := obj.Call(portalShortcutIf+"."+method, 0, callArgs...).Store(&handle); err != nil {
|
||||
return portalResponse{}, err
|
||||
}
|
||||
// The real request path is the one the portal returns; results arrive there.
|
||||
if handle != expected {
|
||||
w.mu.Lock()
|
||||
w.pending[handle] = respCh
|
||||
w.mu.Unlock()
|
||||
defer func() {
|
||||
w.mu.Lock()
|
||||
delete(w.pending, handle)
|
||||
w.mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
select {
|
||||
case resp := <-respCh:
|
||||
return resp, nil
|
||||
case <-time.After(30 * time.Second):
|
||||
return portalResponse{}, fmt.Errorf("timed out waiting for portal response to %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *portalWorker) createSession() error {
|
||||
sessionToken := w.nextToken("session")
|
||||
opts := map[string]dbus.Variant{
|
||||
"session_handle_token": dbus.MakeVariant(sessionToken),
|
||||
}
|
||||
resp, err := w.call("CreateSession", opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.code != 0 {
|
||||
return fmt.Errorf("CreateSession refused (response %d)", resp.code)
|
||||
}
|
||||
if v, ok := resp.results["session_handle"]; ok {
|
||||
if s, ok := v.Value().(string); ok {
|
||||
w.sessionPath = dbus.ObjectPath(s)
|
||||
}
|
||||
}
|
||||
if w.sessionPath == "" {
|
||||
return fmt.Errorf("portal did not return a session handle")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *portalWorker) bindShortcuts() {
|
||||
if w.sessionPath == "" {
|
||||
return
|
||||
}
|
||||
shortcuts := w.p.snapshot()
|
||||
|
||||
// Build the a(sa{sv}) shortcuts argument and refresh the id mapping.
|
||||
newShortcut := make(map[string]int, len(shortcuts))
|
||||
type entry struct {
|
||||
ID string
|
||||
Props map[string]dbus.Variant
|
||||
}
|
||||
list := make([]entry, 0, len(shortcuts))
|
||||
for _, s := range shortcuts {
|
||||
idStr := strconv.Itoa(s.id)
|
||||
newShortcut[idStr] = s.id
|
||||
props := map[string]dbus.Variant{
|
||||
"description": dbus.MakeVariant(s.desc),
|
||||
}
|
||||
if s.trigger != "" {
|
||||
props["preferred_trigger"] = dbus.MakeVariant(s.trigger)
|
||||
}
|
||||
list = append(list, entry{ID: idStr, Props: props})
|
||||
}
|
||||
w.mu.Lock()
|
||||
w.shortcut = newShortcut
|
||||
w.mu.Unlock()
|
||||
|
||||
resp, err := w.call("BindShortcuts", map[string]dbus.Variant{}, w.sessionPath, list, "")
|
||||
if err != nil {
|
||||
w.p.manager.app.handleError(fmt.Errorf("global shortcuts: BindShortcuts failed: %w", err))
|
||||
return
|
||||
}
|
||||
if resp.code != 0 {
|
||||
// response 1 = user cancelled the portal dialog, 2 = ended.
|
||||
w.p.manager.app.handleError(fmt.Errorf("global shortcuts: portal did not grant shortcuts (response %d); they will not fire", resp.code))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *portalWorker) handleSignal(sig *dbus.Signal) {
|
||||
switch {
|
||||
case strings.HasSuffix(sig.Name, ".Response"):
|
||||
w.mu.Lock()
|
||||
ch, ok := w.pending[sig.Path]
|
||||
w.mu.Unlock()
|
||||
if ok {
|
||||
var resp portalResponse
|
||||
if len(sig.Body) >= 1 {
|
||||
if code, ok := sig.Body[0].(uint32); ok {
|
||||
resp.code = code
|
||||
}
|
||||
}
|
||||
if len(sig.Body) >= 2 {
|
||||
if results, ok := sig.Body[1].(map[string]dbus.Variant); ok {
|
||||
resp.results = results
|
||||
}
|
||||
}
|
||||
select {
|
||||
case ch <- resp:
|
||||
default:
|
||||
}
|
||||
}
|
||||
case strings.HasSuffix(sig.Name, ".Activated"):
|
||||
// Activated(o session_handle, s shortcut_id, t timestamp, a{sv} options)
|
||||
if len(sig.Body) < 2 {
|
||||
return
|
||||
}
|
||||
idStr, ok := sig.Body[1].(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
w.mu.Lock()
|
||||
id, ok := w.shortcut[idStr]
|
||||
w.mu.Unlock()
|
||||
if ok {
|
||||
w.p.manager.dispatch(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// portalTrigger renders an accelerator as a portal "preferred_trigger" string.
|
||||
// The portal trigger syntax uses the modifier names CTRL, ALT, SHIFT and LOGO
|
||||
// joined to the key with "+". This is only a preference; the compositor may
|
||||
// bind a different combination.
|
||||
func portalTrigger(accel *accelerator) string {
|
||||
var parts []string
|
||||
hasCtrl, hasAlt, hasShift, hasSuper := false, false, false, false
|
||||
for _, m := range accel.Modifiers {
|
||||
switch m {
|
||||
case CmdOrCtrlKey, ControlKey:
|
||||
hasCtrl = true
|
||||
case OptionOrAltKey:
|
||||
hasAlt = true
|
||||
case ShiftKey:
|
||||
hasShift = true
|
||||
case SuperKey:
|
||||
hasSuper = true
|
||||
}
|
||||
}
|
||||
if hasCtrl {
|
||||
parts = append(parts, "CTRL")
|
||||
}
|
||||
if hasAlt {
|
||||
parts = append(parts, "ALT")
|
||||
}
|
||||
if hasShift {
|
||||
parts = append(parts, "SHIFT")
|
||||
}
|
||||
if hasSuper {
|
||||
parts = append(parts, "LOGO")
|
||||
}
|
||||
key, ok := x11KeysymNames[accel.Key]
|
||||
if !ok {
|
||||
key = accel.Key
|
||||
}
|
||||
parts = append(parts, key)
|
||||
return strings.Join(parts, "+")
|
||||
}
|
||||
364
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_linux_x11.go
generated
vendored
Normal file
364
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_linux_x11.go
generated
vendored
Normal file
@@ -0,0 +1,364 @@
|
||||
//go:build linux && cgo && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#cgo pkg-config: x11
|
||||
#include <X11/Xlib.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
// A grab error flag, set by the X error handler. Because every Xlib call this
|
||||
// file makes happens on a single goroutine (the event loop), no locking is
|
||||
// needed around it.
|
||||
static int gGrabError = 0;
|
||||
|
||||
static int grabErrorHandler(Display *d, XErrorEvent *e) {
|
||||
gGrabError = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Display *gsOpenDisplay(void) {
|
||||
Display *d = XOpenDisplay(NULL);
|
||||
if (d != NULL) {
|
||||
XSetErrorHandler(grabErrorHandler);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
static void gsCloseDisplay(Display *d) {
|
||||
if (d != NULL) {
|
||||
XCloseDisplay(d);
|
||||
}
|
||||
}
|
||||
|
||||
// gsKeycodeForName resolves an X keysym name (e.g. "a", "F1", "Return") to a
|
||||
// hardware keycode for this display. Returns 0 if unknown.
|
||||
static unsigned int gsKeycodeForName(Display *d, const char *name) {
|
||||
KeySym ks = XStringToKeysym(name);
|
||||
if (ks == NoSymbol) {
|
||||
return 0;
|
||||
}
|
||||
return (unsigned int)XKeysymToKeycode(d, ks);
|
||||
}
|
||||
|
||||
// The lock modifiers (CapsLock, NumLock) alter the event state, so each shortcut
|
||||
// must be grabbed for every combination of them or it will not fire while a
|
||||
// lock is engaged.
|
||||
static const unsigned int gsLockMasks[4] = {0, LockMask, Mod2Mask, LockMask | Mod2Mask};
|
||||
|
||||
// gsGrabKey grabs keycode+modmask (and every lock-mask variant) on the root
|
||||
// window. Returns 0 on success, -1 if the grab was refused (BadAccess), which
|
||||
// happens when another client already holds the combination.
|
||||
static int gsGrabKey(Display *d, unsigned int keycode, unsigned int modmask) {
|
||||
Window root = DefaultRootWindow(d);
|
||||
gGrabError = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
XGrabKey(d, keycode, modmask | gsLockMasks[i], root, False, GrabModeAsync, GrabModeAsync);
|
||||
}
|
||||
XSync(d, False);
|
||||
return gGrabError ? -1 : 0;
|
||||
}
|
||||
|
||||
static void gsUngrabKey(Display *d, unsigned int keycode, unsigned int modmask) {
|
||||
Window root = DefaultRootWindow(d);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
XUngrabKey(d, keycode, modmask | gsLockMasks[i], root);
|
||||
}
|
||||
XSync(d, False);
|
||||
}
|
||||
|
||||
// gsWaitForEvent blocks until either an X KeyPress arrives or wakeFd becomes
|
||||
// readable. Returns:
|
||||
// 1 -> a KeyPress; *keycode and *state are filled in
|
||||
// 0 -> woken via wakeFd (caller should service its request queue)
|
||||
// -1 -> the connection was lost
|
||||
static int gsWaitForEvent(Display *d, int wakeFd, unsigned int *keycode, unsigned int *state) {
|
||||
int xfd = ConnectionNumber(d);
|
||||
for (;;) {
|
||||
while (XPending(d) > 0) {
|
||||
XEvent ev;
|
||||
XNextEvent(d, &ev);
|
||||
if (ev.type == KeyPress) {
|
||||
*keycode = ev.xkey.keycode;
|
||||
*state = ev.xkey.state;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(xfd, &fds);
|
||||
FD_SET(wakeFd, &fds);
|
||||
int maxfd = xfd > wakeFd ? xfd : wakeFd;
|
||||
int r = select(maxfd + 1, &fds, NULL, NULL, NULL);
|
||||
if (r < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (FD_ISSET(wakeFd, &fds)) {
|
||||
char buf[64];
|
||||
while (read(wakeFd, buf, sizeof(buf)) > 0) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// X11 keyboard state mask bits (from X.h) that we treat as significant
|
||||
// modifiers. LockMask (CapsLock) and Mod2Mask (NumLock) are deliberately
|
||||
// excluded so that shortcuts fire regardless of those locks.
|
||||
const (
|
||||
x11ShiftMask = 1 << 0 // ShiftMask
|
||||
x11ControlMask = 1 << 2 // ControlMask
|
||||
x11Mod1Mask = 1 << 3 // Mod1Mask (Alt)
|
||||
x11Mod4Mask = 1 << 6 // Mod4Mask (Super)
|
||||
)
|
||||
|
||||
const x11SignificantMask = x11ShiftMask | x11ControlMask | x11Mod1Mask | x11Mod4Mask
|
||||
|
||||
// x11Binding records what was grabbed so it can be matched against incoming
|
||||
// events and ungrabbed later.
|
||||
type x11Binding struct {
|
||||
keycode uint
|
||||
modMask uint
|
||||
}
|
||||
|
||||
// x11GlobalShortcuts implements globalShortcutImpl on X11 using XGrabKey. All
|
||||
// Xlib calls are funnelled onto a single event-loop goroutine; register and
|
||||
// unregister hand work to it over opCh and wake it through a self-pipe. This
|
||||
// keeps Xlib single-threaded (no XInitThreads) while still letting the loop
|
||||
// block in select().
|
||||
type x11GlobalShortcuts struct {
|
||||
manager *GlobalShortcutManager
|
||||
|
||||
display *C.Display
|
||||
wakeR int
|
||||
wakeW int
|
||||
opCh chan func()
|
||||
|
||||
mu sync.RWMutex
|
||||
bindings map[int]x11Binding // id -> grabbed keycode/mask
|
||||
match map[x11Binding]int // keycode/mask -> id (for event lookup)
|
||||
startErr error
|
||||
}
|
||||
|
||||
func newX11GlobalShortcuts(manager *GlobalShortcutManager) globalShortcutImpl {
|
||||
g := &x11GlobalShortcuts{
|
||||
manager: manager,
|
||||
opCh: make(chan func(), 16),
|
||||
bindings: make(map[int]x11Binding),
|
||||
match: make(map[x11Binding]int),
|
||||
}
|
||||
|
||||
g.display = C.gsOpenDisplay()
|
||||
if g.display == nil {
|
||||
g.startErr = fmt.Errorf("could not open an X11 display (global shortcuts via X require an X11 session)")
|
||||
return g
|
||||
}
|
||||
|
||||
fds := make([]int, 2)
|
||||
if err := syscall.Pipe(fds); err != nil {
|
||||
C.gsCloseDisplay(g.display)
|
||||
g.display = nil
|
||||
g.startErr = fmt.Errorf("could not create wake pipe: %w", err)
|
||||
return g
|
||||
}
|
||||
g.wakeR, g.wakeW = fds[0], fds[1]
|
||||
syscall.SetNonblock(g.wakeR, true)
|
||||
syscall.SetNonblock(g.wakeW, true)
|
||||
|
||||
go g.eventLoop()
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *x11GlobalShortcuts) wake() {
|
||||
var b [1]byte
|
||||
_, _ = syscall.Write(g.wakeW, b[:])
|
||||
}
|
||||
|
||||
// run executes fn on the event-loop goroutine and waits for it to complete.
|
||||
func (g *x11GlobalShortcuts) run(fn func()) {
|
||||
done := make(chan struct{})
|
||||
g.opCh <- func() {
|
||||
fn()
|
||||
close(done)
|
||||
}
|
||||
g.wake()
|
||||
<-done
|
||||
}
|
||||
|
||||
func (g *x11GlobalShortcuts) eventLoop() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
for {
|
||||
var keycode, state C.uint
|
||||
r := C.gsWaitForEvent(g.display, C.int(g.wakeR), &keycode, &state)
|
||||
switch r {
|
||||
case 0:
|
||||
g.drainOps()
|
||||
case 1:
|
||||
b := x11Binding{keycode: uint(keycode), modMask: uint(state) & x11SignificantMask}
|
||||
g.mu.RLock()
|
||||
id, ok := g.match[b]
|
||||
g.mu.RUnlock()
|
||||
if ok {
|
||||
g.manager.dispatch(id)
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *x11GlobalShortcuts) drainOps() {
|
||||
for {
|
||||
select {
|
||||
case op := <-g.opCh:
|
||||
op()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *x11GlobalShortcuts) modMask(accel *accelerator) uint {
|
||||
var mask uint
|
||||
for _, m := range accel.Modifiers {
|
||||
switch m {
|
||||
case CmdOrCtrlKey, ControlKey:
|
||||
mask |= x11ControlMask
|
||||
case OptionOrAltKey:
|
||||
mask |= x11Mod1Mask
|
||||
case ShiftKey:
|
||||
mask |= x11ShiftMask
|
||||
case SuperKey:
|
||||
mask |= x11Mod4Mask
|
||||
}
|
||||
}
|
||||
return mask
|
||||
}
|
||||
|
||||
func (g *x11GlobalShortcuts) register(id int, accel *accelerator) error {
|
||||
if g.startErr != nil {
|
||||
return g.startErr
|
||||
}
|
||||
keysymName, ok := x11KeysymNames[accel.Key]
|
||||
if !ok {
|
||||
return fmt.Errorf("key %q is not supported as a global shortcut", accel.Key)
|
||||
}
|
||||
modMask := g.modMask(accel)
|
||||
|
||||
var regErr error
|
||||
var binding x11Binding
|
||||
g.run(func() {
|
||||
cname := C.CString(keysymName)
|
||||
defer C.free(unsafe.Pointer(cname))
|
||||
keycode := uint(C.gsKeycodeForName(g.display, cname))
|
||||
if keycode == 0 {
|
||||
regErr = fmt.Errorf("key %q has no keycode on this keyboard", accel.Key)
|
||||
return
|
||||
}
|
||||
if C.gsGrabKey(g.display, C.uint(keycode), C.uint(modMask)) != 0 {
|
||||
regErr = fmt.Errorf("the shortcut is already registered (possibly by another application)")
|
||||
return
|
||||
}
|
||||
binding = x11Binding{keycode: keycode, modMask: modMask}
|
||||
})
|
||||
if regErr != nil {
|
||||
return regErr
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
g.bindings[id] = binding
|
||||
g.match[binding] = id
|
||||
g.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *x11GlobalShortcuts) unregister(id int) error {
|
||||
g.mu.Lock()
|
||||
binding, ok := g.bindings[id]
|
||||
if ok {
|
||||
delete(g.bindings, id)
|
||||
delete(g.match, binding)
|
||||
}
|
||||
g.mu.Unlock()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
g.run(func() {
|
||||
C.gsUngrabKey(g.display, C.uint(binding.keycode), C.uint(binding.modMask))
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *x11GlobalShortcuts) unregisterAll() error {
|
||||
g.mu.Lock()
|
||||
bindings := g.bindings
|
||||
g.bindings = make(map[int]x11Binding)
|
||||
g.match = make(map[x11Binding]int)
|
||||
g.mu.Unlock()
|
||||
if len(bindings) == 0 {
|
||||
return nil
|
||||
}
|
||||
g.run(func() {
|
||||
for _, b := range bindings {
|
||||
C.gsUngrabKey(g.display, C.uint(b.keycode), C.uint(b.modMask))
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// x11KeysymNames maps Wails accelerator key names (already lower-cased by
|
||||
// parseAccelerator) to X keysym names accepted by XStringToKeysym. Letters and
|
||||
// digits map to themselves.
|
||||
var x11KeysymNames = map[string]string{
|
||||
"a": "a", "b": "b", "c": "c", "d": "d", "e": "e", "f": "f", "g": "g",
|
||||
"h": "h", "i": "i", "j": "j", "k": "k", "l": "l", "m": "m", "n": "n",
|
||||
"o": "o", "p": "p", "q": "q", "r": "r", "s": "s", "t": "t", "u": "u",
|
||||
"v": "v", "w": "w", "x": "x", "y": "y", "z": "z",
|
||||
"0": "0", "1": "1", "2": "2", "3": "3", "4": "4",
|
||||
"5": "5", "6": "6", "7": "7", "8": "8", "9": "9",
|
||||
// Punctuation
|
||||
";": "semicolon", "=": "equal", ",": "comma", "-": "minus", ".": "period",
|
||||
"/": "slash", "`": "grave", "[": "bracketleft", "\\": "backslash",
|
||||
"]": "bracketright", "'": "apostrophe", "+": "plus",
|
||||
// Named keys
|
||||
"backspace": "BackSpace",
|
||||
"tab": "Tab",
|
||||
"return": "Return",
|
||||
"enter": "Return",
|
||||
"escape": "Escape",
|
||||
"space": "space",
|
||||
"page up": "Prior",
|
||||
"page down": "Next",
|
||||
"end": "End",
|
||||
"home": "Home",
|
||||
"left": "Left",
|
||||
"up": "Up",
|
||||
"right": "Right",
|
||||
"down": "Down",
|
||||
"delete": "Delete",
|
||||
"numlock": "Num_Lock",
|
||||
// Function keys
|
||||
"f1": "F1", "f2": "F2", "f3": "F3", "f4": "F4", "f5": "F5", "f6": "F6",
|
||||
"f7": "F7", "f8": "F8", "f9": "F9", "f10": "F10", "f11": "F11", "f12": "F12",
|
||||
"f13": "F13", "f14": "F14", "f15": "F15", "f16": "F16", "f17": "F17",
|
||||
"f18": "F18", "f19": "F19", "f20": "F20", "f21": "F21", "f22": "F22",
|
||||
"f23": "F23", "f24": "F24",
|
||||
}
|
||||
294
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_manager.go
generated
vendored
Normal file
294
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_manager.go
generated
vendored
Normal file
@@ -0,0 +1,294 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// globalShortcutImpl is the platform-specific implementation of global
|
||||
// (system-wide) keyboard shortcuts. Each platform registers a shortcut with the
|
||||
// operating system against an integer id that the native event handler reports
|
||||
// back when the shortcut fires.
|
||||
//
|
||||
// All methods are called on the main thread (see GlobalShortcutManager which
|
||||
// wraps every call in InvokeSync*). Implementations must not assume otherwise.
|
||||
type globalShortcutImpl interface {
|
||||
// register asks the OS to bind the given accelerator to id. It returns an
|
||||
// error if the OS rejects the registration (for example, because another
|
||||
// application already owns the shortcut).
|
||||
register(id int, accel *accelerator) error
|
||||
// unregister releases the OS binding for id.
|
||||
unregister(id int) error
|
||||
// unregisterAll releases every binding owned by this application.
|
||||
unregisterAll() error
|
||||
}
|
||||
|
||||
// globalShortcut is a single registered shortcut.
|
||||
type globalShortcut struct {
|
||||
id int
|
||||
accelerator string // canonical, normalized accelerator string
|
||||
parsed *accelerator
|
||||
callback func()
|
||||
}
|
||||
|
||||
// GlobalShortcutManager manages application-wide (global) keyboard shortcuts.
|
||||
//
|
||||
// Unlike menu accelerators or [KeyBindingManager] - which only fire while a
|
||||
// Wails window has focus - a global shortcut fires regardless of which
|
||||
// application is currently focused, as long as the Wails application is
|
||||
// running.
|
||||
//
|
||||
// Global shortcuts are owned by the application, not by an individual window.
|
||||
// Registering the same accelerator twice within the same application is
|
||||
// reported as an error and the original binding is preserved; see [Register].
|
||||
//
|
||||
// Shortcuts may be registered before [App.Run] is called: the binding with the
|
||||
// operating system is then deferred until the application starts.
|
||||
type GlobalShortcutManager struct {
|
||||
app *App
|
||||
impl globalShortcutImpl
|
||||
|
||||
mu sync.Mutex
|
||||
started bool // set once the app is running and pending shortcuts are flushed
|
||||
byName map[string]*globalShortcut // keyed by canonical accelerator string
|
||||
byID map[int]*globalShortcut // keyed by native id
|
||||
pending []*globalShortcut // registered before the app started; bound on start
|
||||
nextID int
|
||||
}
|
||||
|
||||
// newGlobalShortcutManager creates a new GlobalShortcutManager instance.
|
||||
func newGlobalShortcutManager(app *App) *GlobalShortcutManager {
|
||||
return &GlobalShortcutManager{
|
||||
app: app,
|
||||
byName: make(map[string]*globalShortcut),
|
||||
byID: make(map[int]*globalShortcut),
|
||||
}
|
||||
}
|
||||
|
||||
// getImpl returns the platform implementation, creating it lazily on first use
|
||||
// so that platforms which do not support global shortcuts do not pay any cost
|
||||
// unless the feature is actually used. Callers must hold m.mu.
|
||||
func (m *GlobalShortcutManager) getImpl() globalShortcutImpl {
|
||||
if m.impl == nil {
|
||||
m.impl = newGlobalShortcutImpl(m)
|
||||
}
|
||||
return m.impl
|
||||
}
|
||||
|
||||
// Register binds the given accelerator (for example "Ctrl+Shift+P" or
|
||||
// "Cmd+Option+K") to callback. The callback is invoked - on its own goroutine -
|
||||
// whenever the shortcut is pressed, even when the application does not have
|
||||
// focus.
|
||||
//
|
||||
// Accelerators use the same syntax as menu accelerators (see SetAccelerator).
|
||||
// "CmdOrCtrl" resolves to Command on macOS and Control elsewhere.
|
||||
//
|
||||
// Register may be called before [App.Run]; the OS binding is then performed
|
||||
// when the application starts and any OS-level rejection is reported via the
|
||||
// application's error handler rather than returned here.
|
||||
//
|
||||
// Register returns an error when:
|
||||
// - the accelerator string cannot be parsed;
|
||||
// - the accelerator is already registered by this application (the existing
|
||||
// binding is left untouched - this is "error and preserve" semantics, see
|
||||
// the package documentation on conflicting shortcuts);
|
||||
// - the application is already running and the operating system rejects the
|
||||
// registration, typically because another application has already claimed
|
||||
// the shortcut. Behaviour in this case is platform dependent; see the
|
||||
// documentation.
|
||||
func (m *GlobalShortcutManager) Register(accelerator string, callback func()) error {
|
||||
if callback == nil {
|
||||
return fmt.Errorf("global shortcut callback must not be nil")
|
||||
}
|
||||
|
||||
parsed, err := parseAccelerator(accelerator)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid global shortcut %q: %w", accelerator, err)
|
||||
}
|
||||
name := parsed.String()
|
||||
|
||||
m.mu.Lock()
|
||||
if _, exists := m.byName[name]; exists {
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("global shortcut %q is already registered", name)
|
||||
}
|
||||
shortcut := &globalShortcut{
|
||||
id: m.nextID,
|
||||
accelerator: name,
|
||||
parsed: parsed,
|
||||
callback: callback,
|
||||
}
|
||||
// Reserve the id and slots before the (blocking) native call so that a
|
||||
// concurrent Register of the same accelerator loses the race cleanly.
|
||||
m.nextID++
|
||||
m.byName[name] = shortcut
|
||||
m.byID[shortcut.id] = shortcut
|
||||
|
||||
if !m.started {
|
||||
// The application is not running yet: defer the OS binding until start.
|
||||
m.pending = append(m.pending, shortcut)
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
impl := m.getImpl()
|
||||
m.mu.Unlock()
|
||||
|
||||
if regErr := InvokeSyncWithError(func() error {
|
||||
return impl.register(shortcut.id, parsed)
|
||||
}); regErr != nil {
|
||||
// Roll back the reservation so the accelerator can be retried later.
|
||||
m.mu.Lock()
|
||||
delete(m.byName, name)
|
||||
delete(m.byID, shortcut.id)
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("failed to register global shortcut %q: %w", name, regErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// flushPending binds every shortcut that was registered before the application
|
||||
// started. It is called once, on the main thread, during application startup.
|
||||
// OS-level rejections are reported through the application error handler since
|
||||
// the original Register caller has already returned.
|
||||
func (m *GlobalShortcutManager) flushPending() {
|
||||
m.mu.Lock()
|
||||
if m.started {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
m.started = true
|
||||
pending := m.pending
|
||||
m.pending = nil
|
||||
var impl globalShortcutImpl
|
||||
if len(pending) > 0 {
|
||||
impl = m.getImpl()
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, shortcut := range pending {
|
||||
if err := impl.register(shortcut.id, shortcut.parsed); err != nil {
|
||||
m.mu.Lock()
|
||||
// Only roll back if it is still the shortcut we registered (it may
|
||||
// have been Unregistered in the meantime).
|
||||
if current, ok := m.byID[shortcut.id]; ok && current == shortcut {
|
||||
delete(m.byName, shortcut.accelerator)
|
||||
delete(m.byID, shortcut.id)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
m.app.handleError(fmt.Errorf("failed to register global shortcut %q: %w", shortcut.accelerator, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister releases the given accelerator. It returns an error if the
|
||||
// accelerator is not currently registered or if the OS rejects the request.
|
||||
func (m *GlobalShortcutManager) Unregister(accelerator string) error {
|
||||
parsed, err := parseAccelerator(accelerator)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid global shortcut %q: %w", accelerator, err)
|
||||
}
|
||||
name := parsed.String()
|
||||
|
||||
m.mu.Lock()
|
||||
shortcut, exists := m.byName[name]
|
||||
if !exists {
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("global shortcut %q is not registered", name)
|
||||
}
|
||||
delete(m.byName, name)
|
||||
delete(m.byID, shortcut.id)
|
||||
started := m.started
|
||||
impl := m.getImpl()
|
||||
m.mu.Unlock()
|
||||
|
||||
if !started {
|
||||
// Never bound with the OS yet; just drop it from the pending list.
|
||||
m.removePending(shortcut)
|
||||
return nil
|
||||
}
|
||||
return InvokeSyncWithError(func() error {
|
||||
return impl.unregister(shortcut.id)
|
||||
})
|
||||
}
|
||||
|
||||
// removePending drops a shortcut from the pending queue (used when a shortcut is
|
||||
// unregistered before the application has started).
|
||||
func (m *GlobalShortcutManager) removePending(shortcut *globalShortcut) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for i, p := range m.pending {
|
||||
if p == shortcut {
|
||||
m.pending = append(m.pending[:i], m.pending[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnregisterAll releases every global shortcut registered by this application.
|
||||
// It is called automatically during application shutdown.
|
||||
func (m *GlobalShortcutManager) UnregisterAll() error {
|
||||
m.mu.Lock()
|
||||
hadShortcuts := len(m.byName) > 0
|
||||
m.byName = make(map[string]*globalShortcut)
|
||||
m.byID = make(map[int]*globalShortcut)
|
||||
m.pending = nil
|
||||
impl := m.impl
|
||||
started := m.started
|
||||
m.mu.Unlock()
|
||||
|
||||
// Nothing was ever bound with the OS: avoid forcing the platform impl into
|
||||
// existence just to tear nothing down.
|
||||
if impl == nil || !started || !hadShortcuts {
|
||||
return nil
|
||||
}
|
||||
return InvokeSyncWithError(impl.unregisterAll)
|
||||
}
|
||||
|
||||
// IsRegistered reports whether the given accelerator is currently registered by
|
||||
// this application. It returns false for accelerators that cannot be parsed.
|
||||
//
|
||||
// On Wayland the returned value reflects what the application requested, not
|
||||
// necessarily what the compositor ultimately bound; see the package
|
||||
// documentation on the global shortcuts portal.
|
||||
func (m *GlobalShortcutManager) IsRegistered(accelerator string) bool {
|
||||
parsed, err := parseAccelerator(accelerator)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
_, exists := m.byName[parsed.String()]
|
||||
return exists
|
||||
}
|
||||
|
||||
// GetAll returns the canonical accelerator strings of all shortcuts currently
|
||||
// registered by this application, sorted for stable output.
|
||||
func (m *GlobalShortcutManager) GetAll() []string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
result := make([]string, 0, len(m.byName))
|
||||
for name := range m.byName {
|
||||
result = append(result, name)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// dispatch is called by the platform implementation (from the native event
|
||||
// handler) when a shortcut with the given id fires. The user callback is run on
|
||||
// its own goroutine so that it cannot block the platform's main event loop -
|
||||
// callbacks that need to touch the UI should marshal onto the main thread
|
||||
// themselves (for example via InvokeSync).
|
||||
func (m *GlobalShortcutManager) dispatch(id int) {
|
||||
m.mu.Lock()
|
||||
shortcut, ok := m.byID[id]
|
||||
m.mu.Unlock()
|
||||
if !ok || shortcut.callback == nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
shortcut.callback()
|
||||
}()
|
||||
}
|
||||
23
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_unsupported.go
generated
vendored
Normal file
23
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_unsupported.go
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
//go:build ios || android || server || (linux && !cgo)
|
||||
|
||||
package application
|
||||
|
||||
import "errors"
|
||||
|
||||
// errGlobalShortcutsUnsupported is returned on platforms where system-wide
|
||||
// global shortcuts are not available (mobile, headless/server builds).
|
||||
var errGlobalShortcutsUnsupported = errors.New("global shortcuts are not supported on this platform")
|
||||
|
||||
type unsupportedGlobalShortcuts struct{}
|
||||
|
||||
func newGlobalShortcutImpl(_ *GlobalShortcutManager) globalShortcutImpl {
|
||||
return &unsupportedGlobalShortcuts{}
|
||||
}
|
||||
|
||||
func (unsupportedGlobalShortcuts) register(_ int, _ *accelerator) error {
|
||||
return errGlobalShortcutsUnsupported
|
||||
}
|
||||
|
||||
func (unsupportedGlobalShortcuts) unregister(_ int) error { return nil }
|
||||
|
||||
func (unsupportedGlobalShortcuts) unregisterAll() error { return nil }
|
||||
141
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_windows.go
generated
vendored
Normal file
141
vendor/github.com/wailsapp/wails/v3/pkg/application/global_shortcut_windows.go
generated
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
//go:build windows && !server
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
)
|
||||
|
||||
// windowsGlobalShortcuts implements globalShortcutImpl using the Win32
|
||||
// RegisterHotKey API. Hot keys are registered against the application's hidden
|
||||
// main-thread window so that WM_HOTKEY messages are delivered to the same
|
||||
// message loop the rest of the application already pumps (see wndProc's
|
||||
// WM_HOTKEY case, which calls back into the manager's dispatch).
|
||||
//
|
||||
// RegisterHotKey is thread-affine: the WM_HOTKEY message is posted to the
|
||||
// thread that owns the window passed in. Registration therefore must happen on
|
||||
// the main UI thread - the GlobalShortcutManager guarantees this by wrapping
|
||||
// every call in InvokeSync.
|
||||
type windowsGlobalShortcuts struct {
|
||||
manager *GlobalShortcutManager
|
||||
ids map[int]struct{}
|
||||
}
|
||||
|
||||
func newGlobalShortcutImpl(manager *GlobalShortcutManager) globalShortcutImpl {
|
||||
return &windowsGlobalShortcuts{
|
||||
manager: manager,
|
||||
ids: make(map[int]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *windowsGlobalShortcuts) hwnd() (w32.HWND, error) {
|
||||
app, ok := globalApplication.impl.(*windowsApp)
|
||||
if !ok || app == nil || app.mainThreadWindowHWND == 0 {
|
||||
return 0, fmt.Errorf("global shortcuts require the application to be running")
|
||||
}
|
||||
return app.mainThreadWindowHWND, nil
|
||||
}
|
||||
|
||||
func (g *windowsGlobalShortcuts) register(id int, accel *accelerator) error {
|
||||
vk, ok := winKeyCodes[accel.Key]
|
||||
if !ok {
|
||||
return fmt.Errorf("key %q is not supported as a global shortcut on Windows", accel.Key)
|
||||
}
|
||||
|
||||
// MOD_NOREPEAT prevents auto-repeat from spamming the callback while the
|
||||
// keys are held down.
|
||||
mods := uint(w32.MOD_NOREPEAT)
|
||||
for _, m := range accel.Modifiers {
|
||||
switch m {
|
||||
case CmdOrCtrlKey, ControlKey:
|
||||
mods |= w32.MOD_CONTROL
|
||||
case OptionOrAltKey:
|
||||
mods |= w32.MOD_ALT
|
||||
case ShiftKey:
|
||||
mods |= w32.MOD_SHIFT
|
||||
case SuperKey:
|
||||
mods |= w32.MOD_WIN
|
||||
}
|
||||
}
|
||||
|
||||
hwnd, err := g.hwnd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !w32.RegisterHotKey(hwnd, id, mods, vk) {
|
||||
// RegisterHotKey returns false (ERROR_HOTKEY_ALREADY_REGISTERED) when
|
||||
// the combination is already owned - either by this process or, more
|
||||
// commonly, by another application.
|
||||
return fmt.Errorf("the shortcut is already registered (possibly by another application)")
|
||||
}
|
||||
g.ids[id] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *windowsGlobalShortcuts) unregister(id int) error {
|
||||
if _, ok := g.ids[id]; !ok {
|
||||
return nil
|
||||
}
|
||||
delete(g.ids, id)
|
||||
hwnd, err := g.hwnd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !w32.UnregisterHotKey(hwnd, id) {
|
||||
return fmt.Errorf("UnregisterHotKey failed for shortcut id %d", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *windowsGlobalShortcuts) unregisterAll() error {
|
||||
var firstErr error
|
||||
for id := range g.ids {
|
||||
if err := g.unregister(id); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// winKeyCodes maps Wails accelerator key names (already lower-cased by
|
||||
// parseAccelerator) to Windows virtual-key codes. Letters and digits map to
|
||||
// their ASCII-uppercase value (VK_A == 'A' == 0x41, VK_0 == '0' == 0x30).
|
||||
var winKeyCodes = map[string]uint{
|
||||
// Letters
|
||||
"a": 0x41, "b": 0x42, "c": 0x43, "d": 0x44, "e": 0x45, "f": 0x46,
|
||||
"g": 0x47, "h": 0x48, "i": 0x49, "j": 0x4A, "k": 0x4B, "l": 0x4C,
|
||||
"m": 0x4D, "n": 0x4E, "o": 0x4F, "p": 0x50, "q": 0x51, "r": 0x52,
|
||||
"s": 0x53, "t": 0x54, "u": 0x55, "v": 0x56, "w": 0x57, "x": 0x58,
|
||||
"y": 0x59, "z": 0x5A,
|
||||
// Number row
|
||||
"0": 0x30, "1": 0x31, "2": 0x32, "3": 0x33, "4": 0x34,
|
||||
"5": 0x35, "6": 0x36, "7": 0x37, "8": 0x38, "9": 0x39,
|
||||
// Punctuation (OEM keys, US layout)
|
||||
";": 0xBA, "=": 0xBB, ",": 0xBC, "-": 0xBD, ".": 0xBE, "/": 0xBF,
|
||||
"`": 0xC0, "[": 0xDB, "\\": 0xDC, "]": 0xDD, "'": 0xDE, "+": 0xBB,
|
||||
// Named keys
|
||||
"backspace": 0x08,
|
||||
"tab": 0x09,
|
||||
"return": 0x0D,
|
||||
"enter": 0x0D,
|
||||
"escape": 0x1B,
|
||||
"space": 0x20,
|
||||
"page up": 0x21,
|
||||
"page down": 0x22,
|
||||
"end": 0x23,
|
||||
"home": 0x24,
|
||||
"left": 0x25,
|
||||
"up": 0x26,
|
||||
"right": 0x27,
|
||||
"down": 0x28,
|
||||
"delete": 0x2E,
|
||||
"numlock": 0x90,
|
||||
// Function keys
|
||||
"f1": 0x70, "f2": 0x71, "f3": 0x72, "f4": 0x73, "f5": 0x74, "f6": 0x75,
|
||||
"f7": 0x76, "f8": 0x77, "f9": 0x78, "f10": 0x79, "f11": 0x7A, "f12": 0x7B,
|
||||
"f13": 0x7C, "f14": 0x7D, "f15": 0x7E, "f16": 0x7F, "f17": 0x80, "f18": 0x81,
|
||||
"f19": 0x82, "f20": 0x83, "f21": 0x84, "f22": 0x85, "f23": 0x86, "f24": 0x87,
|
||||
}
|
||||
10
vendor/github.com/wailsapp/wails/v3/pkg/application/gtkdispatch_linux.go
generated
vendored
Normal file
10
vendor/github.com/wailsapp/wails/v3/pkg/application/gtkdispatch_linux.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
//go:build linux && !gtk3 && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
func gtkDispatch(fn func()) {
|
||||
go func() {
|
||||
defer handlePanic()
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
7
vendor/github.com/wailsapp/wails/v3/pkg/application/gtkdispatch_linux_gtk3.go
generated
vendored
Normal file
7
vendor/github.com/wailsapp/wails/v3/pkg/application/gtkdispatch_linux_gtk3.go
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build linux && gtk3 && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
func gtkDispatch(fn func()) {
|
||||
InvokeAsync(fn)
|
||||
}
|
||||
22
vendor/github.com/wailsapp/wails/v3/pkg/application/icon_windows.go
generated
vendored
Normal file
22
vendor/github.com/wailsapp/wails/v3/pkg/application/icon_windows.go
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
//go:build windows
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
)
|
||||
|
||||
// NewIconFromResource loads an icon from an embedded Windows resource. It is
|
||||
// available in both desktop and server builds so that services which only need
|
||||
// the icon helper (e.g. notifications) compile under the `server` tag, matching
|
||||
// the behaviour on macOS and Linux.
|
||||
func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error) {
|
||||
var err error
|
||||
var result w32.HICON
|
||||
if result = w32.LoadIconWithResourceID(instance, resId); result == 0 {
|
||||
err = fmt.Errorf("cannot load icon from resource with id %v", resId)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
37
vendor/github.com/wailsapp/wails/v3/pkg/application/image.go
generated
vendored
Normal file
37
vendor/github.com/wailsapp/wails/v3/pkg/application/image.go
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
func pngToImage(data []byte) (*image.RGBA, error) {
|
||||
img, err := png.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
rgba := image.NewRGBA(bounds)
|
||||
draw.Draw(rgba, bounds, img, bounds.Min, draw.Src)
|
||||
return rgba, nil
|
||||
}
|
||||
|
||||
func ToARGB(img *image.RGBA) (int, int, []byte) {
|
||||
w, h := img.Bounds().Dx(), img.Bounds().Dy()
|
||||
data := make([]byte, w*h*4)
|
||||
i := 0
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
r, g, b, a := img.At(x, y).RGBA()
|
||||
data[i] = byte(a)
|
||||
data[i+1] = byte(r)
|
||||
data[i+2] = byte(g)
|
||||
data[i+3] = byte(b)
|
||||
i += 4
|
||||
}
|
||||
}
|
||||
return w, h, data
|
||||
}
|
||||
9
vendor/github.com/wailsapp/wails/v3/pkg/application/init_android.go
generated
vendored
Normal file
9
vendor/github.com/wailsapp/wails/v3/pkg/application/init_android.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build android
|
||||
|
||||
package application
|
||||
|
||||
func init() {
|
||||
// On Android, we don't call runtime.LockOSThread()
|
||||
// The Android runtime handles thread management via JNI
|
||||
// and calling LockOSThread can interfere with the JNI environment
|
||||
}
|
||||
11
vendor/github.com/wailsapp/wails/v3/pkg/application/init_desktop.go
generated
vendored
Normal file
11
vendor/github.com/wailsapp/wails/v3/pkg/application/init_desktop.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !ios
|
||||
|
||||
package application
|
||||
|
||||
import "runtime"
|
||||
|
||||
func init() {
|
||||
// Lock the main thread for desktop platforms
|
||||
// This ensures UI operations happen on the main thread
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
9
vendor/github.com/wailsapp/wails/v3/pkg/application/init_ios.go
generated
vendored
Normal file
9
vendor/github.com/wailsapp/wails/v3/pkg/application/init_ios.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build ios
|
||||
|
||||
package application
|
||||
|
||||
func init() {
|
||||
// On iOS, we don't call runtime.LockOSThread()
|
||||
// The iOS runtime handles thread management differently
|
||||
// and calling LockOSThread can interfere with signal handling
|
||||
}
|
||||
52
vendor/github.com/wailsapp/wails/v3/pkg/application/inline_event_shim.go
generated
vendored
Normal file
52
vendor/github.com/wailsapp/wails/v3/pkg/application/inline_event_shim.go
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// inlineEventShimJS is a small ES5 script that installs
|
||||
// `window.wails.Events.On / Emit` and `window._wails.dispatchWailsEvent`
|
||||
// for windows whose HTML is loaded directly via WebviewWindowOptions.HTML
|
||||
// rather than served by the asset server. Those pages can't import
|
||||
// `/wails/runtime.js` (their origin is the literal string "null"), so
|
||||
// the framework injects this fallback at construction time when the
|
||||
// owning code asked for it.
|
||||
//
|
||||
// Injection is gated on WebviewWindowOptions.AllowSimpleEventEmit so the
|
||||
// shim only ships into windows that have opted into the simple postMessage
|
||||
// emit path in the first place — same security boundary that gates the
|
||||
// host-side handler. See the field's GoDoc for the threat model.
|
||||
//
|
||||
//go:embed inline_event_shim.js
|
||||
var inlineEventShimJS string
|
||||
|
||||
// maybeInjectInlineEventShim prepends the inline-event shim to the
|
||||
// supplied HTML when the window has opted in via AllowSimpleEventEmit.
|
||||
// Returns the (possibly modified) HTML unchanged otherwise.
|
||||
//
|
||||
// The injected `<script>` is placed at the very top of the document so
|
||||
// that any inline event handlers registered later in the page can rely
|
||||
// on `window.wails.Events` being present.
|
||||
func maybeInjectInlineEventShim(html string, allow bool) string {
|
||||
if !allow || html == "" {
|
||||
return html
|
||||
}
|
||||
const marker = "data-wails-inline-event-shim"
|
||||
if strings.Contains(html, marker) {
|
||||
// Already injected (e.g. caller wrapped manually). Don't duplicate.
|
||||
return html
|
||||
}
|
||||
script := `<script ` + marker + `>` + "\n" + inlineEventShimJS + "\n</script>\n"
|
||||
// If the body starts with <!doctype …>, keep the doctype on line 1 and
|
||||
// drop the script immediately after; otherwise just prepend.
|
||||
lower := strings.ToLower(html)
|
||||
if idx := strings.Index(lower, "<!doctype"); idx >= 0 {
|
||||
closing := strings.Index(html[idx:], ">")
|
||||
if closing >= 0 {
|
||||
cut := idx + closing + 1
|
||||
return html[:cut] + "\n" + script + html[cut:]
|
||||
}
|
||||
}
|
||||
return script + html
|
||||
}
|
||||
67
vendor/github.com/wailsapp/wails/v3/pkg/application/inline_event_shim.js
generated
vendored
Normal file
67
vendor/github.com/wailsapp/wails/v3/pkg/application/inline_event_shim.js
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
// Inline Wails events shim for InitialHTML windows.
|
||||
//
|
||||
// Pages loaded via WebviewWindowOptions.HTML are served with
|
||||
// `window.location.origin === "null"`, so the modern HTTP runtime at
|
||||
// /wails/runtime.js can never be imported (fetch fails). This shim
|
||||
// installs the minimum subset of the runtime that postMessage-based
|
||||
// custom-event traffic needs:
|
||||
//
|
||||
// * window._wails.dispatchWailsEvent — the framework calls this when
|
||||
// the host emits a custom event to the page.
|
||||
// * window.wails.Events.On(name, cb) — the page subscribes.
|
||||
// * window.wails.Events.Emit(name) — the page fires a bare-name
|
||||
// event back to the host. Routed through
|
||||
// `window._wails.invoke("wails:event:emit:" + name)` which the
|
||||
// framework forwards if the owning window has
|
||||
// WebviewWindowOptions.AllowSimpleEventEmit set.
|
||||
//
|
||||
// Once the platform layer has injected window._wails.invoke the shim
|
||||
// fires `wails:runtime:ready` so any pending host-side queued events
|
||||
// flush. If the modern HTTP runtime later loads it will overwrite
|
||||
// window.wails.Events with its richer implementation — that's fine,
|
||||
// our subset is the floor not the ceiling.
|
||||
//
|
||||
// Kept in ES5-compatible syntax (no const/let/arrow) so older WebView
|
||||
// engines that may still surface in unusual platform configurations
|
||||
// don't fail on parse.
|
||||
(function () {
|
||||
var w = window._wails = window._wails || {};
|
||||
if (window.wails && window.wails.Events) {
|
||||
return; // a full runtime is already in scope
|
||||
}
|
||||
var listeners = Object.create(null);
|
||||
w.dispatchWailsEvent = w.dispatchWailsEvent || function (ev) {
|
||||
if (!ev || !ev.name) return;
|
||||
var cbs = listeners[ev.name];
|
||||
if (!cbs) return;
|
||||
for (var i = 0; i < cbs.length; i++) {
|
||||
try { cbs[i](ev); } catch (_) { /* swallow handler errors */ }
|
||||
}
|
||||
};
|
||||
window.wails = window.wails || {};
|
||||
window.wails.Events = {
|
||||
On: function (name, cb) {
|
||||
(listeners[name] = listeners[name] || []).push(cb);
|
||||
return function () {
|
||||
var arr = listeners[name];
|
||||
if (!arr) return;
|
||||
var i = arr.indexOf(cb);
|
||||
if (i >= 0) arr.splice(i, 1);
|
||||
};
|
||||
},
|
||||
Emit: function (eventOrName) {
|
||||
var name = (typeof eventOrName === "string")
|
||||
? eventOrName
|
||||
: (eventOrName && eventOrName.name);
|
||||
if (!name || typeof w.invoke !== "function") return;
|
||||
w.invoke("wails:event:emit:" + name);
|
||||
},
|
||||
};
|
||||
(function ready() {
|
||||
if (typeof w.invoke === "function") {
|
||||
w.invoke("wails:runtime:ready");
|
||||
} else {
|
||||
setTimeout(ready, 30);
|
||||
}
|
||||
})();
|
||||
})();
|
||||
8
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_logging_debug.go
generated
vendored
Normal file
8
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_logging_debug.go
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build ios && !production
|
||||
|
||||
package application
|
||||
|
||||
// iosVerboseLogging enables the framework's internal iOS diagnostics
|
||||
// (request tracing, bridge messages, lifecycle markers). Debug builds only;
|
||||
// production builds compile these call sites away via the constant.
|
||||
const iosVerboseLogging = true
|
||||
5
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_logging_production.go
generated
vendored
Normal file
5
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_logging_production.go
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
//go:build ios && production
|
||||
|
||||
package application
|
||||
|
||||
const iosVerboseLogging = false
|
||||
16
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_runtime_api.go
generated
vendored
Normal file
16
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_runtime_api.go
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
//go:build ios
|
||||
|
||||
package application
|
||||
|
||||
// Exported API for use by applications to mutate iOS WKWebView at runtime.
|
||||
// These call into the internal platform-specific implementations.
|
||||
|
||||
func (iosManager) SetScrollEnabled(enabled bool) { iosSetScrollEnabled(enabled) }
|
||||
func (iosManager) SetBounceEnabled(enabled bool) { iosSetBounceEnabled(enabled) }
|
||||
func (iosManager) SetScrollIndicatorsEnabled(enabled bool) { iosSetScrollIndicatorsEnabled(enabled) }
|
||||
func (iosManager) SetBackForwardGesturesEnabled(enabled bool) {
|
||||
iosSetBackForwardGesturesEnabled(enabled)
|
||||
}
|
||||
func (iosManager) SetLinkPreviewEnabled(enabled bool) { iosSetLinkPreviewEnabled(enabled) }
|
||||
func (iosManager) SetInspectableEnabled(enabled bool) { iosSetInspectableEnabled(enabled) }
|
||||
func (iosManager) SetCustomUserAgent(ua string) { iosSetCustomUserAgent(ua) }
|
||||
79
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_runtime_ios.go
generated
vendored
Normal file
79
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_runtime_ios.go
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
//go:build ios
|
||||
|
||||
package application
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c -fmodules -fobjc-arc
|
||||
#cgo LDFLAGS: -framework UIKit
|
||||
#include <stdlib.h>
|
||||
#include "application_ios.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// iosHapticsImpact triggers an iOS haptic impact using the provided style.
|
||||
// The style parameter specifies the impact style name understood by the native haptic engine.
|
||||
func iosHapticsImpact(style string) {
|
||||
cstr := C.CString(style)
|
||||
defer C.free(unsafe.Pointer(cstr))
|
||||
C.ios_haptics_impact(cstr)
|
||||
}
|
||||
|
||||
type deviceInfo struct {
|
||||
Model string `json:"model"`
|
||||
SystemName string `json:"systemName"`
|
||||
SystemVersion string `json:"systemVersion"`
|
||||
IsSimulator bool `json:"isSimulator"`
|
||||
}
|
||||
|
||||
func iosDeviceInfo() deviceInfo {
|
||||
ptr := C.ios_device_info_json()
|
||||
if ptr == nil {
|
||||
return deviceInfo{}
|
||||
}
|
||||
defer C.free(unsafe.Pointer(ptr))
|
||||
goStr := C.GoString(ptr)
|
||||
var out deviceInfo
|
||||
_ = json.Unmarshal([]byte(goStr), &out)
|
||||
return out
|
||||
}
|
||||
|
||||
// iosSetScrollEnabled sets whether scrolling is enabled in the iOS runtime.
|
||||
func iosSetScrollEnabled(enabled bool) { C.ios_runtime_set_scroll_enabled(C.bool(enabled)) }
|
||||
// iosSetBounceEnabled sets whether scroll bounce (rubber-band) behavior is enabled at runtime.
|
||||
// If enabled is true, scrollable content will bounce when pulled past its edges; if false, that bounce is disabled.
|
||||
func iosSetBounceEnabled(enabled bool) { C.ios_runtime_set_bounce_enabled(C.bool(enabled)) }
|
||||
// iosSetScrollIndicatorsEnabled configures whether the iOS runtime shows scroll indicators.
|
||||
// The enabled parameter controls visibility: true shows indicators, false hides them.
|
||||
func iosSetScrollIndicatorsEnabled(enabled bool) {
|
||||
C.ios_runtime_set_scroll_indicators_enabled(C.bool(enabled))
|
||||
}
|
||||
// iosSetBackForwardGesturesEnabled enables back-forward navigation gestures when enabled is true and disables them when enabled is false.
|
||||
func iosSetBackForwardGesturesEnabled(enabled bool) {
|
||||
C.ios_runtime_set_back_forward_gestures_enabled(C.bool(enabled))
|
||||
}
|
||||
// iosSetLinkPreviewEnabled sets whether link previews are enabled in the iOS runtime.
|
||||
// Pass true to enable link previews, false to disable them.
|
||||
func iosSetLinkPreviewEnabled(enabled bool) { C.ios_runtime_set_link_preview_enabled(C.bool(enabled)) }
|
||||
// iosSetInspectableEnabled sets whether runtime web content inspection is enabled.
|
||||
// When enabled is true the runtime allows inspection of web content; when false inspection is disabled.
|
||||
func iosSetInspectableEnabled(enabled bool) { C.ios_runtime_set_inspectable_enabled(C.bool(enabled)) }
|
||||
// iosSetCustomUserAgent sets the runtime's custom User-Agent string.
|
||||
// If ua is an empty string, the custom User-Agent is cleared.
|
||||
func iosSetCustomUserAgent(ua string) {
|
||||
var cstr *C.char
|
||||
if ua != "" {
|
||||
cstr = C.CString(ua)
|
||||
defer C.free(unsafe.Pointer(cstr))
|
||||
}
|
||||
C.ios_runtime_set_custom_user_agent(cstr)
|
||||
}
|
||||
|
||||
// Native tabs
|
||||
func iosSetNativeTabsEnabled(enabled bool) { C.ios_native_tabs_set_enabled(C.bool(enabled)) }
|
||||
func iosNativeTabsIsEnabled() bool { return bool(C.ios_native_tabs_is_enabled()) }
|
||||
func iosSelectNativeTab(index int) { C.ios_native_tabs_select_index(C.int(index)) }
|
||||
27
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_runtime_stub.go
generated
vendored
Normal file
27
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_runtime_stub.go
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
//go:build !ios
|
||||
|
||||
package application
|
||||
|
||||
func iosHapticsImpact(style string) {
|
||||
// no-op on non-iOS
|
||||
}
|
||||
|
||||
type deviceInfo struct {
|
||||
Model string `json:"model"`
|
||||
SystemName string `json:"systemName"`
|
||||
SystemVersion string `json:"systemVersion"`
|
||||
IsSimulator bool `json:"isSimulator"`
|
||||
}
|
||||
|
||||
func iosDeviceInfo() deviceInfo {
|
||||
return deviceInfo{}
|
||||
}
|
||||
|
||||
// Live mutation stubs
|
||||
func iosSetScrollEnabled(enabled bool) {}
|
||||
func iosSetBounceEnabled(enabled bool) {}
|
||||
func iosSetScrollIndicatorsEnabled(enabled bool) {}
|
||||
func iosSetBackForwardGesturesEnabled(enabled bool) {}
|
||||
func iosSetLinkPreviewEnabled(enabled bool) {}
|
||||
func iosSetInspectableEnabled(enabled bool) {}
|
||||
func iosSetCustomUserAgent(ua string) {}
|
||||
128
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_system_events.m
generated
vendored
Normal file
128
vendor/github.com/wailsapp/wails/v3/pkg/application/ios_system_events.m
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
//go:build ios
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Network/Network.h>
|
||||
#import "application_ios.h"
|
||||
#import "../events/events_ios.h"
|
||||
|
||||
// processApplicationEvent is a Go //export (application_ios.go). It takes an
|
||||
// event ID and an optional data pointer; we pass a JSON object string, which Go
|
||||
// decodes into the ApplicationEvent context. Declared locally (matching the
|
||||
// other .m files) so we don't pull in cgo's _cgo_export.h.
|
||||
extern void processApplicationEvent(unsigned int eventID, void* data);
|
||||
|
||||
// Emit an application event with an optional JSON payload string.
|
||||
static void emitEvent(unsigned int eventID, NSString *json) {
|
||||
processApplicationEvent(eventID, json ? (void *)[json UTF8String] : NULL);
|
||||
}
|
||||
|
||||
// ---- Battery -------------------------------------------------------------
|
||||
|
||||
static NSString *batteryStateString(UIDeviceBatteryState s) {
|
||||
switch (s) {
|
||||
case UIDeviceBatteryStateCharging: return @"charging";
|
||||
case UIDeviceBatteryStateFull: return @"full";
|
||||
case UIDeviceBatteryStateUnplugged: return @"unplugged";
|
||||
default: return @"unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void emitBattery(void) {
|
||||
UIDevice *dev = [UIDevice currentDevice];
|
||||
float level = dev.batteryLevel; // 0..1, or -1 when unknown (e.g. Simulator)
|
||||
NSString *state = batteryStateString(dev.batteryState);
|
||||
BOOL lowPower = [NSProcessInfo processInfo].lowPowerModeEnabled;
|
||||
emitEvent(EventBatteryChanged, [NSString stringWithFormat:
|
||||
@"{\"level\":%.2f,\"state\":\"%@\",\"lowPowerMode\":%@}",
|
||||
level, state, lowPower ? @"true" : @"false"]);
|
||||
}
|
||||
|
||||
// ---- Theme ---------------------------------------------------------------
|
||||
// "isDarkMode" matches the key the desktop platforms set (ApplicationEventContext.IsDarkMode()).
|
||||
|
||||
static void emitTheme(void) {
|
||||
emitEvent(EventThemeChanged, ios_is_dark_mode() ? @"{\"isDarkMode\":true}" : @"{\"isDarkMode\":false}");
|
||||
}
|
||||
|
||||
// ---- Network (NWPathMonitor) --------------------------------------------
|
||||
|
||||
static nw_path_monitor_t g_pathMonitor = nil;
|
||||
|
||||
static void emitNetwork(nw_path_t path) {
|
||||
BOOL connected = (nw_path_get_status(path) == nw_path_status_satisfied);
|
||||
NSString *type = @"none";
|
||||
if (connected) {
|
||||
if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) {
|
||||
type = @"wifi";
|
||||
} else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) {
|
||||
type = @"cellular";
|
||||
} else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) {
|
||||
type = @"wired";
|
||||
} else {
|
||||
type = @"other";
|
||||
}
|
||||
}
|
||||
BOOL expensive = nw_path_is_expensive(path); // cellular / hotspot
|
||||
BOOL constrained = NO; // Low Data Mode
|
||||
if (@available(iOS 13.0, *)) {
|
||||
constrained = nw_path_is_constrained(path);
|
||||
}
|
||||
emitEvent(EventNetworkChanged, [NSString stringWithFormat:
|
||||
@"{\"connected\":%@,\"type\":\"%@\",\"expensive\":%@,\"constrained\":%@}",
|
||||
connected ? @"true" : @"false", type,
|
||||
expensive ? @"true" : @"false", constrained ? @"true" : @"false"]);
|
||||
}
|
||||
|
||||
// ---- Setup ---------------------------------------------------------------
|
||||
|
||||
void ios_start_system_event_monitors(void) {
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
||||
|
||||
// Battery: monitoring must be enabled before level/state are valid.
|
||||
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
|
||||
[nc addObserverForName:UIDeviceBatteryLevelDidChangeNotification
|
||||
object:nil queue:nil
|
||||
usingBlock:^(NSNotification *n){ emitBattery(); }];
|
||||
[nc addObserverForName:UIDeviceBatteryStateDidChangeNotification
|
||||
object:nil queue:nil
|
||||
usingBlock:^(NSNotification *n){ emitBattery(); }];
|
||||
[nc addObserverForName:NSProcessInfoPowerStateDidChangeNotification
|
||||
object:nil queue:nil
|
||||
usingBlock:^(NSNotification *n){ emitBattery(); }];
|
||||
|
||||
// Refresh battery/theme snapshots when the app comes to the front.
|
||||
// (Lifecycle itself is delivered by the generated UIApplication
|
||||
// delegate events, so we don't re-emit those here.)
|
||||
[nc addObserverForName:UIApplicationDidBecomeActiveNotification
|
||||
object:nil queue:nil
|
||||
usingBlock:^(NSNotification *n){ emitBattery(); emitTheme(); }];
|
||||
|
||||
// Lock / unlock: approximated via data-protection availability.
|
||||
// Only fires when the device has a passcode set.
|
||||
[nc addObserverForName:UIApplicationProtectedDataWillBecomeUnavailable
|
||||
object:nil queue:nil
|
||||
usingBlock:^(NSNotification *n){ emitEvent(EventScreenLocked, nil); }];
|
||||
[nc addObserverForName:UIApplicationProtectedDataDidBecomeAvailable
|
||||
object:nil queue:nil
|
||||
usingBlock:^(NSNotification *n){ emitEvent(EventScreenUnlocked, nil); }];
|
||||
|
||||
// Network reachability / interface type. The update handler fires
|
||||
// immediately with the current path, giving listeners an initial
|
||||
// value as well as live changes.
|
||||
g_pathMonitor = nw_path_monitor_create();
|
||||
nw_path_monitor_set_queue(g_pathMonitor, dispatch_get_main_queue());
|
||||
nw_path_monitor_set_update_handler(g_pathMonitor, ^(nw_path_t path){
|
||||
emitNetwork(path);
|
||||
});
|
||||
nw_path_monitor_start(g_pathMonitor);
|
||||
|
||||
// Initial snapshot so listeners mounted before any change still get
|
||||
// current battery/theme.
|
||||
emitBattery();
|
||||
emitTheme();
|
||||
});
|
||||
});
|
||||
}
|
||||
66
vendor/github.com/wailsapp/wails/v3/pkg/application/key_binding_manager.go
generated
vendored
Normal file
66
vendor/github.com/wailsapp/wails/v3/pkg/application/key_binding_manager.go
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
package application
|
||||
|
||||
// KeyBindingManager manages all key binding operations
|
||||
type KeyBindingManager struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
// newKeyBindingManager creates a new KeyBindingManager instance
|
||||
func newKeyBindingManager(app *App) *KeyBindingManager {
|
||||
return &KeyBindingManager{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a key binding
|
||||
func (kbm *KeyBindingManager) Add(accelerator string, callback func(window Window)) {
|
||||
kbm.app.keyBindingsLock.Lock()
|
||||
defer kbm.app.keyBindingsLock.Unlock()
|
||||
kbm.app.keyBindings[accelerator] = callback
|
||||
}
|
||||
|
||||
// Remove removes a key binding
|
||||
func (kbm *KeyBindingManager) Remove(accelerator string) {
|
||||
kbm.app.keyBindingsLock.Lock()
|
||||
defer kbm.app.keyBindingsLock.Unlock()
|
||||
delete(kbm.app.keyBindings, accelerator)
|
||||
}
|
||||
|
||||
// Process processes a key binding and returns true if handled
|
||||
func (kbm *KeyBindingManager) Process(accelerator string, window Window) bool {
|
||||
kbm.app.keyBindingsLock.RLock()
|
||||
callback, exists := kbm.app.keyBindings[accelerator]
|
||||
kbm.app.keyBindingsLock.RUnlock()
|
||||
|
||||
if exists && callback != nil {
|
||||
callback(window)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// KeyBinding represents a key binding with its accelerator and callback
|
||||
type KeyBinding struct {
|
||||
Accelerator string
|
||||
Callback func(window Window)
|
||||
}
|
||||
|
||||
// GetAll returns all registered key bindings as a slice
|
||||
func (kbm *KeyBindingManager) GetAll() []*KeyBinding {
|
||||
kbm.app.keyBindingsLock.RLock()
|
||||
defer kbm.app.keyBindingsLock.RUnlock()
|
||||
|
||||
result := make([]*KeyBinding, 0, len(kbm.app.keyBindings))
|
||||
for accelerator, callback := range kbm.app.keyBindings {
|
||||
result = append(result, &KeyBinding{
|
||||
Accelerator: accelerator,
|
||||
Callback: callback,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// HandleWindowKeyEvent handles window key events (internal use)
|
||||
func (kbm *KeyBindingManager) handleWindowKeyEvent(event *windowKeyEvent) {
|
||||
kbm.app.handleWindowKeyEvent(event)
|
||||
}
|
||||
220
vendor/github.com/wailsapp/wails/v3/pkg/application/keys.go
generated
vendored
Normal file
220
vendor/github.com/wailsapp/wails/v3/pkg/application/keys.go
generated
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// modifier is actually a string
|
||||
type modifier int
|
||||
|
||||
const (
|
||||
// CmdOrCtrlKey represents Command on Mac and Control on other platforms
|
||||
CmdOrCtrlKey modifier = 0 << iota
|
||||
// OptionOrAltKey represents Option on Mac and Alt on other platforms
|
||||
OptionOrAltKey modifier = 1 << iota
|
||||
// ShiftKey represents the shift key on all systems
|
||||
ShiftKey modifier = 2 << iota
|
||||
// SuperKey represents Command on Mac and the Windows key on the other platforms
|
||||
SuperKey modifier = 3 << iota
|
||||
// ControlKey represents the control key on all systems
|
||||
ControlKey modifier = 4 << iota
|
||||
)
|
||||
|
||||
func (m modifier) String() string {
|
||||
return modifierStringMap[runtime.GOOS][m]
|
||||
}
|
||||
|
||||
var modifierStringMap = map[string]map[modifier]string{
|
||||
"windows": {
|
||||
CmdOrCtrlKey: "Ctrl",
|
||||
ControlKey: "Ctrl",
|
||||
OptionOrAltKey: "Alt",
|
||||
ShiftKey: "Shift",
|
||||
SuperKey: "Win",
|
||||
},
|
||||
"darwin": {
|
||||
CmdOrCtrlKey: "Cmd",
|
||||
ControlKey: "Ctrl",
|
||||
OptionOrAltKey: "Option",
|
||||
ShiftKey: "Shift",
|
||||
SuperKey: "Cmd",
|
||||
},
|
||||
"linux": {
|
||||
CmdOrCtrlKey: "Ctrl",
|
||||
ControlKey: "Ctrl",
|
||||
OptionOrAltKey: "Alt",
|
||||
ShiftKey: "Shift",
|
||||
SuperKey: "Super",
|
||||
},
|
||||
}
|
||||
|
||||
var modifierMap = map[string]modifier{
|
||||
"cmdorctrl": CmdOrCtrlKey,
|
||||
"cmd": CmdOrCtrlKey,
|
||||
"command": CmdOrCtrlKey,
|
||||
"ctrl": ControlKey,
|
||||
"optionoralt": OptionOrAltKey,
|
||||
"alt": OptionOrAltKey,
|
||||
"option": OptionOrAltKey,
|
||||
"shift": ShiftKey,
|
||||
"super": SuperKey,
|
||||
}
|
||||
|
||||
// accelerator holds the keyboard shortcut for a menu item
|
||||
type accelerator struct {
|
||||
Key string
|
||||
Modifiers []modifier
|
||||
}
|
||||
|
||||
func (a *accelerator) clone() *accelerator {
|
||||
result := *a
|
||||
return &result
|
||||
}
|
||||
|
||||
func (a *accelerator) String() string {
|
||||
var result []string
|
||||
// Sort modifiers
|
||||
for _, modifier := range a.Modifiers {
|
||||
result = append(result, modifier.String())
|
||||
}
|
||||
slices.Sort(result)
|
||||
if len(a.Key) > 0 {
|
||||
result = append(result, strings.ToUpper(a.Key))
|
||||
}
|
||||
return strings.Join(result, "+")
|
||||
}
|
||||
|
||||
var namedKeys = map[string]struct{}{
|
||||
"backspace": {},
|
||||
"tab": {},
|
||||
"return": {},
|
||||
"enter": {},
|
||||
"escape": {},
|
||||
"left": {},
|
||||
"right": {},
|
||||
"up": {},
|
||||
"down": {},
|
||||
"space": {},
|
||||
"delete": {},
|
||||
"home": {},
|
||||
"end": {},
|
||||
"page up": {},
|
||||
"page down": {},
|
||||
"f1": {},
|
||||
"f2": {},
|
||||
"f3": {},
|
||||
"f4": {},
|
||||
"f5": {},
|
||||
"f6": {},
|
||||
"f7": {},
|
||||
"f8": {},
|
||||
"f9": {},
|
||||
"f10": {},
|
||||
"f11": {},
|
||||
"f12": {},
|
||||
"f13": {},
|
||||
"f14": {},
|
||||
"f15": {},
|
||||
"f16": {},
|
||||
"f17": {},
|
||||
"f18": {},
|
||||
"f19": {},
|
||||
"f20": {},
|
||||
"f21": {},
|
||||
"f22": {},
|
||||
"f23": {},
|
||||
"f24": {},
|
||||
"f25": {},
|
||||
"f26": {},
|
||||
"f27": {},
|
||||
"f28": {},
|
||||
"f29": {},
|
||||
"f30": {},
|
||||
"f31": {},
|
||||
"f32": {},
|
||||
"f33": {},
|
||||
"f34": {},
|
||||
"f35": {},
|
||||
"numlock": {},
|
||||
}
|
||||
|
||||
func parseKey(key string) (string, bool) {
|
||||
|
||||
// Lowercase!
|
||||
key = strings.ToLower(key)
|
||||
|
||||
// Check special case
|
||||
if key == "plus" {
|
||||
return "+", true
|
||||
}
|
||||
|
||||
// Handle named keys
|
||||
_, namedKey := namedKeys[key]
|
||||
if namedKey {
|
||||
return key, true
|
||||
}
|
||||
|
||||
// Check we only have a single character
|
||||
if len(key) != 1 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
runeKey := rune(key[0])
|
||||
|
||||
// This may be too inclusive
|
||||
if strconv.IsPrint(runeKey) {
|
||||
return key, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
|
||||
}
|
||||
|
||||
// parseAccelerator parses a string into an accelerator
|
||||
func parseAccelerator(shortcut string) (*accelerator, error) {
|
||||
|
||||
var result accelerator
|
||||
|
||||
// Split the shortcut by +
|
||||
components := strings.Split(shortcut, "+")
|
||||
|
||||
// If we only have one it should be a key
|
||||
// We require components
|
||||
if len(components) == 0 {
|
||||
return nil, fmt.Errorf("no components given to validateComponents")
|
||||
}
|
||||
|
||||
modifiers := map[modifier]struct{}{}
|
||||
|
||||
// Check components
|
||||
for index, component := range components {
|
||||
|
||||
// If last component
|
||||
if index == len(components)-1 {
|
||||
processedKey, validKey := parseKey(component)
|
||||
if !validKey {
|
||||
return nil, fmt.Errorf("'%s' is not a valid key", component)
|
||||
}
|
||||
result.Key = strings.ToLower(processedKey)
|
||||
continue
|
||||
}
|
||||
|
||||
// Not last component - needs to be modifier
|
||||
lowercaseComponent := strings.ToLower(component)
|
||||
thisModifier, valid := modifierMap[lowercaseComponent]
|
||||
if !valid {
|
||||
return nil, fmt.Errorf("'%s' is not a valid modifier", component)
|
||||
}
|
||||
// Save this data
|
||||
modifiers[thisModifier] = struct{}{}
|
||||
}
|
||||
// return the keys as a slice
|
||||
for thisModifier := range modifiers {
|
||||
result.Modifiers = append(result.Modifiers, thisModifier)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
9
vendor/github.com/wailsapp/wails/v3/pkg/application/keys_android.go
generated
vendored
Normal file
9
vendor/github.com/wailsapp/wails/v3/pkg/application/keys_android.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build android
|
||||
|
||||
package application
|
||||
|
||||
// Android keyboard handling stub
|
||||
|
||||
func acceleratorToString(accelerator *accelerator) string {
|
||||
return ""
|
||||
}
|
||||
28
vendor/github.com/wailsapp/wails/v3/pkg/application/keys_darwin.go
generated
vendored
Normal file
28
vendor/github.com/wailsapp/wails/v3/pkg/application/keys_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
//go:build darwin && !ios && !server
|
||||
|
||||
package application
|
||||
|
||||
const (
|
||||
NSEventModifierFlagShift = 1 << 17 // Set if Shift key is pressed.
|
||||
NSEventModifierFlagControl = 1 << 18 // Set if Control key is pressed.
|
||||
NSEventModifierFlagOption = 1 << 19 // Set if Option or Alternate key is pressed.
|
||||
NSEventModifierFlagCommand = 1 << 20 // Set if Command key is pressed.
|
||||
)
|
||||
|
||||
// macModifierMap maps accelerator modifiers to macOS modifiers.
|
||||
var macModifierMap = map[modifier]int{
|
||||
CmdOrCtrlKey: NSEventModifierFlagCommand,
|
||||
ControlKey: NSEventModifierFlagControl,
|
||||
OptionOrAltKey: NSEventModifierFlagOption,
|
||||
ShiftKey: NSEventModifierFlagShift,
|
||||
SuperKey: NSEventModifierFlagCommand,
|
||||
}
|
||||
|
||||
// toMacModifier converts the accelerator to a macOS modifier.
|
||||
func toMacModifier(modifiers []modifier) int {
|
||||
result := 0
|
||||
for _, modifier := range modifiers {
|
||||
result |= macModifierMap[modifier]
|
||||
}
|
||||
return result
|
||||
}
|
||||
20
vendor/github.com/wailsapp/wails/v3/pkg/application/keys_ios.go
generated
vendored
Normal file
20
vendor/github.com/wailsapp/wails/v3/pkg/application/keys_ios.go
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build ios
|
||||
|
||||
package application
|
||||
|
||||
// iOS key codes - these would map to UIKeyCommand
|
||||
const (
|
||||
KeyReturn = "Return"
|
||||
KeyEscape = "Escape"
|
||||
KeyDelete = "Delete"
|
||||
KeyTab = "Tab"
|
||||
KeySpace = "Space"
|
||||
KeyUp = "Up"
|
||||
KeyDown = "Down"
|
||||
KeyLeft = "Left"
|
||||
KeyRight = "Right"
|
||||
KeyHome = "Home"
|
||||
KeyEnd = "End"
|
||||
KeyPageUp = "PageUp"
|
||||
KeyPageDown = "PageDown"
|
||||
)
|
||||
165
vendor/github.com/wailsapp/wails/v3/pkg/application/keys_linux.go
generated
vendored
Normal file
165
vendor/github.com/wailsapp/wails/v3/pkg/application/keys_linux.go
generated
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
//go:build linux && !android && !server
|
||||
|
||||
package application
|
||||
|
||||
var VirtualKeyCodes = map[uint]string{
|
||||
0xff08: "backspace",
|
||||
0xff09: "tab",
|
||||
0xff0a: "linefeed",
|
||||
0xff0b: "clear",
|
||||
0xff0d: "return",
|
||||
0xff13: "pause",
|
||||
0xff14: "scrolllock",
|
||||
0xff15: "sysreq",
|
||||
0xff1b: "escape",
|
||||
0xffff: "delete",
|
||||
0xff50: "home",
|
||||
0xff51: "left",
|
||||
|
||||
0xffe1: "lshift",
|
||||
0xffe2: "rshift",
|
||||
0xffe3: "lcontrol",
|
||||
0xffe4: "rcontrol",
|
||||
0xffeb: "lmeta",
|
||||
0xffec: "rmeta",
|
||||
0xffed: "lalt",
|
||||
0xffee: "ralt",
|
||||
// Multi-Lang
|
||||
0xff21: "kanji",
|
||||
0xff22: "muhenkan",
|
||||
0xff24: "henkan",
|
||||
0xff25: "hiragana",
|
||||
0xff26: "katakana",
|
||||
0xff27: "hiragana/katakana",
|
||||
0xff28: "zenkaku",
|
||||
0xff29: "hankaku",
|
||||
0xff2a: "zenkaku/hankaku",
|
||||
0xff2b: "touroku",
|
||||
0xff2c: "massyo",
|
||||
0xff2d: "kana lock",
|
||||
0xff2e: "kana shift",
|
||||
0xff2f: "eisu shift",
|
||||
0xff30: "eisu toggle",
|
||||
0xff37: "kanji bangou",
|
||||
|
||||
// Directions
|
||||
0xff52: "up",
|
||||
0xff53: "right",
|
||||
0xff54: "down",
|
||||
0xff55: "pageup",
|
||||
0xff56: "pagedown",
|
||||
0xff57: "end",
|
||||
0xff58: "begin",
|
||||
|
||||
|
||||
// Alphabet
|
||||
0x41: "a",
|
||||
0x42: "b",
|
||||
0x43: "c",
|
||||
0x44: "d",
|
||||
0x45: "e",
|
||||
0x46: "f",
|
||||
0x47: "g",
|
||||
0x48: "h",
|
||||
0x49: "i",
|
||||
0x4a: "j",
|
||||
0x4b: "k",
|
||||
0x4c: "l",
|
||||
0x4d: "m",
|
||||
0x4e: "n",
|
||||
0x4f: "o",
|
||||
0x50: "p",
|
||||
0x51: "q",
|
||||
0x52: "r",
|
||||
0x53: "s",
|
||||
0x54: "t",
|
||||
0x55: "u",
|
||||
0x56: "v",
|
||||
0x57: "w",
|
||||
0x58: "x",
|
||||
0x59: "y",
|
||||
0x5a: "z",
|
||||
|
||||
0x5b: "lbracket",
|
||||
0x5c: "backslash",
|
||||
0x5d: "rbracket",
|
||||
0x5e: "caret",
|
||||
0x5f: "underscore",
|
||||
0x60: "grave",
|
||||
// Capital Alphabet
|
||||
0x61: "a",
|
||||
0x62: "b",
|
||||
0x63: "c",
|
||||
0x64: "d",
|
||||
0x65: "e",
|
||||
0x66: "f",
|
||||
0x67: "g",
|
||||
0x68: "h",
|
||||
0x69: "i",
|
||||
0x6a: "j",
|
||||
0x6b: "k",
|
||||
0x6c: "l",
|
||||
0x6d: "m",
|
||||
0x6e: "n",
|
||||
0x6f: "o",
|
||||
0x70: "p",
|
||||
0x71: "q",
|
||||
0x72: "r",
|
||||
0x73: "s",
|
||||
0x74: "t",
|
||||
0x75: "u",
|
||||
0x76: "v",
|
||||
0x77: "w",
|
||||
0x78: "x",
|
||||
0x79: "y",
|
||||
0x7a: "z",
|
||||
0x7b: "lbrace",
|
||||
0x7c: "pipe",
|
||||
0x7d: "rbrace",
|
||||
0x7e: "tilde",
|
||||
0xa1: "exclam",
|
||||
0xa2: "cent",
|
||||
0xa3: "sterling",
|
||||
0xa4: "currency",
|
||||
0xa5: "yen",
|
||||
0xa6: "brokenbar",
|
||||
0xa7: "section",
|
||||
0xa8: "diaeresis",
|
||||
0xa9: "copyright",
|
||||
0xaa: "ordfeminine",
|
||||
0xab: "guillemotleft",
|
||||
0xad: "hyphen",
|
||||
0xae: "registered",
|
||||
0xaf: "macron",
|
||||
0xb0: "degree",
|
||||
0xb1: "plusminus",
|
||||
0xb2: "twosuperior",
|
||||
|
||||
// Function Keys
|
||||
0xffbe: "f1",
|
||||
0xffbf: "f2",
|
||||
0xffc0: "f3",
|
||||
0xffc1: "f4",
|
||||
0xffc2: "f5",
|
||||
0xffc3: "f6",
|
||||
0xffc4: "f7",
|
||||
0xffc5: "f8",
|
||||
0xffc6: "f9",
|
||||
0xffc7: "f10",
|
||||
0xffc8: "f11",
|
||||
0xffc9: "f12",
|
||||
0xffca: "f13",
|
||||
0xffcb: "f14",
|
||||
0xffcc: "f15",
|
||||
0xffcd: "f16",
|
||||
0xffce: "f17",
|
||||
0xffcf: "f18",
|
||||
0xffd0: "f19",
|
||||
0xffd1: "f20",
|
||||
0xffd2: "f21",
|
||||
0xffd3: "f22",
|
||||
0xffd4: "f23",
|
||||
0xffd5: "f24",
|
||||
|
||||
|
||||
}
|
||||
230
vendor/github.com/wailsapp/wails/v3/pkg/application/keys_windows.go
generated
vendored
Normal file
230
vendor/github.com/wailsapp/wails/v3/pkg/application/keys_windows.go
generated
vendored
Normal file
@@ -0,0 +1,230 @@
|
||||
//go:build windows && !server
|
||||
|
||||
package application
|
||||
|
||||
var VirtualKeyCodes = map[uint]string{
|
||||
0x01: "lbutton",
|
||||
0x02: "rbutton",
|
||||
0x03: "cancel",
|
||||
0x04: "mbutton",
|
||||
0x05: "xbutton1",
|
||||
0x06: "xbutton2",
|
||||
0x08: "back",
|
||||
0x09: "tab",
|
||||
0x0C: "clear",
|
||||
0x0D: "return",
|
||||
0x10: "shift",
|
||||
0x11: "ctrl",
|
||||
0x12: "menu",
|
||||
0x13: "pause",
|
||||
0x14: "capital",
|
||||
0x15: "kana",
|
||||
0x17: "junja",
|
||||
0x18: "final",
|
||||
0x19: "hanja",
|
||||
0x1B: "escape",
|
||||
0x1C: "convert",
|
||||
0x1D: "nonconvert",
|
||||
0x1E: "accept",
|
||||
0x1F: "modechange",
|
||||
0x20: "space",
|
||||
0x21: "prior",
|
||||
0x22: "next",
|
||||
0x23: "end",
|
||||
0x24: "home",
|
||||
0x25: "left",
|
||||
0x26: "up",
|
||||
0x27: "right",
|
||||
0x28: "down",
|
||||
0x29: "select",
|
||||
0x2A: "print",
|
||||
0x2B: "execute",
|
||||
0x2C: "snapshot",
|
||||
0x2D: "insert",
|
||||
0x2E: "delete",
|
||||
0x2F: "help",
|
||||
0x30: "0",
|
||||
0x31: "1",
|
||||
0x32: "2",
|
||||
0x33: "3",
|
||||
0x34: "4",
|
||||
0x35: "5",
|
||||
0x36: "6",
|
||||
0x37: "7",
|
||||
0x38: "8",
|
||||
0x39: "9",
|
||||
0x41: "a",
|
||||
0x42: "b",
|
||||
0x43: "c",
|
||||
0x44: "d",
|
||||
0x45: "e",
|
||||
0x46: "f",
|
||||
0x47: "g",
|
||||
0x48: "h",
|
||||
0x49: "i",
|
||||
0x4A: "j",
|
||||
0x4B: "k",
|
||||
0x4C: "l",
|
||||
0x4D: "m",
|
||||
0x4E: "n",
|
||||
0x4F: "o",
|
||||
0x50: "p",
|
||||
0x51: "q",
|
||||
0x52: "r",
|
||||
0x53: "s",
|
||||
0x54: "t",
|
||||
0x55: "u",
|
||||
0x56: "v",
|
||||
0x57: "w",
|
||||
0x58: "x",
|
||||
0x59: "y",
|
||||
0x5A: "z",
|
||||
0x5B: "lwin",
|
||||
0x5C: "rwin",
|
||||
0x5D: "apps",
|
||||
0x5F: "sleep",
|
||||
0x60: "numpad0",
|
||||
0x61: "numpad1",
|
||||
0x62: "numpad2",
|
||||
0x63: "numpad3",
|
||||
0x64: "numpad4",
|
||||
0x65: "numpad5",
|
||||
0x66: "numpad6",
|
||||
0x67: "numpad7",
|
||||
0x68: "numpad8",
|
||||
0x69: "numpad9",
|
||||
0x6A: "multiply",
|
||||
0x6B: "add",
|
||||
0x6C: "separator",
|
||||
0x6D: "subtract",
|
||||
0x6E: "decimal",
|
||||
0x6F: "divide",
|
||||
0x70: "f1",
|
||||
0x71: "f2",
|
||||
0x72: "f3",
|
||||
0x73: "f4",
|
||||
0x74: "f5",
|
||||
0x75: "f6",
|
||||
0x76: "f7",
|
||||
0x77: "f8",
|
||||
0x78: "f9",
|
||||
0x79: "f10",
|
||||
0x7A: "f11",
|
||||
0x7B: "f12",
|
||||
0x7C: "f13",
|
||||
0x7D: "f14",
|
||||
0x7E: "f15",
|
||||
0x7F: "f16",
|
||||
0x80: "f17",
|
||||
0x81: "f18",
|
||||
0x82: "f19",
|
||||
0x83: "f20",
|
||||
0x84: "f21",
|
||||
0x85: "f22",
|
||||
0x86: "f23",
|
||||
0x87: "f24",
|
||||
0x88: "navigation_view",
|
||||
0x89: "navigation_menu",
|
||||
0x8A: "navigation_up",
|
||||
0x8B: "navigation_down",
|
||||
0x8C: "navigation_left",
|
||||
0x8D: "navigation_right",
|
||||
0x8E: "navigation_accept",
|
||||
0x8F: "navigation_cancel",
|
||||
0x90: "numlock",
|
||||
0x91: "scroll",
|
||||
0x92: "oem_nec_equal",
|
||||
0x93: "oem_fj_masshou",
|
||||
0x94: "oem_fj_touroku",
|
||||
0x95: "oem_fj_loya",
|
||||
0x96: "oem_fj_roya",
|
||||
0xA0: "lshift",
|
||||
0xA1: "rshift",
|
||||
0xA2: "lcontrol",
|
||||
0xA3: "rcontrol",
|
||||
0xA4: "lmenu",
|
||||
0xA5: "rmenu",
|
||||
0xA6: "browser_back",
|
||||
0xA7: "browser_forward",
|
||||
0xA8: "browser_refresh",
|
||||
0xA9: "browser_stop",
|
||||
0xAA: "browser_search",
|
||||
0xAB: "browser_favorites",
|
||||
0xAC: "browser_home",
|
||||
0xAD: "volume_mute",
|
||||
0xAE: "volume_down",
|
||||
0xAF: "volume_up",
|
||||
0xB0: "media_next_track",
|
||||
0xB1: "media_prev_track",
|
||||
0xB2: "media_stop",
|
||||
0xB3: "media_play_pause",
|
||||
0xB4: "launch_mail",
|
||||
0xB5: "launch_media_select",
|
||||
0xB6: "launch_app1",
|
||||
0xB7: "launch_app2",
|
||||
0xBA: "oem_1",
|
||||
0xBB: "oem_plus",
|
||||
0xBC: "oem_comma",
|
||||
0xBD: "oem_minus",
|
||||
0xBE: "oem_period",
|
||||
0xBF: "oem_2",
|
||||
0xC0: "oem_3",
|
||||
0xC3: "gamepad_a",
|
||||
0xC4: "gamepad_b",
|
||||
0xC5: "gamepad_x",
|
||||
0xC6: "gamepad_y",
|
||||
0xC7: "gamepad_right_shoulder",
|
||||
0xC8: "gamepad_left_shoulder",
|
||||
0xC9: "gamepad_left_trigger",
|
||||
0xCA: "gamepad_right_trigger",
|
||||
0xCB: "gamepad_dpad_up",
|
||||
0xCC: "gamepad_dpad_down",
|
||||
0xCD: "gamepad_dpad_left",
|
||||
0xCE: "gamepad_dpad_right",
|
||||
0xCF: "gamepad_menu",
|
||||
0xD0: "gamepad_view",
|
||||
0xD1: "gamepad_left_thumbstick_button",
|
||||
0xD2: "gamepad_right_thumbstick_button",
|
||||
0xD3: "gamepad_left_thumbstick_up",
|
||||
0xD4: "gamepad_left_thumbstick_down",
|
||||
0xD5: "gamepad_left_thumbstick_right",
|
||||
0xD6: "gamepad_left_thumbstick_left",
|
||||
0xD7: "gamepad_right_thumbstick_up",
|
||||
0xD8: "gamepad_right_thumbstick_down",
|
||||
0xD9: "gamepad_right_thumbstick_right",
|
||||
0xDA: "gamepad_right_thumbstick_left",
|
||||
0xDB: "oem_4",
|
||||
0xDC: "oem_5",
|
||||
0xDD: "oem_6",
|
||||
0xDE: "oem_7",
|
||||
0xDF: "oem_8",
|
||||
0xE1: "oem_ax",
|
||||
0xE2: "oem_102",
|
||||
0xE3: "ico_help",
|
||||
0xE4: "ico_00",
|
||||
0xE5: "processkey",
|
||||
0xE6: "ico_clear",
|
||||
0xE7: "packet",
|
||||
0xE9: "oem_reset",
|
||||
0xEA: "oem_jump",
|
||||
0xEB: "oem_pa1",
|
||||
0xEC: "oem_pa2",
|
||||
0xED: "oem_pa3",
|
||||
0xEE: "oem_wsctrl",
|
||||
0xEF: "oem_cusel",
|
||||
0xF0: "oem_attn",
|
||||
0xF1: "oem_finish",
|
||||
0xF2: "oem_copy",
|
||||
0xF3: "oem_auto",
|
||||
0xF4: "oem_enlw",
|
||||
0xF5: "oem_backtab",
|
||||
0xF6: "attn",
|
||||
0xF7: "crsel",
|
||||
0xF8: "exsel",
|
||||
0xF9: "ereof",
|
||||
0xFA: "play",
|
||||
0xFB: "zoom",
|
||||
0xFC: "noname",
|
||||
0xFD: "pa1",
|
||||
0xFE: "oem_clear",
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user