diff --git a/v2/cmd/wails/internal/commands/build/build.go b/v2/cmd/wails/internal/commands/build/build.go index e537c4eca..94ec4f195 100644 --- a/v2/cmd/wails/internal/commands/build/build.go +++ b/v2/cmd/wails/internal/commands/build/build.go @@ -99,7 +99,7 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) { "darwin/amd64", "darwin/arm64", "darwin/universal", - //"linux", + "linux", //"linux/amd64", //"linux/arm-7", "windows", diff --git a/v2/internal/appng/app_default_linux.go b/v2/internal/appng/app_default_linux.go new file mode 100644 index 000000000..3356debae --- /dev/null +++ b/v2/internal/appng/app_default_linux.go @@ -0,0 +1,32 @@ +//go:build !dev && !production && !bindings && linux + +package appng + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/pkg/options" +) + +// App defines a Wails application structure +type App struct{} + +func (a *App) Run() error { + return nil +} + +// CreateApp creates the app! +func CreateApp(_ *options.App) (*App, error) { + // result := w32.MessageBox(0, + // `Wails applications will not build without the correct build tags. + //Please use "wails build" or press "OK" to open the documentation on how to use "go build"`, + // "Error", + // w32.MB_ICONERROR|w32.MB_OKCANCEL) + // if result == 1 { + // exec.Command("rundll32", "url.dll,FileProtocolHandler", "https://wails.io").Start() + // } + + err := fmt.Errorf(`Wails applications will not build without the correct build tags.`) + + return nil, err +} diff --git a/v2/internal/appng/app_linux.go b/v2/internal/appng/app_linux.go new file mode 100644 index 000000000..a268da51a --- /dev/null +++ b/v2/internal/appng/app_linux.go @@ -0,0 +1,15 @@ +//go:build linux && !bindings + +package appng + +import ( + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func PreflightChecks(options *options.App, logger *logger.Logger) error { + + _ = options + + return nil +} diff --git a/v2/internal/frontend/desktop/desktop_linux.go b/v2/internal/frontend/desktop/desktop_linux.go new file mode 100644 index 000000000..e057a9e18 --- /dev/null +++ b/v2/internal/frontend/desktop/desktop_linux.go @@ -0,0 +1,17 @@ +//go:build linux +// +build linux + +package desktop + +import ( + "context" + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/linux" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +func NewFrontend(ctx context.Context, appoptions *options.App, logger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) frontend.Frontend { + return linux.NewFrontend(ctx, appoptions, logger, appBindings, dispatcher) +} diff --git a/v2/internal/frontend/desktop/linux/browser.go b/v2/internal/frontend/desktop/linux/browser.go new file mode 100644 index 000000000..47bf0ba5d --- /dev/null +++ b/v2/internal/frontend/desktop/linux/browser.go @@ -0,0 +1,12 @@ +//go:build linux +// +build linux + +package linux + +import "github.com/pkg/browser" + +// BrowserOpenURL Use the default browser to open the url +func (f *Frontend) BrowserOpenURL(url string) { + // Specific method implementation + _ = browser.OpenURL(url) +} diff --git a/v2/internal/frontend/desktop/linux/dialog.go b/v2/internal/frontend/desktop/linux/dialog.go new file mode 100644 index 000000000..4e3f1e05b --- /dev/null +++ b/v2/internal/frontend/desktop/linux/dialog.go @@ -0,0 +1,26 @@ +//go:build linux +// +build linux + +package linux + +import "github.com/wailsapp/wails/v2/internal/frontend" + +func (f *Frontend) OpenFileDialog(dialogOptions frontend.OpenDialogOptions) (string, error) { + panic("implement me") +} + +func (f *Frontend) OpenMultipleFilesDialog(dialogOptions frontend.OpenDialogOptions) ([]string, error) { + panic("implement me") +} + +func (f *Frontend) OpenDirectoryDialog(dialogOptions frontend.OpenDialogOptions) (string, error) { + panic("implement me") +} + +func (f *Frontend) SaveFileDialog(dialogOptions frontend.SaveDialogOptions) (string, error) { + panic("implement me") +} + +func (f *Frontend) MessageDialog(dialogOptions frontend.MessageDialogOptions) (string, error) { + panic("implement me") +} diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go new file mode 100644 index 000000000..9e912e30e --- /dev/null +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -0,0 +1,397 @@ +//go:build linux +// +build linux + +package linux + +import ( + "context" + "encoding/json" + "log" + "text/template" + + "github.com/wailsapp/wails/v2/internal/binding" + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/assetserver" + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/options" +) + +type Frontend struct { + + // Context + ctx context.Context + + frontendOptions *options.App + logger *logger.Logger + debug bool + + // Assets + assets *assetserver.DesktopAssetServer + startURL string + + // main window handle + mainWindow *Window + minWidth, minHeight, maxWidth, maxHeight int + bindings *binding.Bindings + dispatcher frontend.Dispatcher + servingFromDisk bool +} + +func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { + + result := &Frontend{ + frontendOptions: appoptions, + logger: myLogger, + bindings: appBindings, + dispatcher: dispatcher, + ctx: ctx, + minHeight: appoptions.MinHeight, + minWidth: appoptions.MinWidth, + maxHeight: appoptions.MaxHeight, + maxWidth: appoptions.MaxWidth, + startURL: "file://wails/", + } + + bindingsJSON, err := appBindings.ToJSON() + if err != nil { + log.Fatal(err) + } + + _devServerURL := ctx.Value("devserverurl") + if _devServerURL != nil { + devServerURL := _devServerURL.(string) + if len(devServerURL) > 0 && devServerURL != "http://localhost:34115" { + result.startURL = devServerURL + return result + } + } + + // Check if we have been given a directory to serve assets from. + // If so, this means we are in dev mode and are serving assets off disk. + // We indicate this through the `servingFromDisk` flag to ensure requests + // aren't cached by WebView2 in dev mode + + _assetdir := ctx.Value("assetdir") + if _assetdir != nil { + result.servingFromDisk = true + } + + assets, err := assetserver.NewDesktopAssetServer(ctx, appoptions.Assets, bindingsJSON) + if err != nil { + log.Fatal(err) + } + result.assets = assets + + return result +} + +func (f *Frontend) WindowReload() { + f.ExecJS("runtime.WindowReload();") +} + +func (f *Frontend) Run(ctx context.Context) error { + + f.ctx = context.WithValue(ctx, "frontend", f) + + mainWindow := NewWindow(f.frontendOptions) + f.mainWindow = mainWindow + + var _debug = ctx.Value("debug") + if _debug != nil { + f.debug = _debug.(bool) + } + + f.WindowCenter() + //f.setupChromium() + // + //mainWindow.OnSize().Bind(func(arg *winc.Event) { + // f.chromium.Resize() + //}) + // + //mainWindow.OnClose().Bind(func(arg *winc.Event) { + // if f.frontendOptions.HideWindowOnClose { + // f.WindowHide() + // } else { + // f.Quit() + // } + //}) + + go func() { + if f.frontendOptions.OnStartup != nil { + f.frontendOptions.OnStartup(f.ctx) + } + }() + + if f.frontendOptions.Fullscreen { + mainWindow.Fullscreen() + } + + mainWindow.Run() + mainWindow.Close() + return nil +} + +func (f *Frontend) WindowCenter() { + f.mainWindow.Center() +} + +func (f *Frontend) WindowSetPos(x, y int) { + f.mainWindow.SetPos(x, y) +} +func (f *Frontend) WindowGetPos() (int, int) { + return f.mainWindow.Pos() +} + +func (f *Frontend) WindowSetSize(width, height int) { + f.mainWindow.SetSize(width, height) +} + +func (f *Frontend) WindowGetSize() (int, int) { + return f.mainWindow.Size() +} + +func (f *Frontend) WindowSetTitle(title string) { + f.mainWindow.SetText(title) +} + +func (f *Frontend) WindowFullscreen() { + f.mainWindow.SetMaxSize(0, 0) + f.mainWindow.SetMinSize(0, 0) + f.mainWindow.Fullscreen() +} + +func (f *Frontend) WindowUnFullscreen() { + f.mainWindow.UnFullscreen() + f.mainWindow.SetMaxSize(f.maxWidth, f.maxHeight) + f.mainWindow.SetMinSize(f.minWidth, f.minHeight) +} + +func (f *Frontend) WindowShow() { + f.mainWindow.Show() +} + +func (f *Frontend) WindowHide() { + f.mainWindow.Hide() +} +func (f *Frontend) WindowMaximise() { + f.mainWindow.Maximise() +} +func (f *Frontend) WindowUnmaximise() { + f.mainWindow.Restore() +} +func (f *Frontend) WindowMinimise() { + f.mainWindow.Minimise() +} +func (f *Frontend) WindowUnminimise() { + f.mainWindow.Restore() +} + +func (f *Frontend) WindowSetMinSize(width int, height int) { + f.minWidth = width + f.minHeight = height + f.mainWindow.SetMinSize(width, height) +} +func (f *Frontend) WindowSetMaxSize(width int, height int) { + f.maxWidth = width + f.maxHeight = height + f.mainWindow.SetMaxSize(width, height) +} + +func (f *Frontend) WindowSetRGBA(col *options.RGBA) { + if col == nil { + return + } + // + //f.mainWindow.Dispatch(func() { + // controller := f.chromium.GetController() + // controller2 := controller.GetICoreWebView2Controller2() + // + // backgroundCol := edge.COREWEBVIEW2_COLOR{ + // A: col.A, + // R: col.R, + // G: col.G, + // B: col.B, + // } + // + // // Webview2 only has 0 and 255 as valid values. + // if backgroundCol.A > 0 && backgroundCol.A < 255 { + // backgroundCol.A = 255 + // } + // + // if f.frontendOptions.Windows != nil && f.frontendOptions.Windows.WebviewIsTransparent { + // backgroundCol.A = 0 + // } + // + // err := controller2.PutDefaultBackgroundColor(backgroundCol) + // if err != nil { + // log.Fatal(err) + // } + //}) +} + +func (f *Frontend) Quit() { + //winc.Exit() +} + +//func (f *Frontend) setupChromium() { +// chromium := edge.NewChromium() +// f.chromium = chromium +// chromium.MessageCallback = f.processMessage +// chromium.WebResourceRequestedCallback = f.processRequest +// chromium.NavigationCompletedCallback = f.navigationCompleted +// chromium.AcceleratorKeyCallback = func(vkey uint) bool { +// w32.PostMessage(f.mainWindow.Handle(), w32.WM_KEYDOWN, uintptr(vkey), 0) +// return false +// } +// chromium.Embed(f.mainWindow.Handle()) +// chromium.Resize() +// settings, err := chromium.GetSettings() +// if err != nil { +// log.Fatal(err) +// } +// err = settings.PutAreDefaultContextMenusEnabled(f.debug) +// if err != nil { +// log.Fatal(err) +// } +// err = settings.PutAreDevToolsEnabled(f.debug) +// if err != nil { +// log.Fatal(err) +// } +// err = settings.PutIsZoomControlEnabled(false) +// if err != nil { +// log.Fatal(err) +// } +// err = settings.PutIsStatusBarEnabled(false) +// if err != nil { +// log.Fatal(err) +// } +// err = settings.PutAreBrowserAcceleratorKeysEnabled(false) +// if err != nil { +// log.Fatal(err) +// } +// err = settings.PutIsSwipeNavigationEnabled(false) +// if err != nil { +// log.Fatal(err) +// } +// +// // Set background colour +// f.WindowSetRGBA(f.frontendOptions.RGBA) +// +// chromium.SetGlobalPermission(edge.CoreWebView2PermissionStateAllow) +// chromium.AddWebResourceRequestedFilter("*", edge.COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL) +// chromium.Navigate(f.startURL) +//} + +type EventNotify struct { + Name string `json:"name"` + Data []interface{} `json:"data"` +} + +func (f *Frontend) Notify(name string, data ...interface{}) { + notification := EventNotify{ + Name: name, + Data: data, + } + payload, err := json.Marshal(notification) + if err != nil { + f.logger.Error(err.Error()) + return + } + f.ExecJS(`window.wails.EventsNotify('` + template.JSEscapeString(string(payload)) + `');`) +} + +//func (f *Frontend) processRequest(req *edge.ICoreWebView2WebResourceRequest, args *edge.ICoreWebView2WebResourceRequestedEventArgs) { +// //Get the request +// uri, _ := req.GetUri() +// +// // Translate URI +// uri = strings.TrimPrefix(uri, "file://wails") +// if !strings.HasPrefix(uri, "/") { +// return +// } +// +// // Load file from asset store +// content, mimeType, err := f.assets.Load(uri) +// if err != nil { +// return +// } +// +// env := f.chromium.Environment() +// headers := "Content-Type: " + mimeType +// if f.servingFromDisk { +// headers += "\nPragma: no-cache" +// } +// response, err := env.CreateWebResourceResponse(content, 200, "OK", headers) +// if err != nil { +// return +// } +// // Send response back +// err = args.PutResponse(response) +// if err != nil { +// return +// } +// return +//} + +func (f *Frontend) processMessage(message string) { + if message == "drag" { + if !f.mainWindow.IsFullScreen() { + err := f.startDrag() + if err != nil { + f.logger.Error(err.Error()) + } + } + return + } + result, err := f.dispatcher.ProcessMessage(message, f) + if err != nil { + f.logger.Error(err.Error()) + f.Callback(result) + return + } + if result == "" { + return + } + + switch result[0] { + case 'c': + // Callback from a method call + f.Callback(result[1:]) + default: + f.logger.Info("Unknown message returned from dispatcher: %+v", result) + } +} + +func (f *Frontend) Callback(message string) { + //f.mainWindow.Dispatch(func() { + // f.chromium.Eval(`window.wails.Callback(` + strconv.Quote(message) + `);`) + //}) +} + +func (f *Frontend) startDrag() error { + //if !w32.ReleaseCapture() { + // return fmt.Errorf("unable to release mouse capture") + //} + //w32.SendMessage(f.mainWindow.Handle(), w32.WM_NCLBUTTONDOWN, w32.HTCAPTION, 0) + return nil +} + +func (f *Frontend) ExecJS(js string) { + //f.mainWindow.Dispatch(func() { + // f.chromium.Eval(js) + //}) +} + +//func (f *Frontend) navigationCompleted(sender *edge.ICoreWebView2, args *edge.ICoreWebView2NavigationCompletedEventArgs) { +// if f.frontendOptions.OnDomReady != nil { +// go f.frontendOptions.OnDomReady(f.ctx) +// } +// +// // If you want to start hidden, return +// if f.frontendOptions.StartHidden { +// return +// } +// +// f.mainWindow.Show() +// +//} diff --git a/v2/internal/frontend/desktop/linux/menu.go b/v2/internal/frontend/desktop/linux/menu.go new file mode 100644 index 000000000..0524429aa --- /dev/null +++ b/v2/internal/frontend/desktop/linux/menu.go @@ -0,0 +1,14 @@ +//go:build linux +// +build linux + +package linux + +import "github.com/wailsapp/wails/v2/pkg/menu" + +func (f *Frontend) MenuSetApplicationMenu(menu *menu.Menu) { + panic("implement me") +} + +func (f *Frontend) MenuUpdateApplicationMenu() { + panic("implement me") +} diff --git a/v2/internal/frontend/desktop/linux/window.go b/v2/internal/frontend/desktop/linux/window.go new file mode 100644 index 000000000..39b029434 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/window.go @@ -0,0 +1,164 @@ +//go:build linux +// +build linux + +package linux + +import ( + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/options" + "sync" +) + +type Window struct { + frontendOptions *options.App + applicationMenu *menu.Menu + m sync.Mutex + //dispatchq []func() +} + +func NewWindow(options *options.App) *Window { + result := new(Window) + result.frontendOptions = options + //result.SetIsForm(true) + // + //var exStyle int + //if options.Windows != nil { + // exStyle = w32.WS_EX_CONTROLPARENT | w32.WS_EX_APPWINDOW + // if options.Windows.WindowIsTranslucent { + // exStyle |= w32.WS_EX_NOREDIRECTIONBITMAP + // } + //} + //if options.AlwaysOnTop { + // exStyle |= w32.WS_EX_TOPMOST + //} + // + //var dwStyle = w32.WS_OVERLAPPEDWINDOW + //if options.Frameless { + // dwStyle = w32.WS_POPUP + //} + // + //winc.RegClassOnlyOnce("wailsWindow") + //result.SetHandle(winc.CreateWindow("wailsWindow", parent, uint(exStyle), uint(dwStyle))) + //result.SetParent(parent) + // + //loadIcon := true + //if options.Windows != nil && options.Windows.DisableWindowIcon == true { + // loadIcon = false + //} + //if loadIcon { + // if ico, err := winc.NewIconFromResource(winc.GetAppInstance(), uint16(winc.AppIconID)); err == nil { + // result.SetIcon(0, ico) + // } + //} + // + //result.SetSize(options.Width, options.Height) + //result.SetText(options.Title) + //if options.Frameless == false && !options.Fullscreen { + // result.EnableMaxButton(!options.DisableResize) + // result.EnableSizable(!options.DisableResize) + // result.SetMinSize(options.MinWidth, options.MinHeight) + // result.SetMaxSize(options.MaxWidth, options.MaxHeight) + //} + // + //if options.Windows != nil { + // if options.Windows.WindowIsTranslucent { + // result.SetTranslucentBackground() + // } + // + // if options.Windows.DisableWindowIcon { + // result.DisableIcon() + // } + //} + // + //// Dlg forces display of focus rectangles, as soon as the user starts to type. + //w32.SendMessage(result.Handle(), w32.WM_CHANGEUISTATE, w32.UIS_INITIALIZE, 0) + //winc.RegMsgHandler(result) + // + //result.SetFont(winc.DefaultFont) + // + //if options.Menu != nil { + // result.SetApplicationMenu(options.Menu) + //} + + return result +} + +func (w *Window) Run() { + +} + +func (w *Window) Dispatch(f func()) { + //w.m.Lock() + //w.dispatchq = append(w.dispatchq, f) + //w.m.Unlock() + //w32.PostMainThreadMessage(w32.WM_APP, 0, 0) +} + +func (w *Window) Fullscreen() { + +} + +func (w *Window) Close() { + +} + +func (w *Window) Center() { + +} + +func (w *Window) SetPos(x int, y int) { + +} + +func (w *Window) Pos() (int, int) { + return 0, 0 +} + +func (w *Window) SetSize(width int, height int) { + +} + +func (w *Window) Size() (int, int) { + return 0, 0 + +} + +func (w *Window) SetText(title string) { + +} + +func (w *Window) SetMaxSize(maxWidth int, maxHeight int) { + +} + +func (w *Window) SetMinSize(minWidth int, minHeight int) { + +} + +func (w *Window) UnFullscreen() { + +} + +func (w *Window) Show() { + +} + +func (w *Window) Hide() { + +} + +func (w *Window) Maximise() { + +} + +func (w *Window) Restore() { + +} + +func (w *Window) Minimise() { + +} + +func (w *Window) IsFullScreen() bool { + return false +} diff --git a/v2/pkg/commands/build/base.go b/v2/pkg/commands/build/base.go index e87012f7d..cd26835d9 100644 --- a/v2/pkg/commands/build/base.go +++ b/v2/pkg/commands/build/base.go @@ -295,10 +295,12 @@ func (b *BaseBuilder) CompileProject(options *Options) error { if options.Platform != "windows" { // Use upsertEnv so we don't overwrite user's CGO_CFLAGS cmd.Env = upsertEnv(cmd.Env, "CGO_CFLAGS", func(v string) string { - if v != "" { - v += " " + if options.Platform == "darwin" { + if v != "" { + v += " " + } + v += "-mmacosx-version-min=10.13" } - v += "-mmacosx-version-min=10.13" return v }) // Use upsertEnv so we don't overwrite user's CGO_CXXFLAGS @@ -313,7 +315,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error { cmd.Env = upsertEnv(cmd.Env, "CGO_ENABLED", func(v string) string { return "1" }) - if runtime.GOOS == "darwin" { + if options.Platform == "darwin" { // Set the minimum Mac SDK to 10.13 cmd.Env = upsertEnv(cmd.Env, "CGO_LDFLAGS", func(v string) string { if v != "" { diff --git a/v2/pkg/commands/build/packager.go b/v2/pkg/commands/build/packager.go index 8587a28ea..bcacfde17 100644 --- a/v2/pkg/commands/build/packager.go +++ b/v2/pkg/commands/build/packager.go @@ -27,6 +27,8 @@ func packageProject(options *Options, platform string) error { err = packageApplicationForDarwin(options) case "windows": err = packageApplicationForWindows(options) + case "linux": + err = packageApplicationForLinux(options) default: err = fmt.Errorf("packing not supported for %s yet", platform) } @@ -204,6 +206,29 @@ func packageApplicationForWindows(options *Options) error { return nil } +func packageApplicationForLinux(options *Options) error { + // Generate icon + //var err error + //err = generateIcoFile(options) + //if err != nil { + // return err + //} + // + //// Ensure Manifest is present + //err = generateManifest(options) + //if err != nil { + // return err + //} + // + //// Create syso file + //err = compileResources(options) + //if err != nil { + // return err + //} + + return nil +} + func generateManifest(options *Options) error { filename := options.ProjectData.Name + ".exe.manifest" manifestFile := filepath.Join(options.ProjectData.Path, "build", "windows", filename)