github.com/secoba/wails/v2@v2.6.4/internal/frontend/desktop/linux/frontend.go (about)

     1  //go:build linux
     2  // +build linux
     3  
     4  package linux
     5  
     6  /*
     7  #cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
     8  
     9  #include "gtk/gtk.h"
    10  #include "webkit2/webkit2.h"
    11  
    12  // CREDIT: https://github.com/rainycape/magick
    13  #include <errno.h>
    14  #include <signal.h>
    15  #include <stdio.h>
    16  #include <string.h>
    17  
    18  static void fix_signal(int signum)
    19  {
    20      struct sigaction st;
    21  
    22      if (sigaction(signum, NULL, &st) < 0) {
    23          goto fix_signal_error;
    24      }
    25      st.sa_flags |= SA_ONSTACK;
    26      if (sigaction(signum, &st,  NULL) < 0) {
    27          goto fix_signal_error;
    28      }
    29      return;
    30  fix_signal_error:
    31          fprintf(stderr, "error fixing handler for signal %d, please "
    32                  "report this issue to "
    33                  "https://github.com/wailsapp/wails: %s\n",
    34                  signum, strerror(errno));
    35  }
    36  
    37  static void install_signal_handlers()
    38  {
    39  #if defined(SIGCHLD)
    40      fix_signal(SIGCHLD);
    41  #endif
    42  #if defined(SIGHUP)
    43      fix_signal(SIGHUP);
    44  #endif
    45  #if defined(SIGINT)
    46      fix_signal(SIGINT);
    47  #endif
    48  #if defined(SIGQUIT)
    49      fix_signal(SIGQUIT);
    50  #endif
    51  #if defined(SIGABRT)
    52      fix_signal(SIGABRT);
    53  #endif
    54  #if defined(SIGFPE)
    55      fix_signal(SIGFPE);
    56  #endif
    57  #if defined(SIGTERM)
    58      fix_signal(SIGTERM);
    59  #endif
    60  #if defined(SIGBUS)
    61      fix_signal(SIGBUS);
    62  #endif
    63  #if defined(SIGSEGV)
    64      fix_signal(SIGSEGV);
    65  #endif
    66  #if defined(SIGXCPU)
    67      fix_signal(SIGXCPU);
    68  #endif
    69  #if defined(SIGXFSZ)
    70      fix_signal(SIGXFSZ);
    71  #endif
    72  }
    73  
    74  */
    75  import "C"
    76  import (
    77  	"context"
    78  	"encoding/json"
    79  	"errors"
    80  	"fmt"
    81  	"log"
    82  	"net"
    83  	"net/url"
    84  	"os"
    85  	"runtime"
    86  	"strings"
    87  	"sync"
    88  	"text/template"
    89  	"unsafe"
    90  
    91  	"github.com/secoba/wails/v2/pkg/assetserver"
    92  	"github.com/secoba/wails/v2/pkg/assetserver/webview"
    93  
    94  	"github.com/secoba/wails/v2/internal/binding"
    95  	"github.com/secoba/wails/v2/internal/frontend"
    96  	wailsruntime "github.com/secoba/wails/v2/internal/frontend/runtime"
    97  	"github.com/secoba/wails/v2/internal/logger"
    98  	"github.com/secoba/wails/v2/pkg/options"
    99  )
   100  
   101  var initOnce = sync.Once{}
   102  
   103  const startURL = "wails://wails/"
   104  
   105  var secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
   106  
   107  type Frontend struct {
   108  
   109  	// Context
   110  	ctx context.Context
   111  
   112  	frontendOptions *options.App
   113  	logger          *logger.Logger
   114  	debug           bool
   115  	devtoolsEnabled bool
   116  
   117  	// Assets
   118  	assets   *assetserver.AssetServer
   119  	startURL *url.URL
   120  
   121  	// main window handle
   122  	mainWindow *Window
   123  	bindings   *binding.Bindings
   124  	dispatcher frontend.Dispatcher
   125  }
   126  
   127  func (f *Frontend) RunMainLoop() {
   128  	C.gtk_main()
   129  }
   130  
   131  func (f *Frontend) WindowClose() {
   132  	f.mainWindow.Destroy()
   133  }
   134  
   135  func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend {
   136  	initOnce.Do(func() {
   137  		runtime.LockOSThread()
   138  
   139  		// Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings
   140  		if os.Getenv("GDK_BACKEND") == "" && (os.Getenv("XDG_SESSION_TYPE") == "" || os.Getenv("XDG_SESSION_TYPE") == "unspecified" || os.Getenv("XDG_SESSION_TYPE") == "x11") {
   141  			_ = os.Setenv("GDK_BACKEND", "x11")
   142  		}
   143  
   144  		if ok := C.gtk_init_check(nil, nil); ok != 1 {
   145  			panic(errors.New("failed to init GTK"))
   146  		}
   147  	})
   148  
   149  	result := &Frontend{
   150  		frontendOptions: appoptions,
   151  		logger:          myLogger,
   152  		bindings:        appBindings,
   153  		dispatcher:      dispatcher,
   154  		ctx:             ctx,
   155  	}
   156  	result.startURL, _ = url.Parse(startURL)
   157  
   158  	if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
   159  		result.startURL = _starturl
   160  	} else {
   161  		if port, _ := ctx.Value("assetserverport").(string); port != "" {
   162  			result.startURL.Host = net.JoinHostPort(result.startURL.Host+".localhost", port)
   163  		}
   164  
   165  		var bindings string
   166  		var err error
   167  		if _obfuscated, _ := ctx.Value("obfuscated").(bool); !_obfuscated {
   168  			bindings, err = appBindings.ToJSON()
   169  			if err != nil {
   170  				log.Fatal(err)
   171  			}
   172  		} else {
   173  			appBindings.DB().UpdateObfuscatedCallMap()
   174  		}
   175  		assets, err := assetserver.NewAssetServerMainPage(bindings, appoptions, ctx.Value("assetdir") != nil, myLogger, wailsruntime.RuntimeAssetsBundle)
   176  		if err != nil {
   177  			log.Fatal(err)
   178  		}
   179  		result.assets = assets
   180  
   181  		go result.startRequestProcessor()
   182  	}
   183  
   184  	go result.startMessageProcessor()
   185  
   186  	var _debug = ctx.Value("debug")
   187  	var _devtoolsEnabled = ctx.Value("devtoolsEnabled")
   188  
   189  	if _debug != nil {
   190  		result.debug = _debug.(bool)
   191  	}
   192  	if _devtoolsEnabled != nil {
   193  		result.devtoolsEnabled = _devtoolsEnabled.(bool)
   194  	}
   195  
   196  	result.mainWindow = NewWindow(appoptions, result.debug, result.devtoolsEnabled)
   197  
   198  	C.install_signal_handlers()
   199  
   200  	if appoptions.Linux != nil && appoptions.Linux.ProgramName != "" {
   201  		prgname := C.CString(appoptions.Linux.ProgramName)
   202  		C.g_set_prgname(prgname)
   203  		C.free(unsafe.Pointer(prgname))
   204  	}
   205  
   206  	go result.startSecondInstanceProcessor()
   207  
   208  	return result
   209  }
   210  
   211  func (f *Frontend) startMessageProcessor() {
   212  	for message := range messageBuffer {
   213  		f.processMessage(message)
   214  	}
   215  }
   216  
   217  func (f *Frontend) WindowReload() {
   218  	f.ExecJS("runtime.WindowReload();")
   219  }
   220  
   221  func (f *Frontend) WindowSetSystemDefaultTheme() {
   222  	return
   223  }
   224  
   225  func (f *Frontend) WindowSetLightTheme() {
   226  	return
   227  }
   228  
   229  func (f *Frontend) WindowSetDarkTheme() {
   230  	return
   231  }
   232  
   233  func (f *Frontend) Run(ctx context.Context) error {
   234  	f.ctx = ctx
   235  
   236  	go func() {
   237  		if f.frontendOptions.OnStartup != nil {
   238  			f.frontendOptions.OnStartup(f.ctx)
   239  		}
   240  	}()
   241  
   242  	if f.frontendOptions.SingleInstanceLock != nil {
   243  		SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId)
   244  	}
   245  
   246  	f.mainWindow.Run(f.startURL.String())
   247  
   248  	return nil
   249  }
   250  
   251  func (f *Frontend) WindowCenter() {
   252  	f.mainWindow.Center()
   253  }
   254  
   255  func (f *Frontend) WindowSetAlwaysOnTop(b bool) {
   256  	f.mainWindow.SetKeepAbove(b)
   257  }
   258  
   259  func (f *Frontend) WindowSetPosition(x, y int) {
   260  	f.mainWindow.SetPosition(x, y)
   261  }
   262  func (f *Frontend) WindowGetPosition() (int, int) {
   263  	return f.mainWindow.GetPosition()
   264  }
   265  
   266  func (f *Frontend) WindowSetSize(width, height int) {
   267  	f.mainWindow.SetSize(width, height)
   268  }
   269  
   270  func (f *Frontend) WindowGetSize() (int, int) {
   271  	return f.mainWindow.Size()
   272  }
   273  
   274  func (f *Frontend) WindowSetTitle(title string) {
   275  	f.mainWindow.SetTitle(title)
   276  }
   277  
   278  func (f *Frontend) WindowFullscreen() {
   279  	if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
   280  		f.ExecJS("window.wails.flags.enableResize = false;")
   281  	}
   282  	f.mainWindow.Fullscreen()
   283  }
   284  
   285  func (f *Frontend) WindowUnfullscreen() {
   286  	if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
   287  		f.ExecJS("window.wails.flags.enableResize = true;")
   288  	}
   289  	f.mainWindow.UnFullscreen()
   290  }
   291  
   292  func (f *Frontend) WindowReloadApp() {
   293  	f.ExecJS(fmt.Sprintf("window.location.href = '%s';", f.startURL))
   294  }
   295  
   296  func (f *Frontend) WindowShow() {
   297  	f.mainWindow.Show()
   298  }
   299  
   300  func (f *Frontend) WindowHide() {
   301  	f.mainWindow.Hide()
   302  }
   303  
   304  func (f *Frontend) Show() {
   305  	f.mainWindow.Show()
   306  }
   307  
   308  func (f *Frontend) Hide() {
   309  	f.mainWindow.Hide()
   310  }
   311  func (f *Frontend) WindowMaximise() {
   312  	f.mainWindow.Maximise()
   313  }
   314  func (f *Frontend) WindowToggleMaximise() {
   315  	f.mainWindow.ToggleMaximise()
   316  }
   317  func (f *Frontend) WindowUnmaximise() {
   318  	f.mainWindow.UnMaximise()
   319  }
   320  func (f *Frontend) WindowMinimise() {
   321  	f.mainWindow.Minimise()
   322  }
   323  func (f *Frontend) WindowUnminimise() {
   324  	f.mainWindow.UnMinimise()
   325  }
   326  
   327  func (f *Frontend) WindowSetMinSize(width int, height int) {
   328  	f.mainWindow.SetMinSize(width, height)
   329  }
   330  func (f *Frontend) WindowSetMaxSize(width int, height int) {
   331  	f.mainWindow.SetMaxSize(width, height)
   332  }
   333  
   334  func (f *Frontend) WindowSetBackgroundColour(col *options.RGBA) {
   335  	if col == nil {
   336  		return
   337  	}
   338  	f.mainWindow.SetBackgroundColour(col.R, col.G, col.B, col.A)
   339  }
   340  
   341  func (f *Frontend) ScreenGetAll() ([]Screen, error) {
   342  	return GetAllScreens(f.mainWindow.asGTKWindow())
   343  }
   344  
   345  func (f *Frontend) WindowIsMaximised() bool {
   346  	return f.mainWindow.IsMaximised()
   347  }
   348  
   349  func (f *Frontend) WindowIsMinimised() bool {
   350  	return f.mainWindow.IsMinimised()
   351  }
   352  
   353  func (f *Frontend) WindowIsNormal() bool {
   354  	return f.mainWindow.IsNormal()
   355  }
   356  
   357  func (f *Frontend) WindowIsFullscreen() bool {
   358  	return f.mainWindow.IsFullScreen()
   359  }
   360  
   361  func (f *Frontend) Quit() {
   362  	if f.frontendOptions.OnBeforeClose != nil {
   363  		go func() {
   364  			if !f.frontendOptions.OnBeforeClose(f.ctx) {
   365  				f.mainWindow.Quit()
   366  			}
   367  		}()
   368  		return
   369  	}
   370  	f.mainWindow.Quit()
   371  }
   372  
   373  func (f *Frontend) WindowPrint() {
   374  	f.ExecJS("window.print();")
   375  }
   376  
   377  type EventNotify struct {
   378  	Name string        `json:"name"`
   379  	Data []interface{} `json:"data"`
   380  }
   381  
   382  func (f *Frontend) Notify(name string, data ...interface{}) {
   383  	notification := EventNotify{
   384  		Name: name,
   385  		Data: data,
   386  	}
   387  	payload, err := json.Marshal(notification)
   388  	if err != nil {
   389  		f.logger.Error(err.Error())
   390  		return
   391  	}
   392  	f.mainWindow.ExecJS(`window.wails.EventsNotify('` + template.JSEscapeString(string(payload)) + `');`)
   393  }
   394  
   395  var edgeMap = map[string]uintptr{
   396  	"n-resize":  C.GDK_WINDOW_EDGE_NORTH,
   397  	"ne-resize": C.GDK_WINDOW_EDGE_NORTH_EAST,
   398  	"e-resize":  C.GDK_WINDOW_EDGE_EAST,
   399  	"se-resize": C.GDK_WINDOW_EDGE_SOUTH_EAST,
   400  	"s-resize":  C.GDK_WINDOW_EDGE_SOUTH,
   401  	"sw-resize": C.GDK_WINDOW_EDGE_SOUTH_WEST,
   402  	"w-resize":  C.GDK_WINDOW_EDGE_WEST,
   403  	"nw-resize": C.GDK_WINDOW_EDGE_NORTH_WEST,
   404  }
   405  
   406  func (f *Frontend) processMessage(message string) {
   407  	if message == "DomReady" {
   408  		if f.frontendOptions.OnDomReady != nil {
   409  			f.frontendOptions.OnDomReady(f.ctx)
   410  		}
   411  		return
   412  	}
   413  
   414  	if message == "drag" {
   415  		if !f.mainWindow.IsFullScreen() {
   416  			f.startDrag()
   417  		}
   418  		return
   419  	}
   420  
   421  	if message == "wails:showInspector" {
   422  		f.mainWindow.ShowInspector()
   423  		return
   424  	}
   425  
   426  	if strings.HasPrefix(message, "resize:") {
   427  		if !f.mainWindow.IsFullScreen() {
   428  			sl := strings.Split(message, ":")
   429  			if len(sl) != 2 {
   430  				f.logger.Info("Unknown message returned from dispatcher: %+v", message)
   431  				return
   432  			}
   433  			edge := edgeMap[sl[1]]
   434  			err := f.startResize(edge)
   435  			if err != nil {
   436  				f.logger.Error(err.Error())
   437  			}
   438  		}
   439  		return
   440  	}
   441  
   442  	if message == "runtime:ready" {
   443  		cmd := fmt.Sprintf(
   444  			"window.wails.setCSSDragProperties('%s', '%s');\n"+
   445  				"window.wails.flags.deferDragToMouseMove = true;", f.frontendOptions.CSSDragProperty, f.frontendOptions.CSSDragValue)
   446  		f.ExecJS(cmd)
   447  
   448  		if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
   449  			f.ExecJS("window.wails.flags.enableResize = true;")
   450  		}
   451  		return
   452  	}
   453  
   454  	go func() {
   455  		result, err := f.dispatcher.ProcessMessage(message, f)
   456  		if err != nil {
   457  			f.logger.Error(err.Error())
   458  			f.Callback(result)
   459  			return
   460  		}
   461  		if result == "" {
   462  			return
   463  		}
   464  
   465  		switch result[0] {
   466  		case 'c':
   467  			// Callback from a method call
   468  			f.Callback(result[1:])
   469  		default:
   470  			f.logger.Info("Unknown message returned from dispatcher: %+v", result)
   471  		}
   472  	}()
   473  }
   474  
   475  func (f *Frontend) Callback(message string) {
   476  	escaped, err := json.Marshal(message)
   477  	if err != nil {
   478  		panic(err)
   479  	}
   480  	f.ExecJS(`window.wails.Callback(` + string(escaped) + `);`)
   481  }
   482  
   483  func (f *Frontend) startDrag() {
   484  	f.mainWindow.StartDrag()
   485  }
   486  
   487  func (f *Frontend) startResize(edge uintptr) error {
   488  	f.mainWindow.StartResize(edge)
   489  	return nil
   490  }
   491  
   492  func (f *Frontend) ExecJS(js string) {
   493  	f.mainWindow.ExecJS(js)
   494  }
   495  
   496  var messageBuffer = make(chan string, 100)
   497  
   498  //export processMessage
   499  func processMessage(message *C.char) {
   500  	goMessage := C.GoString(message)
   501  	messageBuffer <- goMessage
   502  }
   503  
   504  var requestBuffer = make(chan webview.Request, 100)
   505  
   506  func (f *Frontend) startRequestProcessor() {
   507  	for request := range requestBuffer {
   508  		f.assets.ServeWebViewRequest(request)
   509  	}
   510  }
   511  
   512  //export processURLRequest
   513  func processURLRequest(request unsafe.Pointer) {
   514  	requestBuffer <- webview.NewRequest(request)
   515  }
   516  
   517  func (f *Frontend) startSecondInstanceProcessor() {
   518  	for secondInstanceData := range secondInstanceBuffer {
   519  		if f.frontendOptions.SingleInstanceLock != nil &&
   520  			f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil {
   521  			f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData)
   522  		}
   523  	}
   524  }