+
+## Inhaltsverzeichnis
+
+- [Inhaltsverzeichnis](#inhaltsverzeichnis)
+- [Einführung](#einführung)
+- [Funktionen](#funktionen)
+ - [Roadmap](#roadmap)
+- [Loslegen](#loslegen)
+- [Sponsoren](#sponsoren)
+- [FAQ](#faq)
+- [Sterne Überblick](#sterne-überblick)
+- [Mitwirkende](#mitwirkende)
+- [Lizenz](#lizenz)
+- [Inspiration](#inspiration)
+
+## Einführung
+
+Die herkömmliche Methode zur Bereitstellung von Web-Interfaces für Go ist über einen eingebauten Webserver.
+Wails nutzt einen anderen Weg. Es kann sowohl Go-Code als auch ein Web-Frontend in eine einzige Datei bauen.
+Beigelieferte Werkzeuge übernehmen die Projekterstellung, den Kompilierungsprozess und das bauen.
+Du musst nur kreativ werden.
+
+## Funktionen
+
+- Nutze Standard Go für das Backend
+- Nutze eine Frontend Technologie mit der du dich bereits auskennst um dein UI zu bauen.
+- Erschaffe schnell und einfach Frontends mit vorgefertigten Vorlagen für deine Go-Programme
+- Nutze Javascript um Go Methoden aufzurufen
+- Automatisch generierte Typescript Definitionen für deine Go Strukturen und Methoden
+- Native Dialoge und Menüs
+- Native Dark-/Lightmode Unterstützung
+- Unterstützt moderne Transluzenz- und Milchglaseffekte
+- Vereinheitlichtes Eventsystem zwischen Go und Javascript
+- Leistungsstarkes CLI-Tool zum einfachen erstellen und bauen von Projekten
+- Multiplattformen
+- Nutze native Render-Engines - _keine eingebetteten Browser_!
+
+### Roadmap
+
+Die Projekt Roadmap kann [hier](https://github.com/wailsapp/wails/discussions/1484) gefunden werden. Bitte lies diese
+durch bevor du eine Idee vorschlägst
+
+## Loslegen
+
+Die Installationsinstruktionen sind auf der [offiziellen Website](https://wails.io/docs/gettingstarted/installation).
+
+## Sponsoren
+
+Dieses Projekt wird von diesen freundlichen Leuten und Firmen unterstützt:
+
+
+
+
+
+
+## FAQ
+
+- Ist das eine Alternative zu Electron?
+
+ Hängt von deinen Anforderungen ab. Wails wurde entwickelt um das Go-Programmieren leicht zu machen und effiziente
+ Desktop-Anwendungen zu erstellen oder ein Frontend zu einer bestehenden Anwendung hinzuzufügen.
+ Wails bietet native Elemente wie Dialoge und Menüs und könnte somit als eine leichte effiziente Electron-Alternative
+ betrachtet werden.
+
+- Für wen ist dieses projekt geeignet?
+
+ Go Entwickler, die ein HTML/CSS/JS-Frontend in ihre Anwendung integrieren möchten, ohne einen Webserver zu erstellen und
+ einen Browser öffnen zu müssen, um dieses zu sehen
+
+- Wie kam es zu diesem Namen?
+
+ Als ich WebView sah dachte ich "Was ich wirklich will, ist ein Werkzeug für die Erstellung von WebView Anwendungen so wie Rails für Ruby".
+ Also war es zunächst ein Wortspiel (Webview on Rails). Zufälligerweise ist es auch ein Homophon des englischen Namens des [Landes](https://en.wikipedia.org/wiki/Wales), aus dem ich komme.
+ Also ist es dabei geblieben.
+
+## Sterne Überblick
+
+
+
+
+
+
+
+
+
+## Mitwirkende
+
+Die Liste der Mitwirkenden wird zu groß für diese Readme. All die fantastischen Menschen, die zu diesem
+Projekt beigetragen haben, haben [hier](https://wails.io/credits#contributors) ihre eigene Seite.
+
+## Lizenz
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
+
+## Inspiration
+
+Dieses Projekt wurde hauptsächlich zu den folgenden Alben entwickelt
+
+- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA)
+- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN)
+- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8)
+- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr)
+- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m)
+- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle)
+- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs)
+- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM)
+- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm)
+- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug)
+- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB)
+- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF)
+- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
diff --git a/README.es.md b/README.es.md
new file mode 100644
index 000000000..277d1c1fd
--- /dev/null
+++ b/README.es.md
@@ -0,0 +1,169 @@
+
+
+## Tabla de Contenidos
+
+- [Tabla de Contenidos](#tabla-de-contenidos)
+- [Introducción](#introducción)
+- [Funcionalidades](#funcionalidades)
+ - [Plan de Trabajo](#plan-de-trabajo)
+- [Empezando](#empezando)
+- [Patrocinadores](#patrocinadores)
+- [Preguntas Frecuentes](#preguntas-frecuentes)
+- [Estrellas a lo Largo del Tiempo](#estrellas-a-lo-largo-del-tiempo)
+- [Colaboradores](#colaboradores)
+- [Licencia](#licencia)
+- [Inspiración](#inspiración)
+
+## Introducción
+
+El método tradicional para proveer una interfaz web en programas hechos con Go
+es a través del servidor web incorporado. Wails ofrece un enfoque diferente al
+permitir combinar el código hecho en Go con un frontend web en un solo archivo
+binario. Las herramientas que proporcionamos facilitan este trabajo para ti, al
+crear, compilar y empaquetar tu proyecto. ¡Lo único que debes hacer es ponerte
+creativo!
+
+## Funcionalidades
+
+- Utiliza Go estándar para el backend
+- Utiliza cualquier tecnología frontend con la que ya estés familiarizado para
+ construir tu interfaz de usuario
+- Crea rápidamente interfaces de usuario enriquecidas para tus programas en Go
+ utilizando plantillas predefinidas
+- Invoca fácilmente métodos de Go desde Javascript
+- Definiciones de Typescript generadas automáticamente para tus structs y
+ métodos de Go
+- Diálogos y menús nativos
+- Soporte nativo de modo oscuro / claro
+- Soporte de translucidez y efectos de ventana esmerilada
+- Sistema de eventos unificado entre Go y Javascript
+- Herramienta CLI potente para generar y construir tus proyectos rápidamente
+- Multiplataforma
+- Usa motores de renderizado nativos - ¡_sin navegador integrado_!
+
+### Plan de Trabajo
+
+El plan de trabajo se puede encontrar
+[aqui](https://github.com/wailsapp/wails/discussions/1484). Por favor,
+consúltalo antes de abrir una solicitud de mejora.
+
+## Empezando
+
+Las instrucciones de instalacion se encuentran en nuestra
+[pagina web oficial](https://wails.io/docs/gettingstarted/installation).
+
+## Patrocinadores
+
+Este Proyecto cuenta con el apoyo de estas amables personas/ compañías:
+
+
+
+
+
+
+## Preguntas Frecuentes
+
+- ¿Es esta una alternativa a Electron?
+
+ Depende de tus requisitos. Está diseñado para facilitar a los programadores de
+ Go la creación de aplicaciones de escritorio livianas o agregar una interfaz
+ gráfica a sus aplicaciones existentes. Wails ofrece elementos nativos como
+ menús y diálogos, por lo que podría considerarse una alternativa liviana a
+ Electron.
+
+- ¿A quien esta dirigido este proyecto?
+
+ El proyecto esta dirigido a programadores de Go que desean integrar una
+ interfaz HMTL/JS/CSS en sus aplicaciones, sin tener que recurrir a la creación
+ de un servidor y abrir el navegador para visualizarla.
+
+- ¿Cual es el significado del nombre?
+
+ Cuando vi WebView, pensé: "Lo que realmente quiero es una herramienta para
+ construir una aplicación WebView, algo similar a lo que Rails es para Ruby".
+ Así que inicialmente fue un juego de palabras (WebView en Rails). Además, por
+ casualidad, también es homófono del nombre en inglés del
+ [país](https://en.wikipedia.org/wiki/Wales) del que provengo. Así que se quedó
+ con ese nombre.
+
+## Estrellas a lo Largo del Tiempo
+
+[](https://star-history.com/#wailsapp/wails&Date)
+
+## Colaboradores
+
+¡La lista de colaboradores se está volviendo demasiado grande para el archivo
+readme! Todas las personas increíbles que han contribuido a este proyecto tienen
+su propia página [aqui](https://wails.io/credits#contributors).
+
+## Licencia
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
+
+## Inspiración
+
+Este proyecto fue construido mientras se escuchaban estos álbumes:
+
+- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA)
+- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN)
+- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8)
+- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr)
+- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m)
+- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle)
+- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs)
+- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM)
+- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm)
+- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug)
+- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB)
+- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF)
+- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
+ [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
diff --git a/README.fr.md b/README.fr.md
new file mode 100644
index 000000000..61230f353
--- /dev/null
+++ b/README.fr.md
@@ -0,0 +1,144 @@
+
+
+## Sommaire
+
+- [Sommaire](#sommaire)
+- [Introduction](#introduction)
+- [Fonctionnalités](#fonctionnalités)
+ - [Feuille de route](#feuille-de-route)
+- [Démarrage](#démarrage)
+- [Les sponsors](#les-sponsors)
+- [Foire aux questions](#foire-aux-questions)
+- [Les étoiles au fil du temps](#les-étoiles-au-fil-du-temps)
+- [Les contributeurs](#les-contributeurs)
+- [License](#license)
+- [Inspiration](#inspiration)
+
+## Introduction
+
+La méthode traditionnelle pour fournir des interfaces web aux programmes Go consiste à utiliser un serveur web intégré. Wails propose une approche différente : il offre la possibilité d'intégrer à la fois le code Go et une interface web dans un seul binaire. Des outils sont fournis pour vous faciliter la tâche en gérant la création, la compilation et le regroupement des projets. Il ne vous reste plus qu'à faire preuve de créativité!
+
+## Fonctionnalités
+
+- Utiliser Go pour le backend
+- Utilisez n'importe quelle technologie frontend avec laquelle vous êtes déjà familier pour construire votre interface utilisateur.
+- Créez rapidement des interfaces riches pour vos programmes Go à l'aide de modèles prédéfinis.
+- Appeler facilement des méthodes Go à partir de Javascript
+- Définitions Typescript auto-générées pour vos structures et méthodes Go
+- Dialogues et menus natifs
+- Prise en charge native des modes sombre et clair
+- Prise en charge des effets modernes de translucidité et de "frosted window".
+- Système d'événements unifié entre Go et Javascript
+- Outil puissant pour générer et construire rapidement vos projets
+- Multiplateforme
+- Utilise des moteurs de rendu natifs - _pas de navigateur intégré_ !
+
+### Feuille de route
+
+La feuille de route du projet peut être consultée [ici](https://github.com/wailsapp/wails/discussions/1484). Veuillez consulter avant d'ouvrir une demande d'amélioration.
+
+## Démarrage
+
+Les instructions d'installation se trouvent sur le site [site officiel](https://wails.io/docs/gettingstarted/installation).
+
+## Les sponsors
+
+Ce projet est soutenu par ces personnes aimables et entreprises:
+
+
+
+
+
+
+## Foire aux questions
+
+- S'agit-il d'une alternative à Electron ?
+
+ Cela dépend de vos besoins. Il est conçu pour permettre aux programmeurs Go de créer facilement des applications de bureau légères ou d'ajouter une interface à leurs applications existantes. Wails offre des éléments natifs tels que des menus et des boîtes de dialogue, il peut donc être considéré comme une alternative légère à electron.
+
+- À qui s'adresse ce projet ?
+
+ Les programmeurs Go qui souhaitent intégrer une interface HTML/JS/CSS à leurs applications, sans avoir à créer un serveur et à ouvrir un navigateur pour l'afficher.
+
+- Pourquoi ce nom ??
+
+ Lorsque j'ai vu WebView, je me suis dit : "Ce que je veux vraiment, c'est un outil pour construire une application WebView, un peu comme Rails l'est pour Ruby". Au départ, il s'agissait donc d'un jeu de mots (Webview on Rails). Il se trouve que c'est aussi un homophone du nom anglais du [Pays](https://en.wikipedia.org/wiki/Wales) d'où je viens. Il s'est donc imposé.
+
+## Les étoiles au fil du temps
+
+[](https://star-history.com/#wailsapp/wails&Date)
+
+## Les contributeurs
+
+La liste des contributeurs devient trop importante pour le readme ! Toutes les personnes extraordinaires qui ont contribué à ce projet ont leur propre page [ici](https://wails.io/credits#contributors).
+
+## License
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
+
+## Inspiration
+
+Ce projet a été principalement codé sur les albums suivants :
+
+- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA)
+- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN)
+- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8)
+- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr)
+- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m)
+- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle)
+- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs)
+- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM)
+- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm)
+- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug)
+- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB)
+- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF)
+- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
diff --git a/README.ja.md b/README.ja.md
new file mode 100644
index 000000000..ffd9f8103
--- /dev/null
+++ b/README.ja.md
@@ -0,0 +1,152 @@
+
+
+## 목차
+
+- [목차](#목차)
+- [소개](#소개)
+- [기능](#기능)
+ - [로드맵](#로드맵)
+- [시작하기](#시작하기)
+- [스폰서](#스폰서)
+- [FAQ](#faq)
+- [Stargazers 성장 추세](#stargazers-성장-추세)
+- [기여자](#기여자)
+- [라이센스](#라이센스)
+- [영감](#영감)
+
+## 소개
+
+Go 프로그램에 웹 인터페이스를 제공하는 전통적인 방법은 내장 웹 서버를 이용하는 것입니다.
+Wails는 다르게 접근합니다: Go 코드와 웹 프론트엔드를 단일 바이너리로 래핑하는 기능을 제공합니다.
+프로젝트 생성, 컴파일 및 번들링을 처리하여 이를 쉽게 수행할 수 있도록 도구가 제공됩니다.
+창의력을 발휘하기만 하면 됩니다!
+
+## 기능
+
+- 백엔드에 표준 Go 사용
+- 이미 익숙한 프론트엔드 기술을 사용하여 UI 구축
+- 사전 구축된 템플릿을 사용하여 Go 프로그램을 위한 풍부한 프론트엔드를 빠르게 생성
+- Javascript에서 Go 메서드를 쉽게 호출
+- Go 구조체 및 메서드에 대한 자동 생성된 Typescript 정의
+- 기본 대화 및 메뉴
+- 네이티브 다크/라이트 모드 지원
+- 최신 반투명도 및 "반투명 창" 효과 지원
+- Go와 Javascript 간의 통합 이벤트 시스템
+- 프로젝트를 빠르게 생성하고 구축하는 강력한 CLI 도구
+- 멀티플랫폼
+- 기본 렌더링 엔진 사용 - _내장 브라우저 없음_!
+
+### 로드맵
+
+프로젝트 로드맵은 [여기](https://github.com/wailsapp/wails/discussions/1484)에서
+확인할 수 있습니다. 개선 요청을 하기 전에 이것을 참조하십시오.
+
+## 시작하기
+
+설치 지침은
+[공식 웹사이트](https://wails.io/docs/gettingstarted/installation)에 있습니다.
+
+## 스폰서
+
+이 프로젝트는 친절한 사람들 / 회사들이 지원합니다.
+
+
+## FAQ
+
+- 이것은 Electron의 대안인가요?
+
+ 요구 사항에 따라 다릅니다. Go 프로그래머가 쉽게 가벼운 데스크톱 애플리케이션을
+ 만들거나 기존 애플리케이션에 프론트엔드를 추가할 수 있도록 설계되었습니다.
+ Wails는 메뉴 및 대화 상자와 같은 기본 요소를 제공하므로 가벼운 Electron 대안으로
+ 간주될 수 있습니다.
+
+- 이 프로젝트는 누구를 대상으로 하나요?
+
+ 서버를 생성하고 이를 보기 위해 브라우저를 열 필요 없이 HTML/JS/CSS 프런트엔드를
+ 애플리케이션과 함께 묶고자 하는 프로그래머를 대상으로 합니다.
+
+- Wails 이름의 의미는 무엇인가요?
+
+ WebView를 보았을 때 저는 "내가 정말로 원하는 것은 WebView 앱을 구축하기 위한
+ 도구를 사용하는거야. 마치 Ruby on Rails 처럼 말이야."라고 생각했습니다.
+ 그래서 처음에는 말장난(Webview on Rails)이었습니다.
+ [국가](https://en.wikipedia.org/wiki/Wales)에 대한 영어 이름의 동음이의어이기도 하여 정했습니다.
+
+## Stargazers 성장 추세
+
+[](https://star-history.com/#wailsapp/wails&Date)
+
+## 기여자
+
+기여자 목록이 추가 정보에 비해 너무 커지고 있습니다! 이 프로젝트에 기여한 모든 놀라운 사람들은
+[여기](https://wails.io/credits#contributors)에 자신의 페이지를 가지고 있습니다.
+
+## 라이센스
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
+
+## 영감
+
+이 프로젝트는 주로 다음 앨범을 들으며 코딩되었습니다.
+
+- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA)
+- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN)
+- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8)
+- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr)
+- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m)
+- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle)
+- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs)
+- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM)
+- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm)
+- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug)
+- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB)
+- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF)
+- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
diff --git a/README.md b/README.md
index 8276d9204..5ab9309b4 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,159 @@
-# Coming Soon
\ No newline at end of file
+
+
+## Table of Contents
+
+- [Table of Contents](#table-of-contents)
+- [Introduction](#introduction)
+- [Features](#features)
+ - [Roadmap](#roadmap)
+- [Getting Started](#getting-started)
+- [Sponsors](#sponsors)
+- [FAQ](#faq)
+- [Stargazers over time](#stargazers-over-time)
+- [Contributors](#contributors)
+- [License](#license)
+- [Inspiration](#inspiration)
+
+## Introduction
+
+The traditional method of providing web interfaces to Go programs is via a built-in web server. Wails offers a different
+approach: it provides the ability to wrap both Go code and a web frontend into a single binary. Tools are provided to
+make this easy for you by handling project creation, compilation and bundling. All you have to do is get creative!
+
+## Features
+
+- Use standard Go for the backend
+- Use any frontend technology you are already familiar with to build your UI
+- Quickly create rich frontends for your Go programs using pre-built templates
+- Easily call Go methods from Javascript
+- Auto-generated Typescript definitions for your Go structs and methods
+- Native Dialogs & Menus
+- Native Dark / Light mode support
+- Supports modern translucency and "frosted window" effects
+- Unified eventing system between Go and Javascript
+- Powerful cli tool to quickly generate and build your projects
+- Multiplatform
+- Uses native rendering engines - _no embedded browser_!
+
+### Roadmap
+
+The project roadmap may be found [here](https://github.com/wailsapp/wails/discussions/1484). Please consult
+it before creating an enhancement request.
+
+## Getting Started
+
+The installation instructions are on the [official website](https://wails.io/docs/gettingstarted/installation).
+
+## Sponsors
+
+This project is supported by these kind people / companies:
+
+
+## Powered By
+
+[](https://jb.gg/OpenSource)
+
+## FAQ
+
+- Is this an alternative to Electron?
+
+ Depends on your requirements. It's designed to make it easy for Go programmers to make lightweight desktop
+ applications or add a frontend to their existing applications. Wails does offer native elements such as menus
+ and dialogs, so it could be considered a lightweight electron alternative.
+
+- Who is this project aimed at?
+
+ Go programmers who want to bundle an HTML/JS/CSS frontend with their applications, without resorting to creating a
+ server and opening a browser to view it.
+
+- What's with the name?
+
+ When I saw WebView, I thought "What I really want is tooling around building a WebView app, a bit like Rails is to
+ Ruby". So initially it was a play on words (Webview on Rails). It just so happened to also be a homophone of the
+ English name for the [Country](https://en.wikipedia.org/wiki/Wales) I am from. So it stuck.
+
+## Stargazers over time
+
+
+
+
+
+
+
+
+
+## Contributors
+
+The contributors list is getting too big for the readme! All the amazing people who have contributed to this
+project have their own page [here](https://wails.io/credits#contributors).
+
+## License
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
+
+## Inspiration
+
+This project was mainly coded to the following albums:
+
+- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA)
+- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN)
+- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8)
+- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr)
+- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m)
+- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle)
+- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs)
+- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM)
+- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm)
+- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug)
+- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB)
+- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF)
+- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
diff --git a/README.pt-br.md b/README.pt-br.md
new file mode 100644
index 000000000..0e3883352
--- /dev/null
+++ b/README.pt-br.md
@@ -0,0 +1,151 @@
+
+
+## Índice
+
+- [Índice](#índice)
+- [Introdução](#introdução)
+- [Recursos e funcionalidades](#recursos-e-funcionalidades)
+ - [Plano de trabalho](#plano-de-trabalho)
+- [Iniciando](#iniciando)
+- [Patrocinadores](#patrocinadores)
+- [Perguntas frequentes](#perguntas-frequentes)
+- [Estrelas ao longo do tempo](#estrelas-ao-longo-do-tempo)
+- [Colaboradores](#colaboradores)
+- [Licença](#licença)
+- [Inspiração](#inspiração)
+
+## Introdução
+
+O método tradicional de fornecer interfaces da Web para programas Go é por meio de um servidor da Web integrado. Wails oferece uma
+abordagem: fornece a capacidade de agrupar o código Go e um front-end da Web em um único binário. As ferramentas são fornecidas para
+que torne isso mais fácil para você lidando com a criação, compilação e agrupamento de projetos. Tudo o que você precisa fazer é ser criativo!
+
+## Recursos e funcionalidades
+
+- Use Go padrão para o back-end
+- Use qualquer tecnologia de front-end com a qual você já esteja familiarizado para criar sua interface do usuário
+- Crie rapidamente um front-end avançado para seus programas Go usando modelos pré-construídos
+- Chame facilmente métodos Go com JavaScript
+- Definições TypeScript geradas automaticamente para suas estruturas e métodos Go
+- Diálogos e menus nativos
+- Suporte nativo ao modo escuro/claro
+- Suporta translucidez moderna e efeitos de "janela fosca"
+- Sistema de eventos unificado entre Go e JavaScript
+- Poderosa ferramenta cli para gerar e construir rapidamente seus projetos
+- Multiplataforma
+- Usa mecanismos de renderização nativos - _sem navegador incorporado_!
+
+### Plano de trabalho
+
+O plano de trabalho do projeto pode ser encontrado [aqui](https://github.com/wailsapp/wails/discussions/1484). Por favor consulte
+isso antes de abrir um pedido de melhoria.
+
+## Iniciando
+
+As instruções de instalação estão no [site oficial](https://wails.io/docs/gettingstarted/installation).
+
+## Patrocinadores
+
+Este projeto é apoiado por estas simpáticas pessoas/empresas:
+
+
+
+
+
+
+## Perguntas frequentes
+
+- Esta é uma alternativa ao Electron?
+
+ Depende de seus requisitos. Ele foi projetado para tornar mais fácil para os programadores Go criar aplicações desktop
+ e adicionar um front-end aos seus aplicativos existentes. O Wails oferece elementos nativos, como menus
+ e diálogos, por isso pode ser considerada uma alternativa leve, se comparado ao Electron.
+
+- A quem se destina este projeto?
+
+ Programadores Go que desejam agrupar um front-end HTML/JS/CSS com seus aplicativos, sem recorrer à criação de um
+ servidor e abrir um navegador para visualizá-lo.
+
+- Qual é o significado do nome?
+
+ Quando vi o WebView, pensei "O que eu realmente quero é ferramentas para construir um aplicativo WebView, algo semelhante ao que Rails é para Ruby". Portanto, inicialmente era um jogo de palavras (WebView on Rails). Por acaso, também era um homófono do
+ Nome em inglês para o [país](https://en.wikipedia.org/wiki/Wales) de onde eu sou. Então ficou com esse nome.
+
+## Estrelas ao longo do tempo
+
+[](https://star-history.com/#wailsapp/wails&Date)
+
+## Colaboradores
+
+A lista de colaboradores está ficando grande demais para o arquivo readme! Todas as pessoas incríveis que contribuíram para o
+projeto tem sua própria página [aqui](https://wails.io/credits#contributors).
+
+## Licença
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
+
+## Inspiração
+
+Este projeto foi construído ouvindo esses álbuns:
+
+- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA)
+- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN)
+- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8)
+- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr)
+- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m)
+- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle)
+- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs)
+- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM)
+- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm)
+- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug)
+- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB)
+- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF)
+- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
diff --git a/README.ru.md b/README.ru.md
new file mode 100644
index 000000000..76fa59d07
--- /dev/null
+++ b/README.ru.md
@@ -0,0 +1,153 @@
+
+
+## Содержание
+
+- [Содержание](#содержание)
+- [Вступление](#вступление)
+- [Особенности](#особенности)
+ - [Roadmap](#roadmap)
+- [Быстрый старт](#быстрый-старт)
+- [Спонсоры](#спонсоры)
+- [FAQ](#faq)
+- [График звёздочек](#график-звёздочек-репозитория-относительно-времени)
+- [Контребьюторы](#контребьюторы)
+- [Лицензия](#лицензия)
+- [Вдохновение](#вдохновение)
+
+## Вступление
+
+Обычно, веб-интерфейсы для программ Go - это встроенный веб-сервер и веб-браузер.
+У Walls другой подход: он оборачивает как код Go, так и веб-интерфейс в один бинарник (EXE файл).
+Облегчает вам создание вашего приложения, управляя созданием, компиляцией и объединением проектов.
+Все ограничивается лишь вашей фантазией!
+
+## Особенности
+
+- Использование Go для backend
+- Поддержка любой frontend технологии, с которой вы уже знакомы для создания вашего UI
+- Быстрое создание frontend для ваших программ, используя готовые шаблоны
+- Очень лёгкий вызов функций Go из JavaScript
+- Автогенерация TypeScript типов для Go структур и функций
+- Нативные диалоги и меню
+- Нативная поддержка тёмной и светлой темы
+- Поддержка современных эффектов прозрачности и "матового окна"
+- Единая система эвентов для Go и JavaScript
+- Мощный CLI для быстрого создания ваших проектов
+- Мультиплатформенность
+- Использование нативного движка рендеринга - нет встроенному браузеру!
+
+### Roadmap
+
+Roadmap проекта вы можете найти [здесь](https://github.com/wailsapp/wails/discussions/1484).
+Пожалуйста, проконсультируйтесь перед предложением улучшения.
+
+## Быстрый старт
+
+Инструкции по установке находятся на [официальном сайте](https://wails.io/docs/gettingstarted/installation).
+
+## Спонсоры
+
+Проект поддерживается этими добрыми людьми / компаниями:
+
+
+
+
+
+
+## FAQ
+
+- Это альтернатива Electron?
+
+ Зависит от ваших требований. Wails разработан для легкого создания Desktop приложений или
+ расширения интерфейсной части существующих приложений для программистов на Go. Wails действительно
+ предлагает встроенные элементы, такие как меню и диалоги, так что его можно считать облегченной альтернативой Electron.
+
+- Для кого предназначен этот проект?
+
+ Для Golang программистов, которые хотят создавать приложения, используя HTML, JS и CSS,
+ без создания веб-сервера и открытия браузера для их просмотра.
+
+- Что это за название?
+
+ Когда я увидел WebView, я подумал: "Что мне действительно нужно, так это инструменты для создания приложения WebView,
+ немного похожие на Rails для Ruby". Изначально это была игра слов (Webview on Rails). Просто так получилось, что это
+ также омофон английского названия для [Страны](https://en.wikipedia.org/wiki/Wales) от куда я родом. Так что это прижилось.
+
+## График звёздочек репозитория по времени
+
+[](https://star-history.com/#wailsapp/wails&Date)
+
+## Контрибьюторы
+
+Список участников слишком велик для README! У всех замечательных людей, которые внесли свой вклад в этот
+проект, есть своя [страничка](https://wails.io/credits#contributors).
+
+## Лицензия
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
+
+## Вдохновение
+
+Этот проект был создан, в основном, под эти альбомы:
+
+- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA)
+- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN)
+- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8)
+- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr)
+- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m)
+- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle)
+- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs)
+- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM)
+- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm)
+- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug)
+- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB)
+- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF)
+- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
diff --git a/README.tr.md b/README.tr.md
new file mode 100644
index 000000000..e9b16ca76
--- /dev/null
+++ b/README.tr.md
@@ -0,0 +1,156 @@
+
+
+## İçerik
+
+- [İçerik](#içerik)
+- [Giriş](#giriş)
+- [Özellikler](#özellikler)
+ - [Yol Haritası](#yol-haritası)
+- [Başlarken](#başlarken)
+- [Sponsorlar](#sponsorlar)
+- [Sıkça sorulan sorular](#sıkça-sorulan-sorular)
+- [Zaman içinda yıldızlayanlar](#zaman-içinde-yıldızlayanlar)
+- [Katkıda bulunanlar](#katkıda-bulunanlar)
+- [Lisans](#lisans)
+- [İlham](#ilham)
+
+## Giriş
+
+Go programlarına web arayüzleri sağlamak için geleneksel yöntem, yerleşik bir web sunucusu kullanmaktır. Wails, farklı bir yaklaşım sunar: Hem Go kodunu hem de bir web ön yüzünü tek bir ikili dosyada paketleme yeteneği sağlar. Proje oluşturma, derleme ve paketleme işlemlerini kolaylaştıran araçlar sunar. Tek yapmanız gereken yaratıcı olmaktır!
+
+## Özellikler
+
+- Backend için standart Go kullanın
+- Kullanıcı arayüzünüzü oluşturmak için zaten aşina olduğunuz herhangi bir frontend teknolojisini kullanın
+- Hazır şablonlar kullanarak Go programlarınız için hızlıca zengin ön yüzler oluşturun
+- Javascript'ten Go metodlarını kolayca çağırın
+- Go yapı ve metodlarınız için otomatik oluşturulan Typescript tanımları
+- Yerel Diyaloglar ve Menüler
+- Yerel Karanlık / Aydınlık mod desteği
+- Modern saydamlık ve "buzlu cam" efektlerini destekler
+- Go ve Javascript arasında birleşik olay sistemi
+- Projelerinizi hızlıca oluşturmak ve derlemek için güçlü bir komut satırı aracı
+- Çoklu platform desteği
+- Yerel render motorlarını kullanır - _gömülü tarayıcı yok_!
+
+
+### Yol Haritesı
+
+Proje yol haritasına [buradan](https://github.com/wailsapp/wails/discussions/1484) ulaşabilirsiniz. Lütfen bir iyileştirme talebi oluşturmadan önce danışın.
+
+
+## Başlarken
+
+Kurulum talimatları [resmi web sitesinde](https://wails.io/docs/gettingstarted/installation) bulunmaktadır.
+
+
+## Sponsorlar
+
+Bu proje, aşağıdaki nazik insanlar / şirketler tarafından desteklenmektedir:
+
+
+
+
+
+
+## Sıkça Sorulan Sorular
+
+- Bu Electron'a alternatif mi?
+
+ Gereksinimlerinize bağlıdır. Go programcılarının hafif masaüstü uygulamaları yapmasını veya mevcut uygulamalarına bir ön yüz eklemelerini kolaylaştırmak için tasarlanmıştır. Wails, menüler ve diyaloglar gibi yerel öğeler sunduğundan, hafif bir Electron alternatifi olarak kabul edilebilir.
+
+- Bu proje kimlere yöneliktir?
+
+ HTML/JS/CSS ön yüzünü uygulamalarıyla birlikte paketlemek isteyen, ancak bir sunucu oluşturup bir tarayıcı açmaya başvurmadan bunu yapmak isteyen Go programcıları için.
+
+- İsmin anlamı nedir?
+
+ WebView'i gördüğümde, "Aslında istediğim şey, WebView uygulaması oluşturmak için araçlar, biraz Rails'in Ruby için olduğu gibi" diye düşündüm. Bu nedenle başlangıçta kelime oyunu (Rails üzerinde Webview) olarak ortaya çıktı. Ayrıca, benim geldiğim [ülkenin](https://en.wikipedia.org/wiki/Wales) İngilizce adıyla homofon olması tesadüf oldu. Bu yüzden bu isim kaldı.
+
+
+## Zaman içinda yıldızlayanlar
+
+
+
+
+
+
+
+
+
+## Katkıda Bulunanlar
+
+Katkıda bulunanların listesi, README için çok büyük hale geldi! Bu projeye katkıda bulunan tüm harika insanların kendi sayfaları [burada](https://wails.io/credits#contributors) bulunmaktadır.
+
+
+## Lisans
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
+
+## İlham
+
+Bu proje esas olarak aşağıdaki albümler dinlenilerek kodlandı:
+
+- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA)
+- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN)
+- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8)
+- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr)
+- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m)
+- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle)
+- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs)
+- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM)
+- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm)
+- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug)
+- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB)
+- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF)
+- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
+
diff --git a/README.uz.md b/README.uz.md
new file mode 100644
index 000000000..807262405
--- /dev/null
+++ b/README.uz.md
@@ -0,0 +1,159 @@
+
+
+## Tarkib
+
+- [Tarkib](#tarkib)
+- [Kirish](#kirish)
+- [Xususiyatlari](#xususiyatlari)
+ - [Yo'l xaritasi](#yol-xaritasi)
+- [Ishni boshlash](#ishni-boshlash)
+- [Homiylar](#homiylar)
+- [FAQ](#faq)
+- [Vaqt o'tishi bilan yulduzlar](#vaqt-otishi-bilan-yulduzlar)
+- [Ishtirokchilar](#homiylar)
+- [Litsenziya](#litsenziya)
+- [Ilhomlanish](#ilhomlanish)
+
+## Kirish
+
+Odatda, Go dasturlari uchun veb-interfeyslar o'rnatilgan veb-server va veb-brauzerdir.
+Walls boshqacha yondashuvni qo'llaydi: u Go kodini ham, veb-interfeysni ham bitta ikkilik (e.g: EXE)fayliga o'raydi.
+Loyihalarni yaratish, kompilyatsiya qilish va birlashtirishni boshqarish orqali ilovangizni yaratishni osonlashtiradi.
+Hamma narsa faqat sizning tasavvuringiz bilan cheklangan!
+
+## Xususiyatlari
+
+- Backend uchun standart Go dan foydalaning
+- UI yaratish uchun siz allaqachon tanish bo'lgan har qanday frontend texnologiyasidan foydalaning
+- Oldindan tayyorlangan shablonlardan foydalanib, Go dasturlaringiz uchun tezda boy frontendlarni yarating
+- Javascriptdan Go methodlarini osongina chaqiring
+- Go struktura va methodlari uchun avtomatik yaratilgan Typescript ta'riflari
+- Mahalliy Dialoglar va Menyular
+- Mahalliy Dark / Light rejimini qo'llab-quvvatlash
+- Zamonaviy shaffoflik va "muzli oyna" effektlarini qo'llab-quvvatlaydi
+- Go va Javascript o'rtasidagi yagona hodisa tizimi
+- Loyihalaringizni tezda yaratish va qurish uchun kuchli cli vositasi
+- Ko'p platformali
+- Mahalliy renderlash mexanizmlaridan foydalanadi - _o'rnatilgan brauzer yo'q_!
+
+### Yo'l xaritasi
+
+Loyihaning yoʻl xaritasini [bu yerdan](https://github.com/wailsapp/wails/discussions/1484) topish mumkin. Iltimos, maslahatlashing
+Buni yaxshilash so'rovini ochishdan oldin.
+
+## Ishni boshlash
+
+O'rnatish bo'yicha ko'rsatmalar [Rasmiy veb saytda](https://wails.io/docs/gettingstarted/installation) mavjud.
+
+## Homiylar
+
+Ushbu loyiha quyidagi mehribon odamlar / kompaniyalar tomonidan qo'llab-quvvatlanadi:
+
+
+
+
+
+
+## FAQ
+
+- Bu Elektronga muqobilmi?
+
+ Sizning talablaringizga bog'liq. Bu Go dasturchilariga yengil ish stoli yaratishni osonlashtirish uchun yaratilgan
+ ilovalar yoki ularning mavjud ilovalariga frontend qo'shing. Wails menyular kabi mahalliy elementlarni taklif qiladi
+ va dialoglar, shuning uchun uni yengil elektron muqobili deb hisoblash mumkin.
+
+- Ushbu loyiha kimlar uchun?
+
+ Server yaratmasdan va uni ko'rish uchun brauzerni ochmasdan, o'z ilovalari bilan HTML/JS/CSS orqali frontendini birlashtirmoqchi bo'lgan dasturchilar uchun.
+
+- Bu qanday nom?
+
+ Men WebViewni ko'rganimda, men shunday deb o'yladim: "Menga WebView ilovasini yaratish uchun vositalar kerak.
+ biroz Rails for Rubyga o'xshaydi." Demak, dastlab bu so'zlar ustida o'yin edi (Railsda Webview). Shunday bo'ldi.
+ u men kelgan [Mamlakat](https://en.wikipedia.org/wiki/Wales)ning inglizcha nomining omofonidir.
+
+## Vaqt o'tishi bilan yulduzlar
+
+
+
+
+
+
+
+
+
+## Ishtirokchilar
+
+Ishtirokchilar roʻyxati oʻqish uchun juda kattalashib bormoqda! Bunga hissa qo'shgan barcha ajoyib odamlarning
+loyihada o'z sahifasi bor [bu yerga](https://wails.io/credits#contributors).
+
+## Litsenziya
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
+
+## Ilhomlanish
+
+Ushbu loyiha asosan quyidagi albomlar uchun kodlangan:
+
+- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA)
+- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN)
+- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8)
+- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr)
+- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m)
+- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle)
+- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs)
+- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM)
+- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm)
+- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug)
+- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB)
+- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF)
+- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
diff --git a/README.zh-Hans.md b/README.zh-Hans.md
new file mode 100644
index 000000000..4c09d0c45
--- /dev/null
+++ b/README.zh-Hans.md
@@ -0,0 +1,144 @@
+
+
+## Table of Contents
+
+
+ Click me to Open/Close the directory listing
+
+- [Table of Contents](#table-of-contents)
+- [Introduction](#introduction)
+ - [Roadmap](#roadmap)
+- [Features](#features)
+- [Sponsors](#sponsors)
+- [Getting Started](#getting-started)
+- [FAQ](#faq)
+- [Contributors](#contributors)
+- [License](#license)
+
+
+
+## Introduction
+
+The traditional method of providing web interfaces to Go programs is via a built-in web server. Wails offers a different
+approach: it provides the ability to wrap both Go code and a web frontend into a single binary. Tools are provided to
+make this easy for you by handling project creation, compilation and bundling. All you have to do is get creative!
+
+## Features
+
+- Use standard Go for the backend
+- Use any frontend technology you are already familiar with to build your UI
+- Quickly create rich frontends for your Go programs using pre-built templates
+- Easily call Go methods from Javascript
+- Auto-generated Typescript definitions for your Go structs and methods
+- Native Dialogs & Menus
+- Native Dark / Light mode support
+- Supports modern translucency and "frosted window" effects
+- Unified eventing system between Go and Javascript
+- Powerful cli tool to quickly generate and build your projects
+- Multiplatform
+- Uses native rendering engines - _no embedded browser_!
+
+### Roadmap
+
+The project roadmap may be found [here](https://github.com/wailsapp/wails/discussions/1484). Please consult
+this before open up an enhancement request.
+
+## Sponsors
+
+This project is supported by these kind people / companies:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Getting Started
+
+The installation instructions are on the [official website](https://wails.io/docs/gettingstarted/installation).
+
+## FAQ
+
+- Is this an alternative to Electron?
+
+ Depends on your requirements. It's designed to make it easy for Go programmers to make lightweight desktop
+ applications or add a frontend to their existing applications. Wails does offer native elements such as menus
+ and dialogs, so it could be considered a lightweight electron alternative.
+
+- Who is this project aimed at?
+
+ Go programmers who want to bundle an HTML/JS/CSS frontend with their applications, without resorting to creating a
+ server and opening a browser to view it.
+
+- What's with the name?
+
+ When I saw WebView, I thought "What I really want is tooling around building a WebView app, a bit like Rails is to
+ Ruby". So initially it was a play on words (Webview on Rails). It just so happened to also be a homophone of the
+ English name for the [Country](https://en.wikipedia.org/wiki/Wales) I am from. So it stuck.
+
+## Stargazers over time
+
+[](https://starchart.cc/wailsapp/wails)
+
+## Contributors
+
+The contributors list is getting too big for the readme! All the amazing people who have contributed to this
+project have their own page [here](https://wails.io/credits#contributors).
+
+## License
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
+
+## Inspiration
+
+This project was mainly coded to the following albums:
+
+- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA)
+- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN)
+- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8)
+- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr)
+- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m)
+- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle)
+- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs)
+- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM)
+- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm)
+- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug)
+- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB)
+- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF)
+- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v)
diff --git a/v2/Taskfile.yaml b/v2/Taskfile.yaml
new file mode 100644
index 000000000..d1893732b
--- /dev/null
+++ b/v2/Taskfile.yaml
@@ -0,0 +1,28 @@
+# https://taskfile.dev
+
+version: "3"
+
+tasks:
+ download:
+ summary: Run go mod tidy
+ cmds:
+ - go mod tidy
+
+ lint:
+ summary: Run golangci-lint
+ cmds:
+ - golangci-lint run ./... --timeout=3m -v
+
+ release:
+ summary: Release a new version of Task. Call with `task v2:release -- `
+ dir: tools/release
+ cmds:
+ - go run release.go {{.CLI_ARGS}}
+
+ format:md:
+ cmds:
+ - npx prettier --write "**/*.md"
+
+ format:
+ cmds:
+ - task: format:md
diff --git a/v2/cmd/wails/build.go b/v2/cmd/wails/build.go
new file mode 100644
index 000000000..39ad00d2f
--- /dev/null
+++ b/v2/cmd/wails/build.go
@@ -0,0 +1,276 @@
+package main
+
+import (
+ "fmt"
+ "github.com/wailsapp/wails/v2/pkg/commands/buildtags"
+ "os"
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/leaanthony/slicer"
+ "github.com/pterm/pterm"
+ "github.com/wailsapp/wails/v2/cmd/wails/flags"
+ "github.com/wailsapp/wails/v2/cmd/wails/internal/gomod"
+ "github.com/wailsapp/wails/v2/internal/colour"
+ "github.com/wailsapp/wails/v2/internal/project"
+ "github.com/wailsapp/wails/v2/pkg/clilogger"
+ "github.com/wailsapp/wails/v2/pkg/commands/build"
+)
+
+func buildApplication(f *flags.Build) error {
+ if f.NoColour {
+ pterm.DisableColor()
+ colour.ColourEnabled = false
+ }
+
+ quiet := f.Verbosity == flags.Quiet
+
+ // Create logger
+ logger := clilogger.New(os.Stdout)
+ logger.Mute(quiet)
+
+ if quiet {
+ pterm.DisableOutput()
+ } else {
+ app.PrintBanner()
+ }
+
+ err := f.Process()
+ if err != nil {
+ return err
+ }
+
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ projectOptions, err := project.Load(cwd)
+ if err != nil {
+ return err
+ }
+
+ // Set obfuscation from project file
+ if projectOptions.Obfuscated {
+ f.Obfuscated = projectOptions.Obfuscated
+ }
+
+ // Set garble args from project file
+ if projectOptions.GarbleArgs != "" {
+ f.GarbleArgs = projectOptions.GarbleArgs
+ }
+
+ projectTags, err := buildtags.Parse(projectOptions.BuildTags)
+ if err != nil {
+ return err
+ }
+ userTags := f.GetTags()
+ compiledTags := append(projectTags, userTags...)
+
+ // Create BuildOptions
+ buildOptions := &build.Options{
+ Logger: logger,
+ OutputType: "desktop",
+ OutputFile: f.OutputFilename,
+ CleanBinDirectory: f.Clean,
+ Mode: f.GetBuildMode(),
+ Devtools: f.Debug || f.Devtools,
+ Pack: !f.NoPackage,
+ LDFlags: f.LdFlags,
+ Compiler: f.Compiler,
+ SkipModTidy: f.SkipModTidy,
+ Verbosity: f.Verbosity,
+ ForceBuild: f.ForceBuild,
+ IgnoreFrontend: f.SkipFrontend,
+ Compress: f.Upx,
+ CompressFlags: f.UpxFlags,
+ UserTags: compiledTags,
+ WebView2Strategy: f.GetWebView2Strategy(),
+ TrimPath: f.TrimPath,
+ RaceDetector: f.RaceDetector,
+ WindowsConsole: f.WindowsConsole,
+ Obfuscated: f.Obfuscated,
+ GarbleArgs: f.GarbleArgs,
+ SkipBindings: f.SkipBindings,
+ ProjectData: projectOptions,
+ SkipEmbedCreate: f.SkipEmbedCreate,
+ }
+
+ tableData := pterm.TableData{
+ {"Platform(s)", f.Platform},
+ {"Compiler", f.GetCompilerPath()},
+ {"Skip Bindings", bool2Str(f.SkipBindings)},
+ {"Build Mode", f.GetBuildModeAsString()},
+ {"Devtools", bool2Str(buildOptions.Devtools)},
+ {"Frontend Directory", projectOptions.GetFrontendDir()},
+ {"Obfuscated", bool2Str(f.Obfuscated)},
+ }
+ if f.Obfuscated {
+ tableData = append(tableData, []string{"Garble Args", f.GarbleArgs})
+ }
+ tableData = append(tableData, pterm.TableData{
+ {"Skip Frontend", bool2Str(f.SkipFrontend)},
+ {"Compress", bool2Str(f.Upx)},
+ {"Package", bool2Str(!f.NoPackage)},
+ {"Clean Bin Dir", bool2Str(f.Clean)},
+ {"LDFlags", f.LdFlags},
+ {"Tags", "[" + strings.Join(compiledTags, ",") + "]"},
+ {"Race Detector", bool2Str(f.RaceDetector)},
+ }...)
+ if len(buildOptions.OutputFile) > 0 && f.GetTargets().Length() == 1 {
+ tableData = append(tableData, []string{"Output File", f.OutputFilename})
+ }
+ pterm.DefaultSection.Println("Build Options")
+
+ err = pterm.DefaultTable.WithData(tableData).Render()
+ if err != nil {
+ return err
+ }
+
+ if !f.NoSyncGoMod {
+ err = gomod.SyncGoMod(logger, f.UpdateWailsVersionGoMod)
+ if err != nil {
+ return err
+ }
+ }
+
+ // Check platform
+ validPlatformArch := slicer.String([]string{
+ "darwin",
+ "darwin/amd64",
+ "darwin/arm64",
+ "darwin/universal",
+ "linux",
+ "linux/amd64",
+ "linux/arm64",
+ "linux/arm",
+ "windows",
+ "windows/amd64",
+ "windows/arm64",
+ "windows/386",
+ })
+
+ outputBinaries := map[string]string{}
+
+ // Allows cancelling the build after the first error. It would be nice if targets.Each would support funcs
+ // returning an error.
+ var targetErr error
+ targets := f.GetTargets()
+ targets.Each(func(platform string) {
+ if targetErr != nil {
+ return
+ }
+
+ if !validPlatformArch.Contains(platform) {
+ buildOptions.Logger.Println("platform '%s' is not supported - skipping. Supported platforms: %s", platform, validPlatformArch.Join(","))
+ return
+ }
+
+ desiredFilename := projectOptions.OutputFilename
+ if desiredFilename == "" {
+ desiredFilename = projectOptions.Name
+ }
+ desiredFilename = strings.TrimSuffix(desiredFilename, ".exe")
+
+ // Calculate platform and arch
+ platformSplit := strings.Split(platform, "/")
+ buildOptions.Platform = platformSplit[0]
+ buildOptions.Arch = f.GetDefaultArch()
+ if len(platformSplit) > 1 {
+ buildOptions.Arch = platformSplit[1]
+ }
+ banner := "Building target: " + buildOptions.Platform + "/" + buildOptions.Arch
+ pterm.DefaultSection.Println(banner)
+
+ if f.Upx && platform == "darwin/universal" {
+ pterm.Warning.Println("Warning: compress flag unsupported for universal binaries. Ignoring.")
+ f.Upx = false
+ }
+
+ switch buildOptions.Platform {
+ case "linux":
+ if runtime.GOOS != "linux" {
+ pterm.Warning.Println("Crosscompiling to Linux not currently supported.")
+ return
+ }
+ case "darwin":
+ if runtime.GOOS != "darwin" {
+ pterm.Warning.Println("Crosscompiling to Mac not currently supported.")
+ return
+ }
+ macTargets := targets.Filter(func(platform string) bool {
+ return strings.HasPrefix(platform, "darwin")
+ })
+ if macTargets.Length() == 2 {
+ buildOptions.BundleName = fmt.Sprintf("%s-%s.app", desiredFilename, buildOptions.Arch)
+ }
+ }
+
+ if targets.Length() > 1 {
+ // target filename
+ switch buildOptions.Platform {
+ case "windows":
+ desiredFilename = fmt.Sprintf("%s-%s", desiredFilename, buildOptions.Arch)
+ case "linux", "darwin":
+ desiredFilename = fmt.Sprintf("%s-%s-%s", desiredFilename, buildOptions.Platform, buildOptions.Arch)
+ }
+ }
+ if buildOptions.Platform == "windows" {
+ desiredFilename += ".exe"
+ }
+ buildOptions.OutputFile = desiredFilename
+
+ if f.OutputFilename != "" {
+ buildOptions.OutputFile = f.OutputFilename
+ }
+
+ if f.Obfuscated && f.SkipBindings {
+ pterm.Warning.Println("obfuscated flag overrides skipbindings flag.")
+ buildOptions.SkipBindings = false
+ }
+
+ if !f.DryRun {
+ // Start Time
+ start := time.Now()
+
+ compiledBinary, err := build.Build(buildOptions)
+ if err != nil {
+ pterm.Error.Println(err.Error())
+ targetErr = err
+ return
+ }
+
+ buildOptions.IgnoreFrontend = true
+ buildOptions.CleanBinDirectory = false
+
+ // Output stats
+ buildOptions.Logger.Println(fmt.Sprintf("Built '%s' in %s.\n", compiledBinary, time.Since(start).Round(time.Millisecond).String()))
+
+ outputBinaries[buildOptions.Platform+"/"+buildOptions.Arch] = compiledBinary
+ } else {
+ pterm.Info.Println("Dry run: skipped build.")
+ }
+ })
+
+ if targetErr != nil {
+ return targetErr
+ }
+
+ if f.DryRun {
+ return nil
+ }
+
+ if f.NSIS {
+ amd64Binary := outputBinaries["windows/amd64"]
+ arm64Binary := outputBinaries["windows/arm64"]
+ if amd64Binary == "" && arm64Binary == "" {
+ return fmt.Errorf("cannot build nsis installer - no windows targets")
+ }
+
+ if err := build.GenerateNSISInstaller(buildOptions, amd64Binary, arm64Binary); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/v2/cmd/wails/dev.go b/v2/cmd/wails/dev.go
new file mode 100644
index 000000000..30213a68e
--- /dev/null
+++ b/v2/cmd/wails/dev.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+ "os"
+
+ "github.com/pterm/pterm"
+ "github.com/wailsapp/wails/v2/cmd/wails/flags"
+ "github.com/wailsapp/wails/v2/cmd/wails/internal/dev"
+ "github.com/wailsapp/wails/v2/internal/colour"
+ "github.com/wailsapp/wails/v2/pkg/clilogger"
+)
+
+func devApplication(f *flags.Dev) error {
+ if f.NoColour {
+ pterm.DisableColor()
+ colour.ColourEnabled = false
+ }
+
+ quiet := f.Verbosity == flags.Quiet
+
+ // Create logger
+ logger := clilogger.New(os.Stdout)
+ logger.Mute(quiet)
+
+ if quiet {
+ pterm.DisableOutput()
+ } else {
+ app.PrintBanner()
+ }
+
+ err := f.Process()
+ if err != nil {
+ return err
+ }
+
+ return dev.Application(f, logger)
+}
diff --git a/v2/cmd/wails/doctor.go b/v2/cmd/wails/doctor.go
new file mode 100644
index 000000000..7f453133d
--- /dev/null
+++ b/v2/cmd/wails/doctor.go
@@ -0,0 +1,262 @@
+package main
+
+import (
+ "fmt"
+ "runtime"
+ "runtime/debug"
+ "strconv"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+
+ "github.com/pterm/pterm"
+
+ "github.com/jaypipes/ghw"
+ "github.com/wailsapp/wails/v2/cmd/wails/flags"
+ "github.com/wailsapp/wails/v2/internal/colour"
+ "github.com/wailsapp/wails/v2/internal/system"
+ "github.com/wailsapp/wails/v2/internal/system/packagemanager"
+)
+
+func diagnoseEnvironment(f *flags.Doctor) error {
+ if f.NoColour {
+ pterm.DisableColor()
+ colour.ColourEnabled = false
+ }
+
+ pterm.DefaultSection = *pterm.DefaultSection.
+ WithBottomPadding(0).
+ WithStyle(pterm.NewStyle(pterm.FgBlue, pterm.Bold))
+
+ pterm.Println() // Spacer
+ pterm.DefaultHeader.WithBackgroundStyle(pterm.NewStyle(pterm.BgLightBlue)).WithMargin(10).Println("Wails Doctor")
+ pterm.Println() // Spacer
+
+ spinner, _ := pterm.DefaultSpinner.WithRemoveWhenDone().Start("Scanning system - Please wait (this may take a long time)...")
+
+ // Get system info
+ info, err := system.GetInfo()
+ if err != nil {
+ spinner.Fail()
+ pterm.Error.Println("Failed to get system information")
+ return err
+ }
+ spinner.Success()
+
+ pterm.DefaultSection.Println("Wails")
+
+ wailsTableData := pterm.TableData{
+ {"Version", app.Version()},
+ }
+
+ if buildInfo, _ := debug.ReadBuildInfo(); buildInfo != nil {
+ buildSettingToName := map[string]string{
+ "vcs.revision": "Revision",
+ "vcs.modified": "Modified",
+ }
+ for _, buildSetting := range buildInfo.Settings {
+ name := buildSettingToName[buildSetting.Key]
+ if name == "" {
+ continue
+ }
+ wailsTableData = append(wailsTableData, []string{name, buildSetting.Value})
+ }
+ }
+
+ // Exit early if PM not found
+ if info.PM != nil {
+ wailsTableData = append(wailsTableData, []string{"Package Manager", info.PM.Name()})
+ }
+
+ err = pterm.DefaultTable.WithData(wailsTableData).Render()
+ if err != nil {
+ return err
+ }
+
+ pterm.DefaultSection.Println("System")
+
+ systemTabledata := pterm.TableData{
+ {pterm.Bold.Sprint("OS"), info.OS.Name},
+ {pterm.Bold.Sprint("Version"), info.OS.Version},
+ {pterm.Bold.Sprint("ID"), info.OS.ID},
+ {pterm.Bold.Sprint("Branding"), info.OS.Branding},
+ {pterm.Bold.Sprint("Go Version"), runtime.Version()},
+ {pterm.Bold.Sprint("Platform"), runtime.GOOS},
+ {pterm.Bold.Sprint("Architecture"), runtime.GOARCH},
+ }
+
+ // Probe CPU
+ cpus, _ := ghw.CPU()
+ if cpus != nil {
+ prefix := "CPU"
+ for idx, cpu := range cpus.Processors {
+ if len(cpus.Processors) > 1 {
+ prefix = "CPU " + strconv.Itoa(idx+1)
+ }
+ systemTabledata = append(systemTabledata, []string{prefix, cpu.Model})
+ }
+ } else {
+ cpuInfo := "Unknown"
+ if runtime.GOOS == "darwin" {
+ // Try to get CPU info from sysctl
+ if stdout, _, err := shell.RunCommand("", "sysctl", "-n", "machdep.cpu.brand_string"); err == nil {
+ cpuInfo = strings.TrimSpace(stdout)
+ }
+ }
+ systemTabledata = append(systemTabledata, []string{"CPU", cpuInfo})
+ }
+
+ // Probe GPU
+ gpu, _ := ghw.GPU(ghw.WithDisableWarnings())
+ if gpu != nil {
+ prefix := "GPU"
+ for idx, card := range gpu.GraphicsCards {
+ if len(gpu.GraphicsCards) > 1 {
+ prefix = "GPU " + strconv.Itoa(idx+1) + " "
+ }
+ if card.DeviceInfo == nil {
+ systemTabledata = append(systemTabledata, []string{prefix, "Unknown"})
+ continue
+ }
+ details := fmt.Sprintf("%s (%s) - Driver: %s", card.DeviceInfo.Product.Name, card.DeviceInfo.Vendor.Name, card.DeviceInfo.Driver)
+ systemTabledata = append(systemTabledata, []string{prefix, details})
+ }
+ } else {
+ gpuInfo := "Unknown"
+ if runtime.GOOS == "darwin" {
+ // Try to get GPU info from system_profiler
+ if stdout, _, err := shell.RunCommand("", "system_profiler", "SPDisplaysDataType"); err == nil {
+ var (
+ startCapturing bool
+ gpuInfoDetails []string
+ )
+ for _, line := range strings.Split(stdout, "\n") {
+ if strings.Contains(line, "Chipset Model") {
+ startCapturing = true
+ }
+ if startCapturing {
+ gpuInfoDetails = append(gpuInfoDetails, strings.TrimSpace(line))
+ }
+ if strings.Contains(line, "Metal Support") {
+ break
+ }
+ }
+ if len(gpuInfoDetails) > 0 {
+ gpuInfo = strings.Join(gpuInfoDetails, " ")
+ }
+ }
+ }
+ systemTabledata = append(systemTabledata, []string{"GPU", gpuInfo})
+ }
+
+ memory, _ := ghw.Memory()
+ if memory != nil {
+ systemTabledata = append(systemTabledata, []string{"Memory", strconv.Itoa(int(memory.TotalPhysicalBytes/1024/1024/1024)) + "GB"})
+ } else {
+ memInfo := "Unknown"
+ if runtime.GOOS == "darwin" {
+ // Try to get Memory info from sysctl
+ if stdout, _, err := shell.RunCommand("", "sysctl", "-n", "hw.memsize"); err == nil {
+ if memSize, err := strconv.Atoi(strings.TrimSpace(stdout)); err == nil {
+ memInfo = strconv.Itoa(memSize/1024/1024/1024) + "GB"
+ }
+ }
+ }
+ systemTabledata = append(systemTabledata, []string{"Memory", memInfo})
+ }
+
+ err = pterm.DefaultTable.WithBoxed().WithData(systemTabledata).Render()
+ if err != nil {
+ return err
+ }
+
+ pterm.DefaultSection.Println("Dependencies")
+
+ // Output Dependencies Status
+ var dependenciesMissing []string
+ var externalPackages []*packagemanager.Dependency
+ dependenciesAvailableRequired := 0
+ dependenciesAvailableOptional := 0
+
+ dependenciesTableData := pterm.TableData{
+ {"Dependency", "Package Name", "Status", "Version"},
+ }
+
+ hasOptionalDependencies := false
+ // Loop over dependencies
+ for _, dependency := range info.Dependencies {
+ name := dependency.Name
+
+ if dependency.Optional {
+ name = pterm.Gray("*") + name
+ hasOptionalDependencies = true
+ }
+
+ packageName := "Unknown"
+ status := pterm.LightRed("Not Found")
+
+ // If we found the package
+ if dependency.PackageName != "" {
+ packageName = dependency.PackageName
+
+ // If it's installed, update the status
+ if dependency.Installed {
+ status = pterm.LightGreen("Installed")
+ } else {
+ // Generate meaningful status text
+ status = pterm.LightMagenta("Available")
+
+ if dependency.Optional {
+ dependenciesAvailableOptional++
+ } else {
+ dependenciesAvailableRequired++
+ }
+ }
+ } else {
+ if !dependency.Optional {
+ dependenciesMissing = append(dependenciesMissing, dependency.Name)
+ }
+
+ if dependency.External {
+ externalPackages = append(externalPackages, dependency)
+ }
+ }
+
+ dependenciesTableData = append(dependenciesTableData, []string{name, packageName, status, dependency.Version})
+ }
+
+ dependenciesTableString, _ := pterm.DefaultTable.WithHasHeader(true).WithData(dependenciesTableData).Srender()
+ dependenciesBox := pterm.DefaultBox.WithTitleBottomCenter()
+
+ if hasOptionalDependencies {
+ dependenciesBox = dependenciesBox.WithTitle(pterm.Gray("*") + " - Optional Dependency")
+ }
+
+ dependenciesBox.Println(dependenciesTableString)
+
+ pterm.DefaultSection.Println("Diagnosis")
+
+ // Generate an appropriate diagnosis
+
+ if dependenciesAvailableRequired != 0 {
+ pterm.Println("Required package(s) installation details: \n" + info.Dependencies.InstallAllRequiredCommand())
+ }
+
+ if dependenciesAvailableOptional != 0 {
+ pterm.Println("Optional package(s) installation details: \n" + info.Dependencies.InstallAllOptionalCommand())
+ }
+
+ if len(dependenciesMissing) == 0 && dependenciesAvailableRequired == 0 {
+ pterm.Success.Println("Your system is ready for Wails development!")
+ } else {
+ pterm.Warning.Println("Your system has missing dependencies!")
+ }
+
+ if len(dependenciesMissing) != 0 {
+ pterm.Println("Fatal:")
+ pterm.Println("Required dependencies missing: " + strings.Join(dependenciesMissing, " "))
+ }
+
+ pterm.Println() // Spacer for sponsor message
+ return nil
+}
diff --git a/v2/cmd/wails/flags/build.go b/v2/cmd/wails/flags/build.go
new file mode 100644
index 000000000..db05c9035
--- /dev/null
+++ b/v2/cmd/wails/flags/build.go
@@ -0,0 +1,166 @@
+package flags
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "runtime"
+ "strings"
+
+ "github.com/leaanthony/slicer"
+ "github.com/wailsapp/wails/v2/internal/system"
+ "github.com/wailsapp/wails/v2/pkg/commands/build"
+ "github.com/wailsapp/wails/v2/pkg/commands/buildtags"
+)
+
+const (
+ Quiet int = 0
+ Normal int = 1
+ Verbose int = 2
+)
+
+// TODO: unify this and `build.Options`
+type Build struct {
+ Common
+ BuildCommon
+
+ NoPackage bool `description:"Skips platform specific packaging"`
+ Upx bool `description:"Compress final binary with UPX (if installed)"`
+ UpxFlags string `description:"Flags to pass to upx"`
+ Platform string `description:"Platform to target. Comma separate multiple platforms"`
+ OutputFilename string `name:"o" description:"Output filename"`
+ Clean bool `description:"Clean the bin directory before building"`
+ WebView2 string `description:"WebView2 installer strategy: download,embed,browser,error"`
+ ForceBuild bool `name:"f" description:"Force build of application"`
+ UpdateWailsVersionGoMod bool `name:"u" description:"Updates go.mod to use the same Wails version as the CLI"`
+ Debug bool `description:"Builds the application in debug mode"`
+ Devtools bool `description:"Enable Devtools in productions, Already enabled in debug mode (-debug)"`
+ NSIS bool `description:"Generate NSIS installer for Windows"`
+ TrimPath bool `description:"Remove all file system paths from the resulting executable"`
+ WindowsConsole bool `description:"Keep the console when building for Windows"`
+ Obfuscated bool `description:"Code obfuscation of bound Wails methods"`
+ GarbleArgs string `description:"Arguments to pass to garble"`
+ DryRun bool `description:"Prints the build command without executing it"`
+
+ // Build Specific
+
+ // Internal state
+ compilerPath string
+ userTags []string
+ wv2rtstrategy string // WebView2 runtime strategy
+ defaultArch string // Default architecture
+}
+
+func (b *Build) Default() *Build {
+ defaultPlatform := os.Getenv("GOOS")
+ if defaultPlatform == "" {
+ defaultPlatform = runtime.GOOS
+ }
+ defaultArch := os.Getenv("GOARCH")
+ if defaultArch == "" {
+ if system.IsAppleSilicon {
+ defaultArch = "arm64"
+ } else {
+ defaultArch = runtime.GOARCH
+ }
+ }
+
+ result := &Build{
+ Platform: defaultPlatform + "/" + defaultArch,
+ WebView2: "download",
+ GarbleArgs: "-literals -tiny -seed=random",
+
+ defaultArch: defaultArch,
+ }
+ result.BuildCommon = result.BuildCommon.Default()
+ return result
+}
+
+func (b *Build) GetBuildMode() build.Mode {
+ if b.Debug {
+ return build.Debug
+ }
+ return build.Production
+}
+
+func (b *Build) GetWebView2Strategy() string {
+ return b.wv2rtstrategy
+}
+
+func (b *Build) GetTargets() *slicer.StringSlicer {
+ var targets slicer.StringSlicer
+ targets.AddSlice(strings.Split(b.Platform, ","))
+ targets.Deduplicate()
+ return &targets
+}
+
+func (b *Build) GetCompilerPath() string {
+ return b.compilerPath
+}
+
+func (b *Build) GetTags() []string {
+ return b.userTags
+}
+
+func (b *Build) Process() error {
+ // Lookup compiler path
+ var err error
+ b.compilerPath, err = exec.LookPath(b.Compiler)
+ if err != nil {
+ return fmt.Errorf("unable to find compiler: %s", b.Compiler)
+ }
+
+ // Process User Tags
+ b.userTags, err = buildtags.Parse(b.Tags)
+ if err != nil {
+ return err
+ }
+
+ // WebView2 installer strategy (download by default)
+ b.WebView2 = strings.ToLower(b.WebView2)
+ if b.WebView2 != "" {
+ validWV2Runtime := slicer.String([]string{"download", "embed", "browser", "error"})
+ if !validWV2Runtime.Contains(b.WebView2) {
+ return fmt.Errorf("invalid option for flag 'webview2': %s", b.WebView2)
+ }
+ b.wv2rtstrategy = "wv2runtime." + b.WebView2
+ }
+
+ return nil
+}
+
+func bool2Str(b bool) string {
+ if b {
+ return "true"
+ }
+ return "false"
+}
+
+func (b *Build) GetBuildModeAsString() string {
+ if b.Debug {
+ return "debug"
+ }
+ return "production"
+}
+
+func (b *Build) GetDefaultArch() string {
+ return b.defaultArch
+}
+
+/*
+ _, _ = fmt.Fprintf(w, "Frontend Directory: \t%s\n", projectOptions.GetFrontendDir())
+ _, _ = fmt.Fprintf(w, "Obfuscated: \t%t\n", buildOptions.Obfuscated)
+ if buildOptions.Obfuscated {
+ _, _ = fmt.Fprintf(w, "Garble Args: \t%s\n", buildOptions.GarbleArgs)
+ }
+ _, _ = fmt.Fprintf(w, "Skip Frontend: \t%t\n", skipFrontend)
+ _, _ = fmt.Fprintf(w, "Compress: \t%t\n", buildOptions.Compress)
+ _, _ = fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack)
+ _, _ = fmt.Fprintf(w, "Clean Bin Dir: \t%t\n", buildOptions.CleanBinDirectory)
+ _, _ = fmt.Fprintf(w, "LDFlags: \t\"%s\"\n", buildOptions.LDFlags)
+ _, _ = fmt.Fprintf(w, "Tags: \t[%s]\n", strings.Join(buildOptions.UserTags, ","))
+ _, _ = fmt.Fprintf(w, "Race Detector: \t%t\n", buildOptions.RaceDetector)
+ if len(buildOptions.OutputFile) > 0 && targets.Length() == 1 {
+ _, _ = fmt.Fprintf(w, "Output File: \t%s\n", buildOptions.OutputFile)
+ }
+*/
diff --git a/v2/cmd/wails/flags/buildcommon.go b/v2/cmd/wails/flags/buildcommon.go
new file mode 100644
index 000000000..a22f7a502
--- /dev/null
+++ b/v2/cmd/wails/flags/buildcommon.go
@@ -0,0 +1,21 @@
+package flags
+
+type BuildCommon struct {
+ LdFlags string `description:"Additional ldflags to pass to the compiler"`
+ Compiler string `description:"Use a different go compiler to build, eg go1.15beta1"`
+ SkipBindings bool `description:"Skips generation of bindings"`
+ RaceDetector bool `name:"race" description:"Build with Go's race detector"`
+ SkipFrontend bool `name:"s" description:"Skips building the frontend"`
+ Verbosity int `name:"v" description:"Verbosity level (0 = quiet, 1 = normal, 2 = verbose)"`
+ Tags string `description:"Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated"`
+ NoSyncGoMod bool `description:"Don't sync go.mod"`
+ SkipModTidy bool `name:"m" description:"Skip mod tidy before compile"`
+ SkipEmbedCreate bool `description:"Skips creation of embed files"`
+}
+
+func (c BuildCommon) Default() BuildCommon {
+ return BuildCommon{
+ Compiler: "go",
+ Verbosity: 1,
+ }
+}
diff --git a/v2/cmd/wails/flags/common.go b/v2/cmd/wails/flags/common.go
new file mode 100644
index 000000000..e58eff411
--- /dev/null
+++ b/v2/cmd/wails/flags/common.go
@@ -0,0 +1,5 @@
+package flags
+
+type Common struct {
+ NoColour bool `description:"Disable colour in output"`
+}
diff --git a/v2/cmd/wails/flags/dev.go b/v2/cmd/wails/flags/dev.go
new file mode 100644
index 000000000..d31d8bc87
--- /dev/null
+++ b/v2/cmd/wails/flags/dev.go
@@ -0,0 +1,157 @@
+package flags
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "os"
+ "path/filepath"
+ "runtime"
+
+ "github.com/samber/lo"
+ "github.com/wailsapp/wails/v2/internal/project"
+ "github.com/wailsapp/wails/v2/pkg/commands/build"
+)
+
+type Dev struct {
+ BuildCommon
+
+ AssetDir string `flag:"assetdir" description:"Serve assets from the given directory instead of using the provided asset FS"`
+ Extensions string `flag:"e" description:"Extensions to trigger rebuilds (comma separated) eg go"`
+ ReloadDirs string `flag:"reloaddirs" description:"Additional directories to trigger reloads (comma separated)"`
+ Browser bool `flag:"browser" description:"Open the application in a browser"`
+ NoReload bool `flag:"noreload" description:"Disable reload on asset change"`
+ NoColour bool `flag:"nocolor" description:"Disable colour in output"`
+ NoGoRebuild bool `flag:"nogorebuild" description:"Disable automatic rebuilding on backend file changes/additions"`
+ WailsJSDir string `flag:"wailsjsdir" description:"Directory to generate the Wails JS modules"`
+ LogLevel string `flag:"loglevel" description:"LogLevel to use - Trace, Debug, Info, Warning, Error)"`
+ ForceBuild bool `flag:"f" description:"Force build of application"`
+ Debounce int `flag:"debounce" description:"The amount of time to wait to trigger a reload on change"`
+ DevServer string `flag:"devserver" description:"The address of the wails dev server"`
+ AppArgs string `flag:"appargs" description:"arguments to pass to the underlying app (quoted and space separated)"`
+ Save bool `flag:"save" description:"Save the given flags as defaults"`
+ FrontendDevServerURL string `flag:"frontenddevserverurl" description:"The url of the external frontend dev server to use"`
+ ViteServerTimeout int `flag:"viteservertimeout" description:"The timeout in seconds for Vite server detection (default: 10)"`
+
+ // Internal state
+ devServerURL *url.URL
+ projectConfig *project.Project
+}
+
+func (*Dev) Default() *Dev {
+ result := &Dev{
+ Extensions: "go",
+ Debounce: 100,
+ LogLevel: "Info",
+ }
+ result.BuildCommon = result.BuildCommon.Default()
+ return result
+}
+
+func (d *Dev) Process() error {
+ var err error
+ err = d.loadAndMergeProjectConfig()
+ if err != nil {
+ return err
+ }
+
+ if _, _, err := net.SplitHostPort(d.DevServer); err != nil {
+ return fmt.Errorf("DevServer is not of the form 'host:port', please check your wails.json")
+ }
+
+ d.devServerURL, err = url.Parse("http://" + d.DevServer)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (d *Dev) loadAndMergeProjectConfig() error {
+ var err error
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ d.projectConfig, err = project.Load(cwd)
+ if err != nil {
+ return err
+ }
+
+ d.AssetDir, _ = lo.Coalesce(d.AssetDir, d.projectConfig.AssetDirectory)
+ d.projectConfig.AssetDirectory = filepath.ToSlash(d.AssetDir)
+ if d.AssetDir != "" {
+ d.AssetDir, err = filepath.Abs(d.AssetDir)
+ if err != nil {
+ return err
+ }
+ }
+
+ d.ReloadDirs, _ = lo.Coalesce(d.ReloadDirs, d.projectConfig.ReloadDirectories)
+ d.projectConfig.ReloadDirectories = filepath.ToSlash(d.ReloadDirs)
+ d.DevServer, _ = lo.Coalesce(d.DevServer, d.projectConfig.DevServer)
+ d.projectConfig.DevServer = d.DevServer
+ d.FrontendDevServerURL, _ = lo.Coalesce(d.FrontendDevServerURL, d.projectConfig.FrontendDevServerURL)
+ d.projectConfig.FrontendDevServerURL = d.FrontendDevServerURL
+ d.WailsJSDir, _ = lo.Coalesce(d.WailsJSDir, d.projectConfig.GetWailsJSDir(), d.projectConfig.GetFrontendDir())
+ d.projectConfig.WailsJSDir = filepath.ToSlash(d.WailsJSDir)
+
+ if d.Debounce == 100 && d.projectConfig.DebounceMS != 100 {
+ if d.projectConfig.DebounceMS == 0 {
+ d.projectConfig.DebounceMS = 100
+ }
+ d.Debounce = d.projectConfig.DebounceMS
+ }
+ d.projectConfig.DebounceMS = d.Debounce
+
+ d.AppArgs, _ = lo.Coalesce(d.AppArgs, d.projectConfig.AppArgs)
+
+ if d.ViteServerTimeout == 0 && d.projectConfig.ViteServerTimeout != 0 {
+ d.ViteServerTimeout = d.projectConfig.ViteServerTimeout
+ } else if d.ViteServerTimeout == 0 {
+ d.ViteServerTimeout = 10 // Default timeout
+ }
+ d.projectConfig.ViteServerTimeout = d.ViteServerTimeout
+
+ if d.Save {
+ err = d.projectConfig.Save()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// GenerateBuildOptions creates a build.Options using the flags
+func (d *Dev) GenerateBuildOptions() *build.Options {
+ result := &build.Options{
+ OutputType: "dev",
+ Mode: build.Dev,
+ Devtools: true,
+ Arch: runtime.GOARCH,
+ Pack: true,
+ Platform: runtime.GOOS,
+ LDFlags: d.LdFlags,
+ Compiler: d.Compiler,
+ ForceBuild: d.ForceBuild,
+ IgnoreFrontend: d.SkipFrontend,
+ SkipBindings: d.SkipBindings,
+ SkipModTidy: d.SkipModTidy,
+ Verbosity: d.Verbosity,
+ WailsJSDir: d.WailsJSDir,
+ RaceDetector: d.RaceDetector,
+ ProjectData: d.projectConfig,
+ SkipEmbedCreate: d.SkipEmbedCreate,
+ }
+
+ return result
+}
+
+func (d *Dev) ProjectConfig() *project.Project {
+ return d.projectConfig
+}
+
+func (d *Dev) DevServerURL() *url.URL {
+ return d.devServerURL
+}
diff --git a/v2/cmd/wails/flags/doctor.go b/v2/cmd/wails/flags/doctor.go
new file mode 100644
index 000000000..e4816b969
--- /dev/null
+++ b/v2/cmd/wails/flags/doctor.go
@@ -0,0 +1,9 @@
+package flags
+
+type Doctor struct {
+ Common
+}
+
+func (b *Doctor) Default() *Doctor {
+ return &Doctor{}
+}
diff --git a/v2/cmd/wails/flags/generate.go b/v2/cmd/wails/flags/generate.go
new file mode 100644
index 000000000..b14d67017
--- /dev/null
+++ b/v2/cmd/wails/flags/generate.go
@@ -0,0 +1,21 @@
+package flags
+
+type GenerateModule struct {
+ Common
+ Compiler string `description:"Use a different go compiler to build, eg go1.15beta1"`
+ Tags string `description:"Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated"`
+ Verbosity int `name:"v" description:"Verbosity level (0 = quiet, 1 = normal, 2 = verbose)"`
+}
+
+type GenerateTemplate struct {
+ Common
+ Name string `description:"Name of the template to generate"`
+ Frontend string `description:"Frontend to use for the template"`
+ Quiet bool `description:"Suppress output"`
+}
+
+func (c *GenerateModule) Default() *GenerateModule {
+ return &GenerateModule{
+ Compiler: "go",
+ }
+}
diff --git a/v2/cmd/wails/flags/init.go b/v2/cmd/wails/flags/init.go
new file mode 100644
index 000000000..16d56a207
--- /dev/null
+++ b/v2/cmd/wails/flags/init.go
@@ -0,0 +1,21 @@
+package flags
+
+type Init struct {
+ Common
+
+ TemplateName string `name:"t" description:"Name of built-in template to use, path to template or template url"`
+ ProjectName string `name:"n" description:"Name of project"`
+ CIMode bool `name:"ci" description:"CI Mode"`
+ ProjectDir string `name:"d" description:"Project directory"`
+ Quiet bool `name:"q" description:"Suppress output to console"`
+ InitGit bool `name:"g" description:"Initialise git repository"`
+ IDE string `name:"ide" description:"Generate IDE project files"`
+ List bool `name:"l" description:"List templates"`
+}
+
+func (i *Init) Default() *Init {
+ result := &Init{
+ TemplateName: "vanilla",
+ }
+ return result
+}
diff --git a/v2/cmd/wails/flags/show.go b/v2/cmd/wails/flags/show.go
new file mode 100644
index 000000000..a8220f3cc
--- /dev/null
+++ b/v2/cmd/wails/flags/show.go
@@ -0,0 +1,6 @@
+package flags
+
+type ShowReleaseNotes struct {
+ Common
+ Version string `description:"The version to show the release notes for"`
+}
diff --git a/v2/cmd/wails/flags/update.go b/v2/cmd/wails/flags/update.go
new file mode 100644
index 000000000..ffd143a9f
--- /dev/null
+++ b/v2/cmd/wails/flags/update.go
@@ -0,0 +1,7 @@
+package flags
+
+type Update struct {
+ Common
+ Version string `description:"The version to update to"`
+ PreRelease bool `name:"pre" description:"Update to latest pre-release"`
+}
diff --git a/v2/cmd/wails/generate.go b/v2/cmd/wails/generate.go
new file mode 100644
index 000000000..15a6b33d8
--- /dev/null
+++ b/v2/cmd/wails/generate.go
@@ -0,0 +1,250 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/leaanthony/debme"
+ "github.com/leaanthony/gosod"
+ "github.com/pterm/pterm"
+ "github.com/tidwall/sjson"
+ "github.com/wailsapp/wails/v2/cmd/wails/flags"
+ "github.com/wailsapp/wails/v2/cmd/wails/internal/template"
+ "github.com/wailsapp/wails/v2/internal/colour"
+ "github.com/wailsapp/wails/v2/internal/fs"
+ "github.com/wailsapp/wails/v2/internal/project"
+ "github.com/wailsapp/wails/v2/pkg/clilogger"
+ "github.com/wailsapp/wails/v2/pkg/commands/bindings"
+ "github.com/wailsapp/wails/v2/pkg/commands/buildtags"
+)
+
+func generateModule(f *flags.GenerateModule) error {
+ if f.NoColour {
+ pterm.DisableColor()
+ colour.ColourEnabled = false
+ }
+
+ quiet := f.Verbosity == flags.Quiet
+ logger := clilogger.New(os.Stdout)
+ logger.Mute(quiet)
+
+ buildTags, err := buildtags.Parse(f.Tags)
+ if err != nil {
+ return err
+ }
+
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ projectConfig, err := project.Load(cwd)
+ if err != nil {
+ return err
+ }
+
+ if projectConfig.Bindings.TsGeneration.OutputType == "" {
+ projectConfig.Bindings.TsGeneration.OutputType = "classes"
+ }
+
+ _, err = bindings.GenerateBindings(bindings.Options{
+ Compiler: f.Compiler,
+ Tags: buildTags,
+ TsPrefix: projectConfig.Bindings.TsGeneration.Prefix,
+ TsSuffix: projectConfig.Bindings.TsGeneration.Suffix,
+ TsOutputType: projectConfig.Bindings.TsGeneration.OutputType,
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func generateTemplate(f *flags.GenerateTemplate) error {
+ if f.NoColour {
+ pterm.DisableColor()
+ colour.ColourEnabled = false
+ }
+
+ quiet := f.Quiet
+ logger := clilogger.New(os.Stdout)
+ logger.Mute(quiet)
+
+ // name is mandatory
+ if f.Name == "" {
+ return fmt.Errorf("please provide a template name using the -name flag")
+ }
+
+ // If the current directory is not empty, we create a new directory
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ templateDir := filepath.Join(cwd, f.Name)
+ if !fs.DirExists(templateDir) {
+ err := os.MkdirAll(templateDir, 0o755)
+ if err != nil {
+ return err
+ }
+ }
+ empty, err := fs.DirIsEmpty(templateDir)
+ if err != nil {
+ return err
+ }
+
+ pterm.DefaultSection.Println("Generating template")
+
+ if !empty {
+ templateDir = filepath.Join(cwd, f.Name)
+ printBulletPoint("Creating new template directory:", f.Name)
+ err = fs.Mkdir(templateDir)
+ if err != nil {
+ return err
+ }
+ }
+
+ // Create base template
+ baseTemplate, err := debme.FS(template.Base, "base")
+ if err != nil {
+ return err
+ }
+ g := gosod.New(baseTemplate)
+ g.SetTemplateFilters([]string{".template"})
+
+ err = os.Chdir(templateDir)
+ if err != nil {
+ return err
+ }
+
+ type templateData struct {
+ Name string
+ Description string
+ TemplateDir string
+ WailsVersion string
+ }
+
+ printBulletPoint("Extracting base template files...")
+
+ err = g.Extract(templateDir, &templateData{
+ Name: f.Name,
+ TemplateDir: templateDir,
+ WailsVersion: app.Version(),
+ })
+ if err != nil {
+ return err
+ }
+
+ err = os.Chdir(cwd)
+ if err != nil {
+ return err
+ }
+
+ // If we aren't migrating the files, just exit
+ if f.Frontend == "" {
+ pterm.Println()
+ pterm.Println()
+ pterm.Info.Println("No frontend specified to migrate. Template created.")
+ pterm.Println()
+ return nil
+ }
+
+ // Remove frontend directory
+ frontendDir := filepath.Join(templateDir, "frontend")
+ err = os.RemoveAll(frontendDir)
+ if err != nil {
+ return err
+ }
+
+ // Copy the files into a new frontend directory
+ printBulletPoint("Migrating existing project files to frontend directory...")
+
+ sourceDir, err := filepath.Abs(f.Frontend)
+ if err != nil {
+ return err
+ }
+
+ newFrontendDir := filepath.Join(templateDir, "frontend")
+ err = fs.CopyDirExtended(sourceDir, newFrontendDir, []string{f.Name, "node_modules"})
+ if err != nil {
+ return err
+ }
+
+ // Process package.json
+ err = processPackageJSON(frontendDir)
+ if err != nil {
+ return err
+ }
+
+ // Process package-lock.json
+ err = processPackageLockJSON(frontendDir)
+ if err != nil {
+ return err
+ }
+
+ // Remove node_modules - ignore error, eg it doesn't exist
+ _ = os.RemoveAll(filepath.Join(frontendDir, "node_modules"))
+
+ return nil
+}
+
+func processPackageJSON(frontendDir string) error {
+ var err error
+
+ packageJSON := filepath.Join(frontendDir, "package.json")
+ if !fs.FileExists(packageJSON) {
+ return fmt.Errorf("no package.json found - cannot process")
+ }
+
+ json, err := os.ReadFile(packageJSON)
+ if err != nil {
+ return err
+ }
+
+ // We will ignore these errors - it's not critical
+ printBulletPoint("Updating package.json data...")
+ json, _ = sjson.SetBytes(json, "name", "{{.ProjectName}}")
+ json, _ = sjson.SetBytes(json, "author", "{{.AuthorName}}")
+
+ err = os.WriteFile(packageJSON, json, 0o644)
+ if err != nil {
+ return err
+ }
+ baseDir := filepath.Dir(packageJSON)
+ printBulletPoint("Renaming package.json -> package.tmpl.json...")
+ err = os.Rename(packageJSON, filepath.Join(baseDir, "package.tmpl.json"))
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func processPackageLockJSON(frontendDir string) error {
+ var err error
+
+ filename := filepath.Join(frontendDir, "package-lock.json")
+ if !fs.FileExists(filename) {
+ return fmt.Errorf("no package-lock.json found - cannot process")
+ }
+
+ data, err := os.ReadFile(filename)
+ if err != nil {
+ return err
+ }
+ json := string(data)
+
+ // We will ignore these errors - it's not critical
+ printBulletPoint("Updating package-lock.json data...")
+ json, _ = sjson.Set(json, "name", "{{.ProjectName}}")
+
+ err = os.WriteFile(filename, []byte(json), 0o644)
+ if err != nil {
+ return err
+ }
+ baseDir := filepath.Dir(filename)
+ printBulletPoint("Renaming package-lock.json -> package-lock.tmpl.json...")
+ err = os.Rename(filename, filepath.Join(baseDir, "package-lock.tmpl.json"))
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/v2/cmd/wails/init.go b/v2/cmd/wails/init.go
new file mode 100644
index 000000000..f79e37ffc
--- /dev/null
+++ b/v2/cmd/wails/init.go
@@ -0,0 +1,295 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/flytam/filenamify"
+ "github.com/leaanthony/slicer"
+ "github.com/pkg/errors"
+ "github.com/pterm/pterm"
+ "github.com/wailsapp/wails/v2/cmd/wails/flags"
+ "github.com/wailsapp/wails/v2/internal/colour"
+ "github.com/wailsapp/wails/v2/pkg/buildassets"
+ "github.com/wailsapp/wails/v2/pkg/clilogger"
+ "github.com/wailsapp/wails/v2/pkg/git"
+ "github.com/wailsapp/wails/v2/pkg/templates"
+)
+
+func initProject(f *flags.Init) error {
+ if f.NoColour {
+ pterm.DisableColor()
+ colour.ColourEnabled = false
+ }
+
+ quiet := f.Quiet
+
+ // Create logger
+ logger := clilogger.New(os.Stdout)
+ logger.Mute(quiet)
+
+ // Are we listing templates?
+ if f.List {
+ app.PrintBanner()
+ templateList, err := templates.List()
+ if err != nil {
+ return err
+ }
+
+ pterm.DefaultSection.Println("Available templates")
+
+ table := pterm.TableData{{"Template", "Short Name", "Description"}}
+ for _, template := range templateList {
+ table = append(table, []string{template.Name, template.ShortName, template.Description})
+ }
+ err = pterm.DefaultTable.WithHasHeader(true).WithBoxed(true).WithData(table).Render()
+ pterm.Println()
+ return err
+ }
+
+ // Validate name
+ if len(f.ProjectName) == 0 {
+ return fmt.Errorf("please provide a project name using the -n flag")
+ }
+
+ // Validate IDE option
+ supportedIDEs := slicer.String([]string{"vscode", "goland"})
+ ide := strings.ToLower(f.IDE)
+ if ide != "" {
+ if !supportedIDEs.Contains(ide) {
+ return fmt.Errorf("ide '%s' not supported. Valid values: %s", ide, supportedIDEs.Join(" "))
+ }
+ }
+
+ if !quiet {
+ app.PrintBanner()
+ }
+
+ pterm.DefaultSection.Printf("Initialising Project '%s'", f.ProjectName)
+
+ projectFilename, err := filenamify.Filenamify(f.ProjectName, filenamify.Options{
+ Replacement: "_",
+ MaxLength: 255,
+ })
+ if err != nil {
+ return err
+ }
+ goBinary, err := exec.LookPath("go")
+ if err != nil {
+ return fmt.Errorf("unable to find Go compiler. Please download and install Go: https://golang.org/dl/")
+ }
+
+ // Get base path and convert to forward slashes
+ goPath := filepath.ToSlash(filepath.Dir(goBinary))
+ // Trim bin directory
+ goSDKPath := strings.TrimSuffix(goPath, "/bin")
+
+ // Create Template Options
+ options := &templates.Options{
+ ProjectName: f.ProjectName,
+ TargetDir: f.ProjectDir,
+ TemplateName: f.TemplateName,
+ Logger: logger,
+ IDE: ide,
+ InitGit: f.InitGit,
+ ProjectNameFilename: projectFilename,
+ WailsVersion: app.Version(),
+ GoSDKPath: goSDKPath,
+ }
+
+ // Try to discover author details from git config
+ findAuthorDetails(options)
+
+ // Start Time
+ start := time.Now()
+
+ // Install the template
+ remote, template, err := templates.Install(options)
+ if err != nil {
+ return err
+ }
+
+ // Install the default assets
+ err = buildassets.Install(options.TargetDir)
+ if err != nil {
+ return err
+ }
+
+ err = os.Chdir(options.TargetDir)
+ if err != nil {
+ return err
+ }
+
+ // Change the module name to project name
+ err = updateModuleNameToProjectName(options, quiet)
+ if err != nil {
+ return err
+ }
+
+ if !f.CIMode {
+ // Run `go mod tidy` to ensure `go.sum` is up to date
+ cmd := exec.Command("go", "mod", "tidy")
+ cmd.Dir = options.TargetDir
+ cmd.Stderr = os.Stderr
+ if !quiet {
+ cmd.Stdout = os.Stdout
+ }
+ err = cmd.Run()
+ if err != nil {
+ return err
+ }
+ } else {
+ // Update go mod
+ workspace := os.Getenv("GITHUB_WORKSPACE")
+ pterm.Println("GitHub workspace:", workspace)
+ if workspace == "" {
+ os.Exit(1)
+ }
+ updateReplaceLine(workspace)
+ }
+
+ // Remove the `.git`` directory in the template project
+ err = os.RemoveAll(".git")
+ if err != nil {
+ return err
+ }
+
+ if options.InitGit {
+ err = initGit(options)
+ if err != nil {
+ return err
+ }
+ }
+
+ if quiet {
+ return nil
+ }
+
+ // Output stats
+ elapsed := time.Since(start)
+
+ // Create pterm table
+ table := pterm.TableData{
+ {"Project Name", options.ProjectName},
+ {"Project Directory", options.TargetDir},
+ {"Template", template.Name},
+ {"Template Source", template.HelpURL},
+ }
+ err = pterm.DefaultTable.WithData(table).Render()
+ if err != nil {
+ return err
+ }
+
+ // IDE message
+ switch options.IDE {
+ case "vscode":
+ pterm.Println()
+ pterm.Info.Println("VSCode config files generated.")
+ case "goland":
+ pterm.Println()
+ pterm.Info.Println("Goland config files generated.")
+ }
+
+ if options.InitGit {
+ pterm.Info.Println("Git repository initialised.")
+ }
+
+ if remote {
+ pterm.Warning.Println("NOTE: You have created a project using a remote template. The Wails project takes no responsibility for 3rd party templates. Only use remote templates that you trust.")
+ }
+
+ pterm.Println("")
+ pterm.Printf("Initialised project '%s' in %s.\n", options.ProjectName, elapsed.Round(time.Millisecond).String())
+ pterm.Println("")
+
+ return nil
+}
+
+func initGit(options *templates.Options) error {
+ err := git.InitRepo(options.TargetDir)
+ if err != nil {
+ return errors.Wrap(err, "Unable to initialise git repository:")
+ }
+
+ ignore := []string{
+ "build/bin",
+ "frontend/dist",
+ "frontend/node_modules",
+ }
+ err = os.WriteFile(filepath.Join(options.TargetDir, ".gitignore"), []byte(strings.Join(ignore, "\n")), 0o644)
+ if err != nil {
+ return errors.Wrap(err, "Unable to create gitignore")
+ }
+
+ return nil
+}
+
+// findAuthorDetails tries to find the user's name and email
+// from gitconfig. If it finds them, it stores them in the project options
+func findAuthorDetails(options *templates.Options) {
+ if git.IsInstalled() {
+ name, err := git.Name()
+ if err == nil {
+ options.AuthorName = strings.TrimSpace(name)
+ }
+
+ email, err := git.Email()
+ if err == nil {
+ options.AuthorEmail = strings.TrimSpace(email)
+ }
+ }
+}
+
+func updateReplaceLine(targetPath string) {
+ file, err := os.Open("go.mod")
+ if err != nil {
+ fatal(err.Error())
+ }
+
+ var lines []string
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ lines = append(lines, scanner.Text())
+ }
+
+ err = file.Close()
+ if err != nil {
+ fatal(err.Error())
+ }
+
+ if err := scanner.Err(); err != nil {
+ fatal(err.Error())
+ }
+
+ for i, line := range lines {
+ println(line)
+ if strings.HasPrefix(line, "// replace") {
+ pterm.Println("Found replace line")
+ splitLine := strings.Split(line, " ")
+ splitLine[5] = targetPath + "/v2"
+ lines[i] = strings.Join(splitLine[1:], " ")
+ continue
+ }
+ }
+
+ err = os.WriteFile("go.mod", []byte(strings.Join(lines, "\n")), 0o644)
+ if err != nil {
+ fatal(err.Error())
+ }
+}
+
+func updateModuleNameToProjectName(options *templates.Options, quiet bool) error {
+ cmd := exec.Command("go", "mod", "edit", "-module", options.ProjectName)
+ cmd.Dir = options.TargetDir
+ cmd.Stderr = os.Stderr
+ if !quiet {
+ cmd.Stdout = os.Stdout
+ }
+
+ return cmd.Run()
+}
diff --git a/v2/cmd/wails/internal/dev/dev.go b/v2/cmd/wails/internal/dev/dev.go
new file mode 100644
index 000000000..9495b5bf2
--- /dev/null
+++ b/v2/cmd/wails/internal/dev/dev.go
@@ -0,0 +1,525 @@
+package dev
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "os/exec"
+ "os/signal"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "syscall"
+ "time"
+
+ "github.com/samber/lo"
+ "github.com/wailsapp/wails/v2/cmd/wails/flags"
+ "github.com/wailsapp/wails/v2/cmd/wails/internal/gomod"
+ "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils"
+ "golang.org/x/mod/semver"
+
+ "github.com/wailsapp/wails/v2/pkg/commands/buildtags"
+
+ "github.com/google/shlex"
+
+ "github.com/pkg/browser"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/wailsapp/wails/v2/internal/fs"
+ "github.com/wailsapp/wails/v2/internal/process"
+ "github.com/wailsapp/wails/v2/pkg/clilogger"
+ "github.com/wailsapp/wails/v2/pkg/commands/build"
+)
+
+const (
+ viteMinVersion = "v3.0.0"
+)
+
+func sliceToMap(input []string) map[string]struct{} {
+ result := map[string]struct{}{}
+ for _, value := range input {
+ result[value] = struct{}{}
+ }
+ return result
+}
+
+// Application runs the application in dev mode
+func Application(f *flags.Dev, logger *clilogger.CLILogger) error {
+ cwd := lo.Must(os.Getwd())
+
+ // Update go.mod to use current wails version
+ err := gomod.SyncGoMod(logger, !f.NoSyncGoMod)
+ if err != nil {
+ return err
+ }
+
+ if !f.SkipModTidy {
+ // Run go mod tidy to ensure we're up-to-date
+ err = runCommand(cwd, false, f.Compiler, "mod", "tidy")
+ if err != nil {
+ return err
+ }
+ }
+
+ buildOptions := f.GenerateBuildOptions()
+ buildOptions.Logger = logger
+
+ userTags, err := buildtags.Parse(f.Tags)
+ if err != nil {
+ return err
+ }
+
+ projectConfig := f.ProjectConfig()
+
+ projectTags, err := buildtags.Parse(projectConfig.BuildTags)
+ if err != nil {
+ return err
+ }
+ compiledTags := append(projectTags, userTags...)
+ buildOptions.UserTags = compiledTags
+
+ // Setup signal handler
+ quitChannel := make(chan os.Signal, 1)
+ signal.Notify(quitChannel, os.Interrupt, syscall.SIGTERM)
+ exitCodeChannel := make(chan int, 1)
+
+ // Build the frontend if requested, but ignore building the application itself.
+ ignoreFrontend := buildOptions.IgnoreFrontend
+ if !ignoreFrontend {
+ buildOptions.IgnoreApplication = true
+ if _, err := build.Build(buildOptions); err != nil {
+ return err
+ }
+ buildOptions.IgnoreApplication = false
+ }
+
+ legacyUseDevServerInsteadofCustomScheme := false
+ // frontend:dev:watcher command.
+ frontendDevAutoDiscovery := projectConfig.IsFrontendDevServerURLAutoDiscovery()
+ if command := projectConfig.DevWatcherCommand; command != "" {
+ closer, devServerURL, devServerViteVersion, err := runFrontendDevWatcherCommand(projectConfig.GetFrontendDir(), command, frontendDevAutoDiscovery, projectConfig.ViteServerTimeout)
+ if err != nil {
+ return err
+ }
+ if devServerURL != "" {
+ projectConfig.FrontendDevServerURL = devServerURL
+ f.FrontendDevServerURL = devServerURL
+ }
+ defer closer()
+
+ if devServerViteVersion != "" && semver.Compare(devServerViteVersion, viteMinVersion) < 0 {
+ logutils.LogRed("Please upgrade your Vite Server to at least '%s' future Wails versions will require at least Vite '%s'", viteMinVersion, viteMinVersion)
+ time.Sleep(3 * time.Second)
+ legacyUseDevServerInsteadofCustomScheme = true
+ }
+ } else if frontendDevAutoDiscovery {
+ return fmt.Errorf("unable to auto discover frontend:dev:serverUrl without a frontend:dev:watcher command, please either set frontend:dev:watcher or remove the auto discovery from frontend:dev:serverUrl")
+ }
+
+ // Do initial build but only for the application.
+ logger.Println("Building application for development...")
+ buildOptions.IgnoreFrontend = true
+ debugBinaryProcess, appBinary, err := restartApp(buildOptions, nil, f, exitCodeChannel, legacyUseDevServerInsteadofCustomScheme)
+ buildOptions.IgnoreFrontend = ignoreFrontend || f.FrontendDevServerURL != ""
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := killProcessAndCleanupBinary(debugBinaryProcess, appBinary); err != nil {
+ logutils.LogDarkYellow("Unable to kill process and cleanup binary: %s", err)
+ }
+ }()
+
+ // open browser
+ if f.Browser {
+ err = browser.OpenURL(f.DevServerURL().String())
+ if err != nil {
+ return err
+ }
+ }
+
+ logutils.LogGreen("Using DevServer URL: %s", f.DevServerURL())
+ if f.FrontendDevServerURL != "" {
+ logutils.LogGreen("Using Frontend DevServer URL: %s", f.FrontendDevServerURL)
+ }
+ logutils.LogGreen("Using reload debounce setting of %d milliseconds", f.Debounce)
+
+ // Show dev server URL in terminal after 3 seconds
+ go func() {
+ time.Sleep(3 * time.Second)
+ logutils.LogGreen("\n\nTo develop in the browser and call your bound Go methods from Javascript, navigate to: %s", f.DevServerURL())
+ }()
+
+ // Watch for changes and trigger restartApp()
+ debugBinaryProcess, err = doWatcherLoop(cwd, projectConfig.ReloadDirectories, buildOptions, debugBinaryProcess, f, exitCodeChannel, quitChannel, f.DevServerURL(), legacyUseDevServerInsteadofCustomScheme)
+ if err != nil {
+ return err
+ }
+
+ // Kill the current program if running and remove dev binary
+ if err := killProcessAndCleanupBinary(debugBinaryProcess, appBinary); err != nil {
+ return err
+ }
+
+ // Reset the process and the binary so defer knows about it and is a nop.
+ debugBinaryProcess = nil
+ appBinary = ""
+
+ logutils.LogGreen("Development mode exited")
+
+ return nil
+}
+
+func killProcessAndCleanupBinary(process *process.Process, binary string) error {
+ if process != nil && process.Running {
+ if err := process.Kill(); err != nil {
+ return err
+ }
+ }
+
+ if binary != "" {
+ err := os.Remove(binary)
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ }
+ return nil
+}
+
+func runCommand(dir string, exitOnError bool, command string, args ...string) error {
+ logutils.LogGreen("Executing: " + command + " " + strings.Join(args, " "))
+ cmd := exec.Command(command, args...)
+ cmd.Dir = dir
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ println(string(output))
+ println(err.Error())
+ if exitOnError {
+ os.Exit(1)
+ }
+ return err
+ }
+ return nil
+}
+
+// runFrontendDevWatcherCommand will run the `frontend:dev:watcher` command if it was given, ex- `npm run dev`
+func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, discoverViteServerURL bool, viteServerTimeout int) (func(), string, string, error) {
+ ctx, cancel := context.WithCancel(context.Background())
+ scanner := NewStdoutScanner()
+ cmdSlice := strings.Split(devCommand, " ")
+ cmd := exec.CommandContext(ctx, cmdSlice[0], cmdSlice[1:]...)
+ cmd.Stderr = os.Stderr
+ cmd.Stdout = scanner
+ cmd.Dir = frontendDirectory
+ setParentGID(cmd)
+
+ if err := cmd.Start(); err != nil {
+ cancel()
+ return nil, "", "", fmt.Errorf("unable to start frontend DevWatcher: %w", err)
+ }
+
+ var viteServerURL string
+ if discoverViteServerURL {
+ select {
+ case serverURL := <-scanner.ViteServerURLChan:
+ viteServerURL = serverURL
+ case <-time.After(time.Second * time.Duration(viteServerTimeout)):
+ cancel()
+ return nil, "", "", fmt.Errorf("failed to find Vite server URL: Timed out waiting for Vite to output a URL after %d seconds", viteServerTimeout)
+ }
+ }
+
+ viteVersion := ""
+ select {
+ case version := <-scanner.ViteServerVersionC:
+ viteVersion = version
+
+ case <-time.After(time.Second * 5):
+ // That's fine, then most probably it was not vite that was running
+ }
+
+ logutils.LogGreen("Running frontend DevWatcher command: '%s'", devCommand)
+ var wg sync.WaitGroup
+ wg.Add(1)
+
+ const (
+ stateRunning int32 = 0
+ stateCanceling int32 = 1
+ stateStopped int32 = 2
+ )
+ state := stateRunning
+ go func() {
+ if err := cmd.Wait(); err != nil {
+ wasRunning := atomic.CompareAndSwapInt32(&state, stateRunning, stateStopped)
+ if err.Error() != "exit status 1" && wasRunning {
+ logutils.LogRed("Error from DevWatcher '%s': %s", devCommand, err.Error())
+ }
+ }
+ atomic.StoreInt32(&state, stateStopped)
+ wg.Done()
+ }()
+
+ return func() {
+ if atomic.CompareAndSwapInt32(&state, stateRunning, stateCanceling) {
+ killProc(cmd, devCommand)
+ }
+ cancel()
+ wg.Wait()
+ }, viteServerURL, viteVersion, nil
+}
+
+// restartApp does the actual rebuilding of the application when files change
+func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, exitCodeChannel chan int, legacyUseDevServerInsteadofCustomScheme bool) (*process.Process, string, error) {
+ appBinary, err := build.Build(buildOptions)
+ println()
+ if err != nil {
+ logutils.LogRed("Build error - " + err.Error())
+
+ msg := "Continuing to run current version"
+ if debugBinaryProcess == nil {
+ msg = "No version running, build will be retriggered as soon as changes have been detected"
+ }
+ logutils.LogDarkYellow(msg)
+ return nil, "", nil
+ }
+
+ // Kill existing binary if need be
+ if debugBinaryProcess != nil {
+ killError := debugBinaryProcess.Kill()
+
+ if killError != nil {
+ buildOptions.Logger.Fatal("Unable to kill debug binary (PID: %d)!", debugBinaryProcess.PID())
+ }
+
+ debugBinaryProcess = nil
+ }
+
+ // parse appargs if any
+ args, err := shlex.Split(f.AppArgs)
+ if err != nil {
+ buildOptions.Logger.Fatal("Unable to parse appargs: %s", err.Error())
+ }
+
+ // Set environment variables accordingly
+ os.Setenv("loglevel", f.LogLevel)
+ os.Setenv("assetdir", f.AssetDir)
+ os.Setenv("devserver", f.DevServer)
+ os.Setenv("frontenddevserverurl", f.FrontendDevServerURL)
+
+ // Start up new binary with correct args
+ newProcess := process.NewProcess(appBinary, args...)
+ err = newProcess.Start(exitCodeChannel)
+ if err != nil {
+ // Remove binary
+ if fs.FileExists(appBinary) {
+ deleteError := fs.DeleteFile(appBinary)
+ if deleteError != nil {
+ buildOptions.Logger.Fatal("Unable to delete app binary: " + appBinary)
+ }
+ }
+ buildOptions.Logger.Fatal("Unable to start application: %s", err.Error())
+ }
+
+ return newProcess, appBinary, nil
+}
+
+// doWatcherLoop is the main watch loop that runs while dev is active
+func doWatcherLoop(cwd string, reloadDirs string, buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, exitCodeChannel chan int, quitChannel chan os.Signal, devServerURL *url.URL, legacyUseDevServerInsteadofCustomScheme bool) (*process.Process, error) {
+ // create the project files watcher
+ watcher, err := initialiseWatcher(cwd, reloadDirs)
+ if err != nil {
+ logutils.LogRed("Unable to create filesystem watcher. Reloads will not occur.")
+ return nil, err
+ }
+
+ defer func(watcher *fsnotify.Watcher) {
+ err := watcher.Close()
+ if err != nil {
+ log.Fatal(err.Error())
+ }
+ }(watcher)
+
+ logutils.LogGreen("Watching (sub)/directory: %s", cwd)
+
+ // Main Loop
+ extensionsThatTriggerARebuild := sliceToMap(strings.Split(f.Extensions, ","))
+ var dirsThatTriggerAReload []string
+ for _, dir := range strings.Split(f.ReloadDirs, ",") {
+ if dir == "" {
+ continue
+ }
+ thePath, err := filepath.Abs(dir)
+ if err != nil {
+ logutils.LogRed("Unable to expand reloadDir '%s': %s", dir, err)
+ continue
+ }
+ dirsThatTriggerAReload = append(dirsThatTriggerAReload, thePath)
+ err = watcher.Add(thePath)
+ if err != nil {
+ logutils.LogRed("Unable to watch path: %s due to error %v", thePath, err)
+ } else {
+ logutils.LogGreen("Watching (sub)/directory: %s", thePath)
+ }
+ }
+
+ quit := false
+ interval := time.Duration(f.Debounce) * time.Millisecond
+ timer := time.NewTimer(interval)
+ rebuild := false
+ reload := false
+ assetDir := ""
+ changedPaths := map[string]struct{}{}
+
+ // If we are using an external dev server, the reloading of the frontend part can be skipped or if the user requested it
+ skipAssetsReload := f.FrontendDevServerURL != "" || f.NoReload
+
+ assetDirURL := joinPath(devServerURL, "/wails/assetdir")
+ reloadURL := joinPath(devServerURL, "/wails/reload")
+ for !quit {
+ // reload := false
+ select {
+ case exitCode := <-exitCodeChannel:
+ if exitCode == 0 {
+ quit = true
+ }
+ case err := <-watcher.Errors:
+ logutils.LogDarkYellow(err.Error())
+ case item := <-watcher.Events:
+ isEligibleFile := func(fileName string) bool {
+ // Iterate all file patterns
+ ext := filepath.Ext(fileName)
+ if ext != "" {
+ ext = ext[1:]
+ if _, exists := extensionsThatTriggerARebuild[ext]; exists {
+ return true
+ }
+ }
+ return false
+ }
+
+ // Handle write operations
+ if item.Op&fsnotify.Write == fsnotify.Write {
+ // Ignore directories
+ itemName := item.Name
+ if fs.DirExists(itemName) {
+ continue
+ }
+
+ if isEligibleFile(itemName) {
+ rebuild = true
+ timer.Reset(interval)
+ continue
+ }
+
+ for _, reloadDir := range dirsThatTriggerAReload {
+ if strings.HasPrefix(itemName, reloadDir) {
+ reload = true
+ break
+ }
+ }
+
+ if !reload {
+ changedPaths[filepath.Dir(itemName)] = struct{}{}
+ }
+
+ timer.Reset(interval)
+ }
+
+ // Handle new fs entries that are created
+ if item.Op&fsnotify.Create == fsnotify.Create {
+ // If this is a folder, add it to our watch list
+ if fs.DirExists(item.Name) {
+ // node_modules is BANNED!
+ if !strings.Contains(item.Name, "node_modules") {
+ err := watcher.Add(item.Name)
+ if err != nil {
+ buildOptions.Logger.Fatal("%s", err.Error())
+ }
+ logutils.LogGreen("Added new directory to watcher: %s", item.Name)
+ }
+ } else if isEligibleFile(item.Name) {
+ // Handle creation of new file.
+ // Note: On some platforms an update to a file is represented as
+ // REMOVE -> CREATE instead of WRITE, so this is not only new files
+ // but also updates to existing files
+ rebuild = true
+ timer.Reset(interval)
+ continue
+ }
+ }
+ case <-timer.C:
+ if rebuild {
+ rebuild = false
+ if f.NoGoRebuild {
+ logutils.LogGreen("[Rebuild triggered] skipping due to flag -nogorebuild")
+ } else {
+ logutils.LogGreen("[Rebuild triggered] files updated")
+ // Try and build the app
+
+ newBinaryProcess, _, err := restartApp(buildOptions, debugBinaryProcess, f, exitCodeChannel, legacyUseDevServerInsteadofCustomScheme)
+ if err != nil {
+ logutils.LogRed("Error during build: %s", err.Error())
+ continue
+ }
+ // If we have a new process, saveConfig it
+ if newBinaryProcess != nil {
+ debugBinaryProcess = newBinaryProcess
+ }
+ }
+ }
+
+ if !skipAssetsReload && len(changedPaths) != 0 {
+ if assetDir == "" {
+ resp, err := http.Get(assetDirURL)
+ if err != nil {
+ logutils.LogRed("Error during retrieving assetdir: %s", err.Error())
+ } else {
+ content, err := io.ReadAll(resp.Body)
+ if err != nil {
+ logutils.LogRed("Error reading assetdir from devserver: %s", err.Error())
+ } else {
+ assetDir = string(content)
+ }
+ resp.Body.Close()
+ }
+ }
+
+ if assetDir != "" {
+ for thePath := range changedPaths {
+ if strings.HasPrefix(thePath, assetDir) {
+ reload = true
+ break
+ }
+ }
+ } else if len(dirsThatTriggerAReload) == 0 {
+ logutils.LogRed("Reloading couldn't be triggered: Please specify -assetdir or -reloaddirs")
+ }
+ }
+ if reload {
+ reload = false
+ _, err := http.Get(reloadURL)
+ if err != nil {
+ logutils.LogRed("Error during refresh: %s", err.Error())
+ }
+ }
+ changedPaths = map[string]struct{}{}
+ case <-quitChannel:
+ logutils.LogGreen("\nCaught quit")
+ quit = true
+ }
+ }
+ return debugBinaryProcess, nil
+}
+
+func joinPath(url *url.URL, subPath string) string {
+ u := *url
+ u.Path = path.Join(u.Path, subPath)
+ return u.String()
+}
diff --git a/v2/cmd/wails/internal/dev/dev_other.go b/v2/cmd/wails/internal/dev/dev_other.go
new file mode 100644
index 000000000..88e170ee3
--- /dev/null
+++ b/v2/cmd/wails/internal/dev/dev_other.go
@@ -0,0 +1,37 @@
+//go:build darwin || linux
+// +build darwin linux
+
+package dev
+
+import (
+ "os/exec"
+ "syscall"
+
+ "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils"
+ "golang.org/x/sys/unix"
+)
+
+func setParentGID(cmd *exec.Cmd) {
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Setpgid: true,
+ }
+}
+
+func killProc(cmd *exec.Cmd, devCommand string) {
+ if cmd == nil || cmd.Process == nil {
+ return
+ }
+
+ // Experiencing the same issue on macOS BigSur
+ // I'm using Vite, but I would imagine this could be an issue with Node (npm) in general
+ // Also, after several edit/rebuild cycles any abnormal shutdown (crash or CTRL-C) may still leave Node running
+ // Credit: https://stackoverflow.com/a/29552044/14764450 (same page as the Windows solution above)
+ // Not tested on *nix
+ pgid, err := syscall.Getpgid(cmd.Process.Pid)
+ if err == nil {
+ err := syscall.Kill(-pgid, unix.SIGTERM) // note the minus sign
+ if err != nil {
+ logutils.LogRed("Error from '%s' when attempting to kill the process: %s", devCommand, err.Error())
+ }
+ }
+}
diff --git a/v2/cmd/wails/internal/dev/dev_windows.go b/v2/cmd/wails/internal/dev/dev_windows.go
new file mode 100644
index 000000000..e219e6519
--- /dev/null
+++ b/v2/cmd/wails/internal/dev/dev_windows.go
@@ -0,0 +1,34 @@
+//go:build windows
+// +build windows
+
+package dev
+
+import (
+ "bytes"
+ "os/exec"
+ "strconv"
+
+ "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils"
+)
+
+func setParentGID(_ *exec.Cmd) {}
+
+func killProc(cmd *exec.Cmd, devCommand string) {
+ // Credit: https://stackoverflow.com/a/44551450
+ // For whatever reason, killing an npm script on windows just doesn't exit properly with cancel
+ if cmd != nil && cmd.Process != nil {
+ kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
+ var errorBuffer bytes.Buffer
+ var stdoutBuffer bytes.Buffer
+ kill.Stderr = &errorBuffer
+ kill.Stdout = &stdoutBuffer
+ err := kill.Run()
+ if err != nil {
+ if err.Error() != "exit status 1" {
+ println(stdoutBuffer.String())
+ println(errorBuffer.String())
+ logutils.LogRed("Error from '%s': %s", devCommand, err.Error())
+ }
+ }
+ }
+}
diff --git a/v2/cmd/wails/internal/dev/stdout_scanner.go b/v2/cmd/wails/internal/dev/stdout_scanner.go
new file mode 100644
index 000000000..dad4e72cf
--- /dev/null
+++ b/v2/cmd/wails/internal/dev/stdout_scanner.go
@@ -0,0 +1,84 @@
+package dev
+
+import (
+ "bufio"
+ "fmt"
+ "net/url"
+ "os"
+ "strings"
+
+ "github.com/acarl005/stripansi"
+ "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils"
+ "golang.org/x/mod/semver"
+)
+
+// stdoutScanner acts as a stdout target that will scan the incoming
+// data to find out the vite server url
+type stdoutScanner struct {
+ ViteServerURLChan chan string
+ ViteServerVersionC chan string
+ versionDetected bool
+}
+
+// NewStdoutScanner creates a new stdoutScanner
+func NewStdoutScanner() *stdoutScanner {
+ return &stdoutScanner{
+ ViteServerURLChan: make(chan string, 2),
+ ViteServerVersionC: make(chan string, 2),
+ }
+}
+
+// Write bytes to the scanner. Will copy the bytes to stdout
+func (s *stdoutScanner) Write(data []byte) (n int, err error) {
+ input := stripansi.Strip(string(data))
+ if !s.versionDetected {
+ v, err := detectViteVersion(input)
+ if v != "" || err != nil {
+ if err != nil {
+ logutils.LogRed("ViteStdoutScanner: %s", err)
+ v = "v0.0.0"
+ }
+ s.ViteServerVersionC <- v
+ s.versionDetected = true
+ }
+ }
+
+ match := strings.Index(input, "Local:")
+ if match != -1 {
+ sc := bufio.NewScanner(strings.NewReader(input))
+ for sc.Scan() {
+ line := sc.Text()
+ index := strings.Index(line, "Local:")
+ if index == -1 || len(line) < 7 {
+ continue
+ }
+ viteServerURL := strings.TrimSpace(line[index+6:])
+ logutils.LogGreen("Vite Server URL: %s", viteServerURL)
+ _, err := url.Parse(viteServerURL)
+ if err != nil {
+ logutils.LogRed(err.Error())
+ } else {
+ s.ViteServerURLChan <- viteServerURL
+ }
+ }
+ }
+ return os.Stdout.Write(data)
+}
+
+func detectViteVersion(line string) (string, error) {
+ s := strings.Split(strings.TrimSpace(line), " ")
+ if strings.ToLower(s[0]) != "vite" {
+ return "", nil
+ }
+
+ if len(line) < 2 {
+ return "", fmt.Errorf("unable to parse vite version")
+ }
+
+ v := s[1]
+ if !semver.IsValid(v) {
+ return "", fmt.Errorf("%s is not a valid vite version string", v)
+ }
+
+ return v, nil
+}
diff --git a/v2/cmd/wails/internal/dev/watcher.go b/v2/cmd/wails/internal/dev/watcher.go
new file mode 100644
index 000000000..e1161f87c
--- /dev/null
+++ b/v2/cmd/wails/internal/dev/watcher.go
@@ -0,0 +1,78 @@
+package dev
+
+import (
+ "bufio"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/fs"
+
+ "github.com/fsnotify/fsnotify"
+ gitignore "github.com/sabhiram/go-gitignore"
+ "github.com/samber/lo"
+)
+
+type Watcher interface {
+ Add(name string) error
+}
+
+// initialiseWatcher creates the project directory watcher that will trigger recompile
+func initialiseWatcher(cwd, reloadDirs string) (*fsnotify.Watcher, error) {
+ // Ignore dot files, node_modules and build directories by default
+ ignoreDirs := getIgnoreDirs(cwd)
+
+ // Get all subdirectories
+ dirs, err := fs.GetSubdirectories(cwd)
+ if err != nil {
+ return nil, err
+ }
+
+ customDirs := dirs.AsSlice()
+ seperatedDirs := strings.Split(reloadDirs, ",")
+ for _, dir := range seperatedDirs {
+ customSub, err := fs.GetSubdirectories(filepath.Join(cwd, dir))
+ if err != nil {
+ return nil, err
+ }
+ customDirs = append(customDirs, customSub.AsSlice()...)
+ }
+
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, dir := range processDirectories(customDirs, ignoreDirs) {
+ err := watcher.Add(dir)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return watcher, nil
+}
+
+func getIgnoreDirs(cwd string) []string {
+ ignoreDirs := []string{filepath.Join(cwd, "build/*"), ".*", "node_modules"}
+ baseDir := filepath.Base(cwd)
+ // Read .gitignore into ignoreDirs
+ f, err := os.Open(filepath.Join(cwd, ".gitignore"))
+ if err == nil {
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if line != baseDir {
+ ignoreDirs = append(ignoreDirs, line)
+ }
+ }
+ }
+
+ return lo.Uniq(ignoreDirs)
+}
+
+func processDirectories(dirs []string, ignoreDirs []string) []string {
+ ignorer := gitignore.CompileIgnoreLines(ignoreDirs...)
+ return lo.Filter(dirs, func(dir string, _ int) bool {
+ return !ignorer.MatchesPath(dir)
+ })
+}
diff --git a/v2/cmd/wails/internal/dev/watcher_test.go b/v2/cmd/wails/internal/dev/watcher_test.go
new file mode 100644
index 000000000..ad228b66c
--- /dev/null
+++ b/v2/cmd/wails/internal/dev/watcher_test.go
@@ -0,0 +1,113 @@
+package dev
+
+import (
+ "github.com/samber/lo"
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/wailsapp/wails/v2/internal/fs"
+)
+
+func Test_processDirectories(t *testing.T) {
+ tests := []struct {
+ name string
+ dirs []string
+ ignoreDirs []string
+ want []string
+ }{
+ {
+ name: "Should ignore .git",
+ ignoreDirs: []string{".git"},
+ dirs: []string{".git", "some/path/to/nested/.git", "some/path/to/nested/.git/CHANGELOG"},
+ want: []string{},
+ },
+ {
+ name: "Should ignore node_modules",
+ ignoreDirs: []string{"node_modules"},
+ dirs: []string{"node_modules", "path/to/node_modules", "path/to/node_modules/some/other/path"},
+ want: []string{},
+ },
+ {
+ name: "Should ignore dirs starting with .",
+ ignoreDirs: []string{".*"},
+ dirs: []string{".test", ".gitignore", ".someother", "valid"},
+ want: []string{"valid"},
+ },
+ {
+ name: "Should ignore dirs in ignoreDirs",
+ dirs: []string{"build", "build/my.exe", "build/my.app"},
+ ignoreDirs: []string{"build"},
+ want: []string{},
+ },
+ {
+ name: "Should ignore subdirectories",
+ dirs: []string{"build", "some/path/to/build", "some/path/to/CHANGELOG", "some/other/path"},
+ ignoreDirs: []string{"some/**/*"},
+ want: []string{"build"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := processDirectories(tt.dirs, tt.ignoreDirs)
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("processDirectories() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_GetIgnoreDirs(t *testing.T) {
+
+ // Remove testdir if it exists
+ _ = os.RemoveAll("testdir")
+
+ tests := []struct {
+ name string
+ files []string
+ want []string
+ shouldErr bool
+ }{
+ {
+ name: "Should have defaults",
+ files: []string{},
+ want: []string{"testdir/build/*", ".*", "node_modules"},
+ },
+ {
+ name: "Should ignore dotFiles",
+ files: []string{".test1", ".wailsignore"},
+ want: []string{"testdir/build/*", ".*", "node_modules"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create temporary file
+ err := fs.Mkdir("testdir")
+ require.NoError(t, err)
+ defer func() {
+ err := os.RemoveAll("testdir")
+ require.NoError(t, err)
+ }()
+ for _, file := range tt.files {
+ fs.MustWriteString(filepath.Join("testdir", file), "")
+ }
+
+ got := getIgnoreDirs("testdir")
+
+ got = lo.Map(got, func(s string, _ int) string {
+ return filepath.ToSlash(s)
+ })
+
+ if (err != nil) != tt.shouldErr {
+ t.Errorf("initialiseWatcher() error = %v, shouldErr %v", err, tt.shouldErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("initialiseWatcher() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/v2/cmd/wails/internal/gomod/gomod.go b/v2/cmd/wails/internal/gomod/gomod.go
new file mode 100644
index 000000000..5da14a5ff
--- /dev/null
+++ b/v2/cmd/wails/internal/gomod/gomod.go
@@ -0,0 +1,68 @@
+package gomod
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/cmd/wails/internal"
+ "github.com/wailsapp/wails/v2/internal/colour"
+ "github.com/wailsapp/wails/v2/internal/fs"
+ "github.com/wailsapp/wails/v2/internal/gomod"
+ "github.com/wailsapp/wails/v2/internal/goversion"
+ "github.com/wailsapp/wails/v2/pkg/clilogger"
+)
+
+func SyncGoMod(logger *clilogger.CLILogger, updateWailsVersion bool) error {
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ gomodFilename := fs.FindFileInParents(cwd, "go.mod")
+ if gomodFilename == "" {
+ return fmt.Errorf("no go.mod file found")
+ }
+ gomodData, err := os.ReadFile(gomodFilename)
+ if err != nil {
+ return err
+ }
+
+ gomodData, updated, err := gomod.SyncGoVersion(gomodData, goversion.MinRequirement)
+ if err != nil {
+ return err
+ } else if updated {
+ LogGreen("Updated go.mod to use Go '%s'", goversion.MinRequirement)
+ }
+
+ internalVersion := strings.TrimSpace(internal.Version)
+ if outOfSync, err := gomod.GoModOutOfSync(gomodData, internalVersion); err != nil {
+ return err
+ } else if outOfSync {
+ if updateWailsVersion {
+ LogGreen("Updating go.mod to use Wails '%s'", internalVersion)
+ gomodData, err = gomod.UpdateGoModVersion(gomodData, internalVersion)
+ if err != nil {
+ return err
+ }
+ updated = true
+ } else {
+ gomodversion, err := gomod.GetWailsVersionFromModFile(gomodData)
+ if err != nil {
+ return err
+ }
+
+ logger.Println("Warning: go.mod is using Wails '%s' but the CLI is '%s'. Consider updating your project's `go.mod` file.\n", gomodversion.String(), internal.Version)
+ }
+ }
+
+ if updated {
+ return os.WriteFile(gomodFilename, gomodData, 0o755)
+ }
+
+ return nil
+}
+
+func LogGreen(message string, args ...interface{}) {
+ text := fmt.Sprintf(message, args...)
+ println(colour.Green(text))
+}
diff --git a/v2/cmd/wails/internal/logutils/color-logs.go b/v2/cmd/wails/internal/logutils/color-logs.go
new file mode 100644
index 000000000..65553df3f
--- /dev/null
+++ b/v2/cmd/wails/internal/logutils/color-logs.go
@@ -0,0 +1,31 @@
+package logutils
+
+import (
+ "fmt"
+
+ "github.com/wailsapp/wails/v2/internal/colour"
+)
+
+func LogGreen(message string, args ...interface{}) {
+ if len(message) == 0 {
+ return
+ }
+ text := fmt.Sprintf(message, args...)
+ println(colour.Green(text))
+}
+
+func LogRed(message string, args ...interface{}) {
+ if len(message) == 0 {
+ return
+ }
+ text := fmt.Sprintf(message, args...)
+ println(colour.Red(text))
+}
+
+func LogDarkYellow(message string, args ...interface{}) {
+ if len(message) == 0 {
+ return
+ }
+ text := fmt.Sprintf(message, args...)
+ println(colour.DarkYellow(text))
+}
diff --git a/v2/cmd/wails/internal/template/base/NEXTSTEPS.md.template b/v2/cmd/wails/internal/template/base/NEXTSTEPS.md.template
new file mode 100644
index 000000000..5363d10f2
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/NEXTSTEPS.md.template
@@ -0,0 +1,41 @@
+# Next Steps
+
+Congratulations on generating your template!
+
+## Completing your template
+
+The next steps to complete the template are:
+
+ 1. Complete the fields in the `template.json` file.
+ - It is really important to ensure `helpurl` is valid as this is where users of the template will be directed for help.
+ 2. Update `README.md`.
+ 3. Edit `wails.json` and ensure all fields are correct, especially:
+ - `wailsjsdir` - path to generate wailsjs modules
+ - `frontend:install` - The command to install your frontend dependencies
+ - `frontend:build` - The command to build your frontend
+ 4. Remove any `public` or `dist` directories.
+ 5. Delete this file.
+
+## Testing your template
+
+You can test your template by running this command:
+
+`wails init -n test -t {{.TemplateDir}}`
+
+### Checklist
+
+Once generated, do the following tests:
+ - Change into the new project directory and run `wails build`. A working binary should be generated in the `build/bin` project directory.
+ - Run `wails dev`. This will compile your app and run it.
+ - You should be able to see your application running on http://localhost:34115/
+
+## Publishing your template
+
+You can publish a template to a git repository and use it as follows:
+
+`wails init -name test -t https://your/git/url`
+
+EG:
+
+`wails init -name test -t https://github.com/leaanthony/testtemplate`
+
diff --git a/v2/cmd/wails/internal/template/base/README.md b/v2/cmd/wails/internal/template/base/README.md
new file mode 100644
index 000000000..ed259fcff
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/README.md
@@ -0,0 +1,15 @@
+# README
+
+## About
+
+About your template
+
+## Live Development
+
+To run in live development mode, run `wails dev` in the project directory. In another terminal, go into the `frontend`
+directory and run `npm run dev`. The frontend dev server will run on http://localhost:34115. Connect to this in your
+browser and connect to your application.
+
+## Building
+
+To build a redistributable, production mode package, use `wails build`.
diff --git a/v2/cmd/wails/internal/template/base/app.tmpl.go b/v2/cmd/wails/internal/template/base/app.tmpl.go
new file mode 100644
index 000000000..224be7156
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/app.tmpl.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "context"
+ "fmt"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called at application startup
+func (a *App) startup(ctx context.Context) {
+ // Perform your setup here
+ a.ctx = ctx
+}
+
+// domReady is called after front-end resources have been loaded
+func (a App) domReady(ctx context.Context) {
+ // Add your action here
+}
+
+// beforeClose is called when the application is about to quit,
+// either by clicking the window close button or calling runtime.Quit.
+// Returning true will cause the application to continue, false will continue shutdown as normal.
+func (a *App) beforeClose(ctx context.Context) (prevent bool) {
+ return false
+}
+
+// shutdown is called at application termination
+func (a *App) shutdown(ctx context.Context) {
+ // Perform your teardown here
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+ return fmt.Sprintf("Hello %s, It's show time!", name)
+}
diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/OFL.txt b/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/assets/images/logo-universal.png b/v2/cmd/wails/internal/template/base/frontend/dist/assets/images/logo-universal.png
new file mode 100644
index 000000000..27ef13655
Binary files /dev/null and b/v2/cmd/wails/internal/template/base/frontend/dist/assets/images/logo-universal.png differ
diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/index.html b/v2/cmd/wails/internal/template/base/frontend/dist/index.html
new file mode 100644
index 000000000..e2a14c1b5
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/frontend/dist/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
Please enter your name below 👇
+
+
+
+
+
+
+
+
diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/main.css b/v2/cmd/wails/internal/template/base/frontend/dist/main.css
new file mode 100644
index 000000000..f35a69f99
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/frontend/dist/main.css
@@ -0,0 +1,79 @@
+html {
+ background-color: rgba(27, 38, 54, 1);
+ text-align: center;
+ color: white;
+}
+
+body {
+ margin: 0;
+ color: white;
+ font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+}
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
+}
+
+#app {
+ height: 100vh;
+ text-align: center;
+}
+
+.logo {
+ display: block;
+ width: 50%;
+ height: 50%;
+ margin: auto;
+ padding: 10% 0 0;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: url("./assets/images/logo-universal.png");
+ background-size: 100% 100%;
+ background-origin: content-box;
+}
+.result {
+ height: 20px;
+ line-height: 20px;
+ margin: 1.5rem auto;
+}
+.input-box .btn {
+ width: 60px;
+ height: 30px;
+ line-height: 30px;
+ border-radius: 3px;
+ border: none;
+ margin: 0 0 0 20px;
+ padding: 0 8px;
+ cursor: pointer;
+}
+.input-box .btn:hover {
+ background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
+ color: #333333;
+}
+
+.input-box .input {
+ border: none;
+ border-radius: 3px;
+ outline: none;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 10px;
+ background-color: rgba(240, 240, 240, 1);
+ -webkit-font-smoothing: antialiased;
+}
+
+.input-box .input:hover {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+}
+
+.input-box .input:focus {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+}
diff --git a/v2/cmd/wails/internal/template/base/frontend/dist/main.js b/v2/cmd/wails/internal/template/base/frontend/dist/main.js
new file mode 100644
index 000000000..98510cd39
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/frontend/dist/main.js
@@ -0,0 +1,32 @@
+// Get input + focus
+let nameElement = document.getElementById("name");
+nameElement.focus();
+
+// Setup the greet function
+window.greet = function () {
+ // Get name
+ let name = nameElement.value;
+
+ // Check if the input is empty
+ if (name === "") return;
+
+ // Call App.Greet(name)
+ try {
+ window.go.main.App.Greet(name)
+ .then((result) => {
+ // Update result with data back from App.Greet()
+ document.getElementById("result").innerText = result;
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ } catch (err) {
+ console.error(err);
+ }
+};
+
+nameElement.onkeydown = function (e) {
+ if (e.keyCode == 13) {
+ window.greet();
+ }
+};
diff --git a/v2/cmd/wails/internal/template/base/frontend/package.tmpl.json b/v2/cmd/wails/internal/template/base/frontend/package.tmpl.json
new file mode 100644
index 000000000..01780288d
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/frontend/package.tmpl.json
@@ -0,0 +1,12 @@
+{
+ "name": "{{.ProjectName}}",
+ "version": "1.0.0",
+ "description": "",
+ "main": "",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "build": ""
+ },
+ "keywords": [],
+ "author": "{{.AuthorName}}"
+}
diff --git a/v2/cmd/wails/internal/template/base/go.sum b/v2/cmd/wails/internal/template/base/go.sum
new file mode 100644
index 000000000..92f4d6d57
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/go.sum
@@ -0,0 +1,180 @@
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/flytam/filenamify v1.0.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
+github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
+github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw=
+github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
+github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
+github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/labstack/echo/v4 v4.7.2 h1:Kv2/p8OaQ+M6Ex4eGimg9b9e6icoxA42JSlOR3msKtI=
+github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
+github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
+github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
+github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
+github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4=
+github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM=
+github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
+github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
+github.com/leaanthony/idgen v1.0.0/go.mod h1:4nBZnt8ml/f/ic/EVQuLxuj817RccT2fyrUaZFxrcVA=
+github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
+github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
+github.com/leaanthony/typescriptify-golang-structs v0.1.7 h1:yoznzWzyxkO/iWdlpq+aPcuJ5Y/hpjq/lmgMFmpjwl0=
+github.com/leaanthony/typescriptify-golang-structs v0.1.7/go.mod h1:cWtOkiVhMF77e6phAXUcfNwYmMwCJ67Sij24lfvi9Js=
+github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
+github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
+github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
+github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
+github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc=
+github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/tc-hib/winres v0.1.5/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A=
+github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
+github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tidwall/sjson v1.1.7/go.mod h1:w/yG+ezBeTdUxiKs5NcPicO9diP38nk96QBAbIIGeFs=
+github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ=
+github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
+github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/wailsapp/mimetype v1.4.1-beta.1.0.20220331112158-6df7e41671fe h1:FiWQ7XhDkc4zAH8SEx1BTte/6VHyceraUusH8jf5SQw=
+github.com/wailsapp/mimetype v1.4.1-beta.1.0.20220331112158-6df7e41671fe/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
+github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28=
+github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20=
+github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/ztrue/tracerr v0.3.0/go.mod h1:qEalzze4VN9O8tnhBXScfCrmoJo10o8TN5ciKjm6Mww=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
+golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/v2/cmd/wails/internal/template/base/go.tmpl.mod b/v2/cmd/wails/internal/template/base/go.tmpl.mod
new file mode 100644
index 000000000..42478753c
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/go.tmpl.mod
@@ -0,0 +1,32 @@
+module changeme
+
+ go 1.18
+
+ require github.com/wailsapp/wails/v2 {{.WailsVersion}}
+
+ require (
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/google/uuid v1.1.2 // indirect
+ github.com/imdario/mergo v0.3.12 // indirect
+ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
+ github.com/labstack/echo/v4 v4.7.2 // indirect
+ github.com/labstack/gommon v0.3.1 // indirect
+ github.com/leaanthony/go-ansi-parser v1.0.1 // indirect
+ github.com/leaanthony/gosod v1.0.3 // indirect
+ github.com/leaanthony/slicer v1.5.0 // indirect
+ github.com/leaanthony/typescriptify-golang-structs v0.1.7 // indirect
+ github.com/mattn/go-colorable v0.1.11 // indirect
+ github.com/mattn/go-isatty v0.0.14 // indirect
+ github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/tkrajina/go-reflector v0.5.5 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasttemplate v1.2.1 // indirect
+ github.com/wailsapp/mimetype v1.4.1-beta.1.0.20220331112158-6df7e41671fe // indirect
+ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
+ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
+ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
+ golang.org/x/text v0.3.7 // indirect
+ )
+
+ // replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}}
\ No newline at end of file
diff --git a/v2/cmd/wails/internal/template/base/main.go.tmpl b/v2/cmd/wails/internal/template/base/main.go.tmpl
new file mode 100644
index 000000000..d8e902027
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/main.go.tmpl
@@ -0,0 +1,87 @@
+package main
+
+import (
+ "embed"
+ "log"
+
+ "github.com/wailsapp/wails/v2"
+ "github.com/wailsapp/wails/v2/pkg/logger"
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+ "github.com/wailsapp/wails/v2/pkg/options/mac"
+ "github.com/wailsapp/wails/v2/pkg/options/windows"
+)
+
+//go:embed all:frontend/dist
+var assets embed.FS
+
+//go:embed build/appicon.png
+var icon []byte
+
+func main() {
+ // Create an instance of the app structure
+ app := NewApp()
+
+ // Create application with options
+ err := wails.Run(&options.App{
+ Title: "{{.ProjectName}}",
+ Width: 1024,
+ Height: 768,
+ MinWidth: 1024,
+ MinHeight: 768,
+ MaxWidth: 1280,
+ MaxHeight: 800,
+ DisableResize: false,
+ Fullscreen: false,
+ Frameless: false,
+ StartHidden: false,
+ HideWindowOnClose: false,
+ BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
+ AssetServer: &assetserver.Options{
+ Assets: assets,
+ },
+ Menu: nil,
+ Logger: nil,
+ LogLevel: logger.DEBUG,
+ OnStartup: app.startup,
+ OnDomReady: app.domReady,
+ OnBeforeClose: app.beforeClose,
+ OnShutdown: app.shutdown,
+ WindowStartState: options.Normal,
+ Bind: []interface{}{
+ app,
+ },
+ // Windows platform specific options
+ Windows: &windows.Options{
+ WebviewIsTransparent: false,
+ WindowIsTranslucent: false,
+ DisableWindowIcon: false,
+ // DisableFramelessWindowDecorations: false,
+ WebviewUserDataPath: "",
+ ZoomFactor: 1.0,
+ },
+ // Mac platform specific options
+ Mac: &mac.Options{
+ TitleBar: &mac.TitleBar{
+ TitlebarAppearsTransparent: true,
+ HideTitle: false,
+ HideTitleBar: false,
+ FullSizeContent: false,
+ UseToolbar: false,
+ HideToolbarSeparator: true,
+ },
+ Appearance: mac.NSAppearanceNameDarkAqua,
+ WebviewIsTransparent: true,
+ WindowIsTranslucent: true,
+ About: &mac.AboutInfo{
+ Title: "{{.ProjectName}}",
+ Message: "",
+ Icon: icon,
+ },
+ },
+ })
+
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/v2/cmd/wails/internal/template/base/scripts/build-macos-arm.sh b/v2/cmd/wails/internal/template/base/scripts/build-macos-arm.sh
new file mode 100644
index 000000000..bc6ee0acb
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/scripts/build-macos-arm.sh
@@ -0,0 +1,9 @@
+#! /bin/bash
+
+echo -e "Start running the script..."
+cd ../
+
+echo -e "Start building the app for macos platform..."
+wails build --clean --platform darwin/arm64
+
+echo -e "End running the script!"
diff --git a/v2/cmd/wails/internal/template/base/scripts/build-macos-intel.sh b/v2/cmd/wails/internal/template/base/scripts/build-macos-intel.sh
new file mode 100644
index 000000000..f359f633a
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/scripts/build-macos-intel.sh
@@ -0,0 +1,9 @@
+#! /bin/bash
+
+echo -e "Start running the script..."
+cd ../
+
+echo -e "Start building the app for macos platform..."
+wails build --clean --platform darwin
+
+echo -e "End running the script!"
diff --git a/v2/cmd/wails/internal/template/base/scripts/build-macos.sh b/v2/cmd/wails/internal/template/base/scripts/build-macos.sh
new file mode 100644
index 000000000..d61531fd7
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/scripts/build-macos.sh
@@ -0,0 +1,9 @@
+#! /bin/bash
+
+echo -e "Start running the script..."
+cd ../
+
+echo -e "Start building the app for macos platform..."
+wails build --clean --platform darwin/universal
+
+echo -e "End running the script!"
diff --git a/v2/cmd/wails/internal/template/base/scripts/build-windows.sh b/v2/cmd/wails/internal/template/base/scripts/build-windows.sh
new file mode 100644
index 000000000..47b778970
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/scripts/build-windows.sh
@@ -0,0 +1,9 @@
+#! /bin/bash
+
+echo -e "Start running the script..."
+cd ../
+
+echo -e "Start building the app for windows platform..."
+wails build --clean --platform windows/amd64
+
+echo -e "End running the script!"
diff --git a/v2/cmd/wails/internal/template/base/scripts/build.sh b/v2/cmd/wails/internal/template/base/scripts/build.sh
new file mode 100644
index 000000000..20ab7eb21
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/scripts/build.sh
@@ -0,0 +1,9 @@
+#! /bin/bash
+
+echo -e "Start running the script..."
+cd ../
+
+echo -e "Start building the app..."
+wails build --clean
+
+echo -e "End running the script!"
diff --git a/v2/cmd/wails/internal/template/base/scripts/install-wails-cli.sh b/v2/cmd/wails/internal/template/base/scripts/install-wails-cli.sh
new file mode 100644
index 000000000..7539d8e33
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/scripts/install-wails-cli.sh
@@ -0,0 +1,14 @@
+#! /bin/bash
+
+echo -e "Start running the script..."
+cd ../
+
+echo -e "Current Go version: \c"
+go version
+
+echo -e "Install the Wails command line tool..."
+go install github.com/wailsapp/wails/v2/cmd/wails@latest
+
+echo -e "Successful installation!"
+
+echo -e "End running the script!"
diff --git a/v2/cmd/wails/internal/template/base/template.json.template b/v2/cmd/wails/internal/template/base/template.json.template
new file mode 100644
index 000000000..bde089e00
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/template.json.template
@@ -0,0 +1,7 @@
+{
+ "name": "Long name",
+ "shortname": "{{.Name}}",
+ "author": "",
+ "description": "Description of the template",
+ "helpurl": "URL for help with the template, eg homepage"
+}
\ No newline at end of file
diff --git a/v2/cmd/wails/internal/template/base/wails.tmpl.json b/v2/cmd/wails/internal/template/base/wails.tmpl.json
new file mode 100644
index 000000000..cdb10e346
--- /dev/null
+++ b/v2/cmd/wails/internal/template/base/wails.tmpl.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://wails.io/schemas/config.v2.json",
+ "name": "{{.ProjectName}}",
+ "outputfilename": "{{.BinaryName}}",
+ "frontend:install": "npm install",
+ "frontend:build": "npm run build",
+ "author": {
+ "name": "{{.AuthorName}}",
+ "email": "{{.AuthorEmail}}"
+ }
+}
diff --git a/v2/cmd/wails/internal/template/template.go b/v2/cmd/wails/internal/template/template.go
new file mode 100644
index 000000000..6b4937db6
--- /dev/null
+++ b/v2/cmd/wails/internal/template/template.go
@@ -0,0 +1,8 @@
+package template
+
+import (
+ "embed"
+)
+
+//go:embed base
+var Base embed.FS
diff --git a/v2/cmd/wails/internal/version.go b/v2/cmd/wails/internal/version.go
new file mode 100644
index 000000000..cfc37182c
--- /dev/null
+++ b/v2/cmd/wails/internal/version.go
@@ -0,0 +1,6 @@
+package internal
+
+import _ "embed"
+
+//go:embed version.txt
+var Version string
diff --git a/v2/cmd/wails/internal/version.txt b/v2/cmd/wails/internal/version.txt
new file mode 100644
index 000000000..805579f30
--- /dev/null
+++ b/v2/cmd/wails/internal/version.txt
@@ -0,0 +1 @@
+v2.11.0
\ No newline at end of file
diff --git a/v2/cmd/wails/main.go b/v2/cmd/wails/main.go
new file mode 100644
index 000000000..ccf1576e9
--- /dev/null
+++ b/v2/cmd/wails/main.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/pterm/pterm"
+ "github.com/wailsapp/wails/v2/cmd/wails/internal"
+
+ "github.com/wailsapp/wails/v2/internal/colour"
+
+ "github.com/leaanthony/clir"
+)
+
+func banner(_ *clir.Cli) string {
+ return fmt.Sprintf("%s %s",
+ colour.Green("Wails CLI"),
+ colour.DarkRed(internal.Version))
+}
+
+func fatal(message string) {
+ printer := pterm.PrefixPrinter{
+ MessageStyle: &pterm.ThemeDefault.FatalMessageStyle,
+ Prefix: pterm.Prefix{
+ Style: &pterm.ThemeDefault.FatalPrefixStyle,
+ Text: " FATAL ",
+ },
+ }
+ printer.Println(message)
+ os.Exit(1)
+}
+
+func printBulletPoint(text string, args ...any) {
+ item := pterm.BulletListItem{
+ Level: 2,
+ Text: text,
+ }
+ t, err := pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{item}).Srender()
+ if err != nil {
+ fatal(err.Error())
+ }
+ t = strings.Trim(t, "\n\r")
+ pterm.Printfln(t, args...)
+}
+
+func printFooter() {
+ printer := pterm.PrefixPrinter{
+ MessageStyle: pterm.NewStyle(pterm.FgLightGreen),
+ Prefix: pterm.Prefix{
+ Style: pterm.NewStyle(pterm.FgRed, pterm.BgLightWhite),
+ Text: "♥ ",
+ },
+ }
+ printer.Println("If Wails is useful to you or your company, please consider sponsoring the project:")
+ pterm.Println("https://github.com/sponsors/leaanthony")
+}
+
+func bool2Str(b bool) string {
+ if b {
+ return "true"
+ }
+ return "false"
+}
+
+var app *clir.Cli
+
+func main() {
+ var err error
+
+ app = clir.NewCli("Wails", "Go/HTML Appkit", internal.Version)
+
+ app.SetBannerFunction(banner)
+ defer printFooter()
+
+ app.NewSubCommandFunction("build", "Builds the application", buildApplication)
+ app.NewSubCommandFunction("dev", "Runs the application in development mode", devApplication)
+ app.NewSubCommandFunction("doctor", "Diagnose your environment", diagnoseEnvironment)
+ app.NewSubCommandFunction("init", "Initialises a new Wails project", initProject)
+ app.NewSubCommandFunction("update", "Update the Wails CLI", update)
+
+ show := app.NewSubCommand("show", "Shows various information")
+ show.NewSubCommandFunction("releasenotes", "Shows the release notes for the current version", showReleaseNotes)
+
+ generate := app.NewSubCommand("generate", "Code Generation Tools")
+ generate.NewSubCommandFunction("module", "Generates a new Wails module", generateModule)
+ generate.NewSubCommandFunction("template", "Generates a new Wails template", generateTemplate)
+
+ command := app.NewSubCommand("version", "The Wails CLI version")
+ command.Action(func() error {
+ pterm.Println(internal.Version)
+ return nil
+ })
+
+ err = app.Run()
+ if err != nil {
+ pterm.Println()
+ pterm.Error.Println(err.Error())
+ printFooter()
+ os.Exit(1)
+ }
+}
diff --git a/v2/cmd/wails/show.go b/v2/cmd/wails/show.go
new file mode 100644
index 000000000..c83900c8d
--- /dev/null
+++ b/v2/cmd/wails/show.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "github.com/pterm/pterm"
+ "github.com/wailsapp/wails/v2/cmd/wails/flags"
+ "github.com/wailsapp/wails/v2/cmd/wails/internal"
+ "github.com/wailsapp/wails/v2/internal/colour"
+ "github.com/wailsapp/wails/v2/internal/github"
+)
+
+func showReleaseNotes(f *flags.ShowReleaseNotes) error {
+ if f.NoColour {
+ pterm.DisableColor()
+ colour.ColourEnabled = false
+ }
+
+ version := internal.Version
+ if f.Version != "" {
+ version = f.Version
+ }
+
+ app.PrintBanner()
+ releaseNotes := github.GetReleaseNotes(version, f.NoColour)
+ pterm.Println(releaseNotes)
+
+ return nil
+}
diff --git a/v2/cmd/wails/update.go b/v2/cmd/wails/update.go
new file mode 100644
index 000000000..9f8b6e604
--- /dev/null
+++ b/v2/cmd/wails/update.go
@@ -0,0 +1,156 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/labstack/gommon/color"
+ "github.com/pterm/pterm"
+ "github.com/wailsapp/wails/v2/cmd/wails/flags"
+ "github.com/wailsapp/wails/v2/internal/colour"
+ "github.com/wailsapp/wails/v2/internal/shell"
+
+ "github.com/wailsapp/wails/v2/internal/github"
+)
+
+// AddSubcommand adds the `init` command for the Wails application
+func update(f *flags.Update) error {
+ if f.NoColour {
+ colour.ColourEnabled = false
+ pterm.DisableColor()
+
+ }
+ // Print banner
+ app.PrintBanner()
+ pterm.Println("Checking for updates...")
+
+ var desiredVersion *github.SemanticVersion
+ var err error
+ var valid bool
+
+ if len(f.Version) > 0 {
+ // Check if this is a valid version
+ valid, err = github.IsValidTag(f.Version)
+ if err == nil {
+ if !valid {
+ err = fmt.Errorf("version '%s' is invalid", f.Version)
+ } else {
+ desiredVersion, err = github.NewSemanticVersion(f.Version)
+ }
+ }
+ } else {
+ if f.PreRelease {
+ desiredVersion, err = github.GetLatestPreRelease()
+ } else {
+ desiredVersion, err = github.GetLatestStableRelease()
+ if err != nil {
+ pterm.Println("")
+ pterm.Println("No stable release found for this major version. To update to the latest pre-release (eg beta), run:")
+ pterm.Println(" wails update -pre")
+ return nil
+ }
+ }
+ }
+ if err != nil {
+ return err
+ }
+ pterm.Println()
+
+ pterm.Println(" Current Version : " + app.Version())
+
+ if len(f.Version) > 0 {
+ fmt.Printf(" Desired Version : v%s\n", desiredVersion)
+ } else {
+ if f.PreRelease {
+ fmt.Printf(" Latest Prerelease : v%s\n", desiredVersion)
+ } else {
+ fmt.Printf(" Latest Release : v%s\n", desiredVersion)
+ }
+ }
+
+ return updateToVersion(desiredVersion, len(f.Version) > 0, app.Version())
+}
+
+func updateToVersion(targetVersion *github.SemanticVersion, force bool, currentVersion string) error {
+ targetVersionString := "v" + targetVersion.String()
+
+ if targetVersionString == currentVersion {
+ pterm.Println("\nLooks like you're up to date!")
+ return nil
+ }
+
+ var desiredVersion string
+
+ if !force {
+
+ compareVersion := currentVersion
+
+ currentVersion, err := github.NewSemanticVersion(compareVersion)
+ if err != nil {
+ return err
+ }
+
+ var success bool
+
+ // Release -> Pre-Release = Massage current version to prerelease format
+ if targetVersion.IsPreRelease() && currentVersion.IsRelease() {
+ testVersion, err := github.NewSemanticVersion(compareVersion + "-0")
+ if err != nil {
+ return err
+ }
+ success, _ = targetVersion.IsGreaterThan(testVersion)
+ }
+ // Pre-Release -> Release = Massage target version to prerelease format
+ if targetVersion.IsRelease() && currentVersion.IsPreRelease() {
+ // We are ok with greater than or equal
+ mainversion := currentVersion.MainVersion()
+ targetVersion, err = github.NewSemanticVersion(targetVersion.String())
+ if err != nil {
+ return err
+ }
+ success, _ = targetVersion.IsGreaterThanOrEqual(mainversion)
+ }
+
+ // Release -> Release = Standard check
+ if (targetVersion.IsRelease() && currentVersion.IsRelease()) ||
+ (targetVersion.IsPreRelease() && currentVersion.IsPreRelease()) {
+
+ success, _ = targetVersion.IsGreaterThan(currentVersion)
+ }
+
+ // Compare
+ if !success {
+ pterm.Println("Error: The requested version is lower than the current version.")
+ pterm.Println(fmt.Sprintf("If this is what you really want to do, use `wails update -version "+"%s`", targetVersionString))
+
+ return nil
+ }
+
+ desiredVersion = "v" + targetVersion.String()
+
+ } else {
+ desiredVersion = "v" + targetVersion.String()
+ }
+
+ pterm.Println()
+ pterm.Print("Installing Wails CLI " + desiredVersion + "...")
+
+ // Run command in non module directory
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ fatal("Cannot find home directory! Please file a bug report!")
+ }
+
+ sout, serr, err := shell.RunCommand(homeDir, "go", "install", "github.com/wailsapp/wails/v2/cmd/wails@"+desiredVersion)
+ if err != nil {
+ pterm.Println("Failed.")
+ pterm.Error.Println(sout + `\n` + serr)
+ return err
+ }
+ pterm.Println("Done.")
+ pterm.Println(color.Green("\nMake sure you update your project go.mod file to use " + desiredVersion + ":"))
+ pterm.Println(color.Green(" require github.com/wailsapp/wails/v2 " + desiredVersion))
+ pterm.Println(color.Red("\nTo view the release notes, please run `wails show releasenotes`"))
+
+ return nil
+}
diff --git a/v2/examples/customlayout/.gitignore b/v2/examples/customlayout/.gitignore
new file mode 100644
index 000000000..53e9ed8b5
--- /dev/null
+++ b/v2/examples/customlayout/.gitignore
@@ -0,0 +1,3 @@
+build/bin
+node_modules
+myfrontend/wailsjs
\ No newline at end of file
diff --git a/v2/examples/customlayout/README.md b/v2/examples/customlayout/README.md
new file mode 100644
index 000000000..e4d79d4ec
--- /dev/null
+++ b/v2/examples/customlayout/README.md
@@ -0,0 +1,4 @@
+# README
+
+This is an example project that shows how to use a custom layout.
+Run `wails build` in the `cmd/customlayout` directory to build the project.
\ No newline at end of file
diff --git a/v2/examples/customlayout/build/README.md b/v2/examples/customlayout/build/README.md
new file mode 100644
index 000000000..3018a06c4
--- /dev/null
+++ b/v2/examples/customlayout/build/README.md
@@ -0,0 +1,35 @@
+# Build Directory
+
+The build directory is used to house all the build files and assets for your application.
+
+The structure is:
+
+* bin - Output directory
+* darwin - macOS specific files
+* windows - Windows specific files
+
+## Mac
+
+The `darwin` directory holds files specific to Mac builds.
+These may be customised and used as part of the build. To return these files to the default state, simply delete them
+and
+build with `wails build`.
+
+The directory contains the following files:
+
+- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
+- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
+
+## Windows
+
+The `windows` directory contains the manifest and rc files used when building with `wails build`.
+These may be customised for your application. To return these files to the default state, simply delete them and
+build with `wails build`.
+
+- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
+ use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
+ will be created using the `appicon.png` file in the build directory.
+- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
+- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
+ as well as the application itself (right click the exe -> properties -> details)
+- `wails.exe.manifest` - The main application manifest file.
\ No newline at end of file
diff --git a/v2/examples/customlayout/build/appicon.png b/v2/examples/customlayout/build/appicon.png
new file mode 100644
index 000000000..63617fe4f
Binary files /dev/null and b/v2/examples/customlayout/build/appicon.png differ
diff --git a/v2/examples/customlayout/build/darwin/Info.dev.plist b/v2/examples/customlayout/build/darwin/Info.dev.plist
new file mode 100644
index 000000000..02e7358ee
--- /dev/null
+++ b/v2/examples/customlayout/build/darwin/Info.dev.plist
@@ -0,0 +1,32 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.Name}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
+
+
\ No newline at end of file
diff --git a/v2/examples/customlayout/build/darwin/Info.plist b/v2/examples/customlayout/build/darwin/Info.plist
new file mode 100644
index 000000000..e7819a7e8
--- /dev/null
+++ b/v2/examples/customlayout/build/darwin/Info.plist
@@ -0,0 +1,27 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.Name}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+
+
\ No newline at end of file
diff --git a/v2/examples/customlayout/build/windows/icon.ico b/v2/examples/customlayout/build/windows/icon.ico
new file mode 100644
index 000000000..f33479841
Binary files /dev/null and b/v2/examples/customlayout/build/windows/icon.ico differ
diff --git a/v2/examples/customlayout/build/windows/info.json b/v2/examples/customlayout/build/windows/info.json
new file mode 100644
index 000000000..c23c173c9
--- /dev/null
+++ b/v2/examples/customlayout/build/windows/info.json
@@ -0,0 +1,15 @@
+{
+ "fixed": {
+ "file_version": "{{.Info.ProductVersion}}"
+ },
+ "info": {
+ "0000": {
+ "ProductVersion": "{{.Info.ProductVersion}}",
+ "CompanyName": "{{.Info.CompanyName}}",
+ "FileDescription": "{{.Info.ProductName}}",
+ "LegalCopyright": "{{.Info.Copyright}}",
+ "ProductName": "{{.Info.ProductName}}",
+ "Comments": "{{.Info.Comments}}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/v2/examples/customlayout/build/windows/installer/project.nsi b/v2/examples/customlayout/build/windows/installer/project.nsi
new file mode 100644
index 000000000..2ccc0f3f3
--- /dev/null
+++ b/v2/examples/customlayout/build/windows/installer/project.nsi
@@ -0,0 +1,104 @@
+Unicode true
+
+####
+## Please note: Template replacements don't work in this file. They are provided with default defines like
+## mentioned underneath.
+## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
+## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
+## from outside of Wails for debugging and development of the installer.
+##
+## For development first make a wails nsis build to populate the "wails_tools.nsh":
+## > wails build --target windows/amd64 --nsis
+## Then you can call makensis on this file with specifying the path to your binary:
+## For a AMD64 only installer:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
+## For a ARM64 only installer:
+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
+## For a installer with both architectures:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
+####
+## The following information is taken from the ProjectInfo file, but they can be overwritten here.
+####
+## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
+## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
+## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
+## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
+## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
+###
+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+####
+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
+####
+## Include the wails tools
+####
+!include "wails_tools.nsh"
+
+# The version information for this two must consist of 4 parts
+VIProductVersion "${INFO_PRODUCTVERSION}.0"
+VIFileVersion "${INFO_PRODUCTVERSION}.0"
+
+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
+
+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
+ManifestDPIAware true
+
+!include "MUI.nsh"
+
+!define MUI_ICON "..\icon.ico"
+!define MUI_UNICON "..\icon.ico"
+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
+
+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
+!insertmacro MUI_PAGE_INSTFILES # Installing page.
+!insertmacro MUI_PAGE_FINISH # Finished installation page.
+
+!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
+
+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
+
+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
+#!uninstfinalize 'signtool --file "%1"'
+#!finalize 'signtool --file "%1"'
+
+Name "${INFO_PRODUCTNAME}"
+OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
+InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
+ShowInstDetails show # This will always show the installation details.
+
+Function .onInit
+ !insertmacro wails.checkArchitecture
+FunctionEnd
+
+Section
+ !insertmacro wails.webview2runtime
+
+ SetOutPath $INSTDIR
+
+ !insertmacro wails.files
+
+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+
+ !insertmacro wails.writeUninstaller
+SectionEnd
+
+Section "uninstall"
+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
+
+ RMDir /r $INSTDIR
+
+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
+
+ !insertmacro wails.deleteUninstaller
+SectionEnd
diff --git a/v2/examples/customlayout/build/windows/installer/wails_tools.nsh b/v2/examples/customlayout/build/windows/installer/wails_tools.nsh
new file mode 100644
index 000000000..66dc209a3
--- /dev/null
+++ b/v2/examples/customlayout/build/windows/installer/wails_tools.nsh
@@ -0,0 +1,171 @@
+# DO NOT EDIT - Generated automatically by `wails build`
+
+!include "x64.nsh"
+!include "WinVer.nsh"
+!include "FileFunc.nsh"
+
+!ifndef INFO_PROJECTNAME
+ !define INFO_PROJECTNAME "{{.Name}}"
+!endif
+!ifndef INFO_COMPANYNAME
+ !define INFO_COMPANYNAME "{{.Info.CompanyName}}"
+!endif
+!ifndef INFO_PRODUCTNAME
+ !define INFO_PRODUCTNAME "{{.Info.ProductName}}"
+!endif
+!ifndef INFO_PRODUCTVERSION
+ !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
+!endif
+!ifndef INFO_COPYRIGHT
+ !define INFO_COPYRIGHT "{{.Info.Copyright}}"
+!endif
+!ifndef PRODUCT_EXECUTABLE
+ !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
+!endif
+!ifndef UNINST_KEY_NAME
+ !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+!endif
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
+
+!ifndef REQUEST_EXECUTION_LEVEL
+ !define REQUEST_EXECUTION_LEVEL "admin"
+!endif
+
+RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
+
+!ifdef ARG_WAILS_AMD64_BINARY
+ !define SUPPORTS_AMD64
+!endif
+
+!ifdef ARG_WAILS_ARM64_BINARY
+ !define SUPPORTS_ARM64
+!endif
+
+!ifdef SUPPORTS_AMD64
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "amd64_arm64"
+ !else
+ !define ARCH "amd64"
+ !endif
+!else
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "arm64"
+ !else
+ !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
+ !endif
+!endif
+
+!macro wails.checkArchitecture
+ !ifndef WAILS_WIN10_REQUIRED
+ !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
+ !endif
+
+ !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
+ !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
+ !endif
+
+ ${If} ${AtLeastWin10}
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ IfSilent silentArch notSilentArch
+ silentArch:
+ SetErrorLevel 65
+ Abort
+ notSilentArch:
+ MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
+ Quit
+ ${else}
+ IfSilent silentWin notSilentWin
+ silentWin:
+ SetErrorLevel 64
+ Abort
+ notSilentWin:
+ MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
+ Quit
+ ${EndIf}
+
+ ok:
+!macroend
+
+!macro wails.files
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
+ ${EndIf}
+ !endif
+!macroend
+
+!macro wails.writeUninstaller
+ WriteUninstaller "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+ WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
+
+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
+ IntFmt $0 "0x%08X" $0
+ WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
+!macroend
+
+!macro wails.deleteUninstaller
+ Delete "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ DeleteRegKey HKLM "${UNINST_KEY}"
+!macroend
+
+# Install webview2 by launching the bootstrapper
+# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
+!macro wails.webview2runtime
+ !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
+ !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
+ !endif
+
+ SetRegView 64
+ # If the admin key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
+ # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+ ${EndIf}
+
+ SetDetailsPrint both
+ DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
+ SetDetailsPrint listonly
+
+ InitPluginsDir
+ CreateDirectory "$pluginsdir\webview2bootstrapper"
+ SetOutPath "$pluginsdir\webview2bootstrapper"
+ File "tmp\MicrosoftEdgeWebview2Setup.exe"
+ ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
+
+ SetDetailsPrint both
+ ok:
+!macroend
\ No newline at end of file
diff --git a/cmd/packages/windows/wails.exe.manifest b/v2/examples/customlayout/build/windows/wails.exe.manifest
similarity index 61%
rename from cmd/packages/windows/wails.exe.manifest
rename to v2/examples/customlayout/build/windows/wails.exe.manifest
index b236d268f..17e1a2387 100644
--- a/cmd/packages/windows/wails.exe.manifest
+++ b/v2/examples/customlayout/build/windows/wails.exe.manifest
@@ -1,12 +1,15 @@
-
-
+
+
+
+
+
+ true/pmpermonitorv2,permonitor
- true
\ No newline at end of file
diff --git a/v2/examples/customlayout/cmd/customlayout/app.go b/v2/examples/customlayout/cmd/customlayout/app.go
new file mode 100644
index 000000000..af53038a1
--- /dev/null
+++ b/v2/examples/customlayout/cmd/customlayout/app.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "context"
+ "fmt"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called when the app starts. The context is saved
+// so we can call the runtime methods
+func (a *App) startup(ctx context.Context) {
+ a.ctx = ctx
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+ return fmt.Sprintf("Hello %s, It's show time!", name)
+}
diff --git a/v2/examples/customlayout/cmd/customlayout/main.go b/v2/examples/customlayout/cmd/customlayout/main.go
new file mode 100644
index 000000000..dcb59a80c
--- /dev/null
+++ b/v2/examples/customlayout/cmd/customlayout/main.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "changeme/myfrontend"
+ "github.com/wailsapp/wails/v2"
+ "github.com/wailsapp/wails/v2/pkg/options"
+)
+
+func main() {
+ // Create an instance of the app structure
+ app := NewApp()
+
+ // Create application with options
+ err := wails.Run(&options.App{
+ Title: "customlayout",
+ Width: 1024,
+ Height: 768,
+ Assets: myfrontend.Assets,
+ BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+ OnStartup: app.startup,
+ Bind: []interface{}{
+ app,
+ },
+ })
+
+ if err != nil {
+ println("Error:", err.Error())
+ }
+}
diff --git a/v2/examples/customlayout/cmd/customlayout/wails.json b/v2/examples/customlayout/cmd/customlayout/wails.json
new file mode 100644
index 000000000..e37c2ec7d
--- /dev/null
+++ b/v2/examples/customlayout/cmd/customlayout/wails.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://wails.io/schemas/config.v2.json",
+ "name": "customlayout",
+ "outputfilename": "customlayout",
+ "build:dir": "../../build",
+ "frontend:dir": "../../myfrontend",
+ "frontend:install": "npm install",
+ "frontend:build": "npm run build",
+ "frontend:dev:watcher": "npm run dev",
+ "frontend:dev:serverUrl": "auto",
+ "author": {
+ "name": "Lea Anthony",
+ "email": "lea.anthony@gmail.com"
+ }
+}
diff --git a/v2/examples/customlayout/go.mod b/v2/examples/customlayout/go.mod
new file mode 100644
index 000000000..e1a17304e
--- /dev/null
+++ b/v2/examples/customlayout/go.mod
@@ -0,0 +1,39 @@
+module changeme
+
+go 1.22.0
+
+toolchain go1.24.1
+
+require github.com/wailsapp/wails/v2 v2.1.0
+
+require (
+ github.com/bep/debounce v1.2.1 // indirect
+ github.com/go-ole/go-ole v1.3.0 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
+ github.com/labstack/echo/v4 v4.13.3 // indirect
+ github.com/labstack/gommon v0.4.2 // indirect
+ github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
+ github.com/leaanthony/gosod v1.0.4 // indirect
+ github.com/leaanthony/slicer v1.6.0 // indirect
+ github.com/leaanthony/u v1.1.1 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/samber/lo v1.49.1 // indirect
+ github.com/tkrajina/go-reflector v0.5.8 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasttemplate v1.2.2 // indirect
+ github.com/wailsapp/go-webview2 v1.0.22 // indirect
+ github.com/wailsapp/mimetype v1.4.1 // indirect
+ golang.org/x/crypto v0.33.0 // indirect
+ golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
+ golang.org/x/net v0.35.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+)
+
+replace github.com/wailsapp/wails/v2 v2.1.0 => ../..
diff --git a/v2/examples/customlayout/go.sum b/v2/examples/customlayout/go.sum
new file mode 100644
index 000000000..f1995affb
--- /dev/null
+++ b/v2/examples/customlayout/go.sum
@@ -0,0 +1,111 @@
+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
+github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
+github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
+github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
+github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
+github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
+github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
+github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
+github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
+github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
+github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
+github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
+github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
+github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
+github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
+github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
+github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
+github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
+github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
+github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
+github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
+github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w=
+github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
+github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
+github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
+github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
+github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/v2/examples/customlayout/myfrontend/assets.go b/v2/examples/customlayout/myfrontend/assets.go
new file mode 100644
index 000000000..a6dec2f8f
--- /dev/null
+++ b/v2/examples/customlayout/myfrontend/assets.go
@@ -0,0 +1,6 @@
+package myfrontend
+
+import "embed"
+
+//go:embed all:dist
+var Assets embed.FS
diff --git a/v2/examples/customlayout/myfrontend/index.html b/v2/examples/customlayout/myfrontend/index.html
new file mode 100644
index 000000000..1ceda7392
--- /dev/null
+++ b/v2/examples/customlayout/myfrontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ customlayout
+
+
+
+
+
+
diff --git a/v2/examples/customlayout/myfrontend/package.json b/v2/examples/customlayout/myfrontend/package.json
new file mode 100644
index 000000000..a1b6f8e1a
--- /dev/null
+++ b/v2/examples/customlayout/myfrontend/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^3.0.7"
+ }
+}
\ No newline at end of file
diff --git a/v2/examples/customlayout/myfrontend/src/app.css b/v2/examples/customlayout/myfrontend/src/app.css
new file mode 100644
index 000000000..59d06f692
--- /dev/null
+++ b/v2/examples/customlayout/myfrontend/src/app.css
@@ -0,0 +1,54 @@
+#logo {
+ display: block;
+ width: 50%;
+ height: 50%;
+ margin: auto;
+ padding: 10% 0 0;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ background-origin: content-box;
+}
+
+.result {
+ height: 20px;
+ line-height: 20px;
+ margin: 1.5rem auto;
+}
+
+.input-box .btn {
+ width: 60px;
+ height: 30px;
+ line-height: 30px;
+ border-radius: 3px;
+ border: none;
+ margin: 0 0 0 20px;
+ padding: 0 8px;
+ cursor: pointer;
+}
+
+.input-box .btn:hover {
+ background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
+ color: #333333;
+}
+
+.input-box .input {
+ border: none;
+ border-radius: 3px;
+ outline: none;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 10px;
+ background-color: rgba(240, 240, 240, 1);
+ -webkit-font-smoothing: antialiased;
+}
+
+.input-box .input:hover {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+}
+
+.input-box .input:focus {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+}
\ No newline at end of file
diff --git a/v2/examples/customlayout/myfrontend/src/assets/fonts/OFL.txt b/v2/examples/customlayout/myfrontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/examples/customlayout/myfrontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/examples/customlayout/myfrontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/examples/customlayout/myfrontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/examples/customlayout/myfrontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/examples/customlayout/myfrontend/src/assets/images/logo-universal.png b/v2/examples/customlayout/myfrontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..d63303bfa
Binary files /dev/null and b/v2/examples/customlayout/myfrontend/src/assets/images/logo-universal.png differ
diff --git a/v2/examples/customlayout/myfrontend/src/main.js b/v2/examples/customlayout/myfrontend/src/main.js
new file mode 100644
index 000000000..6cb4ad78d
--- /dev/null
+++ b/v2/examples/customlayout/myfrontend/src/main.js
@@ -0,0 +1,48 @@
+import './style.css';
+import './app.css';
+
+import logo from './assets/images/logo-universal.png';
+import { Greet } from '../wailsjs/go/main/App';
+
+document.querySelector('#app').innerHTML = `
+
+
Please enter your name below 👇
+
+
+
+
+
+`;
+document.getElementById('logo').src = logo;
+document.addEventListener("keydown", (e) => {
+ if (e.code === "Enter") {
+ window.greet();
+ }
+});
+
+let nameElement = document.getElementById("name");
+nameElement.focus();
+let resultElement = document.getElementById("result");
+
+// Setup the greet function
+window.greet = function () {
+ // Get name
+ let name = nameElement.value;
+
+ // Check if the input is empty
+ if (name === "") return;
+
+ // Call App.Greet(name)
+ try {
+ Greet(name)
+ .then((result) => {
+ // Update result with data back from App.Greet()
+ resultElement.innerText = result;
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ } catch (err) {
+ console.error(err);
+ }
+};
diff --git a/v2/examples/customlayout/myfrontend/src/style.css b/v2/examples/customlayout/myfrontend/src/style.css
new file mode 100644
index 000000000..3940d6c63
--- /dev/null
+++ b/v2/examples/customlayout/myfrontend/src/style.css
@@ -0,0 +1,26 @@
+html {
+ background-color: rgba(27, 38, 54, 1);
+ text-align: center;
+ color: white;
+}
+
+body {
+ margin: 0;
+ color: white;
+ font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+}
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
+}
+
+#app {
+ height: 100vh;
+ text-align: center;
+}
diff --git a/v2/examples/dragdrop-test/.gitignore b/v2/examples/dragdrop-test/.gitignore
new file mode 100644
index 000000000..a11bbf414
--- /dev/null
+++ b/v2/examples/dragdrop-test/.gitignore
@@ -0,0 +1,4 @@
+build/bin
+node_modules
+frontend/dist
+frontend/wailsjs
diff --git a/v2/examples/dragdrop-test/README.md b/v2/examples/dragdrop-test/README.md
new file mode 100644
index 000000000..397b08b92
--- /dev/null
+++ b/v2/examples/dragdrop-test/README.md
@@ -0,0 +1,19 @@
+# README
+
+## About
+
+This is the official Wails Vanilla template.
+
+You can configure the project by editing `wails.json`. More information about the project settings can be found
+here: https://wails.io/docs/reference/project-config
+
+## Live Development
+
+To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
+server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
+and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
+to this in your browser, and you can call your Go code from devtools.
+
+## Building
+
+To build a redistributable, production mode package, use `wails build`.
diff --git a/v2/examples/dragdrop-test/app.go b/v2/examples/dragdrop-test/app.go
new file mode 100644
index 000000000..af53038a1
--- /dev/null
+++ b/v2/examples/dragdrop-test/app.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "context"
+ "fmt"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called when the app starts. The context is saved
+// so we can call the runtime methods
+func (a *App) startup(ctx context.Context) {
+ a.ctx = ctx
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+ return fmt.Sprintf("Hello %s, It's show time!", name)
+}
diff --git a/v2/examples/dragdrop-test/build/README.md b/v2/examples/dragdrop-test/build/README.md
new file mode 100644
index 000000000..1ae2f677f
--- /dev/null
+++ b/v2/examples/dragdrop-test/build/README.md
@@ -0,0 +1,35 @@
+# Build Directory
+
+The build directory is used to house all the build files and assets for your application.
+
+The structure is:
+
+* bin - Output directory
+* darwin - macOS specific files
+* windows - Windows specific files
+
+## Mac
+
+The `darwin` directory holds files specific to Mac builds.
+These may be customised and used as part of the build. To return these files to the default state, simply delete them
+and
+build with `wails build`.
+
+The directory contains the following files:
+
+- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
+- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
+
+## Windows
+
+The `windows` directory contains the manifest and rc files used when building with `wails build`.
+These may be customised for your application. To return these files to the default state, simply delete them and
+build with `wails build`.
+
+- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
+ use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
+ will be created using the `appicon.png` file in the build directory.
+- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
+- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
+ as well as the application itself (right click the exe -> properties -> details)
+- `wails.exe.manifest` - The main application manifest file.
\ No newline at end of file
diff --git a/v2/examples/dragdrop-test/build/appicon.png b/v2/examples/dragdrop-test/build/appicon.png
new file mode 100644
index 000000000..63617fe4f
Binary files /dev/null and b/v2/examples/dragdrop-test/build/appicon.png differ
diff --git a/v2/examples/dragdrop-test/build/darwin/Info.dev.plist b/v2/examples/dragdrop-test/build/darwin/Info.dev.plist
new file mode 100644
index 000000000..14121ef7c
--- /dev/null
+++ b/v2/examples/dragdrop-test/build/darwin/Info.dev.plist
@@ -0,0 +1,68 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.OutputFilename}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.wails.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
+
+
diff --git a/v2/examples/dragdrop-test/build/darwin/Info.plist b/v2/examples/dragdrop-test/build/darwin/Info.plist
new file mode 100644
index 000000000..d17a7475c
--- /dev/null
+++ b/v2/examples/dragdrop-test/build/darwin/Info.plist
@@ -0,0 +1,63 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.OutputFilename}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.wails.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+
+
diff --git a/v2/examples/dragdrop-test/build/windows/icon.ico b/v2/examples/dragdrop-test/build/windows/icon.ico
new file mode 100644
index 000000000..f33479841
Binary files /dev/null and b/v2/examples/dragdrop-test/build/windows/icon.ico differ
diff --git a/v2/examples/dragdrop-test/build/windows/info.json b/v2/examples/dragdrop-test/build/windows/info.json
new file mode 100644
index 000000000..9727946b7
--- /dev/null
+++ b/v2/examples/dragdrop-test/build/windows/info.json
@@ -0,0 +1,15 @@
+{
+ "fixed": {
+ "file_version": "{{.Info.ProductVersion}}"
+ },
+ "info": {
+ "0000": {
+ "ProductVersion": "{{.Info.ProductVersion}}",
+ "CompanyName": "{{.Info.CompanyName}}",
+ "FileDescription": "{{.Info.ProductName}}",
+ "LegalCopyright": "{{.Info.Copyright}}",
+ "ProductName": "{{.Info.ProductName}}",
+ "Comments": "{{.Info.Comments}}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/v2/examples/dragdrop-test/build/windows/installer/project.nsi b/v2/examples/dragdrop-test/build/windows/installer/project.nsi
new file mode 100644
index 000000000..654ae2e49
--- /dev/null
+++ b/v2/examples/dragdrop-test/build/windows/installer/project.nsi
@@ -0,0 +1,114 @@
+Unicode true
+
+####
+## Please note: Template replacements don't work in this file. They are provided with default defines like
+## mentioned underneath.
+## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
+## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
+## from outside of Wails for debugging and development of the installer.
+##
+## For development first make a wails nsis build to populate the "wails_tools.nsh":
+## > wails build --target windows/amd64 --nsis
+## Then you can call makensis on this file with specifying the path to your binary:
+## For a AMD64 only installer:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
+## For a ARM64 only installer:
+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
+## For a installer with both architectures:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
+####
+## The following information is taken from the ProjectInfo file, but they can be overwritten here.
+####
+## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
+## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
+## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
+## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
+## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
+###
+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+####
+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
+####
+## Include the wails tools
+####
+!include "wails_tools.nsh"
+
+# The version information for this two must consist of 4 parts
+VIProductVersion "${INFO_PRODUCTVERSION}.0"
+VIFileVersion "${INFO_PRODUCTVERSION}.0"
+
+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
+
+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
+ManifestDPIAware true
+
+!include "MUI.nsh"
+
+!define MUI_ICON "..\icon.ico"
+!define MUI_UNICON "..\icon.ico"
+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
+
+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
+!insertmacro MUI_PAGE_INSTFILES # Installing page.
+!insertmacro MUI_PAGE_FINISH # Finished installation page.
+
+!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
+
+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
+
+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
+#!uninstfinalize 'signtool --file "%1"'
+#!finalize 'signtool --file "%1"'
+
+Name "${INFO_PRODUCTNAME}"
+OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
+InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
+ShowInstDetails show # This will always show the installation details.
+
+Function .onInit
+ !insertmacro wails.checkArchitecture
+FunctionEnd
+
+Section
+ !insertmacro wails.setShellContext
+
+ !insertmacro wails.webview2runtime
+
+ SetOutPath $INSTDIR
+
+ !insertmacro wails.files
+
+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+
+ !insertmacro wails.associateFiles
+ !insertmacro wails.associateCustomProtocols
+
+ !insertmacro wails.writeUninstaller
+SectionEnd
+
+Section "uninstall"
+ !insertmacro wails.setShellContext
+
+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
+
+ RMDir /r $INSTDIR
+
+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
+
+ !insertmacro wails.unassociateFiles
+ !insertmacro wails.unassociateCustomProtocols
+
+ !insertmacro wails.deleteUninstaller
+SectionEnd
diff --git a/v2/examples/dragdrop-test/build/windows/installer/wails_tools.nsh b/v2/examples/dragdrop-test/build/windows/installer/wails_tools.nsh
new file mode 100644
index 000000000..f9c0f8852
--- /dev/null
+++ b/v2/examples/dragdrop-test/build/windows/installer/wails_tools.nsh
@@ -0,0 +1,249 @@
+# DO NOT EDIT - Generated automatically by `wails build`
+
+!include "x64.nsh"
+!include "WinVer.nsh"
+!include "FileFunc.nsh"
+
+!ifndef INFO_PROJECTNAME
+ !define INFO_PROJECTNAME "{{.Name}}"
+!endif
+!ifndef INFO_COMPANYNAME
+ !define INFO_COMPANYNAME "{{.Info.CompanyName}}"
+!endif
+!ifndef INFO_PRODUCTNAME
+ !define INFO_PRODUCTNAME "{{.Info.ProductName}}"
+!endif
+!ifndef INFO_PRODUCTVERSION
+ !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
+!endif
+!ifndef INFO_COPYRIGHT
+ !define INFO_COPYRIGHT "{{.Info.Copyright}}"
+!endif
+!ifndef PRODUCT_EXECUTABLE
+ !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
+!endif
+!ifndef UNINST_KEY_NAME
+ !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+!endif
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
+
+!ifndef REQUEST_EXECUTION_LEVEL
+ !define REQUEST_EXECUTION_LEVEL "admin"
+!endif
+
+RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
+
+!ifdef ARG_WAILS_AMD64_BINARY
+ !define SUPPORTS_AMD64
+!endif
+
+!ifdef ARG_WAILS_ARM64_BINARY
+ !define SUPPORTS_ARM64
+!endif
+
+!ifdef SUPPORTS_AMD64
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "amd64_arm64"
+ !else
+ !define ARCH "amd64"
+ !endif
+!else
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "arm64"
+ !else
+ !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
+ !endif
+!endif
+
+!macro wails.checkArchitecture
+ !ifndef WAILS_WIN10_REQUIRED
+ !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
+ !endif
+
+ !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
+ !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
+ !endif
+
+ ${If} ${AtLeastWin10}
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ IfSilent silentArch notSilentArch
+ silentArch:
+ SetErrorLevel 65
+ Abort
+ notSilentArch:
+ MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
+ Quit
+ ${else}
+ IfSilent silentWin notSilentWin
+ silentWin:
+ SetErrorLevel 64
+ Abort
+ notSilentWin:
+ MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
+ Quit
+ ${EndIf}
+
+ ok:
+!macroend
+
+!macro wails.files
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
+ ${EndIf}
+ !endif
+!macroend
+
+!macro wails.writeUninstaller
+ WriteUninstaller "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+ WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
+
+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
+ IntFmt $0 "0x%08X" $0
+ WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
+!macroend
+
+!macro wails.deleteUninstaller
+ Delete "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ DeleteRegKey HKLM "${UNINST_KEY}"
+!macroend
+
+!macro wails.setShellContext
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
+ SetShellVarContext all
+ ${else}
+ SetShellVarContext current
+ ${EndIf}
+!macroend
+
+# Install webview2 by launching the bootstrapper
+# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
+!macro wails.webview2runtime
+ !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
+ !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
+ !endif
+
+ SetRegView 64
+ # If the admin key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
+ # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+ ${EndIf}
+
+ SetDetailsPrint both
+ DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
+ SetDetailsPrint listonly
+
+ InitPluginsDir
+ CreateDirectory "$pluginsdir\webview2bootstrapper"
+ SetOutPath "$pluginsdir\webview2bootstrapper"
+ File "tmp\MicrosoftEdgeWebview2Setup.exe"
+ ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
+
+ SetDetailsPrint both
+ ok:
+!macroend
+
+# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
+!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
+!macroend
+
+!macro APP_UNASSOCIATE EXT FILECLASS
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
+
+ DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
+!macroend
+
+!macro wails.associateFiles
+ ; Create file associations
+ {{range .Info.FileAssociations}}
+ !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+ File "..\{{.IconName}}.ico"
+ {{end}}
+!macroend
+
+!macro wails.unassociateFiles
+ ; Delete app associations
+ {{range .Info.FileAssociations}}
+ !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
+
+ Delete "$INSTDIR\{{.IconName}}.ico"
+ {{end}}
+!macroend
+
+!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
+!macroend
+
+!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+!macroend
+
+!macro wails.associateCustomProtocols
+ ; Create custom protocols associations
+ {{range .Info.Protocols}}
+ !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+ {{end}}
+!macroend
+
+!macro wails.unassociateCustomProtocols
+ ; Delete app custom protocol associations
+ {{range .Info.Protocols}}
+ !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
+ {{end}}
+!macroend
diff --git a/v2/examples/dragdrop-test/build/windows/wails.exe.manifest b/v2/examples/dragdrop-test/build/windows/wails.exe.manifest
new file mode 100644
index 000000000..17e1a2387
--- /dev/null
+++ b/v2/examples/dragdrop-test/build/windows/wails.exe.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
\ No newline at end of file
diff --git a/v2/examples/dragdrop-test/frontend/index.html b/v2/examples/dragdrop-test/frontend/index.html
new file mode 100644
index 000000000..4010f1be6
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ dragdrop-test
+
+
+
+
+
+
diff --git a/v2/examples/dragdrop-test/frontend/package-lock.json b/v2/examples/dragdrop-test/frontend/package-lock.json
new file mode 100644
index 000000000..8eed5313c
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/package-lock.json
@@ -0,0 +1,653 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "devDependencies": {
+ "vite": "^3.0.7"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
+ "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
+ "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz",
+ "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.15.18",
+ "@esbuild/linux-loong64": "0.15.18",
+ "esbuild-android-64": "0.15.18",
+ "esbuild-android-arm64": "0.15.18",
+ "esbuild-darwin-64": "0.15.18",
+ "esbuild-darwin-arm64": "0.15.18",
+ "esbuild-freebsd-64": "0.15.18",
+ "esbuild-freebsd-arm64": "0.15.18",
+ "esbuild-linux-32": "0.15.18",
+ "esbuild-linux-64": "0.15.18",
+ "esbuild-linux-arm": "0.15.18",
+ "esbuild-linux-arm64": "0.15.18",
+ "esbuild-linux-mips64le": "0.15.18",
+ "esbuild-linux-ppc64le": "0.15.18",
+ "esbuild-linux-riscv64": "0.15.18",
+ "esbuild-linux-s390x": "0.15.18",
+ "esbuild-netbsd-64": "0.15.18",
+ "esbuild-openbsd-64": "0.15.18",
+ "esbuild-sunos-64": "0.15.18",
+ "esbuild-windows-32": "0.15.18",
+ "esbuild-windows-64": "0.15.18",
+ "esbuild-windows-arm64": "0.15.18"
+ }
+ },
+ "node_modules/esbuild-android-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
+ "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-android-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
+ "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-darwin-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
+ "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-darwin-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
+ "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-freebsd-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
+ "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-freebsd-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
+ "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-32": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
+ "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
+ "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-arm": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
+ "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
+ "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-mips64le": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
+ "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-ppc64le": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
+ "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-riscv64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
+ "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-s390x": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
+ "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-netbsd-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
+ "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-openbsd-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
+ "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-sunos-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
+ "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-32": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
+ "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
+ "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
+ "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "2.79.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
+ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/vite": {
+ "version": "3.2.11",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz",
+ "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.15.9",
+ "postcss": "^8.4.18",
+ "resolve": "^1.22.1",
+ "rollup": "^2.79.1"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/v2/examples/dragdrop-test/frontend/package.json b/v2/examples/dragdrop-test/frontend/package.json
new file mode 100644
index 000000000..a1b6f8e1a
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^3.0.7"
+ }
+}
\ No newline at end of file
diff --git a/v2/examples/dragdrop-test/frontend/src/app.css b/v2/examples/dragdrop-test/frontend/src/app.css
new file mode 100644
index 000000000..1d3b595bc
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/src/app.css
@@ -0,0 +1,229 @@
+/* #app styles are in style.css to avoid conflicts */
+
+.compact-container {
+ display: flex;
+ gap: 15px;
+ margin: 15px 0;
+ justify-content: center;
+ align-items: flex-start;
+}
+
+.drag-source {
+ background: white;
+ border: 2px solid #5c6bc0;
+ padding: 12px;
+ min-width: 140px;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.drag-source h4 {
+ color: #3949ab;
+ margin: 0 0 8px 0;
+ font-size: 14px;
+}
+
+.draggable {
+ background: #f5f5f5;
+ color: #1a1a1a;
+ padding: 8px;
+ margin: 6px 0;
+ border-radius: 4px;
+ cursor: move;
+ text-align: center;
+ transition: all 0.3s ease;
+ font-weight: 600;
+ font-size: 14px;
+ border: 2px solid #c5cae9;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.draggable:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ background: #e8eaf6;
+ border-color: #7986cb;
+}
+
+.draggable.dragging {
+ opacity: 0.5;
+ transform: scale(0.95);
+ background: #c5cae9;
+}
+
+.drop-zone {
+ background: #f8f9fa;
+ border: 2px dashed #9e9e9e;
+ padding: 12px;
+ min-width: 180px;
+ min-height: 120px;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+}
+
+.drop-zone h4 {
+ color: #5c6bc0;
+ margin: 0 0 8px 0;
+ font-size: 14px;
+}
+
+.drop-zone.drag-over {
+ background: #e3f2fd;
+ border-color: #2196F3;
+ transform: scale(1.02);
+ box-shadow: 0 4px 12px rgba(33, 150, 243, 0.2);
+}
+
+.file-drop-zone {
+ background: #fff8e1;
+ border: 2px dashed #ffc107;
+ padding: 12px;
+ min-width: 180px;
+ min-height: 120px;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+}
+
+.file-drop-zone h4 {
+ color: #f57c00;
+ margin: 0 0 8px 0;
+ font-size: 14px;
+}
+
+.file-drop-zone.drag-over {
+ background: #fff3cd;
+ border-color: #ff9800;
+ transform: scale(1.02);
+ box-shadow: 0 4px 12px rgba(255, 152, 0, 0.2);
+}
+
+.dropped-item {
+ background: linear-gradient(135deg, #42a5f5 0%, #66bb6a 100%);
+ color: white;
+ padding: 6px 8px;
+ margin: 4px 2px;
+ border-radius: 4px;
+ text-align: center;
+ animation: slideIn 0.3s ease;
+ display: inline-block;
+ font-weight: 500;
+ font-size: 13px;
+}
+
+.dropped-file {
+ background: #fff;
+ border: 2px solid #ff9800;
+ color: #333;
+ padding: 6px 8px;
+ margin: 4px 0;
+ border-radius: 4px;
+ text-align: left;
+ animation: slideIn 0.3s ease;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ font-size: 13px;
+}
+
+#dropMessage, #fileDropMessage {
+ font-size: 12px;
+ color: #666;
+ margin: 4px 0;
+}
+
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.status {
+ margin: 15px auto;
+ max-width: 1000px;
+ padding: 12px;
+ background: #2c3e50;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.status h4 {
+ color: white;
+ margin: 0 0 8px 0;
+ font-size: 14px;
+}
+
+#eventLog {
+ background: #1a1a1a;
+ padding: 10px;
+ border-radius: 4px;
+ max-height: 200px;
+ overflow-y: auto;
+ font-family: 'Courier New', monospace;
+ text-align: left;
+ font-size: 12px;
+}
+
+.log-entry {
+ padding: 4px 8px;
+ font-size: 13px;
+ margin: 2px 0;
+ border-radius: 3px;
+}
+
+.log-entry.drag-start {
+ color: #81c784;
+ background: rgba(129, 199, 132, 0.1);
+}
+
+.log-entry.drag-over {
+ color: #64b5f6;
+ background: rgba(100, 181, 246, 0.1);
+}
+
+.log-entry.drag-enter {
+ color: #ffb74d;
+ background: rgba(255, 183, 77, 0.1);
+}
+
+.log-entry.drag-leave {
+ color: #ba68c8;
+ background: rgba(186, 104, 200, 0.1);
+}
+
+.log-entry.drop {
+ color: #e57373;
+ background: rgba(229, 115, 115, 0.1);
+ font-weight: bold;
+}
+
+.log-entry.drag-end {
+ color: #90a4ae;
+ background: rgba(144, 164, 174, 0.1);
+}
+
+.log-entry.file-drop {
+ color: #ffc107;
+ background: rgba(255, 193, 7, 0.1);
+ font-weight: bold;
+}
+
+.log-entry.page-loaded {
+ color: #4caf50;
+ background: rgba(76, 175, 80, 0.1);
+}
+
+.log-entry.wails-status {
+ color: #00bcd4;
+ background: rgba(0, 188, 212, 0.1);
+}
+
+h1 {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ font-size: 1.8em;
+ margin: 10px 0 8px 0;
+}
\ No newline at end of file
diff --git a/v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt b/v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/examples/dragdrop-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/examples/dragdrop-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/examples/dragdrop-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/examples/dragdrop-test/frontend/src/assets/images/logo-universal.png b/v2/examples/dragdrop-test/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..d63303bfa
Binary files /dev/null and b/v2/examples/dragdrop-test/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/examples/dragdrop-test/frontend/src/main.js b/v2/examples/dragdrop-test/frontend/src/main.js
new file mode 100644
index 000000000..60d76ac0f
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/src/main.js
@@ -0,0 +1,231 @@
+import './style.css';
+import './app.css';
+
+// CRITICAL: Register global handlers IMMEDIATELY to prevent file drops from opening new windows
+// This must be done as early as possible, before any other code runs
+(function() {
+ // Helper function to check if drag event contains files
+ function isFileDrop(e) {
+ return e.dataTransfer && e.dataTransfer.types &&
+ (e.dataTransfer.types.includes('Files') ||
+ Array.from(e.dataTransfer.types).includes('Files'));
+ }
+
+ // Global dragover handler - MUST prevent default for file drops
+ window.addEventListener('dragover', function(e) {
+ if (isFileDrop(e)) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'copy';
+ }
+ }, true); // Use capture phase to handle before any other handlers
+
+ // Global drop handler - MUST prevent default for file drops
+ window.addEventListener('drop', function(e) {
+ if (isFileDrop(e)) {
+ e.preventDefault();
+ console.log('Global handler prevented file drop navigation');
+ }
+ }, true); // Use capture phase to handle before any other handlers
+
+ // Global dragleave handler
+ window.addEventListener('dragleave', function(e) {
+ if (isFileDrop(e)) {
+ e.preventDefault();
+ }
+ }, true); // Use capture phase
+
+ console.log('Global file drop prevention handlers registered');
+})();
+
+document.querySelector('#app').innerHTML = `
+
Wails Drag & Drop Test
+
+
+
+
HTML5 Source
+
Item 1
+
Item 2
+
Item 3
+
+
+
+
HTML5 Drop
+
Drop here
+
+
+
+
File Drop
+
Drop files here
+
+
+
+
+
Event Log
+
+
+`;
+
+// Get all draggable items and drop zones
+const draggables = document.querySelectorAll('.draggable');
+const dropZone = document.getElementById('dropZone');
+const fileDropZone = document.getElementById('fileDropZone');
+const eventLog = document.getElementById('eventLog');
+const dropMessage = document.getElementById('dropMessage');
+const fileDropMessage = document.getElementById('fileDropMessage');
+
+let draggedItem = null;
+let eventCounter = 0;
+
+// Function to log events
+function logEvent(eventName, details = '') {
+ eventCounter++;
+ const timestamp = new Date().toLocaleTimeString();
+ const logEntry = document.createElement('div');
+ logEntry.className = `log-entry ${eventName.replace(' ', '-').toLowerCase()}`;
+ logEntry.textContent = `[${timestamp}] ${eventCounter}. ${eventName} ${details}`;
+ eventLog.insertBefore(logEntry, eventLog.firstChild);
+
+ // Keep only last 20 events
+ while (eventLog.children.length > 20) {
+ eventLog.removeChild(eventLog.lastChild);
+ }
+
+ console.log(`Event: ${eventName} ${details}`);
+}
+
+// Add event listeners to draggable items
+draggables.forEach(item => {
+ // Drag start
+ item.addEventListener('dragstart', (e) => {
+ draggedItem = e.target;
+ e.target.classList.add('dragging');
+ e.dataTransfer.effectAllowed = 'copy';
+ e.dataTransfer.setData('text/plain', e.target.dataset.item);
+ logEvent('drag-start', `- Started dragging: ${e.target.dataset.item}`);
+ });
+
+ // Drag end
+ item.addEventListener('dragend', (e) => {
+ e.target.classList.remove('dragging');
+ logEvent('drag-end', `- Ended dragging: ${e.target.dataset.item}`);
+ });
+});
+
+// Add event listeners to HTML drop zone
+dropZone.addEventListener('dragenter', (e) => {
+ e.preventDefault();
+ dropZone.classList.add('drag-over');
+ logEvent('drag-enter', '- Entered HTML drop zone');
+});
+
+dropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'copy';
+ // Don't log every dragover to avoid spam
+});
+
+dropZone.addEventListener('dragleave', (e) => {
+ if (e.target === dropZone) {
+ dropZone.classList.remove('drag-over');
+ logEvent('drag-leave', '- Left HTML drop zone');
+ }
+});
+
+dropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ dropZone.classList.remove('drag-over');
+
+ const data = e.dataTransfer.getData('text/plain');
+ logEvent('drop', `- Dropped in HTML zone: ${data}`);
+
+ if (draggedItem) {
+ // Create a copy of the dragged item
+ const droppedElement = document.createElement('div');
+ droppedElement.className = 'dropped-item';
+ droppedElement.textContent = data;
+
+ // Remove the placeholder message if it exists
+ if (dropMessage) {
+ dropMessage.style.display = 'none';
+ }
+
+ dropZone.appendChild(droppedElement);
+ }
+
+ draggedItem = null;
+});
+
+// Add event listeners to file drop zone
+fileDropZone.addEventListener('dragenter', (e) => {
+ e.preventDefault();
+ fileDropZone.classList.add('drag-over');
+ logEvent('drag-enter', '- Entered file drop zone');
+});
+
+fileDropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'copy';
+});
+
+fileDropZone.addEventListener('dragleave', (e) => {
+ if (e.target === fileDropZone) {
+ fileDropZone.classList.remove('drag-over');
+ logEvent('drag-leave', '- Left file drop zone');
+ }
+});
+
+fileDropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ fileDropZone.classList.remove('drag-over');
+
+ const files = [...e.dataTransfer.files];
+ if (files.length > 0) {
+ logEvent('file-drop', `- Dropped ${files.length} file(s)`);
+
+ // Hide the placeholder message
+ if (fileDropMessage) {
+ fileDropMessage.style.display = 'none';
+ }
+
+ // Display dropped files
+ files.forEach(file => {
+ const fileElement = document.createElement('div');
+ fileElement.className = 'dropped-file';
+
+ // Format file size
+ let size = file.size;
+ let unit = 'bytes';
+ if (size > 1024 * 1024) {
+ size = (size / (1024 * 1024)).toFixed(2);
+ unit = 'MB';
+ } else if (size > 1024) {
+ size = (size / 1024).toFixed(2);
+ unit = 'KB';
+ }
+
+ fileElement.textContent = `📄 ${file.name} (${size} ${unit})`;
+ fileDropZone.appendChild(fileElement);
+ });
+ }
+});
+
+// Log when page loads
+window.addEventListener('DOMContentLoaded', () => {
+ logEvent('page-loaded', '- Wails drag-and-drop test page ready');
+ console.log('Wails Drag and Drop test application loaded');
+
+ // Check if Wails drag and drop is enabled
+ if (window.wails && window.wails.flags) {
+ logEvent('wails-status', `- Wails DnD enabled: ${window.wails.flags.enableWailsDragAndDrop}`);
+ }
+
+ // IMPORTANT: Register Wails drag-and-drop handlers to prevent browser navigation
+ // This will ensure external files don't open in new windows when dropped anywhere
+ if (window.runtime && window.runtime.OnFileDrop) {
+ window.runtime.OnFileDrop((x, y, paths) => {
+ logEvent('wails-file-drop', `- Wails received ${paths.length} file(s) at (${x}, ${y})`);
+ console.log('Wails OnFileDrop:', paths);
+ }, false); // false = don't require drop target, handle all file drops
+ logEvent('wails-setup', '- Wails OnFileDrop handlers registered');
+ }
+});
\ No newline at end of file
diff --git a/v2/examples/dragdrop-test/frontend/src/style.css b/v2/examples/dragdrop-test/frontend/src/style.css
new file mode 100644
index 000000000..f5d071597
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/src/style.css
@@ -0,0 +1,33 @@
+html {
+ background-color: rgba(27, 38, 54, 1);
+ text-align: center;
+ color: white;
+ height: 100%;
+ overflow: hidden;
+}
+
+body {
+ margin: 0;
+ color: white;
+ font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+ height: 100%;
+ overflow: hidden;
+}
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
+}
+
+#app {
+ height: 100vh;
+ text-align: center;
+ overflow: hidden;
+ box-sizing: border-box;
+ padding: 10px;
+}
diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts
new file mode 100644
index 000000000..02a3bb988
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts
@@ -0,0 +1,4 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Greet(arg1:string):Promise;
diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.js b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.js
new file mode 100644
index 000000000..c71ae77cb
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.js
@@ -0,0 +1,7 @@
+// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Greet(arg1) {
+ return window['go']['main']['App']['Greet'](arg1);
+}
diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/runtime/package.json b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/package.json
new file mode 100644
index 000000000..1e7c8a5d7
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@wailsapp/runtime",
+ "version": "2.0.0",
+ "description": "Wails Javascript runtime library",
+ "main": "runtime.js",
+ "types": "runtime.d.ts",
+ "scripts": {
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/wailsapp/wails.git"
+ },
+ "keywords": [
+ "Wails",
+ "Javascript",
+ "Go"
+ ],
+ "author": "Lea Anthony ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/wailsapp/wails/issues"
+ },
+ "homepage": "https://github.com/wailsapp/wails#readme"
+}
diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts
new file mode 100644
index 000000000..4445dac21
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts
@@ -0,0 +1,249 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export interface Size {
+ w: number;
+ h: number;
+}
+
+export interface Screen {
+ isCurrent: boolean;
+ isPrimary: boolean;
+ width : number
+ height : number
+}
+
+// Environment information such as platform, buildtype, ...
+export interface EnvironmentInfo {
+ buildType: string;
+ platform: string;
+ arch: string;
+}
+
+// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
+// emits the given event. Optional data may be passed with the event.
+// This will trigger any event listeners.
+export function EventsEmit(eventName: string, ...data: any): void;
+
+// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
+export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
+// sets up a listener for the given event name, but will only trigger a given number times.
+export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
+
+// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
+// sets up a listener for the given event name, but will only trigger once.
+export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
+// unregisters the listener for the given event name.
+export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
+
+// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
+// unregisters all listeners.
+export function EventsOffAll(): void;
+
+// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
+// logs the given message as a raw message
+export function LogPrint(message: string): void;
+
+// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
+// logs the given message at the `trace` log level.
+export function LogTrace(message: string): void;
+
+// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
+// logs the given message at the `debug` log level.
+export function LogDebug(message: string): void;
+
+// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
+// logs the given message at the `error` log level.
+export function LogError(message: string): void;
+
+// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
+// logs the given message at the `fatal` log level.
+// The application will quit after calling this method.
+export function LogFatal(message: string): void;
+
+// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
+// logs the given message at the `info` log level.
+export function LogInfo(message: string): void;
+
+// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
+// logs the given message at the `warning` log level.
+export function LogWarning(message: string): void;
+
+// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
+// Forces a reload by the main application as well as connected browsers.
+export function WindowReload(): void;
+
+// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
+// Reloads the application frontend.
+export function WindowReloadApp(): void;
+
+// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
+// Sets the window AlwaysOnTop or not on top.
+export function WindowSetAlwaysOnTop(b: boolean): void;
+
+// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
+// *Windows only*
+// Sets window theme to system default (dark/light).
+export function WindowSetSystemDefaultTheme(): void;
+
+// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
+// *Windows only*
+// Sets window to light theme.
+export function WindowSetLightTheme(): void;
+
+// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
+// *Windows only*
+// Sets window to dark theme.
+export function WindowSetDarkTheme(): void;
+
+// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
+// Centers the window on the monitor the window is currently on.
+export function WindowCenter(): void;
+
+// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
+// Sets the text in the window title bar.
+export function WindowSetTitle(title: string): void;
+
+// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
+// Makes the window full screen.
+export function WindowFullscreen(): void;
+
+// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
+// Restores the previous window dimensions and position prior to full screen.
+export function WindowUnfullscreen(): void;
+
+// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
+// Returns the state of the window, i.e. whether the window is in full screen mode or not.
+export function WindowIsFullscreen(): Promise;
+
+// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
+// Sets the width and height of the window.
+export function WindowSetSize(width: number, height: number): void;
+
+// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
+// Gets the width and height of the window.
+export function WindowGetSize(): Promise;
+
+// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
+// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMaxSize(width: number, height: number): void;
+
+// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
+// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMinSize(width: number, height: number): void;
+
+// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
+// Sets the window position relative to the monitor the window is currently on.
+export function WindowSetPosition(x: number, y: number): void;
+
+// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
+// Gets the window position relative to the monitor the window is currently on.
+export function WindowGetPosition(): Promise;
+
+// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
+// Hides the window.
+export function WindowHide(): void;
+
+// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
+// Shows the window, if it is currently hidden.
+export function WindowShow(): void;
+
+// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
+// Maximises the window to fill the screen.
+export function WindowMaximise(): void;
+
+// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
+// Toggles between Maximised and UnMaximised.
+export function WindowToggleMaximise(): void;
+
+// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
+// Restores the window to the dimensions and position prior to maximising.
+export function WindowUnmaximise(): void;
+
+// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
+// Returns the state of the window, i.e. whether the window is maximised or not.
+export function WindowIsMaximised(): Promise;
+
+// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
+// Minimises the window.
+export function WindowMinimise(): void;
+
+// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
+// Restores the window to the dimensions and position prior to minimising.
+export function WindowUnminimise(): void;
+
+// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
+// Returns the state of the window, i.e. whether the window is minimised or not.
+export function WindowIsMinimised(): Promise;
+
+// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
+// Returns the state of the window, i.e. whether the window is normal or not.
+export function WindowIsNormal(): Promise;
+
+// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
+// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
+export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
+
+// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
+// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
+export function ScreenGetAll(): Promise;
+
+// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
+// Opens the given URL in the system browser.
+export function BrowserOpenURL(url: string): void;
+
+// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
+// Returns information about the environment
+export function Environment(): Promise;
+
+// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
+// Quits the application.
+export function Quit(): void;
+
+// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
+// Hides the application.
+export function Hide(): void;
+
+// [Show](https://wails.io/docs/reference/runtime/intro#show)
+// Shows the application.
+export function Show(): void;
+
+// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
+// Returns the current text stored on clipboard
+export function ClipboardGetText(): Promise;
+
+// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
+// Sets a text on the clipboard
+export function ClipboardSetText(text: string): Promise;
+
+// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
+// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
+
+// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
+// OnFileDropOff removes the drag and drop listeners and handlers.
+export function OnFileDropOff() :void
+
+// Check if the file path resolver is available
+export function CanResolveFilePaths(): boolean;
+
+// Resolves file paths for an array of files
+export function ResolveFilePaths(files: File[]): void
\ No newline at end of file
diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js
new file mode 100644
index 000000000..7cb89d750
--- /dev/null
+++ b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js
@@ -0,0 +1,242 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export function LogPrint(message) {
+ window.runtime.LogPrint(message);
+}
+
+export function LogTrace(message) {
+ window.runtime.LogTrace(message);
+}
+
+export function LogDebug(message) {
+ window.runtime.LogDebug(message);
+}
+
+export function LogInfo(message) {
+ window.runtime.LogInfo(message);
+}
+
+export function LogWarning(message) {
+ window.runtime.LogWarning(message);
+}
+
+export function LogError(message) {
+ window.runtime.LogError(message);
+}
+
+export function LogFatal(message) {
+ window.runtime.LogFatal(message);
+}
+
+export function EventsOnMultiple(eventName, callback, maxCallbacks) {
+ return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
+}
+
+export function EventsOn(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, -1);
+}
+
+export function EventsOff(eventName, ...additionalEventNames) {
+ return window.runtime.EventsOff(eventName, ...additionalEventNames);
+}
+
+export function EventsOffAll() {
+ return window.runtime.EventsOffAll();
+}
+
+export function EventsOnce(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, 1);
+}
+
+export function EventsEmit(eventName) {
+ let args = [eventName].slice.call(arguments);
+ return window.runtime.EventsEmit.apply(null, args);
+}
+
+export function WindowReload() {
+ window.runtime.WindowReload();
+}
+
+export function WindowReloadApp() {
+ window.runtime.WindowReloadApp();
+}
+
+export function WindowSetAlwaysOnTop(b) {
+ window.runtime.WindowSetAlwaysOnTop(b);
+}
+
+export function WindowSetSystemDefaultTheme() {
+ window.runtime.WindowSetSystemDefaultTheme();
+}
+
+export function WindowSetLightTheme() {
+ window.runtime.WindowSetLightTheme();
+}
+
+export function WindowSetDarkTheme() {
+ window.runtime.WindowSetDarkTheme();
+}
+
+export function WindowCenter() {
+ window.runtime.WindowCenter();
+}
+
+export function WindowSetTitle(title) {
+ window.runtime.WindowSetTitle(title);
+}
+
+export function WindowFullscreen() {
+ window.runtime.WindowFullscreen();
+}
+
+export function WindowUnfullscreen() {
+ window.runtime.WindowUnfullscreen();
+}
+
+export function WindowIsFullscreen() {
+ return window.runtime.WindowIsFullscreen();
+}
+
+export function WindowGetSize() {
+ return window.runtime.WindowGetSize();
+}
+
+export function WindowSetSize(width, height) {
+ window.runtime.WindowSetSize(width, height);
+}
+
+export function WindowSetMaxSize(width, height) {
+ window.runtime.WindowSetMaxSize(width, height);
+}
+
+export function WindowSetMinSize(width, height) {
+ window.runtime.WindowSetMinSize(width, height);
+}
+
+export function WindowSetPosition(x, y) {
+ window.runtime.WindowSetPosition(x, y);
+}
+
+export function WindowGetPosition() {
+ return window.runtime.WindowGetPosition();
+}
+
+export function WindowHide() {
+ window.runtime.WindowHide();
+}
+
+export function WindowShow() {
+ window.runtime.WindowShow();
+}
+
+export function WindowMaximise() {
+ window.runtime.WindowMaximise();
+}
+
+export function WindowToggleMaximise() {
+ window.runtime.WindowToggleMaximise();
+}
+
+export function WindowUnmaximise() {
+ window.runtime.WindowUnmaximise();
+}
+
+export function WindowIsMaximised() {
+ return window.runtime.WindowIsMaximised();
+}
+
+export function WindowMinimise() {
+ window.runtime.WindowMinimise();
+}
+
+export function WindowUnminimise() {
+ window.runtime.WindowUnminimise();
+}
+
+export function WindowSetBackgroundColour(R, G, B, A) {
+ window.runtime.WindowSetBackgroundColour(R, G, B, A);
+}
+
+export function ScreenGetAll() {
+ return window.runtime.ScreenGetAll();
+}
+
+export function WindowIsMinimised() {
+ return window.runtime.WindowIsMinimised();
+}
+
+export function WindowIsNormal() {
+ return window.runtime.WindowIsNormal();
+}
+
+export function BrowserOpenURL(url) {
+ window.runtime.BrowserOpenURL(url);
+}
+
+export function Environment() {
+ return window.runtime.Environment();
+}
+
+export function Quit() {
+ window.runtime.Quit();
+}
+
+export function Hide() {
+ window.runtime.Hide();
+}
+
+export function Show() {
+ window.runtime.Show();
+}
+
+export function ClipboardGetText() {
+ return window.runtime.ClipboardGetText();
+}
+
+export function ClipboardSetText(text) {
+ return window.runtime.ClipboardSetText(text);
+}
+
+/**
+ * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ *
+ * @export
+ * @callback OnFileDropCallback
+ * @param {number} x - x coordinate of the drop
+ * @param {number} y - y coordinate of the drop
+ * @param {string[]} paths - A list of file paths.
+ */
+
+/**
+ * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+ *
+ * @export
+ * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
+ */
+export function OnFileDrop(callback, useDropTarget) {
+ return window.runtime.OnFileDrop(callback, useDropTarget);
+}
+
+/**
+ * OnFileDropOff removes the drag and drop listeners and handlers.
+ */
+export function OnFileDropOff() {
+ return window.runtime.OnFileDropOff();
+}
+
+export function CanResolveFilePaths() {
+ return window.runtime.CanResolveFilePaths();
+}
+
+export function ResolveFilePaths(files) {
+ return window.runtime.ResolveFilePaths(files);
+}
\ No newline at end of file
diff --git a/v2/examples/dragdrop-test/go.mod b/v2/examples/dragdrop-test/go.mod
new file mode 100644
index 000000000..be13aac19
--- /dev/null
+++ b/v2/examples/dragdrop-test/go.mod
@@ -0,0 +1,37 @@
+module dragdrop-test
+
+go 1.23
+
+require github.com/wailsapp/wails/v2 v2.10.1
+
+require (
+ github.com/bep/debounce v1.2.1 // indirect
+ github.com/go-ole/go-ole v1.3.0 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/websocket v1.5.3 // indirect
+ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
+ github.com/labstack/echo/v4 v4.13.3 // indirect
+ github.com/labstack/gommon v0.4.2 // indirect
+ github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
+ github.com/leaanthony/gosod v1.0.4 // indirect
+ github.com/leaanthony/slicer v1.6.0 // indirect
+ github.com/leaanthony/u v1.1.1 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/samber/lo v1.49.1 // indirect
+ github.com/tkrajina/go-reflector v0.5.8 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasttemplate v1.2.2 // indirect
+ github.com/wailsapp/go-webview2 v1.0.22 // indirect
+ github.com/wailsapp/mimetype v1.4.1 // indirect
+ golang.org/x/crypto v0.33.0 // indirect
+ golang.org/x/net v0.35.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+)
+
+replace github.com/wailsapp/wails/v2 => E:/releases/wails/v2
diff --git a/v2/examples/dragdrop-test/go.sum b/v2/examples/dragdrop-test/go.sum
new file mode 100644
index 000000000..10d4a9b18
--- /dev/null
+++ b/v2/examples/dragdrop-test/go.sum
@@ -0,0 +1,79 @@
+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
+github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
+github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
+github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
+github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
+github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
+github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
+github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
+github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
+github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
+github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
+github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
+github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
+github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
+github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/v2/examples/dragdrop-test/main.go b/v2/examples/dragdrop-test/main.go
new file mode 100644
index 000000000..64a0c2734
--- /dev/null
+++ b/v2/examples/dragdrop-test/main.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+ "embed"
+
+ "github.com/wailsapp/wails/v2"
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+//go:embed all:frontend/dist
+var assets embed.FS
+
+func main() {
+ // Create an instance of the app structure
+ app := NewApp()
+
+ // Create application with options
+ err := wails.Run(&options.App{
+ Title: "Wails Drag & Drop Test",
+ Width: 800,
+ Height: 600,
+ AssetServer: &assetserver.Options{
+ Assets: assets,
+ },
+ BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+ OnStartup: app.startup,
+ Bind: []interface{}{
+ app,
+ },
+ })
+
+ if err != nil {
+ println("Error:", err.Error())
+ }
+}
diff --git a/v2/examples/dragdrop-test/wails.json b/v2/examples/dragdrop-test/wails.json
new file mode 100644
index 000000000..7970ea4ca
--- /dev/null
+++ b/v2/examples/dragdrop-test/wails.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://wails.io/schemas/config.v2.json",
+ "name": "dragdrop-test",
+ "outputfilename": "dragdrop-test",
+ "frontend:install": "npm install",
+ "frontend:build": "npm run build",
+ "frontend:dev:watcher": "npm run dev",
+ "frontend:dev:serverUrl": "auto",
+ "author": {
+ "name": "Lea Anthony",
+ "email": "lea.anthony@gmail.com"
+ }
+}
diff --git a/v2/examples/panic-recovery-test/README.md b/v2/examples/panic-recovery-test/README.md
new file mode 100644
index 000000000..c0a6a7e5a
--- /dev/null
+++ b/v2/examples/panic-recovery-test/README.md
@@ -0,0 +1,76 @@
+# Panic Recovery Test
+
+This example demonstrates the Linux signal handler issue (#3965) and verifies the fix using `runtime.ResetSignalHandlers()`.
+
+## The Problem
+
+On Linux, WebKit installs signal handlers without the `SA_ONSTACK` flag, which prevents Go from recovering panics caused by nil pointer dereferences (SIGSEGV). Without the fix, the application crashes with:
+
+```
+signal 11 received but handler not on signal stack
+fatal error: non-Go code set up signal handler without SA_ONSTACK flag
+```
+
+## The Solution
+
+Call `runtime.ResetSignalHandlers()` immediately before code that might panic:
+
+```go
+import "github.com/wailsapp/wails/v2/pkg/runtime"
+
+go func() {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Printf("Recovered: %v", err)
+ }
+ }()
+ runtime.ResetSignalHandlers()
+ // Code that might panic...
+}()
+```
+
+## How to Reproduce
+
+### Prerequisites
+
+- Linux with WebKit2GTK 4.1 installed
+- Go 1.21+
+- Wails CLI
+
+### Steps
+
+1. Build the example:
+ ```bash
+ cd v2/examples/panic-recovery-test
+ wails build -tags webkit2_41
+ ```
+
+2. Run the application:
+ ```bash
+ ./build/bin/panic-recovery-test
+ ```
+
+3. Wait ~10 seconds (the app auto-calls `Greet` after 5s, then waits another 5s before the nil pointer dereference)
+
+### Expected Result (with fix)
+
+The panic is recovered and you see:
+```
+------------------------------"invalid memory address or nil pointer dereference"
+```
+
+The application continues running.
+
+### Without the fix
+
+Comment out the `runtime.ResetSignalHandlers()` call in `app.go` and rebuild. The application will crash with a fatal signal 11 error.
+
+## Files
+
+- `app.go` - Contains the `Greet` function that demonstrates panic recovery
+- `frontend/src/main.js` - Auto-calls `Greet` after 5 seconds to trigger the test
+
+## Related
+
+- Issue: https://github.com/wailsapp/wails/issues/3965
+- Original fix PR: https://github.com/wailsapp/wails/pull/2152
diff --git a/v2/examples/panic-recovery-test/app.go b/v2/examples/panic-recovery-test/app.go
new file mode 100644
index 000000000..ceb46e8d5
--- /dev/null
+++ b/v2/examples/panic-recovery-test/app.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called when the app starts. The context is saved
+// so we can call the runtime methods
+func (a *App) startup(ctx context.Context) {
+ a.ctx = ctx
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+ go func() {
+ defer func() {
+ if err := recover(); err != nil {
+ fmt.Printf("------------------------------%#v\n", err)
+ }
+ }()
+ time.Sleep(5 * time.Second)
+ // Fix signal handlers right before potential panic using the Wails runtime
+ runtime.ResetSignalHandlers()
+ // Nil pointer dereference - causes SIGSEGV
+ var t *time.Time
+ fmt.Println(t.Unix())
+ }()
+
+ return fmt.Sprintf("Hello %s, It's show time!", name)
+}
diff --git a/v2/examples/panic-recovery-test/frontend/index.html b/v2/examples/panic-recovery-test/frontend/index.html
new file mode 100644
index 000000000..d7aa4e942
--- /dev/null
+++ b/v2/examples/panic-recovery-test/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ panic-test
+
+
+
+
+
+
diff --git a/v2/examples/panic-recovery-test/frontend/package.json b/v2/examples/panic-recovery-test/frontend/package.json
new file mode 100644
index 000000000..a1b6f8e1a
--- /dev/null
+++ b/v2/examples/panic-recovery-test/frontend/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^3.0.7"
+ }
+}
\ No newline at end of file
diff --git a/v2/examples/panic-recovery-test/frontend/src/app.css b/v2/examples/panic-recovery-test/frontend/src/app.css
new file mode 100644
index 000000000..59d06f692
--- /dev/null
+++ b/v2/examples/panic-recovery-test/frontend/src/app.css
@@ -0,0 +1,54 @@
+#logo {
+ display: block;
+ width: 50%;
+ height: 50%;
+ margin: auto;
+ padding: 10% 0 0;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ background-origin: content-box;
+}
+
+.result {
+ height: 20px;
+ line-height: 20px;
+ margin: 1.5rem auto;
+}
+
+.input-box .btn {
+ width: 60px;
+ height: 30px;
+ line-height: 30px;
+ border-radius: 3px;
+ border: none;
+ margin: 0 0 0 20px;
+ padding: 0 8px;
+ cursor: pointer;
+}
+
+.input-box .btn:hover {
+ background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
+ color: #333333;
+}
+
+.input-box .input {
+ border: none;
+ border-radius: 3px;
+ outline: none;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 10px;
+ background-color: rgba(240, 240, 240, 1);
+ -webkit-font-smoothing: antialiased;
+}
+
+.input-box .input:hover {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+}
+
+.input-box .input:focus {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+}
\ No newline at end of file
diff --git a/v2/examples/panic-recovery-test/frontend/src/assets/fonts/OFL.txt b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/examples/panic-recovery-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/examples/panic-recovery-test/frontend/src/assets/images/logo-universal.png b/v2/examples/panic-recovery-test/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..d63303bfa
Binary files /dev/null and b/v2/examples/panic-recovery-test/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/examples/panic-recovery-test/frontend/src/main.js b/v2/examples/panic-recovery-test/frontend/src/main.js
new file mode 100644
index 000000000..ea5e74fc6
--- /dev/null
+++ b/v2/examples/panic-recovery-test/frontend/src/main.js
@@ -0,0 +1,55 @@
+import './style.css';
+import './app.css';
+
+import logo from './assets/images/logo-universal.png';
+import {Greet} from '../wailsjs/go/main/App';
+
+document.querySelector('#app').innerHTML = `
+
+
',ht(e,"class","wails-reconnect-overlay svelte-181h7z")},m(o,c){W(o,e,c),i=!0},i(o){i||($(()=>{n||(n=Y(e,et,{duration:300},!0)),n.run(1)}),i=!0)},o(o){n||(n=Y(e,et,{duration:300},!1)),n.run(0),i=!1},d(o){o&&S(e),o&&n&&n.end()}}}function te(t){let e,n,i=t[0]&&Mt(t);return{c(){i&&i.c(),e=dt()},m(o,c){i&&i.m(o,c),W(o,e,c),n=!0},p(o,[c]){o[0]?i?c&1&&I(i,1):(i=Mt(o),i.c(),I(i,1),i.m(e.parentNode,e)):i&&(gt(),Q(i,1,1,()=>{i=null}),bt())},i(o){n||(I(i),n=!0)},o(o){Q(i),n=!1},d(o){i&&i.d(o),o&&S(e)}}}function ee(t,e,n){let i;return st(t,q,o=>n(0,i=o)),[i]}var St=class extends tt{constructor(e){super();vt(this,e,ee,te,L,{},Yt)}},Ct=St;var ne={},nt=null,j=[];window.WailsInvoke=t=>{if(!nt){console.log("Queueing: "+t),j.push(t);return}nt(t)};window.addEventListener("DOMContentLoaded",()=>{ne.overlay=new Ct({target:document.body,anchor:document.querySelector("#wails-spinner")})});var d=null,kt;window.onbeforeunload=function(){d&&(d.onclose=function(){},d.close(),d=null)};It();function ie(){nt=t=>{d.send(t)};for(let t=0;t=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.12.tgz",
+ "integrity": "sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz",
+ "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==",
+ "dev": true
+ },
+ "node_modules/@types/chai-subset": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz",
+ "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==",
+ "dev": true,
+ "dependencies": {
+ "@types/chai": "*"
+ }
+ },
+ "node_modules/@types/concat-stream": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz",
+ "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/form-data": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
+ "integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "18.11.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.3.tgz",
+ "integrity": "sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A==",
+ "dev": true
+ },
+ "node_modules/@types/qs": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
+ "dev": true
+ },
+ "node_modules/acorn": {
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
+ "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "dev": true
+ },
+ "node_modules/assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
+ "dev": true
+ },
+ "node_modules/chai": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz",
+ "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==",
+ "dev": true,
+ "dependencies": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.2",
+ "deep-eql": "^3.0.1",
+ "get-func-name": "^2.0.0",
+ "loupe": "^2.3.1",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.0.5"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+ "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "node_modules/concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "dev": true,
+ "engines": [
+ "node >= 0.8"
+ ],
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
+ },
+ "node_modules/cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+ "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dev": true,
+ "dependencies": {
+ "object-keys": "^1.0.12"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.18.5",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz",
+ "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.3",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.3",
+ "is-string": "^1.0.6",
+ "object-inspect": "^1.11.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.12.tgz",
+ "integrity": "sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.15.12",
+ "@esbuild/linux-loong64": "0.15.12",
+ "esbuild-android-64": "0.15.12",
+ "esbuild-android-arm64": "0.15.12",
+ "esbuild-darwin-64": "0.15.12",
+ "esbuild-darwin-arm64": "0.15.12",
+ "esbuild-freebsd-64": "0.15.12",
+ "esbuild-freebsd-arm64": "0.15.12",
+ "esbuild-linux-32": "0.15.12",
+ "esbuild-linux-64": "0.15.12",
+ "esbuild-linux-arm": "0.15.12",
+ "esbuild-linux-arm64": "0.15.12",
+ "esbuild-linux-mips64le": "0.15.12",
+ "esbuild-linux-ppc64le": "0.15.12",
+ "esbuild-linux-riscv64": "0.15.12",
+ "esbuild-linux-s390x": "0.15.12",
+ "esbuild-netbsd-64": "0.15.12",
+ "esbuild-openbsd-64": "0.15.12",
+ "esbuild-sunos-64": "0.15.12",
+ "esbuild-windows-32": "0.15.12",
+ "esbuild-windows-64": "0.15.12",
+ "esbuild-windows-arm64": "0.15.12"
+ }
+ },
+ "node_modules/esbuild-android-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.12.tgz",
+ "integrity": "sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-android-arm64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.12.tgz",
+ "integrity": "sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-darwin-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.12.tgz",
+ "integrity": "sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-darwin-arm64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.12.tgz",
+ "integrity": "sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-freebsd-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.12.tgz",
+ "integrity": "sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-freebsd-arm64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.12.tgz",
+ "integrity": "sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-32": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.12.tgz",
+ "integrity": "sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.12.tgz",
+ "integrity": "sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-arm": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.12.tgz",
+ "integrity": "sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-arm64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.12.tgz",
+ "integrity": "sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-mips64le": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.12.tgz",
+ "integrity": "sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-ppc64le": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.12.tgz",
+ "integrity": "sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-riscv64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.12.tgz",
+ "integrity": "sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-s390x": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.12.tgz",
+ "integrity": "sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-netbsd-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.12.tgz",
+ "integrity": "sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-openbsd-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.12.tgz",
+ "integrity": "sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-sunos-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.12.tgz",
+ "integrity": "sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-32": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.12.tgz",
+ "integrity": "sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.12.tgz",
+ "integrity": "sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-arm64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.12.tgz",
+ "integrity": "sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
+ "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/get-func-name": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+ "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-port": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
+ "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
+ "dev": true
+ },
+ "node_modules/happy-dom": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-7.6.0.tgz",
+ "integrity": "sha512-QnNsiblZdyVDzW5ts6E7ub79JnabqHJeJgt+1WGNq9fSYqS/r/RzzTVXCZSDl6EVkipdwI48B4bgXAnMZPecIw==",
+ "dev": true,
+ "dependencies": {
+ "css.escape": "^1.5.1",
+ "he": "^1.2.0",
+ "node-fetch": "^2.x.x",
+ "sync-request": "^6.1.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
+ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true
+ },
+ "node_modules/http-basic": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
+ "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==",
+ "dev": true,
+ "dependencies": {
+ "caseless": "^0.12.0",
+ "concat-stream": "^1.6.2",
+ "http-response-object": "^3.0.1",
+ "parse-cache-control": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/http-response-object": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
+ "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "^10.0.3"
+ }
+ },
+ "node_modules/http-response-object/node_modules/@types/node": {
+ "version": "10.17.60",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
+ "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
+ "dev": true
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "dev": true
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz",
+ "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz",
+ "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
+ "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+ "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz",
+ "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
+ "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz",
+ "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz",
+ "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz",
+ "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "node_modules/json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true
+ },
+ "node_modules/load-json-file": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+ "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^4.0.0",
+ "pify": "^3.0.0",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/local-pkg": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz",
+ "integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
+ "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==",
+ "dev": true,
+ "dependencies": {
+ "get-func-name": "^2.0.0"
+ }
+ },
+ "node_modules/memorystream": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+ "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "node_modules/node-fetch": {
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/npm-run-all": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
+ "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "chalk": "^2.4.1",
+ "cross-spawn": "^6.0.5",
+ "memorystream": "^0.3.1",
+ "minimatch": "^3.0.4",
+ "pidtree": "^0.3.0",
+ "read-pkg": "^3.0.0",
+ "shell-quote": "^1.6.1",
+ "string.prototype.padend": "^3.0.0"
+ },
+ "bin": {
+ "npm-run-all": "bin/npm-run-all/index.js",
+ "run-p": "bin/run-p/index.js",
+ "run-s": "bin/run-s/index.js"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz",
+ "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/parse-cache-control": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
+ "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==",
+ "dev": true
+ },
+ "node_modules/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+ "dev": true,
+ "dependencies": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-type": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+ "dev": true,
+ "dependencies": {
+ "pify": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pathval": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "node_modules/pidtree": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
+ "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
+ "dev": true,
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
+ "node_modules/promise": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-8.2.0.tgz",
+ "integrity": "sha512-+CMAlLHqwRYwBMXKCP+o8ns7DN+xHDUiI+0nArsiJ9y+kJVPLFxEaSw6Ha9s9H0tftxg2Yzl25wqj9G7m5wLZg==",
+ "dev": true,
+ "dependencies": {
+ "asap": "~2.0.6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "dev": true,
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+ "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=",
+ "dev": true,
+ "dependencies": {
+ "load-json-file": "^4.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "dev": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+ "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "2.79.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
+ "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
+ "dev": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
+ "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
+ "dev": true
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
+ "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
+ "dev": true,
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+ "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+ "dev": true
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz",
+ "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==",
+ "dev": true
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string.prototype.padend": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz",
+ "integrity": "sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.18.0-next.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+ "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+ "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-0.4.2.tgz",
+ "integrity": "sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.8.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/svelte": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.49.0.tgz",
+ "integrity": "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/sync-request": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz",
+ "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==",
+ "dev": true,
+ "dependencies": {
+ "http-response-object": "^3.0.1",
+ "sync-rpc": "^1.2.1",
+ "then-request": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/sync-rpc": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz",
+ "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==",
+ "dev": true,
+ "dependencies": {
+ "get-port": "^3.1.0"
+ }
+ },
+ "node_modules/then-request": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz",
+ "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==",
+ "dev": true,
+ "dependencies": {
+ "@types/concat-stream": "^1.6.0",
+ "@types/form-data": "0.0.33",
+ "@types/node": "^8.0.0",
+ "@types/qs": "^6.2.31",
+ "caseless": "~0.12.0",
+ "concat-stream": "^1.6.0",
+ "form-data": "^2.2.0",
+ "http-basic": "^8.1.1",
+ "http-response-object": "^3.0.1",
+ "promise": "^8.0.0",
+ "qs": "^6.4.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/then-request/node_modules/@types/node": {
+ "version": "8.10.66",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
+ "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==",
+ "dev": true
+ },
+ "node_modules/tinybench": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz",
+ "integrity": "sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==",
+ "dev": true
+ },
+ "node_modules/tinypool": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.0.tgz",
+ "integrity": "sha512-NX5KeqHOBZU6Bc0xj9Vr5Szbb1j8tUHIeD18s41aDJaPeC5QTdEhK0SpdpUrZlj2nv5cctNcSjaKNanXlfcVEQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.0.2.tgz",
+ "integrity": "sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "dev": true
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "dev": true
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
+ "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "3.2.10",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz",
+ "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.15.9",
+ "postcss": "^8.4.18",
+ "resolve": "^1.22.1",
+ "rollup": "^2.79.1"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "0.24.3",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.24.3.tgz",
+ "integrity": "sha512-aM0auuPPgMSstWvr851hB74g/LKaKBzSxcG3da7ejfZbx08Y21JpZmbmDYrMTCGhVZKqTGwzcnLMwyfz2WzkhQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/chai": "^4.3.3",
+ "@types/chai-subset": "^1.3.3",
+ "@types/node": "*",
+ "chai": "^4.3.6",
+ "debug": "^4.3.4",
+ "local-pkg": "^0.4.2",
+ "strip-literal": "^0.4.2",
+ "tinybench": "^2.3.0",
+ "tinypool": "^0.3.0",
+ "tinyspy": "^1.0.2",
+ "vite": "^3.0.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": ">=v14.16.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@vitest/browser": "*",
+ "@vitest/ui": "*",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dev": true,
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/whatwg-url/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "dev": true
+ },
+ "node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ }
+ },
+ "dependencies": {
+ "@esbuild/android-arm": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.12.tgz",
+ "integrity": "sha512-IC7TqIqiyE0MmvAhWkl/8AEzpOtbhRNDo7aph47We1NbE5w2bt/Q+giAhe0YYeVpYnIhGMcuZY92qDK6dQauvA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-loong64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.12.tgz",
+ "integrity": "sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==",
+ "dev": true,
+ "optional": true
+ },
+ "@types/chai": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz",
+ "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==",
+ "dev": true
+ },
+ "@types/chai-subset": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz",
+ "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==",
+ "dev": true,
+ "requires": {
+ "@types/chai": "*"
+ }
+ },
+ "@types/concat-stream": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz",
+ "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/form-data": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
+ "integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/node": {
+ "version": "18.11.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.3.tgz",
+ "integrity": "sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A==",
+ "dev": true
+ },
+ "@types/qs": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
+ "dev": true
+ },
+ "acorn": {
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
+ "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "dev": true
+ },
+ "assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+ "dev": true
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ }
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
+ "dev": true
+ },
+ "chai": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz",
+ "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==",
+ "dev": true,
+ "requires": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.2",
+ "deep-eql": "^3.0.1",
+ "get-func-name": "^2.0.0",
+ "loupe": "^2.3.1",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.0.5"
+ }
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "check-error": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+ "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
+ "dev": true
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "requires": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "deep-eql": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+ "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+ "dev": true,
+ "requires": {
+ "type-detect": "^4.0.0"
+ }
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dev": true,
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "es-abstract": {
+ "version": "1.18.5",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz",
+ "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.3",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.3",
+ "is-string": "^1.0.6",
+ "object-inspect": "^1.11.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "esbuild": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.12.tgz",
+ "integrity": "sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==",
+ "dev": true,
+ "requires": {
+ "@esbuild/android-arm": "0.15.12",
+ "@esbuild/linux-loong64": "0.15.12",
+ "esbuild-android-64": "0.15.12",
+ "esbuild-android-arm64": "0.15.12",
+ "esbuild-darwin-64": "0.15.12",
+ "esbuild-darwin-arm64": "0.15.12",
+ "esbuild-freebsd-64": "0.15.12",
+ "esbuild-freebsd-arm64": "0.15.12",
+ "esbuild-linux-32": "0.15.12",
+ "esbuild-linux-64": "0.15.12",
+ "esbuild-linux-arm": "0.15.12",
+ "esbuild-linux-arm64": "0.15.12",
+ "esbuild-linux-mips64le": "0.15.12",
+ "esbuild-linux-ppc64le": "0.15.12",
+ "esbuild-linux-riscv64": "0.15.12",
+ "esbuild-linux-s390x": "0.15.12",
+ "esbuild-netbsd-64": "0.15.12",
+ "esbuild-openbsd-64": "0.15.12",
+ "esbuild-sunos-64": "0.15.12",
+ "esbuild-windows-32": "0.15.12",
+ "esbuild-windows-64": "0.15.12",
+ "esbuild-windows-arm64": "0.15.12"
+ }
+ },
+ "esbuild-android-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.12.tgz",
+ "integrity": "sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-android-arm64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.12.tgz",
+ "integrity": "sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-darwin-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.12.tgz",
+ "integrity": "sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-darwin-arm64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.12.tgz",
+ "integrity": "sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-freebsd-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.12.tgz",
+ "integrity": "sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-freebsd-arm64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.12.tgz",
+ "integrity": "sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-32": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.12.tgz",
+ "integrity": "sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.12.tgz",
+ "integrity": "sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-arm": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.12.tgz",
+ "integrity": "sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-arm64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.12.tgz",
+ "integrity": "sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-mips64le": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.12.tgz",
+ "integrity": "sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-ppc64le": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.12.tgz",
+ "integrity": "sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-riscv64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.12.tgz",
+ "integrity": "sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-s390x": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.12.tgz",
+ "integrity": "sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-netbsd-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.12.tgz",
+ "integrity": "sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-openbsd-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.12.tgz",
+ "integrity": "sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-sunos-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.12.tgz",
+ "integrity": "sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-32": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.12.tgz",
+ "integrity": "sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.12.tgz",
+ "integrity": "sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-arm64": {
+ "version": "0.15.12",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.12.tgz",
+ "integrity": "sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==",
+ "dev": true,
+ "optional": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "form-data": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
+ "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "get-func-name": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+ "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
+ "dev": true
+ },
+ "get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "get-port": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
+ "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==",
+ "dev": true
+ },
+ "graceful-fs": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
+ "dev": true
+ },
+ "happy-dom": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-7.6.0.tgz",
+ "integrity": "sha512-QnNsiblZdyVDzW5ts6E7ub79JnabqHJeJgt+1WGNq9fSYqS/r/RzzTVXCZSDl6EVkipdwI48B4bgXAnMZPecIw==",
+ "dev": true,
+ "requires": {
+ "css.escape": "^1.5.1",
+ "he": "^1.2.0",
+ "node-fetch": "^2.x.x",
+ "sync-request": "^6.1.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0"
+ }
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-bigints": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "has-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
+ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
+ "dev": true
+ },
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true
+ },
+ "hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true
+ },
+ "http-basic": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
+ "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==",
+ "dev": true,
+ "requires": {
+ "caseless": "^0.12.0",
+ "concat-stream": "^1.6.2",
+ "http-response-object": "^3.0.1",
+ "parse-cache-control": "^1.0.1"
+ }
+ },
+ "http-response-object": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
+ "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "^10.0.3"
+ },
+ "dependencies": {
+ "@types/node": {
+ "version": "10.17.60",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
+ "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
+ "dev": true
+ }
+ }
+ },
+ "iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ }
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "dev": true
+ },
+ "is-bigint": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz",
+ "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==",
+ "dev": true
+ },
+ "is-boolean-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz",
+ "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
+ "is-callable": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
+ "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==",
+ "dev": true
+ },
+ "is-core-module": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+ "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-date-object": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz",
+ "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==",
+ "dev": true
+ },
+ "is-negative-zero": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
+ "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==",
+ "dev": true
+ },
+ "is-number-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz",
+ "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==",
+ "dev": true
+ },
+ "is-regex": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz",
+ "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "is-string": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz",
+ "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==",
+ "dev": true
+ },
+ "is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true
+ },
+ "load-json-file": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+ "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^4.0.0",
+ "pify": "^3.0.0",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "local-pkg": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz",
+ "integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==",
+ "dev": true
+ },
+ "loupe": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
+ "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==",
+ "dev": true,
+ "requires": {
+ "get-func-name": "^2.0.0"
+ }
+ },
+ "memorystream": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+ "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=",
+ "dev": true
+ },
+ "mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true
+ },
+ "mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "requires": {
+ "mime-db": "1.52.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "dev": true
+ },
+ "nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "node-fetch": {
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+ "dev": true,
+ "requires": {
+ "whatwg-url": "^5.0.0"
+ }
+ },
+ "normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "npm-run-all": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
+ "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "chalk": "^2.4.1",
+ "cross-spawn": "^6.0.5",
+ "memorystream": "^0.3.1",
+ "minimatch": "^3.0.4",
+ "pidtree": "^0.3.0",
+ "read-pkg": "^3.0.0",
+ "shell-quote": "^1.6.1",
+ "string.prototype.padend": "^3.0.0"
+ }
+ },
+ "object-inspect": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz",
+ "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==",
+ "dev": true
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true
+ },
+ "object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "parse-cache-control": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
+ "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==",
+ "dev": true
+ },
+ "parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+ "dev": true,
+ "requires": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ }
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "path-type": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+ "dev": true,
+ "requires": {
+ "pify": "^3.0.0"
+ }
+ },
+ "pathval": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+ "dev": true
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "pidtree": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
+ "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
+ "dev": true
+ },
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+ "dev": true
+ },
+ "postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "dev": true,
+ "requires": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
+ "promise": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-8.2.0.tgz",
+ "integrity": "sha512-+CMAlLHqwRYwBMXKCP+o8ns7DN+xHDUiI+0nArsiJ9y+kJVPLFxEaSw6Ha9s9H0tftxg2Yzl25wqj9G7m5wLZg==",
+ "dev": true,
+ "requires": {
+ "asap": "~2.0.6"
+ }
+ },
+ "qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "dev": true,
+ "requires": {
+ "side-channel": "^1.0.4"
+ }
+ },
+ "read-pkg": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+ "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=",
+ "dev": true,
+ "requires": {
+ "load-json-file": "^4.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^3.0.0"
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "resolve": {
+ "version": "1.22.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+ "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "rollup": {
+ "version": "2.79.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
+ "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
+ "dev": true,
+ "requires": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true
+ },
+ "shell-quote": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
+ "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
+ "dev": true
+ },
+ "side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
+ "source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "dev": true
+ },
+ "spdx-correct": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
+ "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
+ "dev": true,
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+ "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+ "dev": true
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz",
+ "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==",
+ "dev": true
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "string.prototype.padend": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz",
+ "integrity": "sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.18.0-next.2"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+ "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+ "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+ "dev": true
+ },
+ "strip-literal": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-0.4.2.tgz",
+ "integrity": "sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw==",
+ "dev": true,
+ "requires": {
+ "acorn": "^8.8.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true
+ },
+ "svelte": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.49.0.tgz",
+ "integrity": "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==",
+ "dev": true
+ },
+ "sync-request": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz",
+ "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==",
+ "dev": true,
+ "requires": {
+ "http-response-object": "^3.0.1",
+ "sync-rpc": "^1.2.1",
+ "then-request": "^6.0.0"
+ }
+ },
+ "sync-rpc": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz",
+ "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==",
+ "dev": true,
+ "requires": {
+ "get-port": "^3.1.0"
+ }
+ },
+ "then-request": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz",
+ "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==",
+ "dev": true,
+ "requires": {
+ "@types/concat-stream": "^1.6.0",
+ "@types/form-data": "0.0.33",
+ "@types/node": "^8.0.0",
+ "@types/qs": "^6.2.31",
+ "caseless": "~0.12.0",
+ "concat-stream": "^1.6.0",
+ "form-data": "^2.2.0",
+ "http-basic": "^8.1.1",
+ "http-response-object": "^3.0.1",
+ "promise": "^8.0.0",
+ "qs": "^6.4.0"
+ },
+ "dependencies": {
+ "@types/node": {
+ "version": "8.10.66",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
+ "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==",
+ "dev": true
+ }
+ }
+ },
+ "tinybench": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz",
+ "integrity": "sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==",
+ "dev": true
+ },
+ "tinypool": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.0.tgz",
+ "integrity": "sha512-NX5KeqHOBZU6Bc0xj9Vr5Szbb1j8tUHIeD18s41aDJaPeC5QTdEhK0SpdpUrZlj2nv5cctNcSjaKNanXlfcVEQ==",
+ "dev": true
+ },
+ "tinyspy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.0.2.tgz",
+ "integrity": "sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==",
+ "dev": true
+ },
+ "tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "dev": true
+ },
+ "type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "dev": true
+ },
+ "unbox-primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
+ "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "vite": {
+ "version": "3.2.10",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz",
+ "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==",
+ "dev": true,
+ "requires": {
+ "esbuild": "^0.15.9",
+ "fsevents": "~2.3.2",
+ "postcss": "^8.4.18",
+ "resolve": "^1.22.1",
+ "rollup": "^2.79.1"
+ }
+ },
+ "vitest": {
+ "version": "0.24.3",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.24.3.tgz",
+ "integrity": "sha512-aM0auuPPgMSstWvr851hB74g/LKaKBzSxcG3da7ejfZbx08Y21JpZmbmDYrMTCGhVZKqTGwzcnLMwyfz2WzkhQ==",
+ "dev": true,
+ "requires": {
+ "@types/chai": "^4.3.3",
+ "@types/chai-subset": "^1.3.3",
+ "@types/node": "*",
+ "chai": "^4.3.6",
+ "debug": "^4.3.4",
+ "local-pkg": "^0.4.2",
+ "strip-literal": "^0.4.2",
+ "tinybench": "^2.3.0",
+ "tinypool": "^0.3.0",
+ "tinyspy": "^1.0.2",
+ "vite": "^3.0.0"
+ }
+ },
+ "webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true
+ },
+ "whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "requires": {
+ "iconv-lite": "0.6.3"
+ }
+ },
+ "whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true
+ },
+ "whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dev": true,
+ "requires": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ },
+ "dependencies": {
+ "webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "dev": true
+ }
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "requires": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ }
+ }
+ }
+}
diff --git a/v2/internal/frontend/runtime/package.json b/v2/internal/frontend/runtime/package.json
new file mode 100644
index 000000000..09ff4d50f
--- /dev/null
+++ b/v2/internal/frontend/runtime/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "runtime",
+ "version": "2.0.0",
+ "description": "Wails JS Runtime",
+ "main": "index.js",
+ "scripts": {
+ "build": "run-p build:*",
+ "build:ipc-desktop": "npx esbuild desktop/ipc.js --bundle --minify --outfile=ipc.js",
+ "build:ipc-dev": "cd dev && npm install && npm run build",
+ "build:runtime-desktop-prod": "npx esbuild desktop/main.js --bundle --minify --outfile=runtime_prod_desktop.js --define:DEBUG=false",
+ "build:runtime-desktop-debug": "npx esbuild desktop/main.js --bundle --sourcemap=inline --outfile=runtime_debug_desktop.js --define:DEBUG=true",
+ "test": "vitest"
+ },
+ "author": "Lea Anthony ",
+ "license": "ISC",
+ "devDependencies": {
+ "esbuild": "^0.15.6",
+ "happy-dom": "^7.6.0",
+ "npm-run-all": "^4.1.5",
+ "svelte": "^3.49.0",
+ "vitest": "^0.24.3"
+ }
+}
diff --git a/v2/internal/frontend/runtime/runtime_debug_desktop.go b/v2/internal/frontend/runtime/runtime_debug_desktop.go
new file mode 100644
index 000000000..8dff343c0
--- /dev/null
+++ b/v2/internal/frontend/runtime/runtime_debug_desktop.go
@@ -0,0 +1,8 @@
+//go:build debug || !production
+
+package runtime
+
+import _ "embed"
+
+//go:embed runtime_debug_desktop.js
+var RuntimeDesktopJS []byte
diff --git a/v2/internal/frontend/runtime/runtime_debug_desktop.js b/v2/internal/frontend/runtime/runtime_debug_desktop.js
new file mode 100644
index 000000000..a5f6068e9
--- /dev/null
+++ b/v2/internal/frontend/runtime/runtime_debug_desktop.js
@@ -0,0 +1,792 @@
+(() => {
+ var __defProp = Object.defineProperty;
+ var __export = (target, all) => {
+ for (var name in all)
+ __defProp(target, name, { get: all[name], enumerable: true });
+ };
+
+ // desktop/log.js
+ var log_exports = {};
+ __export(log_exports, {
+ LogDebug: () => LogDebug,
+ LogError: () => LogError,
+ LogFatal: () => LogFatal,
+ LogInfo: () => LogInfo,
+ LogLevel: () => LogLevel,
+ LogPrint: () => LogPrint,
+ LogTrace: () => LogTrace,
+ LogWarning: () => LogWarning,
+ SetLogLevel: () => SetLogLevel
+ });
+ function sendLogMessage(level, message) {
+ window.WailsInvoke("L" + level + message);
+ }
+ function LogTrace(message) {
+ sendLogMessage("T", message);
+ }
+ function LogPrint(message) {
+ sendLogMessage("P", message);
+ }
+ function LogDebug(message) {
+ sendLogMessage("D", message);
+ }
+ function LogInfo(message) {
+ sendLogMessage("I", message);
+ }
+ function LogWarning(message) {
+ sendLogMessage("W", message);
+ }
+ function LogError(message) {
+ sendLogMessage("E", message);
+ }
+ function LogFatal(message) {
+ sendLogMessage("F", message);
+ }
+ function SetLogLevel(loglevel) {
+ sendLogMessage("S", loglevel);
+ }
+ var LogLevel = {
+ TRACE: 1,
+ DEBUG: 2,
+ INFO: 3,
+ WARNING: 4,
+ ERROR: 5
+ };
+
+ // desktop/events.js
+ var Listener = class {
+ constructor(eventName, callback, maxCallbacks) {
+ this.eventName = eventName;
+ this.maxCallbacks = maxCallbacks || -1;
+ this.Callback = (data) => {
+ callback.apply(null, data);
+ if (this.maxCallbacks === -1) {
+ return false;
+ }
+ this.maxCallbacks -= 1;
+ return this.maxCallbacks === 0;
+ };
+ }
+ };
+ var eventListeners = {};
+ function EventsOnMultiple(eventName, callback, maxCallbacks) {
+ eventListeners[eventName] = eventListeners[eventName] || [];
+ const thisListener = new Listener(eventName, callback, maxCallbacks);
+ eventListeners[eventName].push(thisListener);
+ return () => listenerOff(thisListener);
+ }
+ function EventsOn(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, -1);
+ }
+ function EventsOnce(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, 1);
+ }
+ function notifyListeners(eventData) {
+ let eventName = eventData.name;
+ const newEventListenerList = eventListeners[eventName]?.slice() || [];
+ if (newEventListenerList.length) {
+ for (let count = newEventListenerList.length - 1; count >= 0; count -= 1) {
+ const listener = newEventListenerList[count];
+ let data = eventData.data;
+ const destroy = listener.Callback(data);
+ if (destroy) {
+ newEventListenerList.splice(count, 1);
+ }
+ }
+ if (newEventListenerList.length === 0) {
+ removeListener(eventName);
+ } else {
+ eventListeners[eventName] = newEventListenerList;
+ }
+ }
+ }
+ function EventsNotify(notifyMessage) {
+ let message;
+ try {
+ message = JSON.parse(notifyMessage);
+ } catch (e) {
+ const error = "Invalid JSON passed to Notify: " + notifyMessage;
+ throw new Error(error);
+ }
+ notifyListeners(message);
+ }
+ function EventsEmit(eventName) {
+ const payload = {
+ name: eventName,
+ data: [].slice.apply(arguments).slice(1)
+ };
+ notifyListeners(payload);
+ window.WailsInvoke("EE" + JSON.stringify(payload));
+ }
+ function removeListener(eventName) {
+ delete eventListeners[eventName];
+ window.WailsInvoke("EX" + eventName);
+ }
+ function EventsOff(eventName, ...additionalEventNames) {
+ removeListener(eventName);
+ if (additionalEventNames.length > 0) {
+ additionalEventNames.forEach((eventName2) => {
+ removeListener(eventName2);
+ });
+ }
+ }
+ function EventsOffAll() {
+ const eventNames = Object.keys(eventListeners);
+ eventNames.forEach((eventName) => {
+ removeListener(eventName);
+ });
+ }
+ function listenerOff(listener) {
+ const eventName = listener.eventName;
+ if (eventListeners[eventName] === void 0)
+ return;
+ eventListeners[eventName] = eventListeners[eventName].filter((l) => l !== listener);
+ if (eventListeners[eventName].length === 0) {
+ removeListener(eventName);
+ }
+ }
+
+ // desktop/calls.js
+ var callbacks = {};
+ function cryptoRandom() {
+ var array = new Uint32Array(1);
+ return window.crypto.getRandomValues(array)[0];
+ }
+ function basicRandom() {
+ return Math.random() * 9007199254740991;
+ }
+ var randomFunc;
+ if (window.crypto) {
+ randomFunc = cryptoRandom;
+ } else {
+ randomFunc = basicRandom;
+ }
+ function Call(name, args, timeout) {
+ if (timeout == null) {
+ timeout = 0;
+ }
+ return new Promise(function(resolve, reject) {
+ var callbackID;
+ do {
+ callbackID = name + "-" + randomFunc();
+ } while (callbacks[callbackID]);
+ var timeoutHandle;
+ if (timeout > 0) {
+ timeoutHandle = setTimeout(function() {
+ reject(Error("Call to " + name + " timed out. Request ID: " + callbackID));
+ }, timeout);
+ }
+ callbacks[callbackID] = {
+ timeoutHandle,
+ reject,
+ resolve
+ };
+ try {
+ const payload = {
+ name,
+ args,
+ callbackID
+ };
+ window.WailsInvoke("C" + JSON.stringify(payload));
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ }
+ window.ObfuscatedCall = (id, args, timeout) => {
+ if (timeout == null) {
+ timeout = 0;
+ }
+ return new Promise(function(resolve, reject) {
+ var callbackID;
+ do {
+ callbackID = id + "-" + randomFunc();
+ } while (callbacks[callbackID]);
+ var timeoutHandle;
+ if (timeout > 0) {
+ timeoutHandle = setTimeout(function() {
+ reject(Error("Call to method " + id + " timed out. Request ID: " + callbackID));
+ }, timeout);
+ }
+ callbacks[callbackID] = {
+ timeoutHandle,
+ reject,
+ resolve
+ };
+ try {
+ const payload = {
+ id,
+ args,
+ callbackID
+ };
+ window.WailsInvoke("c" + JSON.stringify(payload));
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ };
+ function Callback(incomingMessage) {
+ let message;
+ try {
+ message = JSON.parse(incomingMessage);
+ } catch (e) {
+ const error = `Invalid JSON passed to callback: ${e.message}. Message: ${incomingMessage}`;
+ runtime.LogDebug(error);
+ throw new Error(error);
+ }
+ let callbackID = message.callbackid;
+ let callbackData = callbacks[callbackID];
+ if (!callbackData) {
+ const error = `Callback '${callbackID}' not registered!!!`;
+ console.error(error);
+ throw new Error(error);
+ }
+ clearTimeout(callbackData.timeoutHandle);
+ delete callbacks[callbackID];
+ if (message.error) {
+ callbackData.reject(message.error);
+ } else {
+ callbackData.resolve(message.result);
+ }
+ }
+
+ // desktop/bindings.js
+ window.go = {};
+ function SetBindings(bindingsMap) {
+ try {
+ bindingsMap = JSON.parse(bindingsMap);
+ } catch (e) {
+ console.error(e);
+ }
+ window.go = window.go || {};
+ Object.keys(bindingsMap).forEach((packageName) => {
+ window.go[packageName] = window.go[packageName] || {};
+ Object.keys(bindingsMap[packageName]).forEach((structName) => {
+ window.go[packageName][structName] = window.go[packageName][structName] || {};
+ Object.keys(bindingsMap[packageName][structName]).forEach((methodName) => {
+ window.go[packageName][structName][methodName] = function() {
+ let timeout = 0;
+ function dynamic() {
+ const args = [].slice.call(arguments);
+ return Call([packageName, structName, methodName].join("."), args, timeout);
+ }
+ dynamic.setTimeout = function(newTimeout) {
+ timeout = newTimeout;
+ };
+ dynamic.getTimeout = function() {
+ return timeout;
+ };
+ return dynamic;
+ }();
+ });
+ });
+ });
+ }
+
+ // desktop/window.js
+ var window_exports = {};
+ __export(window_exports, {
+ WindowCenter: () => WindowCenter,
+ WindowFullscreen: () => WindowFullscreen,
+ WindowGetPosition: () => WindowGetPosition,
+ WindowGetSize: () => WindowGetSize,
+ WindowHide: () => WindowHide,
+ WindowIsFullscreen: () => WindowIsFullscreen,
+ WindowIsMaximised: () => WindowIsMaximised,
+ WindowIsMinimised: () => WindowIsMinimised,
+ WindowIsNormal: () => WindowIsNormal,
+ WindowMaximise: () => WindowMaximise,
+ WindowMinimise: () => WindowMinimise,
+ WindowReload: () => WindowReload,
+ WindowReloadApp: () => WindowReloadApp,
+ WindowSetAlwaysOnTop: () => WindowSetAlwaysOnTop,
+ WindowSetBackgroundColour: () => WindowSetBackgroundColour,
+ WindowSetDarkTheme: () => WindowSetDarkTheme,
+ WindowSetLightTheme: () => WindowSetLightTheme,
+ WindowSetMaxSize: () => WindowSetMaxSize,
+ WindowSetMinSize: () => WindowSetMinSize,
+ WindowSetPosition: () => WindowSetPosition,
+ WindowSetSize: () => WindowSetSize,
+ WindowSetSystemDefaultTheme: () => WindowSetSystemDefaultTheme,
+ WindowSetTitle: () => WindowSetTitle,
+ WindowShow: () => WindowShow,
+ WindowToggleMaximise: () => WindowToggleMaximise,
+ WindowUnfullscreen: () => WindowUnfullscreen,
+ WindowUnmaximise: () => WindowUnmaximise,
+ WindowUnminimise: () => WindowUnminimise
+ });
+ function WindowReload() {
+ window.location.reload();
+ }
+ function WindowReloadApp() {
+ window.WailsInvoke("WR");
+ }
+ function WindowSetSystemDefaultTheme() {
+ window.WailsInvoke("WASDT");
+ }
+ function WindowSetLightTheme() {
+ window.WailsInvoke("WALT");
+ }
+ function WindowSetDarkTheme() {
+ window.WailsInvoke("WADT");
+ }
+ function WindowCenter() {
+ window.WailsInvoke("Wc");
+ }
+ function WindowSetTitle(title) {
+ window.WailsInvoke("WT" + title);
+ }
+ function WindowFullscreen() {
+ window.WailsInvoke("WF");
+ }
+ function WindowUnfullscreen() {
+ window.WailsInvoke("Wf");
+ }
+ function WindowIsFullscreen() {
+ return Call(":wails:WindowIsFullscreen");
+ }
+ function WindowSetSize(width, height) {
+ window.WailsInvoke("Ws:" + width + ":" + height);
+ }
+ function WindowGetSize() {
+ return Call(":wails:WindowGetSize");
+ }
+ function WindowSetMaxSize(width, height) {
+ window.WailsInvoke("WZ:" + width + ":" + height);
+ }
+ function WindowSetMinSize(width, height) {
+ window.WailsInvoke("Wz:" + width + ":" + height);
+ }
+ function WindowSetAlwaysOnTop(b) {
+ window.WailsInvoke("WATP:" + (b ? "1" : "0"));
+ }
+ function WindowSetPosition(x, y) {
+ window.WailsInvoke("Wp:" + x + ":" + y);
+ }
+ function WindowGetPosition() {
+ return Call(":wails:WindowGetPos");
+ }
+ function WindowHide() {
+ window.WailsInvoke("WH");
+ }
+ function WindowShow() {
+ window.WailsInvoke("WS");
+ }
+ function WindowMaximise() {
+ window.WailsInvoke("WM");
+ }
+ function WindowToggleMaximise() {
+ window.WailsInvoke("Wt");
+ }
+ function WindowUnmaximise() {
+ window.WailsInvoke("WU");
+ }
+ function WindowIsMaximised() {
+ return Call(":wails:WindowIsMaximised");
+ }
+ function WindowMinimise() {
+ window.WailsInvoke("Wm");
+ }
+ function WindowUnminimise() {
+ window.WailsInvoke("Wu");
+ }
+ function WindowIsMinimised() {
+ return Call(":wails:WindowIsMinimised");
+ }
+ function WindowIsNormal() {
+ return Call(":wails:WindowIsNormal");
+ }
+ function WindowSetBackgroundColour(R, G, B, A) {
+ let rgba = JSON.stringify({ r: R || 0, g: G || 0, b: B || 0, a: A || 255 });
+ window.WailsInvoke("Wr:" + rgba);
+ }
+
+ // desktop/screen.js
+ var screen_exports = {};
+ __export(screen_exports, {
+ ScreenGetAll: () => ScreenGetAll
+ });
+ function ScreenGetAll() {
+ return Call(":wails:ScreenGetAll");
+ }
+
+ // desktop/browser.js
+ var browser_exports = {};
+ __export(browser_exports, {
+ BrowserOpenURL: () => BrowserOpenURL
+ });
+ function BrowserOpenURL(url) {
+ window.WailsInvoke("BO:" + url);
+ }
+
+ // desktop/clipboard.js
+ var clipboard_exports = {};
+ __export(clipboard_exports, {
+ ClipboardGetText: () => ClipboardGetText,
+ ClipboardSetText: () => ClipboardSetText
+ });
+ function ClipboardSetText(text) {
+ return Call(":wails:ClipboardSetText", [text]);
+ }
+ function ClipboardGetText() {
+ return Call(":wails:ClipboardGetText");
+ }
+
+ // desktop/draganddrop.js
+ var draganddrop_exports = {};
+ __export(draganddrop_exports, {
+ CanResolveFilePaths: () => CanResolveFilePaths,
+ OnFileDrop: () => OnFileDrop,
+ OnFileDropOff: () => OnFileDropOff,
+ ResolveFilePaths: () => ResolveFilePaths
+ });
+ var flags = {
+ registered: false,
+ defaultUseDropTarget: true,
+ useDropTarget: true,
+ nextDeactivate: null,
+ nextDeactivateTimeout: null
+ };
+ var DROP_TARGET_ACTIVE = "wails-drop-target-active";
+ function checkStyleDropTarget(style) {
+ const cssDropValue = style.getPropertyValue(window.wails.flags.cssDropProperty).trim();
+ if (cssDropValue) {
+ if (cssDropValue === window.wails.flags.cssDropValue) {
+ return true;
+ }
+ return false;
+ }
+ return false;
+ }
+ function onDragOver(e) {
+ const isFileDrop = e.dataTransfer.types.includes("Files");
+ if (!isFileDrop) {
+ return;
+ }
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "copy";
+ if (!window.wails.flags.enableWailsDragAndDrop) {
+ return;
+ }
+ if (!flags.useDropTarget) {
+ return;
+ }
+ const element = e.target;
+ if (flags.nextDeactivate)
+ flags.nextDeactivate();
+ if (!element || !checkStyleDropTarget(getComputedStyle(element))) {
+ return;
+ }
+ let currentElement = element;
+ while (currentElement) {
+ if (checkStyleDropTarget(getComputedStyle(currentElement))) {
+ currentElement.classList.add(DROP_TARGET_ACTIVE);
+ }
+ currentElement = currentElement.parentElement;
+ }
+ }
+ function onDragLeave(e) {
+ const isFileDrop = e.dataTransfer.types.includes("Files");
+ if (!isFileDrop) {
+ return;
+ }
+ e.preventDefault();
+ if (!window.wails.flags.enableWailsDragAndDrop) {
+ return;
+ }
+ if (!flags.useDropTarget) {
+ return;
+ }
+ if (!e.target || !checkStyleDropTarget(getComputedStyle(e.target))) {
+ return null;
+ }
+ if (flags.nextDeactivate)
+ flags.nextDeactivate();
+ flags.nextDeactivate = () => {
+ Array.from(document.getElementsByClassName(DROP_TARGET_ACTIVE)).forEach((el) => el.classList.remove(DROP_TARGET_ACTIVE));
+ flags.nextDeactivate = null;
+ if (flags.nextDeactivateTimeout) {
+ clearTimeout(flags.nextDeactivateTimeout);
+ flags.nextDeactivateTimeout = null;
+ }
+ };
+ flags.nextDeactivateTimeout = setTimeout(() => {
+ if (flags.nextDeactivate)
+ flags.nextDeactivate();
+ }, 50);
+ }
+ function onDrop(e) {
+ const isFileDrop = e.dataTransfer.types.includes("Files");
+ if (!isFileDrop) {
+ return;
+ }
+ e.preventDefault();
+ if (!window.wails.flags.enableWailsDragAndDrop) {
+ return;
+ }
+ if (CanResolveFilePaths()) {
+ let files = [];
+ if (e.dataTransfer.items) {
+ files = [...e.dataTransfer.items].map((item, i) => {
+ if (item.kind === "file") {
+ return item.getAsFile();
+ }
+ });
+ } else {
+ files = [...e.dataTransfer.files];
+ }
+ window.runtime.ResolveFilePaths(e.x, e.y, files);
+ }
+ if (!flags.useDropTarget) {
+ return;
+ }
+ if (flags.nextDeactivate)
+ flags.nextDeactivate();
+ Array.from(document.getElementsByClassName(DROP_TARGET_ACTIVE)).forEach((el) => el.classList.remove(DROP_TARGET_ACTIVE));
+ }
+ function CanResolveFilePaths() {
+ return window.chrome?.webview?.postMessageWithAdditionalObjects != null;
+ }
+ function ResolveFilePaths(x, y, files) {
+ if (window.chrome?.webview?.postMessageWithAdditionalObjects) {
+ chrome.webview.postMessageWithAdditionalObjects(`file:drop:${x}:${y}`, files);
+ }
+ }
+ function OnFileDrop(callback, useDropTarget) {
+ if (typeof callback !== "function") {
+ console.error("DragAndDropCallback is not a function");
+ return;
+ }
+ if (flags.registered) {
+ return;
+ }
+ flags.registered = true;
+ const uDTPT = typeof useDropTarget;
+ flags.useDropTarget = uDTPT === "undefined" || uDTPT !== "boolean" ? flags.defaultUseDropTarget : useDropTarget;
+ window.addEventListener("dragover", onDragOver);
+ window.addEventListener("dragleave", onDragLeave);
+ window.addEventListener("drop", onDrop);
+ let cb = callback;
+ if (flags.useDropTarget) {
+ cb = function(x, y, paths) {
+ const element = document.elementFromPoint(x, y);
+ if (!element || !checkStyleDropTarget(getComputedStyle(element))) {
+ return null;
+ }
+ callback(x, y, paths);
+ };
+ }
+ EventsOn("wails:file-drop", cb);
+ }
+ function OnFileDropOff() {
+ window.removeEventListener("dragover", onDragOver);
+ window.removeEventListener("dragleave", onDragLeave);
+ window.removeEventListener("drop", onDrop);
+ EventsOff("wails:file-drop");
+ flags.registered = false;
+ }
+
+ // desktop/contextmenu.js
+ function processDefaultContextMenu(event) {
+ const element = event.target;
+ const computedStyle = window.getComputedStyle(element);
+ const defaultContextMenuAction = computedStyle.getPropertyValue("--default-contextmenu").trim();
+ switch (defaultContextMenuAction) {
+ case "show":
+ return;
+ case "hide":
+ event.preventDefault();
+ return;
+ default:
+ if (element.isContentEditable) {
+ return;
+ }
+ const selection = window.getSelection();
+ const hasSelection = selection.toString().length > 0;
+ if (hasSelection) {
+ for (let i = 0; i < selection.rangeCount; i++) {
+ const range = selection.getRangeAt(i);
+ const rects = range.getClientRects();
+ for (let j = 0; j < rects.length; j++) {
+ const rect = rects[j];
+ if (document.elementFromPoint(rect.left, rect.top) === element) {
+ return;
+ }
+ }
+ }
+ }
+ if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
+ if (hasSelection || !element.readOnly && !element.disabled) {
+ return;
+ }
+ }
+ event.preventDefault();
+ }
+ }
+
+ // desktop/main.js
+ function Quit() {
+ window.WailsInvoke("Q");
+ }
+ function Show() {
+ window.WailsInvoke("S");
+ }
+ function Hide() {
+ window.WailsInvoke("H");
+ }
+ function Environment() {
+ return Call(":wails:Environment");
+ }
+ window.runtime = {
+ ...log_exports,
+ ...window_exports,
+ ...browser_exports,
+ ...screen_exports,
+ ...clipboard_exports,
+ ...draganddrop_exports,
+ EventsOn,
+ EventsOnce,
+ EventsOnMultiple,
+ EventsEmit,
+ EventsOff,
+ EventsOffAll,
+ Environment,
+ Show,
+ Hide,
+ Quit
+ };
+ window.wails = {
+ Callback,
+ EventsNotify,
+ SetBindings,
+ eventListeners,
+ callbacks,
+ flags: {
+ disableScrollbarDrag: false,
+ disableDefaultContextMenu: false,
+ enableResize: false,
+ defaultCursor: null,
+ borderThickness: 6,
+ shouldDrag: false,
+ deferDragToMouseMove: true,
+ cssDragProperty: "--wails-draggable",
+ cssDragValue: "drag",
+ cssDropProperty: "--wails-drop-target",
+ cssDropValue: "drop",
+ enableWailsDragAndDrop: false
+ }
+ };
+ if (window.wailsbindings) {
+ window.wails.SetBindings(window.wailsbindings);
+ delete window.wails.SetBindings;
+ }
+ if (false) {
+ delete window.wailsbindings;
+ }
+ var dragTest = function(e) {
+ var val = window.getComputedStyle(e.target).getPropertyValue(window.wails.flags.cssDragProperty);
+ if (val) {
+ val = val.trim();
+ }
+ if (val !== window.wails.flags.cssDragValue) {
+ return false;
+ }
+ if (e.buttons !== 1) {
+ return false;
+ }
+ if (e.detail !== 1) {
+ return false;
+ }
+ return true;
+ };
+ window.wails.setCSSDragProperties = function(property, value) {
+ window.wails.flags.cssDragProperty = property;
+ window.wails.flags.cssDragValue = value;
+ };
+ window.wails.setCSSDropProperties = function(property, value) {
+ window.wails.flags.cssDropProperty = property;
+ window.wails.flags.cssDropValue = value;
+ };
+ window.addEventListener("mousedown", (e) => {
+ if (window.wails.flags.resizeEdge) {
+ window.WailsInvoke("resize:" + window.wails.flags.resizeEdge);
+ e.preventDefault();
+ return;
+ }
+ if (dragTest(e)) {
+ if (window.wails.flags.disableScrollbarDrag) {
+ if (e.offsetX > e.target.clientWidth || e.offsetY > e.target.clientHeight) {
+ return;
+ }
+ }
+ if (window.wails.flags.deferDragToMouseMove) {
+ window.wails.flags.shouldDrag = true;
+ } else {
+ e.preventDefault();
+ window.WailsInvoke("drag");
+ }
+ return;
+ } else {
+ window.wails.flags.shouldDrag = false;
+ }
+ });
+ window.addEventListener("mouseup", () => {
+ window.wails.flags.shouldDrag = false;
+ });
+ function setResize(cursor) {
+ document.documentElement.style.cursor = cursor || window.wails.flags.defaultCursor;
+ window.wails.flags.resizeEdge = cursor;
+ }
+ window.addEventListener("mousemove", function(e) {
+ if (window.wails.flags.shouldDrag) {
+ window.wails.flags.shouldDrag = false;
+ let mousePressed = e.buttons !== void 0 ? e.buttons : e.which;
+ if (mousePressed > 0) {
+ window.WailsInvoke("drag");
+ return;
+ }
+ }
+ if (!window.wails.flags.enableResize) {
+ return;
+ }
+ if (window.wails.flags.defaultCursor == null) {
+ window.wails.flags.defaultCursor = document.documentElement.style.cursor;
+ }
+ if (window.outerWidth - e.clientX < window.wails.flags.borderThickness && window.outerHeight - e.clientY < window.wails.flags.borderThickness) {
+ document.documentElement.style.cursor = "se-resize";
+ }
+ let rightBorder = window.outerWidth - e.clientX < window.wails.flags.borderThickness;
+ let leftBorder = e.clientX < window.wails.flags.borderThickness;
+ let topBorder = e.clientY < window.wails.flags.borderThickness;
+ let bottomBorder = window.outerHeight - e.clientY < window.wails.flags.borderThickness;
+ if (!leftBorder && !rightBorder && !topBorder && !bottomBorder && window.wails.flags.resizeEdge !== void 0) {
+ setResize();
+ } else if (rightBorder && bottomBorder)
+ setResize("se-resize");
+ else if (leftBorder && bottomBorder)
+ setResize("sw-resize");
+ else if (leftBorder && topBorder)
+ setResize("nw-resize");
+ else if (topBorder && rightBorder)
+ setResize("ne-resize");
+ else if (leftBorder)
+ setResize("w-resize");
+ else if (topBorder)
+ setResize("n-resize");
+ else if (bottomBorder)
+ setResize("s-resize");
+ else if (rightBorder)
+ setResize("e-resize");
+ });
+ window.addEventListener("contextmenu", function(e) {
+ if (true)
+ return;
+ if (window.wails.flags.disableDefaultContextMenu) {
+ e.preventDefault();
+ } else {
+ processDefaultContextMenu(e);
+ }
+ });
+ window.WailsInvoke("runtime:ready");
+})();
+//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiZGVza3RvcC9sb2cuanMiLCAiZGVza3RvcC9ldmVudHMuanMiLCAiZGVza3RvcC9jYWxscy5qcyIsICJkZXNrdG9wL2JpbmRpbmdzLmpzIiwgImRlc2t0b3Avd2luZG93LmpzIiwgImRlc2t0b3Avc2NyZWVuLmpzIiwgImRlc2t0b3AvYnJvd3Nlci5qcyIsICJkZXNrdG9wL2NsaXBib2FyZC5qcyIsICJkZXNrdG9wL2RyYWdhbmRkcm9wLmpzIiwgImRlc2t0b3AvY29udGV4dG1lbnUuanMiLCAiZGVza3RvcC9tYWluLmpzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyIvKlxyXG4gXyAgICAgICBfXyAgICAgIF8gX19cclxufCB8ICAgICAvIC9fX18gXyhfKSAvX19fX1xyXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXHJcbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKVxyXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cclxuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xyXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XHJcbiovXHJcblxyXG4vKiBqc2hpbnQgZXN2ZXJzaW9uOiA2ICovXHJcblxyXG4vKipcclxuICogU2VuZHMgYSBsb2cgbWVzc2FnZSB0byB0aGUgYmFja2VuZCB3aXRoIHRoZSBnaXZlbiBsZXZlbCArIG1lc3NhZ2VcclxuICpcclxuICogQHBhcmFtIHtzdHJpbmd9IGxldmVsXHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXHJcbiAqL1xyXG5mdW5jdGlvbiBzZW5kTG9nTWVzc2FnZShsZXZlbCwgbWVzc2FnZSkge1xyXG5cclxuXHQvLyBMb2cgTWVzc2FnZSBmb3JtYXQ6XHJcblx0Ly8gbFt0eXBlXVttZXNzYWdlXVxyXG5cdHdpbmRvdy5XYWlsc0ludm9rZSgnTCcgKyBsZXZlbCArIG1lc3NhZ2UpO1xyXG59XHJcblxyXG4vKipcclxuICogTG9nIHRoZSBnaXZlbiB0cmFjZSBtZXNzYWdlIHdpdGggdGhlIGJhY2tlbmRcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZVxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIExvZ1RyYWNlKG1lc3NhZ2UpIHtcclxuXHRzZW5kTG9nTWVzc2FnZSgnVCcsIG1lc3NhZ2UpO1xyXG59XHJcblxyXG4vKipcclxuICogTG9nIHRoZSBnaXZlbiBtZXNzYWdlIHdpdGggdGhlIGJhY2tlbmRcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZVxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIExvZ1ByaW50KG1lc3NhZ2UpIHtcclxuXHRzZW5kTG9nTWVzc2FnZSgnUCcsIG1lc3NhZ2UpO1xyXG59XHJcblxyXG4vKipcclxuICogTG9nIHRoZSBnaXZlbiBkZWJ1ZyBtZXNzYWdlIHdpdGggdGhlIGJhY2tlbmRcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZVxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIExvZ0RlYnVnKG1lc3NhZ2UpIHtcclxuXHRzZW5kTG9nTWVzc2FnZSgnRCcsIG1lc3NhZ2UpO1xyXG59XHJcblxyXG4vKipcclxuICogTG9nIHRoZSBnaXZlbiBpbmZvIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gTG9nSW5mbyhtZXNzYWdlKSB7XHJcblx0c2VuZExvZ01lc3NhZ2UoJ0knLCBtZXNzYWdlKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIExvZyB0aGUgZ2l2ZW4gd2FybmluZyBtZXNzYWdlIHdpdGggdGhlIGJhY2tlbmRcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZVxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIExvZ1dhcm5pbmcobWVzc2FnZSkge1xyXG5cdHNlbmRMb2dNZXNzYWdlKCdXJywgbWVzc2FnZSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBMb2cgdGhlIGdpdmVuIGVycm9yIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gTG9nRXJyb3IobWVzc2FnZSkge1xyXG5cdHNlbmRMb2dNZXNzYWdlKCdFJywgbWVzc2FnZSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBMb2cgdGhlIGdpdmVuIGZhdGFsIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gTG9nRmF0YWwobWVzc2FnZSkge1xyXG5cdHNlbmRMb2dNZXNzYWdlKCdGJywgbWVzc2FnZSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBTZXRzIHRoZSBMb2cgbGV2ZWwgdG8gdGhlIGdpdmVuIGxvZyBsZXZlbFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBsb2dsZXZlbFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFNldExvZ0xldmVsKGxvZ2xldmVsKSB7XHJcblx0c2VuZExvZ01lc3NhZ2UoJ1MnLCBsb2dsZXZlbCk7XHJcbn1cclxuXHJcbi8vIExvZyBsZXZlbHNcclxuZXhwb3J0IGNvbnN0IExvZ0xldmVsID0ge1xyXG5cdFRSQUNFOiAxLFxyXG5cdERFQlVHOiAyLFxyXG5cdElORk86IDMsXHJcblx0V0FSTklORzogNCxcclxuXHRFUlJPUjogNSxcclxufTtcclxuIiwgIi8qXHJcbiBfICAgICAgIF9fICAgICAgXyBfX1xyXG58IHwgICAgIC8gL19fXyBfKF8pIC9fX19fXHJcbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cclxufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXHJcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xyXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXHJcbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcclxuKi9cclxuLyoganNoaW50IGVzdmVyc2lvbjogNiAqL1xyXG5cclxuLy8gRGVmaW5lcyBhIHNpbmdsZSBsaXN0ZW5lciB3aXRoIGEgbWF4aW11bSBudW1iZXIgb2YgdGltZXMgdG8gY2FsbGJhY2tcclxuXHJcbi8qKlxyXG4gKiBUaGUgTGlzdGVuZXIgY2xhc3MgZGVmaW5lcyBhIGxpc3RlbmVyISA6LSlcclxuICpcclxuICogQGNsYXNzIExpc3RlbmVyXHJcbiAqL1xyXG5jbGFzcyBMaXN0ZW5lciB7XHJcbiAgICAvKipcclxuICAgICAqIENyZWF0ZXMgYW4gaW5zdGFuY2Ugb2YgTGlzdGVuZXIuXHJcbiAgICAgKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXHJcbiAgICAgKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFja1xyXG4gICAgICogQHBhcmFtIHtudW1iZXJ9IG1heENhbGxiYWNrc1xyXG4gICAgICogQG1lbWJlcm9mIExpc3RlbmVyXHJcbiAgICAgKi9cclxuICAgIGNvbnN0cnVjdG9yKGV2ZW50TmFtZSwgY2FsbGJhY2ssIG1heENhbGxiYWNrcykge1xyXG4gICAgICAgIHRoaXMuZXZlbnROYW1lID0gZXZlbnROYW1lO1xyXG4gICAgICAgIC8vIERlZmF1bHQgb2YgLTEgbWVhbnMgaW5maW5pdGVcclxuICAgICAgICB0aGlzLm1heENhbGxiYWNrcyA9IG1heENhbGxiYWNrcyB8fCAtMTtcclxuICAgICAgICAvLyBDYWxsYmFjayBpbnZva2VzIHRoZSBjYWxsYmFjayB3aXRoIHRoZSBnaXZlbiBkYXRhXHJcbiAgICAgICAgLy8gUmV0dXJucyB0cnVlIGlmIHRoaXMgbGlzdGVuZXIgc2hvdWxkIGJlIGRlc3Ryb3llZFxyXG4gICAgICAgIHRoaXMuQ2FsbGJhY2sgPSAoZGF0YSkgPT4ge1xyXG4gICAgICAgICAgICBjYWxsYmFjay5hcHBseShudWxsLCBkYXRhKTtcclxuICAgICAgICAgICAgLy8gSWYgbWF4Q2FsbGJhY2tzIGlzIGluZmluaXRlLCByZXR1cm4gZmFsc2UgKGRvIG5vdCBkZXN0cm95KVxyXG4gICAgICAgICAgICBpZiAodGhpcy5tYXhDYWxsYmFja3MgPT09IC0xKSB7XHJcbiAgICAgICAgICAgICAgICByZXR1cm4gZmFsc2U7XHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICAgICAgLy8gRGVjcmVtZW50IG1heENhbGxiYWNrcy4gUmV0dXJuIHRydWUgaWYgbm93IDAsIG90aGVyd2lzZSBmYWxzZVxyXG4gICAgICAgICAgICB0aGlzLm1heENhbGxiYWNrcyAtPSAxO1xyXG4gICAgICAgICAgICByZXR1cm4gdGhpcy5tYXhDYWxsYmFja3MgPT09IDA7XHJcbiAgICAgICAgfTtcclxuICAgIH1cclxufVxyXG5cclxuZXhwb3J0IGNvbnN0IGV2ZW50TGlzdGVuZXJzID0ge307XHJcblxyXG4vKipcclxuICogUmVnaXN0ZXJzIGFuIGV2ZW50IGxpc3RlbmVyIHRoYXQgd2lsbCBiZSBpbnZva2VkIGBtYXhDYWxsYmFja3NgIHRpbWVzIGJlZm9yZSBiZWluZyBkZXN0cm95ZWRcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXHJcbiAqIEBwYXJhbSB7ZnVuY3Rpb259IGNhbGxiYWNrXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBtYXhDYWxsYmFja3NcclxuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBFdmVudHNPbk11bHRpcGxlKGV2ZW50TmFtZSwgY2FsbGJhY2ssIG1heENhbGxiYWNrcykge1xyXG4gICAgZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXSA9IGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0gfHwgW107XHJcbiAgICBjb25zdCB0aGlzTGlzdGVuZXIgPSBuZXcgTGlzdGVuZXIoZXZlbnROYW1lLCBjYWxsYmFjaywgbWF4Q2FsbGJhY2tzKTtcclxuICAgIGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0ucHVzaCh0aGlzTGlzdGVuZXIpO1xyXG4gICAgcmV0dXJuICgpID0+IGxpc3RlbmVyT2ZmKHRoaXNMaXN0ZW5lcik7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBSZWdpc3RlcnMgYW4gZXZlbnQgbGlzdGVuZXIgdGhhdCB3aWxsIGJlIGludm9rZWQgZXZlcnkgdGltZSB0aGUgZXZlbnQgaXMgZW1pdHRlZFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBldmVudE5hbWVcclxuICogQHBhcmFtIHtmdW5jdGlvbn0gY2FsbGJhY2tcclxuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBFdmVudHNPbihldmVudE5hbWUsIGNhbGxiYWNrKSB7XHJcbiAgICByZXR1cm4gRXZlbnRzT25NdWx0aXBsZShldmVudE5hbWUsIGNhbGxiYWNrLCAtMSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBSZWdpc3RlcnMgYW4gZXZlbnQgbGlzdGVuZXIgdGhhdCB3aWxsIGJlIGludm9rZWQgb25jZSB0aGVuIGRlc3Ryb3llZFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBldmVudE5hbWVcclxuICogQHBhcmFtIHtmdW5jdGlvbn0gY2FsbGJhY2tcclxuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBFdmVudHNPbmNlKGV2ZW50TmFtZSwgY2FsbGJhY2spIHtcclxuICAgIHJldHVybiBFdmVudHNPbk11bHRpcGxlKGV2ZW50TmFtZSwgY2FsbGJhY2ssIDEpO1xyXG59XHJcblxyXG5mdW5jdGlvbiBub3RpZnlMaXN0ZW5lcnMoZXZlbnREYXRhKSB7XHJcblxyXG4gICAgLy8gR2V0IHRoZSBldmVudCBuYW1lXHJcbiAgICBsZXQgZXZlbnROYW1lID0gZXZlbnREYXRhLm5hbWU7XHJcblxyXG4gICAgLy8gS2VlcCBhIGxpc3Qgb2YgbGlzdGVuZXIgaW5kZXhlcyB0byBkZXN0cm95XHJcbiAgICBjb25zdCBuZXdFdmVudExpc3RlbmVyTGlzdCA9IGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0/LnNsaWNlKCkgfHwgW107XHJcblxyXG4gICAgLy8gQ2hlY2sgaWYgd2UgaGF2ZSBhbnkgbGlzdGVuZXJzIGZvciB0aGlzIGV2ZW50XHJcbiAgICBpZiAobmV3RXZlbnRMaXN0ZW5lckxpc3QubGVuZ3RoKSB7XHJcblxyXG4gICAgICAgIC8vIEl0ZXJhdGUgbGlzdGVuZXJzXHJcbiAgICAgICAgZm9yIChsZXQgY291bnQgPSBuZXdFdmVudExpc3RlbmVyTGlzdC5sZW5ndGggLSAxOyBjb3VudCA+PSAwOyBjb3VudCAtPSAxKSB7XHJcblxyXG4gICAgICAgICAgICAvLyBHZXQgbmV4dCBsaXN0ZW5lclxyXG4gICAgICAgICAgICBjb25zdCBsaXN0ZW5lciA9IG5ld0V2ZW50TGlzdGVuZXJMaXN0W2NvdW50XTtcclxuXHJcbiAgICAgICAgICAgIGxldCBkYXRhID0gZXZlbnREYXRhLmRhdGE7XHJcblxyXG4gICAgICAgICAgICAvLyBEbyB0aGUgY2FsbGJhY2tcclxuICAgICAgICAgICAgY29uc3QgZGVzdHJveSA9IGxpc3RlbmVyLkNhbGxiYWNrKGRhdGEpO1xyXG4gICAgICAgICAgICBpZiAoZGVzdHJveSkge1xyXG4gICAgICAgICAgICAgICAgLy8gaWYgdGhlIGxpc3RlbmVyIGluZGljYXRlZCB0byBkZXN0cm95IGl0c2VsZiwgYWRkIGl0IHRvIHRoZSBkZXN0cm95IGxpc3RcclxuICAgICAgICAgICAgICAgIG5ld0V2ZW50TGlzdGVuZXJMaXN0LnNwbGljZShjb3VudCwgMSk7XHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICB9XHJcblxyXG4gICAgICAgIC8vIFVwZGF0ZSBjYWxsYmFja3Mgd2l0aCBuZXcgbGlzdCBvZiBsaXN0ZW5lcnNcclxuICAgICAgICBpZiAobmV3RXZlbnRMaXN0ZW5lckxpc3QubGVuZ3RoID09PSAwKSB7XHJcbiAgICAgICAgICAgIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSk7XHJcbiAgICAgICAgfSBlbHNlIHtcclxuICAgICAgICAgICAgZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXSA9IG5ld0V2ZW50TGlzdGVuZXJMaXN0O1xyXG4gICAgICAgIH1cclxuICAgIH1cclxufVxyXG5cclxuLyoqXHJcbiAqIE5vdGlmeSBpbmZvcm1zIGZyb250ZW5kIGxpc3RlbmVycyB0aGF0IGFuIGV2ZW50IHdhcyBlbWl0dGVkIHdpdGggdGhlIGdpdmVuIGRhdGFcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gbm90aWZ5TWVzc2FnZSAtIGVuY29kZWQgbm90aWZpY2F0aW9uIG1lc3NhZ2VcclxuXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gRXZlbnRzTm90aWZ5KG5vdGlmeU1lc3NhZ2UpIHtcclxuICAgIC8vIFBhcnNlIHRoZSBtZXNzYWdlXHJcbiAgICBsZXQgbWVzc2FnZTtcclxuICAgIHRyeSB7XHJcbiAgICAgICAgbWVzc2FnZSA9IEpTT04ucGFyc2Uobm90aWZ5TWVzc2FnZSk7XHJcbiAgICB9IGNhdGNoIChlKSB7XHJcbiAgICAgICAgY29uc3QgZXJyb3IgPSAnSW52YWxpZCBKU09OIHBhc3NlZCB0byBOb3RpZnk6ICcgKyBub3RpZnlNZXNzYWdlO1xyXG4gICAgICAgIHRocm93IG5ldyBFcnJvcihlcnJvcik7XHJcbiAgICB9XHJcbiAgICBub3RpZnlMaXN0ZW5lcnMobWVzc2FnZSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBFbWl0IGFuIGV2ZW50IHdpdGggdGhlIGdpdmVuIG5hbWUgYW5kIGRhdGFcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gRXZlbnRzRW1pdChldmVudE5hbWUpIHtcclxuXHJcbiAgICBjb25zdCBwYXlsb2FkID0ge1xyXG4gICAgICAgIG5hbWU6IGV2ZW50TmFtZSxcclxuICAgICAgICBkYXRhOiBbXS5zbGljZS5hcHBseShhcmd1bWVudHMpLnNsaWNlKDEpLFxyXG4gICAgfTtcclxuXHJcbiAgICAvLyBOb3RpZnkgSlMgbGlzdGVuZXJzXHJcbiAgICBub3RpZnlMaXN0ZW5lcnMocGF5bG9hZCk7XHJcblxyXG4gICAgLy8gTm90aWZ5IEdvIGxpc3RlbmVyc1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdFRScgKyBKU09OLnN0cmluZ2lmeShwYXlsb2FkKSk7XHJcbn1cclxuXHJcbmZ1bmN0aW9uIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSkge1xyXG4gICAgLy8gUmVtb3ZlIGxvY2FsIGxpc3RlbmVyc1xyXG4gICAgZGVsZXRlIGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV07XHJcblxyXG4gICAgLy8gTm90aWZ5IEdvIGxpc3RlbmVyc1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdFWCcgKyBldmVudE5hbWUpO1xyXG59XHJcblxyXG4vKipcclxuICogT2ZmIHVucmVnaXN0ZXJzIGEgbGlzdGVuZXIgcHJldmlvdXNseSByZWdpc3RlcmVkIHdpdGggT24sXHJcbiAqIG9wdGlvbmFsbHkgbXVsdGlwbGUgbGlzdGVuZXJlcyBjYW4gYmUgdW5yZWdpc3RlcmVkIHZpYSBgYWRkaXRpb25hbEV2ZW50TmFtZXNgXHJcbiAqXHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBldmVudE5hbWVcclxuICogQHBhcmFtICB7Li4uc3RyaW5nfSBhZGRpdGlvbmFsRXZlbnROYW1lc1xyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09mZihldmVudE5hbWUsIC4uLmFkZGl0aW9uYWxFdmVudE5hbWVzKSB7XHJcbiAgICByZW1vdmVMaXN0ZW5lcihldmVudE5hbWUpXHJcblxyXG4gICAgaWYgKGFkZGl0aW9uYWxFdmVudE5hbWVzLmxlbmd0aCA+IDApIHtcclxuICAgICAgICBhZGRpdGlvbmFsRXZlbnROYW1lcy5mb3JFYWNoKGV2ZW50TmFtZSA9PiB7XHJcbiAgICAgICAgICAgIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSlcclxuICAgICAgICB9KVxyXG4gICAgfVxyXG59XHJcblxyXG4vKipcclxuICogT2ZmIHVucmVnaXN0ZXJzIGFsbCBldmVudCBsaXN0ZW5lcnMgcHJldmlvdXNseSByZWdpc3RlcmVkIHdpdGggT25cclxuICovXHJcbiBleHBvcnQgZnVuY3Rpb24gRXZlbnRzT2ZmQWxsKCkge1xyXG4gICAgY29uc3QgZXZlbnROYW1lcyA9IE9iamVjdC5rZXlzKGV2ZW50TGlzdGVuZXJzKTtcclxuICAgIGV2ZW50TmFtZXMuZm9yRWFjaChldmVudE5hbWUgPT4ge1xyXG4gICAgICAgIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSlcclxuICAgIH0pXHJcbn1cclxuXHJcbi8qKlxyXG4gKiBsaXN0ZW5lck9mZiB1bnJlZ2lzdGVycyBhIGxpc3RlbmVyIHByZXZpb3VzbHkgcmVnaXN0ZXJlZCB3aXRoIEV2ZW50c09uXHJcbiAqXHJcbiAqIEBwYXJhbSB7TGlzdGVuZXJ9IGxpc3RlbmVyXHJcbiAqL1xyXG4gZnVuY3Rpb24gbGlzdGVuZXJPZmYobGlzdGVuZXIpIHtcclxuICAgIGNvbnN0IGV2ZW50TmFtZSA9IGxpc3RlbmVyLmV2ZW50TmFtZTtcclxuICAgIGlmIChldmVudExpc3RlbmVyc1tldmVudE5hbWVdID09PSB1bmRlZmluZWQpIHJldHVybjtcclxuXHJcbiAgICAvLyBSZW1vdmUgbG9jYWwgbGlzdGVuZXJcclxuICAgIGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0gPSBldmVudExpc3RlbmVyc1tldmVudE5hbWVdLmZpbHRlcihsID0+IGwgIT09IGxpc3RlbmVyKTtcclxuXHJcbiAgICAvLyBDbGVhbiB1cCBpZiB0aGVyZSBhcmUgbm8gZXZlbnQgbGlzdGVuZXJzIGxlZnRcclxuICAgIGlmIChldmVudExpc3RlbmVyc1tldmVudE5hbWVdLmxlbmd0aCA9PT0gMCkge1xyXG4gICAgICAgIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSk7XHJcbiAgICB9XHJcbn1cclxuIiwgIi8qXHJcbiBfICAgICAgIF9fICAgICAgXyBfX1xyXG58IHwgICAgIC8gL19fXyBfKF8pIC9fX19fXHJcbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cclxufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXHJcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xyXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXHJcbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcclxuKi9cclxuLyoganNoaW50IGVzdmVyc2lvbjogNiAqL1xyXG5cclxuZXhwb3J0IGNvbnN0IGNhbGxiYWNrcyA9IHt9O1xyXG5cclxuLyoqXHJcbiAqIFJldHVybnMgYSBudW1iZXIgZnJvbSB0aGUgbmF0aXZlIGJyb3dzZXIgcmFuZG9tIGZ1bmN0aW9uXHJcbiAqXHJcbiAqIEByZXR1cm5zIG51bWJlclxyXG4gKi9cclxuZnVuY3Rpb24gY3J5cHRvUmFuZG9tKCkge1xyXG5cdHZhciBhcnJheSA9IG5ldyBVaW50MzJBcnJheSgxKTtcclxuXHRyZXR1cm4gd2luZG93LmNyeXB0by5nZXRSYW5kb21WYWx1ZXMoYXJyYXkpWzBdO1xyXG59XHJcblxyXG4vKipcclxuICogUmV0dXJucyBhIG51bWJlciB1c2luZyBkYSBvbGQtc2tvb2wgTWF0aC5SYW5kb21cclxuICogSSBsaWtlcyB0byBjYWxsIGl0IExPTFJhbmRvbVxyXG4gKlxyXG4gKiBAcmV0dXJucyBudW1iZXJcclxuICovXHJcbmZ1bmN0aW9uIGJhc2ljUmFuZG9tKCkge1xyXG5cdHJldHVybiBNYXRoLnJhbmRvbSgpICogOTAwNzE5OTI1NDc0MDk5MTtcclxufVxyXG5cclxuLy8gUGljayBhIHJhbmRvbSBudW1iZXIgZnVuY3Rpb24gYmFzZWQgb24gYnJvd3NlciBjYXBhYmlsaXR5XHJcbnZhciByYW5kb21GdW5jO1xyXG5pZiAod2luZG93LmNyeXB0bykge1xyXG5cdHJhbmRvbUZ1bmMgPSBjcnlwdG9SYW5kb207XHJcbn0gZWxzZSB7XHJcblx0cmFuZG9tRnVuYyA9IGJhc2ljUmFuZG9tO1xyXG59XHJcblxyXG5cclxuLyoqXHJcbiAqIENhbGwgc2VuZHMgYSBtZXNzYWdlIHRvIHRoZSBiYWNrZW5kIHRvIGNhbGwgdGhlIGJpbmRpbmcgd2l0aCB0aGVcclxuICogZ2l2ZW4gZGF0YS4gQSBwcm9taXNlIGlzIHJldHVybmVkIGFuZCB3aWxsIGJlIGNvbXBsZXRlZCB3aGVuIHRoZVxyXG4gKiBiYWNrZW5kIHJlc3BvbmRzLiBUaGlzIHdpbGwgYmUgcmVzb2x2ZWQgd2hlbiB0aGUgY2FsbCB3YXMgc3VjY2Vzc2Z1bFxyXG4gKiBvciByZWplY3RlZCBpZiBhbiBlcnJvciBpcyBwYXNzZWQgYmFjay5cclxuICogVGhlcmUgaXMgYSB0aW1lb3V0IG1lY2hhbmlzbS4gSWYgdGhlIGNhbGwgZG9lc24ndCByZXNwb25kIGluIHRoZSBnaXZlblxyXG4gKiB0aW1lIChpbiBtaWxsaXNlY29uZHMpIHRoZW4gdGhlIHByb21pc2UgaXMgcmVqZWN0ZWQuXHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHBhcmFtIHtzdHJpbmd9IG5hbWVcclxuICogQHBhcmFtIHthbnk9fSBhcmdzXHJcbiAqIEBwYXJhbSB7bnVtYmVyPX0gdGltZW91dFxyXG4gKiBAcmV0dXJuc1xyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIENhbGwobmFtZSwgYXJncywgdGltZW91dCkge1xyXG5cclxuXHQvLyBUaW1lb3V0IGluZmluaXRlIGJ5IGRlZmF1bHRcclxuXHRpZiAodGltZW91dCA9PSBudWxsKSB7XHJcblx0XHR0aW1lb3V0ID0gMDtcclxuXHR9XHJcblxyXG5cdC8vIENyZWF0ZSBhIHByb21pc2VcclxuXHRyZXR1cm4gbmV3IFByb21pc2UoZnVuY3Rpb24gKHJlc29sdmUsIHJlamVjdCkge1xyXG5cclxuXHRcdC8vIENyZWF0ZSBhIHVuaXF1ZSBjYWxsYmFja0lEXHJcblx0XHR2YXIgY2FsbGJhY2tJRDtcclxuXHRcdGRvIHtcclxuXHRcdFx0Y2FsbGJhY2tJRCA9IG5hbWUgKyAnLScgKyByYW5kb21GdW5jKCk7XHJcblx0XHR9IHdoaWxlIChjYWxsYmFja3NbY2FsbGJhY2tJRF0pO1xyXG5cclxuXHRcdHZhciB0aW1lb3V0SGFuZGxlO1xyXG5cdFx0Ly8gU2V0IHRpbWVvdXRcclxuXHRcdGlmICh0aW1lb3V0ID4gMCkge1xyXG5cdFx0XHR0aW1lb3V0SGFuZGxlID0gc2V0VGltZW91dChmdW5jdGlvbiAoKSB7XHJcblx0XHRcdFx0cmVqZWN0KEVycm9yKCdDYWxsIHRvICcgKyBuYW1lICsgJyB0aW1lZCBvdXQuIFJlcXVlc3QgSUQ6ICcgKyBjYWxsYmFja0lEKSk7XHJcblx0XHRcdH0sIHRpbWVvdXQpO1xyXG5cdFx0fVxyXG5cclxuXHRcdC8vIFN0b3JlIGNhbGxiYWNrXHJcblx0XHRjYWxsYmFja3NbY2FsbGJhY2tJRF0gPSB7XHJcblx0XHRcdHRpbWVvdXRIYW5kbGU6IHRpbWVvdXRIYW5kbGUsXHJcblx0XHRcdHJlamVjdDogcmVqZWN0LFxyXG5cdFx0XHRyZXNvbHZlOiByZXNvbHZlXHJcblx0XHR9O1xyXG5cclxuXHRcdHRyeSB7XHJcblx0XHRcdGNvbnN0IHBheWxvYWQgPSB7XHJcblx0XHRcdFx0bmFtZSxcclxuXHRcdFx0XHRhcmdzLFxyXG5cdFx0XHRcdGNhbGxiYWNrSUQsXHJcblx0XHRcdH07XHJcblxyXG4gICAgICAgICAgICAvLyBNYWtlIHRoZSBjYWxsXHJcbiAgICAgICAgICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnQycgKyBKU09OLnN0cmluZ2lmeShwYXlsb2FkKSk7XHJcbiAgICAgICAgfSBjYXRjaCAoZSkge1xyXG4gICAgICAgICAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmVcclxuICAgICAgICAgICAgY29uc29sZS5lcnJvcihlKTtcclxuICAgICAgICB9XHJcbiAgICB9KTtcclxufVxyXG5cclxud2luZG93Lk9iZnVzY2F0ZWRDYWxsID0gKGlkLCBhcmdzLCB0aW1lb3V0KSA9PiB7XHJcblxyXG4gICAgLy8gVGltZW91dCBpbmZpbml0ZSBieSBkZWZhdWx0XHJcbiAgICBpZiAodGltZW91dCA9PSBudWxsKSB7XHJcbiAgICAgICAgdGltZW91dCA9IDA7XHJcbiAgICB9XHJcblxyXG4gICAgLy8gQ3JlYXRlIGEgcHJvbWlzZVxyXG4gICAgcmV0dXJuIG5ldyBQcm9taXNlKGZ1bmN0aW9uIChyZXNvbHZlLCByZWplY3QpIHtcclxuXHJcbiAgICAgICAgLy8gQ3JlYXRlIGEgdW5pcXVlIGNhbGxiYWNrSURcclxuICAgICAgICB2YXIgY2FsbGJhY2tJRDtcclxuICAgICAgICBkbyB7XHJcbiAgICAgICAgICAgIGNhbGxiYWNrSUQgPSBpZCArICctJyArIHJhbmRvbUZ1bmMoKTtcclxuICAgICAgICB9IHdoaWxlIChjYWxsYmFja3NbY2FsbGJhY2tJRF0pO1xyXG5cclxuICAgICAgICB2YXIgdGltZW91dEhhbmRsZTtcclxuICAgICAgICAvLyBTZXQgdGltZW91dFxyXG4gICAgICAgIGlmICh0aW1lb3V0ID4gMCkge1xyXG4gICAgICAgICAgICB0aW1lb3V0SGFuZGxlID0gc2V0VGltZW91dChmdW5jdGlvbiAoKSB7XHJcbiAgICAgICAgICAgICAgICByZWplY3QoRXJyb3IoJ0NhbGwgdG8gbWV0aG9kICcgKyBpZCArICcgdGltZWQgb3V0LiBSZXF1ZXN0IElEOiAnICsgY2FsbGJhY2tJRCkpO1xyXG4gICAgICAgICAgICB9LCB0aW1lb3V0KTtcclxuICAgICAgICB9XHJcblxyXG4gICAgICAgIC8vIFN0b3JlIGNhbGxiYWNrXHJcbiAgICAgICAgY2FsbGJhY2tzW2NhbGxiYWNrSURdID0ge1xyXG4gICAgICAgICAgICB0aW1lb3V0SGFuZGxlOiB0aW1lb3V0SGFuZGxlLFxyXG4gICAgICAgICAgICByZWplY3Q6IHJlamVjdCxcclxuICAgICAgICAgICAgcmVzb2x2ZTogcmVzb2x2ZVxyXG4gICAgICAgIH07XHJcblxyXG4gICAgICAgIHRyeSB7XHJcbiAgICAgICAgICAgIGNvbnN0IHBheWxvYWQgPSB7XHJcblx0XHRcdFx0aWQsXHJcblx0XHRcdFx0YXJncyxcclxuXHRcdFx0XHRjYWxsYmFja0lELFxyXG5cdFx0XHR9O1xyXG5cclxuICAgICAgICAgICAgLy8gTWFrZSB0aGUgY2FsbFxyXG4gICAgICAgICAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ2MnICsgSlNPTi5zdHJpbmdpZnkocGF5bG9hZCkpO1xyXG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcclxuICAgICAgICAgICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lXHJcbiAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoZSk7XHJcbiAgICAgICAgfVxyXG4gICAgfSk7XHJcbn07XHJcblxyXG5cclxuLyoqXHJcbiAqIENhbGxlZCBieSB0aGUgYmFja2VuZCB0byByZXR1cm4gZGF0YSB0byBhIHByZXZpb3VzbHkgY2FsbGVkXHJcbiAqIGJpbmRpbmcgaW52b2NhdGlvblxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSBpbmNvbWluZ01lc3NhZ2VcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBDYWxsYmFjayhpbmNvbWluZ01lc3NhZ2UpIHtcclxuXHQvLyBQYXJzZSB0aGUgbWVzc2FnZVxyXG5cdGxldCBtZXNzYWdlO1xyXG5cdHRyeSB7XHJcblx0XHRtZXNzYWdlID0gSlNPTi5wYXJzZShpbmNvbWluZ01lc3NhZ2UpO1xyXG5cdH0gY2F0Y2ggKGUpIHtcclxuXHRcdGNvbnN0IGVycm9yID0gYEludmFsaWQgSlNPTiBwYXNzZWQgdG8gY2FsbGJhY2s6ICR7ZS5tZXNzYWdlfS4gTWVzc2FnZTogJHtpbmNvbWluZ01lc3NhZ2V9YDtcclxuXHRcdHJ1bnRpbWUuTG9nRGVidWcoZXJyb3IpO1xyXG5cdFx0dGhyb3cgbmV3IEVycm9yKGVycm9yKTtcclxuXHR9XHJcblx0bGV0IGNhbGxiYWNrSUQgPSBtZXNzYWdlLmNhbGxiYWNraWQ7XHJcblx0bGV0IGNhbGxiYWNrRGF0YSA9IGNhbGxiYWNrc1tjYWxsYmFja0lEXTtcclxuXHRpZiAoIWNhbGxiYWNrRGF0YSkge1xyXG5cdFx0Y29uc3QgZXJyb3IgPSBgQ2FsbGJhY2sgJyR7Y2FsbGJhY2tJRH0nIG5vdCByZWdpc3RlcmVkISEhYDtcclxuXHRcdGNvbnNvbGUuZXJyb3IoZXJyb3IpOyAvLyBlc2xpbnQtZGlzYWJsZS1saW5lXHJcblx0XHR0aHJvdyBuZXcgRXJyb3IoZXJyb3IpO1xyXG5cdH1cclxuXHRjbGVhclRpbWVvdXQoY2FsbGJhY2tEYXRhLnRpbWVvdXRIYW5kbGUpO1xyXG5cclxuXHRkZWxldGUgY2FsbGJhY2tzW2NhbGxiYWNrSURdO1xyXG5cclxuXHRpZiAobWVzc2FnZS5lcnJvcikge1xyXG5cdFx0Y2FsbGJhY2tEYXRhLnJlamVjdChtZXNzYWdlLmVycm9yKTtcclxuXHR9IGVsc2Uge1xyXG5cdFx0Y2FsbGJhY2tEYXRhLnJlc29sdmUobWVzc2FnZS5yZXN1bHQpO1xyXG5cdH1cclxufVxyXG4iLCAiLypcclxuIF8gICAgICAgX18gICAgICBfIF9fICAgIFxyXG58IHwgICAgIC8gL19fXyBfKF8pIC9fX19fXHJcbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cclxufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApIFxyXG58X18vfF9fL1xcX18sXy9fL18vX19fXy8gIFxyXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXHJcbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcclxuKi9cclxuLyoganNoaW50IGVzdmVyc2lvbjogNiAqL1xyXG5cclxuaW1wb3J0IHtDYWxsfSBmcm9tICcuL2NhbGxzJztcclxuXHJcbi8vIFRoaXMgaXMgd2hlcmUgd2UgYmluZCBnbyBtZXRob2Qgd3JhcHBlcnNcclxud2luZG93LmdvID0ge307XHJcblxyXG5leHBvcnQgZnVuY3Rpb24gU2V0QmluZGluZ3MoYmluZGluZ3NNYXApIHtcclxuXHR0cnkge1xyXG5cdFx0YmluZGluZ3NNYXAgPSBKU09OLnBhcnNlKGJpbmRpbmdzTWFwKTtcclxuXHR9IGNhdGNoIChlKSB7XHJcblx0XHRjb25zb2xlLmVycm9yKGUpO1xyXG5cdH1cclxuXHJcblx0Ly8gSW5pdGlhbGlzZSB0aGUgYmluZGluZ3MgbWFwXHJcblx0d2luZG93LmdvID0gd2luZG93LmdvIHx8IHt9O1xyXG5cclxuXHQvLyBJdGVyYXRlIHBhY2thZ2UgbmFtZXNcclxuXHRPYmplY3Qua2V5cyhiaW5kaW5nc01hcCkuZm9yRWFjaCgocGFja2FnZU5hbWUpID0+IHtcclxuXHJcblx0XHQvLyBDcmVhdGUgaW5uZXIgbWFwIGlmIGl0IGRvZXNuJ3QgZXhpc3RcclxuXHRcdHdpbmRvdy5nb1twYWNrYWdlTmFtZV0gPSB3aW5kb3cuZ29bcGFja2FnZU5hbWVdIHx8IHt9O1xyXG5cclxuXHRcdC8vIEl0ZXJhdGUgc3RydWN0IG5hbWVzXHJcblx0XHRPYmplY3Qua2V5cyhiaW5kaW5nc01hcFtwYWNrYWdlTmFtZV0pLmZvckVhY2goKHN0cnVjdE5hbWUpID0+IHtcclxuXHJcblx0XHRcdC8vIENyZWF0ZSBpbm5lciBtYXAgaWYgaXQgZG9lc24ndCBleGlzdFxyXG5cdFx0XHR3aW5kb3cuZ29bcGFja2FnZU5hbWVdW3N0cnVjdE5hbWVdID0gd2luZG93LmdvW3BhY2thZ2VOYW1lXVtzdHJ1Y3ROYW1lXSB8fCB7fTtcclxuXHJcblx0XHRcdE9iamVjdC5rZXlzKGJpbmRpbmdzTWFwW3BhY2thZ2VOYW1lXVtzdHJ1Y3ROYW1lXSkuZm9yRWFjaCgobWV0aG9kTmFtZSkgPT4ge1xyXG5cclxuXHRcdFx0XHR3aW5kb3cuZ29bcGFja2FnZU5hbWVdW3N0cnVjdE5hbWVdW21ldGhvZE5hbWVdID0gZnVuY3Rpb24gKCkge1xyXG5cclxuXHRcdFx0XHRcdC8vIE5vIHRpbWVvdXQgYnkgZGVmYXVsdFxyXG5cdFx0XHRcdFx0bGV0IHRpbWVvdXQgPSAwO1xyXG5cclxuXHRcdFx0XHRcdC8vIEFjdHVhbCBmdW5jdGlvblxyXG5cdFx0XHRcdFx0ZnVuY3Rpb24gZHluYW1pYygpIHtcclxuXHRcdFx0XHRcdFx0Y29uc3QgYXJncyA9IFtdLnNsaWNlLmNhbGwoYXJndW1lbnRzKTtcclxuXHRcdFx0XHRcdFx0cmV0dXJuIENhbGwoW3BhY2thZ2VOYW1lLCBzdHJ1Y3ROYW1lLCBtZXRob2ROYW1lXS5qb2luKCcuJyksIGFyZ3MsIHRpbWVvdXQpO1xyXG5cdFx0XHRcdFx0fVxyXG5cclxuXHRcdFx0XHRcdC8vIEFsbG93IHNldHRpbmcgdGltZW91dCB0byBmdW5jdGlvblxyXG5cdFx0XHRcdFx0ZHluYW1pYy5zZXRUaW1lb3V0ID0gZnVuY3Rpb24gKG5ld1RpbWVvdXQpIHtcclxuXHRcdFx0XHRcdFx0dGltZW91dCA9IG5ld1RpbWVvdXQ7XHJcblx0XHRcdFx0XHR9O1xyXG5cclxuXHRcdFx0XHRcdC8vIEFsbG93IGdldHRpbmcgdGltZW91dCB0byBmdW5jdGlvblxyXG5cdFx0XHRcdFx0ZHluYW1pYy5nZXRUaW1lb3V0ID0gZnVuY3Rpb24gKCkge1xyXG5cdFx0XHRcdFx0XHRyZXR1cm4gdGltZW91dDtcclxuXHRcdFx0XHRcdH07XHJcblxyXG5cdFx0XHRcdFx0cmV0dXJuIGR5bmFtaWM7XHJcblx0XHRcdFx0fSgpO1xyXG5cdFx0XHR9KTtcclxuXHRcdH0pO1xyXG5cdH0pO1xyXG59XHJcbiIsICIvKlxyXG4gX1x0ICAgX19cdCAgXyBfX1xyXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xyXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXHJcbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKVxyXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cclxuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xyXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XHJcbiovXHJcblxyXG4vKiBqc2hpbnQgZXN2ZXJzaW9uOiA5ICovXHJcblxyXG5cclxuaW1wb3J0IHtDYWxsfSBmcm9tIFwiLi9jYWxsc1wiO1xyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1JlbG9hZCgpIHtcclxuICAgIHdpbmRvdy5sb2NhdGlvbi5yZWxvYWQoKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1JlbG9hZEFwcCgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV1InKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFN5c3RlbURlZmF1bHRUaGVtZSgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0FTRFQnKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldExpZ2h0VGhlbWUoKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dBTFQnKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldERhcmtUaGVtZSgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0FEVCcpO1xyXG59XHJcblxyXG4vKipcclxuICogUGxhY2UgdGhlIHdpbmRvdyBpbiB0aGUgY2VudGVyIG9mIHRoZSBzY3JlZW5cclxuICpcclxuICogQGV4cG9ydFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd0NlbnRlcigpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV2MnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFNldHMgdGhlIHdpbmRvdyB0aXRsZVxyXG4gKlxyXG4gKiBAcGFyYW0ge3N0cmluZ30gdGl0bGVcclxuICogQGV4cG9ydFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFRpdGxlKHRpdGxlKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dUJyArIHRpdGxlKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIE1ha2VzIHRoZSB3aW5kb3cgZ28gZnVsbHNjcmVlblxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93RnVsbHNjcmVlbigpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0YnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFJldmVydHMgdGhlIHdpbmRvdyBmcm9tIGZ1bGxzY3JlZW5cclxuICpcclxuICogQGV4cG9ydFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1VuZnVsbHNjcmVlbigpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV2YnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFJldHVybnMgdGhlIHN0YXRlIG9mIHRoZSB3aW5kb3csIGkuZS4gd2hldGhlciB0aGUgd2luZG93IGlzIGluIGZ1bGwgc2NyZWVuIG1vZGUgb3Igbm90LlxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEByZXR1cm4ge1Byb21pc2U8Ym9vbGVhbj59IFRoZSBzdGF0ZSBvZiB0aGUgd2luZG93XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93SXNGdWxsc2NyZWVuKCkge1xyXG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6V2luZG93SXNGdWxsc2NyZWVuXCIpO1xyXG59XHJcblxyXG4vKipcclxuICogU2V0IHRoZSBTaXplIG9mIHRoZSB3aW5kb3dcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcGFyYW0ge251bWJlcn0gd2lkdGhcclxuICogQHBhcmFtIHtudW1iZXJ9IGhlaWdodFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFNpemUod2lkdGgsIGhlaWdodCkge1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXczonICsgd2lkdGggKyAnOicgKyBoZWlnaHQpO1xyXG59XHJcblxyXG4vKipcclxuICogR2V0IHRoZSBTaXplIG9mIHRoZSB3aW5kb3dcclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAcmV0dXJuIHtQcm9taXNlPHt3OiBudW1iZXIsIGg6IG51bWJlcn0+fSBUaGUgc2l6ZSBvZiB0aGUgd2luZG93XHJcblxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd0dldFNpemUoKSB7XHJcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dHZXRTaXplXCIpO1xyXG59XHJcblxyXG4vKipcclxuICogU2V0IHRoZSBtYXhpbXVtIHNpemUgb2YgdGhlIHdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB3aWR0aFxyXG4gKiBAcGFyYW0ge251bWJlcn0gaGVpZ2h0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2V0TWF4U2l6ZSh3aWR0aCwgaGVpZ2h0KSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1daOicgKyB3aWR0aCArICc6JyArIGhlaWdodCk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBTZXQgdGhlIG1pbmltdW0gc2l6ZSBvZiB0aGUgd2luZG93XHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHBhcmFtIHtudW1iZXJ9IHdpZHRoXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBoZWlnaHRcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dTZXRNaW5TaXplKHdpZHRoLCBoZWlnaHQpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV3o6JyArIHdpZHRoICsgJzonICsgaGVpZ2h0KTtcclxufVxyXG5cclxuXHJcblxyXG4vKipcclxuICogU2V0IHRoZSB3aW5kb3cgQWx3YXlzT25Ub3Agb3Igbm90IG9uIHRvcFxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2V0QWx3YXlzT25Ub3AoYikge1xyXG5cclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0FUUDonICsgKGIgPyAnMScgOiAnMCcpKTtcclxufVxyXG5cclxuXHJcblxyXG5cclxuLyoqXHJcbiAqIFNldCB0aGUgUG9zaXRpb24gb2YgdGhlIHdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB4XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB5XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2V0UG9zaXRpb24oeCwgeSkge1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXcDonICsgeCArICc6JyArIHkpO1xyXG59XHJcblxyXG4vKipcclxuICogR2V0IHRoZSBQb3NpdGlvbiBvZiB0aGUgd2luZG93XHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHJldHVybiB7UHJvbWlzZTx7eDogbnVtYmVyLCB5OiBudW1iZXJ9Pn0gVGhlIHBvc2l0aW9uIG9mIHRoZSB3aW5kb3dcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dHZXRQb3NpdGlvbigpIHtcclxuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOldpbmRvd0dldFBvc1wiKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIEhpZGUgdGhlIFdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93SGlkZSgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0gnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFNob3cgdGhlIFdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2hvdygpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV1MnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIE1heGltaXNlIHRoZSBXaW5kb3dcclxuICpcclxuICogQGV4cG9ydFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd01heGltaXNlKCkge1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXTScpO1xyXG59XHJcblxyXG4vKipcclxuICogVG9nZ2xlIHRoZSBNYXhpbWlzZSBvZiB0aGUgV2luZG93XHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dUb2dnbGVNYXhpbWlzZSgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV3QnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFVubWF4aW1pc2UgdGhlIFdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93VW5tYXhpbWlzZSgpIHtcclxuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV1UnKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFJldHVybnMgdGhlIHN0YXRlIG9mIHRoZSB3aW5kb3csIGkuZS4gd2hldGhlciB0aGUgd2luZG93IGlzIG1heGltaXNlZCBvciBub3QuXHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHJldHVybiB7UHJvbWlzZTxib29sZWFuPn0gVGhlIHN0YXRlIG9mIHRoZSB3aW5kb3dcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dJc01heGltaXNlZCgpIHtcclxuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOldpbmRvd0lzTWF4aW1pc2VkXCIpO1xyXG59XHJcblxyXG4vKipcclxuICogTWluaW1pc2UgdGhlIFdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93TWluaW1pc2UoKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dtJyk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBVbm1pbmltaXNlIHRoZSBXaW5kb3dcclxuICpcclxuICogQGV4cG9ydFxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1VubWluaW1pc2UoKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1d1Jyk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBSZXR1cm5zIHRoZSBzdGF0ZSBvZiB0aGUgd2luZG93LCBpLmUuIHdoZXRoZXIgdGhlIHdpbmRvdyBpcyBtaW5pbWlzZWQgb3Igbm90LlxyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEByZXR1cm4ge1Byb21pc2U8Ym9vbGVhbj59IFRoZSBzdGF0ZSBvZiB0aGUgd2luZG93XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93SXNNaW5pbWlzZWQoKSB7XHJcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dJc01pbmltaXNlZFwiKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIFJldHVybnMgdGhlIHN0YXRlIG9mIHRoZSB3aW5kb3csIGkuZS4gd2hldGhlciB0aGUgd2luZG93IGlzIG5vcm1hbCBvciBub3QuXHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHJldHVybiB7UHJvbWlzZTxib29sZWFuPn0gVGhlIHN0YXRlIG9mIHRoZSB3aW5kb3dcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dJc05vcm1hbCgpIHtcclxuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOldpbmRvd0lzTm9ybWFsXCIpO1xyXG59XHJcblxyXG4vKipcclxuICogU2V0cyB0aGUgYmFja2dyb3VuZCBjb2xvdXIgb2YgdGhlIHdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBSIFJlZFxyXG4gKiBAcGFyYW0ge251bWJlcn0gRyBHcmVlblxyXG4gKiBAcGFyYW0ge251bWJlcn0gQiBCbHVlXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBBIEFscGhhXHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2V0QmFja2dyb3VuZENvbG91cihSLCBHLCBCLCBBKSB7XHJcbiAgICBsZXQgcmdiYSA9IEpTT04uc3RyaW5naWZ5KHtyOiBSIHx8IDAsIGc6IEcgfHwgMCwgYjogQiB8fCAwLCBhOiBBIHx8IDI1NX0pO1xyXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXcjonICsgcmdiYSk7XHJcbn1cclxuXHJcbiIsICIvKlxyXG4gX1x0ICAgX19cdCAgXyBfX1xyXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xyXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXHJcbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKVxyXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cclxuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xyXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XHJcbiovXHJcblxyXG4vKiBqc2hpbnQgZXN2ZXJzaW9uOiA5ICovXHJcblxyXG5cclxuaW1wb3J0IHtDYWxsfSBmcm9tIFwiLi9jYWxsc1wiO1xyXG5cclxuXHJcbi8qKlxyXG4gKiBHZXRzIHRoZSBhbGwgc2NyZWVucy4gQ2FsbCB0aGlzIGFuZXcgZWFjaCB0aW1lIHlvdSB3YW50IHRvIHJlZnJlc2ggZGF0YSBmcm9tIHRoZSB1bmRlcmx5aW5nIHdpbmRvd2luZyBzeXN0ZW0uXHJcbiAqIEBleHBvcnRcclxuICogQHR5cGVkZWYge2ltcG9ydCgnLi4vd3JhcHBlci9ydW50aW1lJykuU2NyZWVufSBTY3JlZW5cclxuICogQHJldHVybiB7UHJvbWlzZTx7U2NyZWVuW119Pn0gVGhlIHNjcmVlbnNcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBTY3JlZW5HZXRBbGwoKSB7XHJcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpTY3JlZW5HZXRBbGxcIik7XHJcbn1cclxuIiwgIi8qKlxyXG4gKiBAZGVzY3JpcHRpb246IFVzZSB0aGUgc3lzdGVtIGRlZmF1bHQgYnJvd3NlciB0byBvcGVuIHRoZSB1cmxcclxuICogQHBhcmFtIHtzdHJpbmd9IHVybCBcclxuICogQHJldHVybiB7dm9pZH1cclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBCcm93c2VyT3BlblVSTCh1cmwpIHtcclxuICB3aW5kb3cuV2FpbHNJbnZva2UoJ0JPOicgKyB1cmwpO1xyXG59IiwgIi8qXHJcbiBfXHQgICBfX1x0ICBfIF9fXHJcbnwgfFx0IC8gL19fXyBfKF8pIC9fX19fXHJcbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cclxufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXHJcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xyXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXHJcbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcclxuKi9cclxuXHJcbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cclxuXHJcbmltcG9ydCB7Q2FsbH0gZnJvbSBcIi4vY2FsbHNcIjtcclxuXHJcbi8qKlxyXG4gKiBTZXQgdGhlIFNpemUgb2YgdGhlIHdpbmRvd1xyXG4gKlxyXG4gKiBAZXhwb3J0XHJcbiAqIEBwYXJhbSB7c3RyaW5nfSB0ZXh0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gQ2xpcGJvYXJkU2V0VGV4dCh0ZXh0KSB7XHJcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpDbGlwYm9hcmRTZXRUZXh0XCIsIFt0ZXh0XSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBHZXQgdGhlIHRleHQgY29udGVudCBvZiB0aGUgY2xpcGJvYXJkXHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHJldHVybiB7UHJvbWlzZTx7c3RyaW5nfT59IFRleHQgY29udGVudCBvZiB0aGUgY2xpcGJvYXJkXHJcblxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIENsaXBib2FyZEdldFRleHQoKSB7XHJcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpDbGlwYm9hcmRHZXRUZXh0XCIpO1xyXG59IiwgIi8qXHJcbiBfXHQgICBfX1x0ICBfIF9fXHJcbnwgfFx0IC8gL19fXyBfKF8pIC9fX19fXHJcbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cclxufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXHJcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xyXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXHJcbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcclxuKi9cclxuXHJcbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cclxuXHJcbmltcG9ydCB7RXZlbnRzT24sIEV2ZW50c09mZn0gZnJvbSBcIi4vZXZlbnRzXCI7XHJcblxyXG5jb25zdCBmbGFncyA9IHtcclxuICAgIHJlZ2lzdGVyZWQ6IGZhbHNlLFxyXG4gICAgZGVmYXVsdFVzZURyb3BUYXJnZXQ6IHRydWUsXHJcbiAgICB1c2VEcm9wVGFyZ2V0OiB0cnVlLFxyXG4gICAgbmV4dERlYWN0aXZhdGU6IG51bGwsXHJcbiAgICBuZXh0RGVhY3RpdmF0ZVRpbWVvdXQ6IG51bGwsXHJcbn07XHJcblxyXG5jb25zdCBEUk9QX1RBUkdFVF9BQ1RJVkUgPSBcIndhaWxzLWRyb3AtdGFyZ2V0LWFjdGl2ZVwiO1xyXG5cclxuLyoqXHJcbiAqIGNoZWNrU3R5bGVEcm9wVGFyZ2V0IGNoZWNrcyBpZiB0aGUgc3R5bGUgaGFzIHRoZSBkcm9wIHRhcmdldCBhdHRyaWJ1dGVcclxuICogXHJcbiAqIEBwYXJhbSB7Q1NTU3R5bGVEZWNsYXJhdGlvbn0gc3R5bGUgXHJcbiAqIEByZXR1cm5zIFxyXG4gKi9cclxuZnVuY3Rpb24gY2hlY2tTdHlsZURyb3BUYXJnZXQoc3R5bGUpIHtcclxuICAgIGNvbnN0IGNzc0Ryb3BWYWx1ZSA9IHN0eWxlLmdldFByb3BlcnR5VmFsdWUod2luZG93LndhaWxzLmZsYWdzLmNzc0Ryb3BQcm9wZXJ0eSkudHJpbSgpO1xyXG4gICAgaWYgKGNzc0Ryb3BWYWx1ZSkge1xyXG4gICAgICAgIGlmIChjc3NEcm9wVmFsdWUgPT09IHdpbmRvdy53YWlscy5mbGFncy5jc3NEcm9wVmFsdWUpIHtcclxuICAgICAgICAgICAgcmV0dXJuIHRydWU7XHJcbiAgICAgICAgfVxyXG4gICAgICAgIC8vIGlmIHRoZSBlbGVtZW50IGhhcyB0aGUgZHJvcCB0YXJnZXQgYXR0cmlidXRlLCBidXQgXHJcbiAgICAgICAgLy8gdGhlIHZhbHVlIGlzIG5vdCBjb3JyZWN0LCB0ZXJtaW5hdGUgZmluZGluZyBwcm9jZXNzLlxyXG4gICAgICAgIC8vIFRoaXMgY2FuIGJlIHVzZWZ1bCB0byBibG9jayBzb21lIGNoaWxkIGVsZW1lbnRzIGZyb20gYmVpbmcgZHJvcCB0YXJnZXRzLlxyXG4gICAgICAgIHJldHVybiBmYWxzZTtcclxuICAgIH1cclxuICAgIHJldHVybiBmYWxzZTtcclxufVxyXG5cclxuLyoqXHJcbiAqIG9uRHJhZ092ZXIgaXMgY2FsbGVkIHdoZW4gdGhlIGRyYWdvdmVyIGV2ZW50IGlzIGVtaXR0ZWQuXHJcbiAqIEBwYXJhbSB7RHJhZ0V2ZW50fSBlXHJcbiAqIEByZXR1cm5zXHJcbiAqL1xyXG5mdW5jdGlvbiBvbkRyYWdPdmVyKGUpIHtcclxuICAgIC8vIENoZWNrIGlmIHRoaXMgaXMgYW4gZXh0ZXJuYWwgZmlsZSBkcm9wIG9yIGludGVybmFsIEhUTUwgZHJhZ1xyXG4gICAgLy8gRXh0ZXJuYWwgZmlsZSBkcm9wcyB3aWxsIGhhdmUgXCJGaWxlc1wiIGluIHRoZSB0eXBlcyBhcnJheVxyXG4gICAgLy8gSW50ZXJuYWwgSFRNTCBkcmFncyB0eXBpY2FsbHkgaGF2ZSBcInRleHQvcGxhaW5cIiwgXCJ0ZXh0L2h0bWxcIiBvciBjdXN0b20gdHlwZXNcclxuICAgIGNvbnN0IGlzRmlsZURyb3AgPSBlLmRhdGFUcmFuc2Zlci50eXBlcy5pbmNsdWRlcyhcIkZpbGVzXCIpO1xyXG5cclxuICAgIC8vIE9ubHkgaGFuZGxlIGV4dGVybmFsIGZpbGUgZHJvcHMsIGxldCBpbnRlcm5hbCBIVE1MNSBkcmFnLWFuZC1kcm9wIHdvcmsgbm9ybWFsbHlcclxuICAgIGlmICghaXNGaWxlRHJvcCkge1xyXG4gICAgICAgIHJldHVybjtcclxuICAgIH1cclxuXHJcbiAgICAvLyBBTFdBWVMgcHJldmVudCBkZWZhdWx0IGZvciBmaWxlIGRyb3BzIHRvIHN0b3AgYnJvd3NlciBuYXZpZ2F0aW9uXHJcbiAgICBlLnByZXZlbnREZWZhdWx0KCk7XHJcbiAgICBlLmRhdGFUcmFuc2Zlci5kcm9wRWZmZWN0ID0gJ2NvcHknO1xyXG5cclxuICAgIGlmICghd2luZG93LndhaWxzLmZsYWdzLmVuYWJsZVdhaWxzRHJhZ0FuZERyb3ApIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKCFmbGFncy51c2VEcm9wVGFyZ2V0KSB7XHJcbiAgICAgICAgcmV0dXJuO1xyXG4gICAgfVxyXG5cclxuICAgIGNvbnN0IGVsZW1lbnQgPSBlLnRhcmdldDtcclxuXHJcbiAgICAvLyBUcmlnZ2VyIGRlYm91bmNlIGZ1bmN0aW9uIHRvIGRlYWN0aXZhdGUgZHJvcCB0YXJnZXRzXHJcbiAgICBpZihmbGFncy5uZXh0RGVhY3RpdmF0ZSkgZmxhZ3MubmV4dERlYWN0aXZhdGUoKTtcclxuXHJcbiAgICAvLyBpZiB0aGUgZWxlbWVudCBpcyBudWxsIG9yIGVsZW1lbnQgaXMgbm90IGNoaWxkIG9mIGRyb3AgdGFyZ2V0IGVsZW1lbnRcclxuICAgIGlmICghZWxlbWVudCB8fCAhY2hlY2tTdHlsZURyb3BUYXJnZXQoZ2V0Q29tcHV0ZWRTdHlsZShlbGVtZW50KSkpIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgbGV0IGN1cnJlbnRFbGVtZW50ID0gZWxlbWVudDtcclxuICAgIHdoaWxlIChjdXJyZW50RWxlbWVudCkge1xyXG4gICAgICAgIC8vIGNoZWNrIGlmIGN1cnJlbnRFbGVtZW50IGlzIGRyb3AgdGFyZ2V0IGVsZW1lbnRcclxuICAgICAgICBpZiAoY2hlY2tTdHlsZURyb3BUYXJnZXQoZ2V0Q29tcHV0ZWRTdHlsZShjdXJyZW50RWxlbWVudCkpKSB7XHJcbiAgICAgICAgICAgIGN1cnJlbnRFbGVtZW50LmNsYXNzTGlzdC5hZGQoRFJPUF9UQVJHRVRfQUNUSVZFKTtcclxuICAgICAgICB9XHJcbiAgICAgICAgY3VycmVudEVsZW1lbnQgPSBjdXJyZW50RWxlbWVudC5wYXJlbnRFbGVtZW50O1xyXG4gICAgfVxyXG59XHJcblxyXG4vKipcclxuICogb25EcmFnTGVhdmUgaXMgY2FsbGVkIHdoZW4gdGhlIGRyYWdsZWF2ZSBldmVudCBpcyBlbWl0dGVkLlxyXG4gKiBAcGFyYW0ge0RyYWdFdmVudH0gZVxyXG4gKiBAcmV0dXJuc1xyXG4gKi9cclxuZnVuY3Rpb24gb25EcmFnTGVhdmUoZSkge1xyXG4gICAgLy8gQ2hlY2sgaWYgdGhpcyBpcyBhbiBleHRlcm5hbCBmaWxlIGRyb3Agb3IgaW50ZXJuYWwgSFRNTCBkcmFnXHJcbiAgICBjb25zdCBpc0ZpbGVEcm9wID0gZS5kYXRhVHJhbnNmZXIudHlwZXMuaW5jbHVkZXMoXCJGaWxlc1wiKTtcclxuXHJcbiAgICAvLyBPbmx5IGhhbmRsZSBleHRlcm5hbCBmaWxlIGRyb3BzLCBsZXQgaW50ZXJuYWwgSFRNTDUgZHJhZy1hbmQtZHJvcCB3b3JrIG5vcm1hbGx5XHJcbiAgICBpZiAoIWlzRmlsZURyb3ApIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgLy8gQUxXQVlTIHByZXZlbnQgZGVmYXVsdCBmb3IgZmlsZSBkcm9wcyB0byBzdG9wIGJyb3dzZXIgbmF2aWdhdGlvblxyXG4gICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xyXG5cclxuICAgIGlmICghd2luZG93LndhaWxzLmZsYWdzLmVuYWJsZVdhaWxzRHJhZ0FuZERyb3ApIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKCFmbGFncy51c2VEcm9wVGFyZ2V0KSB7XHJcbiAgICAgICAgcmV0dXJuO1xyXG4gICAgfVxyXG5cclxuICAgIC8vIEZpbmQgdGhlIGNsb3NlIGRyb3AgdGFyZ2V0IGVsZW1lbnRcclxuICAgIGlmICghZS50YXJnZXQgfHwgIWNoZWNrU3R5bGVEcm9wVGFyZ2V0KGdldENvbXB1dGVkU3R5bGUoZS50YXJnZXQpKSkge1xyXG4gICAgICAgIHJldHVybiBudWxsO1xyXG4gICAgfVxyXG5cclxuICAgIC8vIFRyaWdnZXIgZGVib3VuY2UgZnVuY3Rpb24gdG8gZGVhY3RpdmF0ZSBkcm9wIHRhcmdldHNcclxuICAgIGlmKGZsYWdzLm5leHREZWFjdGl2YXRlKSBmbGFncy5uZXh0RGVhY3RpdmF0ZSgpO1xyXG4gICAgXHJcbiAgICAvLyBVc2UgZGVib3VuY2UgdGVjaG5pcXVlIHRvIHRhY2xlIGRyYWdsZWF2ZSBldmVudHMgb24gb3ZlcmxhcHBpbmcgZWxlbWVudHMgYW5kIGRyb3AgdGFyZ2V0IGVsZW1lbnRzXHJcbiAgICBmbGFncy5uZXh0RGVhY3RpdmF0ZSA9ICgpID0+IHtcclxuICAgICAgICAvLyBEZWFjdGl2YXRlIGFsbCBkcm9wIHRhcmdldHMsIG5ldyBkcm9wIHRhcmdldCB3aWxsIGJlIGFjdGl2YXRlZCBvbiBuZXh0IGRyYWdvdmVyIGV2ZW50XHJcbiAgICAgICAgQXJyYXkuZnJvbShkb2N1bWVudC5nZXRFbGVtZW50c0J5Q2xhc3NOYW1lKERST1BfVEFSR0VUX0FDVElWRSkpLmZvckVhY2goZWwgPT4gZWwuY2xhc3NMaXN0LnJlbW92ZShEUk9QX1RBUkdFVF9BQ1RJVkUpKTtcclxuICAgICAgICAvLyBSZXNldCBuZXh0RGVhY3RpdmF0ZVxyXG4gICAgICAgIGZsYWdzLm5leHREZWFjdGl2YXRlID0gbnVsbDtcclxuICAgICAgICAvLyBDbGVhciB0aW1lb3V0XHJcbiAgICAgICAgaWYgKGZsYWdzLm5leHREZWFjdGl2YXRlVGltZW91dCkge1xyXG4gICAgICAgICAgICBjbGVhclRpbWVvdXQoZmxhZ3MubmV4dERlYWN0aXZhdGVUaW1lb3V0KTtcclxuICAgICAgICAgICAgZmxhZ3MubmV4dERlYWN0aXZhdGVUaW1lb3V0ID0gbnVsbDtcclxuICAgICAgICB9XHJcbiAgICB9XHJcblxyXG4gICAgLy8gU2V0IHRpbWVvdXQgdG8gZGVhY3RpdmF0ZSBkcm9wIHRhcmdldHMgaWYgbm90IHRyaWdnZXJlZCBieSBuZXh0IGRyYWcgZXZlbnRcclxuICAgIGZsYWdzLm5leHREZWFjdGl2YXRlVGltZW91dCA9IHNldFRpbWVvdXQoKCkgPT4ge1xyXG4gICAgICAgIGlmKGZsYWdzLm5leHREZWFjdGl2YXRlKSBmbGFncy5uZXh0RGVhY3RpdmF0ZSgpO1xyXG4gICAgfSwgNTApO1xyXG59XHJcblxyXG4vKipcclxuICogb25Ecm9wIGlzIGNhbGxlZCB3aGVuIHRoZSBkcm9wIGV2ZW50IGlzIGVtaXR0ZWQuXHJcbiAqIEBwYXJhbSB7RHJhZ0V2ZW50fSBlXHJcbiAqIEByZXR1cm5zXHJcbiAqL1xyXG5mdW5jdGlvbiBvbkRyb3AoZSkge1xyXG4gICAgLy8gQ2hlY2sgaWYgdGhpcyBpcyBhbiBleHRlcm5hbCBmaWxlIGRyb3Agb3IgaW50ZXJuYWwgSFRNTCBkcmFnXHJcbiAgICBjb25zdCBpc0ZpbGVEcm9wID0gZS5kYXRhVHJhbnNmZXIudHlwZXMuaW5jbHVkZXMoXCJGaWxlc1wiKTtcclxuXHJcbiAgICAvLyBPbmx5IGhhbmRsZSBleHRlcm5hbCBmaWxlIGRyb3BzLCBsZXQgaW50ZXJuYWwgSFRNTDUgZHJhZy1hbmQtZHJvcCB3b3JrIG5vcm1hbGx5XHJcbiAgICBpZiAoIWlzRmlsZURyb3ApIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgLy8gQUxXQVlTIHByZXZlbnQgZGVmYXVsdCBmb3IgZmlsZSBkcm9wcyB0byBzdG9wIGJyb3dzZXIgbmF2aWdhdGlvblxyXG4gICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xyXG5cclxuICAgIGlmICghd2luZG93LndhaWxzLmZsYWdzLmVuYWJsZVdhaWxzRHJhZ0FuZERyb3ApIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKENhblJlc29sdmVGaWxlUGF0aHMoKSkge1xyXG4gICAgICAgIC8vIHByb2Nlc3MgZmlsZXNcclxuICAgICAgICBsZXQgZmlsZXMgPSBbXTtcclxuICAgICAgICBpZiAoZS5kYXRhVHJhbnNmZXIuaXRlbXMpIHtcclxuICAgICAgICAgICAgZmlsZXMgPSBbLi4uZS5kYXRhVHJhbnNmZXIuaXRlbXNdLm1hcCgoaXRlbSwgaSkgPT4ge1xyXG4gICAgICAgICAgICAgICAgaWYgKGl0ZW0ua2luZCA9PT0gJ2ZpbGUnKSB7XHJcbiAgICAgICAgICAgICAgICAgICAgcmV0dXJuIGl0ZW0uZ2V0QXNGaWxlKCk7XHJcbiAgICAgICAgICAgICAgICB9XHJcbiAgICAgICAgICAgIH0pO1xyXG4gICAgICAgIH0gZWxzZSB7XHJcbiAgICAgICAgICAgIGZpbGVzID0gWy4uLmUuZGF0YVRyYW5zZmVyLmZpbGVzXTtcclxuICAgICAgICB9XHJcbiAgICAgICAgd2luZG93LnJ1bnRpbWUuUmVzb2x2ZUZpbGVQYXRocyhlLngsIGUueSwgZmlsZXMpO1xyXG4gICAgfVxyXG5cclxuICAgIGlmICghZmxhZ3MudXNlRHJvcFRhcmdldCkge1xyXG4gICAgICAgIHJldHVybjtcclxuICAgIH1cclxuXHJcbiAgICAvLyBUcmlnZ2VyIGRlYm91bmNlIGZ1bmN0aW9uIHRvIGRlYWN0aXZhdGUgZHJvcCB0YXJnZXRzXHJcbiAgICBpZihmbGFncy5uZXh0RGVhY3RpdmF0ZSkgZmxhZ3MubmV4dERlYWN0aXZhdGUoKTtcclxuXHJcbiAgICAvLyBEZWFjdGl2YXRlIGFsbCBkcm9wIHRhcmdldHNcclxuICAgIEFycmF5LmZyb20oZG9jdW1lbnQuZ2V0RWxlbWVudHNCeUNsYXNzTmFtZShEUk9QX1RBUkdFVF9BQ1RJVkUpKS5mb3JFYWNoKGVsID0+IGVsLmNsYXNzTGlzdC5yZW1vdmUoRFJPUF9UQVJHRVRfQUNUSVZFKSk7XHJcbn1cclxuXHJcbi8qKlxyXG4gKiBwb3N0TWVzc2FnZVdpdGhBZGRpdGlvbmFsT2JqZWN0cyBjaGVja3MgdGhlIGJyb3dzZXIncyBjYXBhYmlsaXR5IG9mIHNlbmRpbmcgcG9zdE1lc3NhZ2VXaXRoQWRkaXRpb25hbE9iamVjdHNcclxuICpcclxuICogQHJldHVybnMge2Jvb2xlYW59XHJcbiAqIEBjb25zdHJ1Y3RvclxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIENhblJlc29sdmVGaWxlUGF0aHMoKSB7XHJcbiAgICByZXR1cm4gd2luZG93LmNocm9tZT8ud2Vidmlldz8ucG9zdE1lc3NhZ2VXaXRoQWRkaXRpb25hbE9iamVjdHMgIT0gbnVsbDtcclxufVxyXG5cclxuLyoqXHJcbiAqIFJlc29sdmVGaWxlUGF0aHMgc2VuZHMgZHJvcCBldmVudHMgdG8gdGhlIEdPIHNpZGUgdG8gcmVzb2x2ZSBmaWxlIHBhdGhzIG9uIHdpbmRvd3MuXHJcbiAqXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB4XHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB5XHJcbiAqIEBwYXJhbSB7YW55W119IGZpbGVzXHJcbiAqIEBjb25zdHJ1Y3RvclxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIFJlc29sdmVGaWxlUGF0aHMoeCwgeSwgZmlsZXMpIHtcclxuICAgIC8vIE9ubHkgZm9yIHdpbmRvd3Mgd2VidmlldzIgPj0gMS4wLjE3NzQuMzBcclxuICAgIC8vIGh0dHBzOi8vbGVhcm4ubWljcm9zb2Z0LmNvbS9lbi11cy9taWNyb3NvZnQtZWRnZS93ZWJ2aWV3Mi9yZWZlcmVuY2Uvd2luMzIvaWNvcmV3ZWJ2aWV3MndlYm1lc3NhZ2VyZWNlaXZlZGV2ZW50YXJnczI/dmlldz13ZWJ2aWV3Mi0xLjAuMTgyMy4zMiNhcHBsaWVzLXRvXHJcbiAgICBpZiAod2luZG93LmNocm9tZT8ud2Vidmlldz8ucG9zdE1lc3NhZ2VXaXRoQWRkaXRpb25hbE9iamVjdHMpIHtcclxuICAgICAgICBjaHJvbWUud2Vidmlldy5wb3N0TWVzc2FnZVdpdGhBZGRpdGlvbmFsT2JqZWN0cyhgZmlsZTpkcm9wOiR7eH06JHt5fWAsIGZpbGVzKTtcclxuICAgIH1cclxufVxyXG5cclxuLyoqXHJcbiAqIENhbGxiYWNrIGZvciBPbkZpbGVEcm9wIHJldHVybnMgYSBzbGljZSBvZiBmaWxlIHBhdGggc3RyaW5ncyB3aGVuIGEgZHJvcCBpcyBmaW5pc2hlZC5cclxuICpcclxuICogQGV4cG9ydFxyXG4gKiBAY2FsbGJhY2sgT25GaWxlRHJvcENhbGxiYWNrXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB4IC0geCBjb29yZGluYXRlIG9mIHRoZSBkcm9wXHJcbiAqIEBwYXJhbSB7bnVtYmVyfSB5IC0geSBjb29yZGluYXRlIG9mIHRoZSBkcm9wXHJcbiAqIEBwYXJhbSB7c3RyaW5nW119IHBhdGhzIC0gQSBsaXN0IG9mIGZpbGUgcGF0aHMuXHJcbiAqL1xyXG5cclxuLyoqXHJcbiAqIE9uRmlsZURyb3AgbGlzdGVucyB0byBkcmFnIGFuZCBkcm9wIGV2ZW50cyBhbmQgY2FsbHMgdGhlIGNhbGxiYWNrIHdpdGggdGhlIGNvb3JkaW5hdGVzIG9mIHRoZSBkcm9wIGFuZCBhbiBhcnJheSBvZiBwYXRoIHN0cmluZ3MuXHJcbiAqXHJcbiAqIEBleHBvcnRcclxuICogQHBhcmFtIHtPbkZpbGVEcm9wQ2FsbGJhY2t9IGNhbGxiYWNrIC0gQ2FsbGJhY2sgZm9yIE9uRmlsZURyb3AgcmV0dXJucyBhIHNsaWNlIG9mIGZpbGUgcGF0aCBzdHJpbmdzIHdoZW4gYSBkcm9wIGlzIGZpbmlzaGVkLlxyXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFt1c2VEcm9wVGFyZ2V0PXRydWVdIC0gT25seSBjYWxsIHRoZSBjYWxsYmFjayB3aGVuIHRoZSBkcm9wIGZpbmlzaGVkIG9uIGFuIGVsZW1lbnQgdGhhdCBoYXMgdGhlIGRyb3AgdGFyZ2V0IHN0eWxlLiAoLS13YWlscy1kcm9wLXRhcmdldClcclxuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBPbkZpbGVEcm9wKGNhbGxiYWNrLCB1c2VEcm9wVGFyZ2V0KSB7XHJcbiAgICBpZiAodHlwZW9mIGNhbGxiYWNrICE9PSBcImZ1bmN0aW9uXCIpIHtcclxuICAgICAgICBjb25zb2xlLmVycm9yKFwiRHJhZ0FuZERyb3BDYWxsYmFjayBpcyBub3QgYSBmdW5jdGlvblwiKTtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKGZsYWdzLnJlZ2lzdGVyZWQpIHtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcbiAgICBmbGFncy5yZWdpc3RlcmVkID0gdHJ1ZTtcclxuXHJcbiAgICBjb25zdCB1RFRQVCA9IHR5cGVvZiB1c2VEcm9wVGFyZ2V0O1xyXG4gICAgZmxhZ3MudXNlRHJvcFRhcmdldCA9IHVEVFBUID09PSBcInVuZGVmaW5lZFwiIHx8IHVEVFBUICE9PSBcImJvb2xlYW5cIiA/IGZsYWdzLmRlZmF1bHRVc2VEcm9wVGFyZ2V0IDogdXNlRHJvcFRhcmdldDtcclxuICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdkcmFnb3ZlcicsIG9uRHJhZ092ZXIpO1xyXG4gICAgd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ2RyYWdsZWF2ZScsIG9uRHJhZ0xlYXZlKTtcclxuICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdkcm9wJywgb25Ecm9wKTtcclxuXHJcbiAgICBsZXQgY2IgPSBjYWxsYmFjaztcclxuICAgIGlmIChmbGFncy51c2VEcm9wVGFyZ2V0KSB7XHJcbiAgICAgICAgY2IgPSBmdW5jdGlvbiAoeCwgeSwgcGF0aHMpIHtcclxuICAgICAgICAgICAgY29uc3QgZWxlbWVudCA9IGRvY3VtZW50LmVsZW1lbnRGcm9tUG9pbnQoeCwgeSlcclxuICAgICAgICAgICAgLy8gaWYgdGhlIGVsZW1lbnQgaXMgbnVsbCBvciBlbGVtZW50IGlzIG5vdCBjaGlsZCBvZiBkcm9wIHRhcmdldCBlbGVtZW50LCByZXR1cm4gbnVsbFxyXG4gICAgICAgICAgICBpZiAoIWVsZW1lbnQgfHwgIWNoZWNrU3R5bGVEcm9wVGFyZ2V0KGdldENvbXB1dGVkU3R5bGUoZWxlbWVudCkpKSB7XHJcbiAgICAgICAgICAgICAgICByZXR1cm4gbnVsbDtcclxuICAgICAgICAgICAgfVxyXG4gICAgICAgICAgICBjYWxsYmFjayh4LCB5LCBwYXRocyk7XHJcbiAgICAgICAgfVxyXG4gICAgfVxyXG5cclxuICAgIEV2ZW50c09uKFwid2FpbHM6ZmlsZS1kcm9wXCIsIGNiKTtcclxufVxyXG5cclxuLyoqXHJcbiAqIE9uRmlsZURyb3BPZmYgcmVtb3ZlcyB0aGUgZHJhZyBhbmQgZHJvcCBsaXN0ZW5lcnMgYW5kIGhhbmRsZXJzLlxyXG4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIE9uRmlsZURyb3BPZmYoKSB7XHJcbiAgICB3aW5kb3cucmVtb3ZlRXZlbnRMaXN0ZW5lcignZHJhZ292ZXInLCBvbkRyYWdPdmVyKTtcclxuICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKCdkcmFnbGVhdmUnLCBvbkRyYWdMZWF2ZSk7XHJcbiAgICB3aW5kb3cucmVtb3ZlRXZlbnRMaXN0ZW5lcignZHJvcCcsIG9uRHJvcCk7XHJcbiAgICBFdmVudHNPZmYoXCJ3YWlsczpmaWxlLWRyb3BcIik7XHJcbiAgICBmbGFncy5yZWdpc3RlcmVkID0gZmFsc2U7XHJcbn1cclxuIiwgIi8qXHJcbi0tZGVmYXVsdC1jb250ZXh0bWVudTogYXV0bzsgKGRlZmF1bHQpIHdpbGwgc2hvdyB0aGUgZGVmYXVsdCBjb250ZXh0IG1lbnUgaWYgY29udGVudEVkaXRhYmxlIGlzIHRydWUgT1IgdGV4dCBoYXMgYmVlbiBzZWxlY3RlZCBPUiBlbGVtZW50IGlzIGlucHV0IG9yIHRleHRhcmVhXHJcbi0tZGVmYXVsdC1jb250ZXh0bWVudTogc2hvdzsgd2lsbCBhbHdheXMgc2hvdyB0aGUgZGVmYXVsdCBjb250ZXh0IG1lbnVcclxuLS1kZWZhdWx0LWNvbnRleHRtZW51OiBoaWRlOyB3aWxsIGFsd2F5cyBoaWRlIHRoZSBkZWZhdWx0IGNvbnRleHQgbWVudVxyXG5cclxuVGhpcyBydWxlIGlzIGluaGVyaXRlZCBsaWtlIG5vcm1hbCBDU1MgcnVsZXMsIHNvIG5lc3Rpbmcgd29ya3MgYXMgZXhwZWN0ZWRcclxuKi9cclxuZXhwb3J0IGZ1bmN0aW9uIHByb2Nlc3NEZWZhdWx0Q29udGV4dE1lbnUoZXZlbnQpIHtcclxuICAgIC8vIFByb2Nlc3MgZGVmYXVsdCBjb250ZXh0IG1lbnVcclxuICAgIGNvbnN0IGVsZW1lbnQgPSBldmVudC50YXJnZXQ7XHJcbiAgICBjb25zdCBjb21wdXRlZFN0eWxlID0gd2luZG93LmdldENvbXB1dGVkU3R5bGUoZWxlbWVudCk7XHJcbiAgICBjb25zdCBkZWZhdWx0Q29udGV4dE1lbnVBY3Rpb24gPSBjb21wdXRlZFN0eWxlLmdldFByb3BlcnR5VmFsdWUoXCItLWRlZmF1bHQtY29udGV4dG1lbnVcIikudHJpbSgpO1xyXG4gICAgc3dpdGNoIChkZWZhdWx0Q29udGV4dE1lbnVBY3Rpb24pIHtcclxuICAgICAgICBjYXNlIFwic2hvd1wiOlxyXG4gICAgICAgICAgICByZXR1cm47XHJcbiAgICAgICAgY2FzZSBcImhpZGVcIjpcclxuICAgICAgICAgICAgZXZlbnQucHJldmVudERlZmF1bHQoKTtcclxuICAgICAgICAgICAgcmV0dXJuO1xyXG4gICAgICAgIGRlZmF1bHQ6XHJcbiAgICAgICAgICAgIC8vIENoZWNrIGlmIGNvbnRlbnRFZGl0YWJsZSBpcyB0cnVlXHJcbiAgICAgICAgICAgIGlmIChlbGVtZW50LmlzQ29udGVudEVkaXRhYmxlKSB7XHJcbiAgICAgICAgICAgICAgICByZXR1cm47XHJcbiAgICAgICAgICAgIH1cclxuXHJcbiAgICAgICAgICAgIC8vIENoZWNrIGlmIHRleHQgaGFzIGJlZW4gc2VsZWN0ZWQgYW5kIGFjdGlvbiBpcyBvbiB0aGUgc2VsZWN0ZWQgZWxlbWVudHNcclxuICAgICAgICAgICAgY29uc3Qgc2VsZWN0aW9uID0gd2luZG93LmdldFNlbGVjdGlvbigpO1xyXG4gICAgICAgICAgICBjb25zdCBoYXNTZWxlY3Rpb24gPSAoc2VsZWN0aW9uLnRvU3RyaW5nKCkubGVuZ3RoID4gMClcclxuICAgICAgICAgICAgaWYgKGhhc1NlbGVjdGlvbikge1xyXG4gICAgICAgICAgICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBzZWxlY3Rpb24ucmFuZ2VDb3VudDsgaSsrKSB7XHJcbiAgICAgICAgICAgICAgICAgICAgY29uc3QgcmFuZ2UgPSBzZWxlY3Rpb24uZ2V0UmFuZ2VBdChpKTtcclxuICAgICAgICAgICAgICAgICAgICBjb25zdCByZWN0cyA9IHJhbmdlLmdldENsaWVudFJlY3RzKCk7XHJcbiAgICAgICAgICAgICAgICAgICAgZm9yIChsZXQgaiA9IDA7IGogPCByZWN0cy5sZW5ndGg7IGorKykge1xyXG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zdCByZWN0ID0gcmVjdHNbal07XHJcbiAgICAgICAgICAgICAgICAgICAgICAgIGlmIChkb2N1bWVudC5lbGVtZW50RnJvbVBvaW50KHJlY3QubGVmdCwgcmVjdC50b3ApID09PSBlbGVtZW50KSB7XHJcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm47XHJcbiAgICAgICAgICAgICAgICAgICAgICAgIH1cclxuICAgICAgICAgICAgICAgICAgICB9XHJcbiAgICAgICAgICAgICAgICB9XHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICAgICAgLy8gQ2hlY2sgaWYgdGFnbmFtZSBpcyBpbnB1dCBvciB0ZXh0YXJlYVxyXG4gICAgICAgICAgICBpZiAoZWxlbWVudC50YWdOYW1lID09PSBcIklOUFVUXCIgfHwgZWxlbWVudC50YWdOYW1lID09PSBcIlRFWFRBUkVBXCIpIHtcclxuICAgICAgICAgICAgICAgIGlmIChoYXNTZWxlY3Rpb24gfHwgKCFlbGVtZW50LnJlYWRPbmx5ICYmICFlbGVtZW50LmRpc2FibGVkKSkge1xyXG4gICAgICAgICAgICAgICAgICAgIHJldHVybjtcclxuICAgICAgICAgICAgICAgIH1cclxuICAgICAgICAgICAgfVxyXG5cclxuICAgICAgICAgICAgLy8gaGlkZSBkZWZhdWx0IGNvbnRleHQgbWVudVxyXG4gICAgICAgICAgICBldmVudC5wcmV2ZW50RGVmYXVsdCgpO1xyXG4gICAgfVxyXG59XHJcbiIsICIvKlxyXG4gX1x0ICAgX19cdCAgXyBfX1xyXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xyXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXHJcbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKVxyXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cclxuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xyXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XHJcbiovXHJcbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cclxuaW1wb3J0ICogYXMgTG9nIGZyb20gJy4vbG9nJztcclxuaW1wb3J0IHtcclxuICBldmVudExpc3RlbmVycyxcclxuICBFdmVudHNFbWl0LFxyXG4gIEV2ZW50c05vdGlmeSxcclxuICBFdmVudHNPZmYsXHJcbiAgRXZlbnRzT2ZmQWxsLFxyXG4gIEV2ZW50c09uLFxyXG4gIEV2ZW50c09uY2UsXHJcbiAgRXZlbnRzT25NdWx0aXBsZSxcclxufSBmcm9tIFwiLi9ldmVudHNcIjtcclxuaW1wb3J0IHsgQ2FsbCwgQ2FsbGJhY2ssIGNhbGxiYWNrcyB9IGZyb20gJy4vY2FsbHMnO1xyXG5pbXBvcnQgeyBTZXRCaW5kaW5ncyB9IGZyb20gXCIuL2JpbmRpbmdzXCI7XHJcbmltcG9ydCAqIGFzIFdpbmRvdyBmcm9tIFwiLi93aW5kb3dcIjtcclxuaW1wb3J0ICogYXMgU2NyZWVuIGZyb20gXCIuL3NjcmVlblwiO1xyXG5pbXBvcnQgKiBhcyBCcm93c2VyIGZyb20gXCIuL2Jyb3dzZXJcIjtcclxuaW1wb3J0ICogYXMgQ2xpcGJvYXJkIGZyb20gXCIuL2NsaXBib2FyZFwiO1xyXG5pbXBvcnQgKiBhcyBEcmFnQW5kRHJvcCBmcm9tIFwiLi9kcmFnYW5kZHJvcFwiO1xyXG5pbXBvcnQgKiBhcyBDb250ZXh0TWVudSBmcm9tIFwiLi9jb250ZXh0bWVudVwiO1xyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFF1aXQoKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1EnKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIFNob3coKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1MnKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIEhpZGUoKSB7XHJcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ0gnKTtcclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIEVudmlyb25tZW50KCkge1xyXG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6RW52aXJvbm1lbnRcIik7XHJcbn1cclxuXHJcbi8vIFRoZSBKUyBydW50aW1lXHJcbndpbmRvdy5ydW50aW1lID0ge1xyXG4gICAgLi4uTG9nLFxyXG4gICAgLi4uV2luZG93LFxyXG4gICAgLi4uQnJvd3NlcixcclxuICAgIC4uLlNjcmVlbixcclxuICAgIC4uLkNsaXBib2FyZCxcclxuICAgIC4uLkRyYWdBbmREcm9wLFxyXG4gICAgRXZlbnRzT24sXHJcbiAgICBFdmVudHNPbmNlLFxyXG4gICAgRXZlbnRzT25NdWx0aXBsZSxcclxuICAgIEV2ZW50c0VtaXQsXHJcbiAgICBFdmVudHNPZmYsXHJcbiAgICBFdmVudHNPZmZBbGwsXHJcbiAgICBFbnZpcm9ubWVudCxcclxuICAgIFNob3csXHJcbiAgICBIaWRlLFxyXG4gICAgUXVpdFxyXG59O1xyXG5cclxuLy8gSW50ZXJuYWwgd2FpbHMgZW5kcG9pbnRzXHJcbndpbmRvdy53YWlscyA9IHtcclxuICAgIENhbGxiYWNrLFxyXG4gICAgRXZlbnRzTm90aWZ5LFxyXG4gICAgU2V0QmluZGluZ3MsXHJcbiAgICBldmVudExpc3RlbmVycyxcclxuICAgIGNhbGxiYWNrcyxcclxuICAgIGZsYWdzOiB7XHJcbiAgICAgICAgZGlzYWJsZVNjcm9sbGJhckRyYWc6IGZhbHNlLFxyXG4gICAgICAgIGRpc2FibGVEZWZhdWx0Q29udGV4dE1lbnU6IGZhbHNlLFxyXG4gICAgICAgIGVuYWJsZVJlc2l6ZTogZmFsc2UsXHJcbiAgICAgICAgZGVmYXVsdEN1cnNvcjogbnVsbCxcclxuICAgICAgICBib3JkZXJUaGlja25lc3M6IDYsXHJcbiAgICAgICAgc2hvdWxkRHJhZzogZmFsc2UsXHJcbiAgICAgICAgZGVmZXJEcmFnVG9Nb3VzZU1vdmU6IHRydWUsXHJcbiAgICAgICAgY3NzRHJhZ1Byb3BlcnR5OiBcIi0td2FpbHMtZHJhZ2dhYmxlXCIsXHJcbiAgICAgICAgY3NzRHJhZ1ZhbHVlOiBcImRyYWdcIixcclxuICAgICAgICBjc3NEcm9wUHJvcGVydHk6IFwiLS13YWlscy1kcm9wLXRhcmdldFwiLFxyXG4gICAgICAgIGNzc0Ryb3BWYWx1ZTogXCJkcm9wXCIsXHJcbiAgICAgICAgZW5hYmxlV2FpbHNEcmFnQW5kRHJvcDogZmFsc2UsXHJcbiAgICB9XHJcbn07XHJcblxyXG4vLyBTZXQgdGhlIGJpbmRpbmdzXHJcbmlmICh3aW5kb3cud2FpbHNiaW5kaW5ncykge1xyXG4gICAgd2luZG93LndhaWxzLlNldEJpbmRpbmdzKHdpbmRvdy53YWlsc2JpbmRpbmdzKTtcclxuICAgIGRlbGV0ZSB3aW5kb3cud2FpbHMuU2V0QmluZGluZ3M7XHJcbn1cclxuXHJcbi8vIChib29sKSBUaGlzIGlzIGV2YWx1YXRlZCBhdCBidWlsZCB0aW1lIGluIHBhY2thZ2UuanNvblxyXG5pZiAoIURFQlVHKSB7XHJcbiAgICBkZWxldGUgd2luZG93LndhaWxzYmluZGluZ3M7XHJcbn1cclxuXHJcbmxldCBkcmFnVGVzdCA9IGZ1bmN0aW9uKGUpIHtcclxuICAgIHZhciB2YWwgPSB3aW5kb3cuZ2V0Q29tcHV0ZWRTdHlsZShlLnRhcmdldCkuZ2V0UHJvcGVydHlWYWx1ZSh3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1Byb3BlcnR5KTtcclxuICAgIGlmICh2YWwpIHtcclxuICAgICAgICB2YWwgPSB2YWwudHJpbSgpO1xyXG4gICAgfVxyXG5cclxuICAgIGlmICh2YWwgIT09IHdpbmRvdy53YWlscy5mbGFncy5jc3NEcmFnVmFsdWUpIHtcclxuICAgICAgICByZXR1cm4gZmFsc2U7XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKGUuYnV0dG9ucyAhPT0gMSkge1xyXG4gICAgICAgIC8vIERvIG5vdCBzdGFydCBkcmFnZ2luZyBpZiBub3QgdGhlIHByaW1hcnkgYnV0dG9uIGhhcyBiZWVuIGNsaWNrZWQuXHJcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xyXG4gICAgfVxyXG5cclxuICAgIGlmIChlLmRldGFpbCAhPT0gMSkge1xyXG4gICAgICAgIC8vIERvIG5vdCBzdGFydCBkcmFnZ2luZyBpZiBtb3JlIHRoYW4gb25jZSBoYXMgYmVlbiBjbGlja2VkLCBlLmcuIHdoZW4gZG91YmxlIGNsaWNraW5nXHJcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xyXG4gICAgfVxyXG5cclxuICAgIHJldHVybiB0cnVlO1xyXG59O1xyXG5cclxud2luZG93LndhaWxzLnNldENTU0RyYWdQcm9wZXJ0aWVzID0gZnVuY3Rpb24ocHJvcGVydHksIHZhbHVlKSB7XHJcbiAgICB3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1Byb3BlcnR5ID0gcHJvcGVydHk7XHJcbiAgICB3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1ZhbHVlID0gdmFsdWU7XHJcbn1cclxuXHJcbndpbmRvdy53YWlscy5zZXRDU1NEcm9wUHJvcGVydGllcyA9IGZ1bmN0aW9uKHByb3BlcnR5LCB2YWx1ZSkge1xyXG4gICAgd2luZG93LndhaWxzLmZsYWdzLmNzc0Ryb3BQcm9wZXJ0eSA9IHByb3BlcnR5O1xyXG4gICAgd2luZG93LndhaWxzLmZsYWdzLmNzc0Ryb3BWYWx1ZSA9IHZhbHVlO1xyXG59XHJcblxyXG53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcignbW91c2Vkb3duJywgKGUpID0+IHtcclxuICAgIC8vIENoZWNrIGZvciByZXNpemluZ1xyXG4gICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5yZXNpemVFZGdlKSB7XHJcbiAgICAgICAgd2luZG93LldhaWxzSW52b2tlKFwicmVzaXplOlwiICsgd2luZG93LndhaWxzLmZsYWdzLnJlc2l6ZUVkZ2UpO1xyXG4gICAgICAgIGUucHJldmVudERlZmF1bHQoKTtcclxuICAgICAgICByZXR1cm47XHJcbiAgICB9XHJcblxyXG4gICAgaWYgKGRyYWdUZXN0KGUpKSB7XHJcbiAgICAgICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5kaXNhYmxlU2Nyb2xsYmFyRHJhZykge1xyXG4gICAgICAgICAgICAvLyBUaGlzIGNoZWNrcyBmb3IgY2xpY2tzIG9uIHRoZSBzY3JvbGwgYmFyXHJcbiAgICAgICAgICAgIGlmIChlLm9mZnNldFggPiBlLnRhcmdldC5jbGllbnRXaWR0aCB8fCBlLm9mZnNldFkgPiBlLnRhcmdldC5jbGllbnRIZWlnaHQpIHtcclxuICAgICAgICAgICAgICAgIHJldHVybjtcclxuICAgICAgICAgICAgfVxyXG4gICAgICAgIH1cclxuICAgICAgICBpZiAod2luZG93LndhaWxzLmZsYWdzLmRlZmVyRHJhZ1RvTW91c2VNb3ZlKSB7XHJcbiAgICAgICAgICAgIHdpbmRvdy53YWlscy5mbGFncy5zaG91bGREcmFnID0gdHJ1ZTtcclxuICAgICAgICB9IGVsc2Uge1xyXG4gICAgICAgICAgICBlLnByZXZlbnREZWZhdWx0KClcclxuICAgICAgICAgICAgd2luZG93LldhaWxzSW52b2tlKFwiZHJhZ1wiKTtcclxuICAgICAgICB9XHJcbiAgICAgICAgcmV0dXJuO1xyXG4gICAgfSBlbHNlIHtcclxuICAgICAgICB3aW5kb3cud2FpbHMuZmxhZ3Muc2hvdWxkRHJhZyA9IGZhbHNlO1xyXG4gICAgfVxyXG59KTtcclxuXHJcbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdtb3VzZXVwJywgKCkgPT4ge1xyXG4gICAgd2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcgPSBmYWxzZTtcclxufSk7XHJcblxyXG5mdW5jdGlvbiBzZXRSZXNpemUoY3Vyc29yKSB7XHJcbiAgICBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuc3R5bGUuY3Vyc29yID0gY3Vyc29yIHx8IHdpbmRvdy53YWlscy5mbGFncy5kZWZhdWx0Q3Vyc29yO1xyXG4gICAgd2luZG93LndhaWxzLmZsYWdzLnJlc2l6ZUVkZ2UgPSBjdXJzb3I7XHJcbn1cclxuXHJcbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdtb3VzZW1vdmUnLCBmdW5jdGlvbihlKSB7XHJcbiAgICBpZiAod2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcpIHtcclxuICAgICAgICB3aW5kb3cud2FpbHMuZmxhZ3Muc2hvdWxkRHJhZyA9IGZhbHNlO1xyXG4gICAgICAgIGxldCBtb3VzZVByZXNzZWQgPSBlLmJ1dHRvbnMgIT09IHVuZGVmaW5lZCA/IGUuYnV0dG9ucyA6IGUud2hpY2g7XHJcbiAgICAgICAgaWYgKG1vdXNlUHJlc3NlZCA+IDApIHtcclxuICAgICAgICAgICAgd2luZG93LldhaWxzSW52b2tlKFwiZHJhZ1wiKTtcclxuICAgICAgICAgICAgcmV0dXJuO1xyXG4gICAgICAgIH1cclxuICAgIH1cclxuICAgIGlmICghd2luZG93LndhaWxzLmZsYWdzLmVuYWJsZVJlc2l6ZSkge1xyXG4gICAgICAgIHJldHVybjtcclxuICAgIH1cclxuICAgIGlmICh3aW5kb3cud2FpbHMuZmxhZ3MuZGVmYXVsdEN1cnNvciA9PSBudWxsKSB7XHJcbiAgICAgICAgd2luZG93LndhaWxzLmZsYWdzLmRlZmF1bHRDdXJzb3IgPSBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuc3R5bGUuY3Vyc29yO1xyXG4gICAgfVxyXG4gICAgaWYgKHdpbmRvdy5vdXRlcldpZHRoIC0gZS5jbGllbnRYIDwgd2luZG93LndhaWxzLmZsYWdzLmJvcmRlclRoaWNrbmVzcyAmJiB3aW5kb3cub3V0ZXJIZWlnaHQgLSBlLmNsaWVudFkgPCB3aW5kb3cud2FpbHMuZmxhZ3MuYm9yZGVyVGhpY2tuZXNzKSB7XHJcbiAgICAgICAgZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LnN0eWxlLmN1cnNvciA9IFwic2UtcmVzaXplXCI7XHJcbiAgICB9XHJcbiAgICBsZXQgcmlnaHRCb3JkZXIgPSB3aW5kb3cub3V0ZXJXaWR0aCAtIGUuY2xpZW50WCA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3M7XHJcbiAgICBsZXQgbGVmdEJvcmRlciA9IGUuY2xpZW50WCA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3M7XHJcbiAgICBsZXQgdG9wQm9yZGVyID0gZS5jbGllbnRZIDwgd2luZG93LndhaWxzLmZsYWdzLmJvcmRlclRoaWNrbmVzcztcclxuICAgIGxldCBib3R0b21Cb3JkZXIgPSB3aW5kb3cub3V0ZXJIZWlnaHQgLSBlLmNsaWVudFkgPCB3aW5kb3cud2FpbHMuZmxhZ3MuYm9yZGVyVGhpY2tuZXNzO1xyXG5cclxuICAgIC8vIElmIHdlIGFyZW4ndCBvbiBhbiBlZGdlLCBidXQgd2VyZSwgcmVzZXQgdGhlIGN1cnNvciB0byBkZWZhdWx0XHJcbiAgICBpZiAoIWxlZnRCb3JkZXIgJiYgIXJpZ2h0Qm9yZGVyICYmICF0b3BCb3JkZXIgJiYgIWJvdHRvbUJvcmRlciAmJiB3aW5kb3cud2FpbHMuZmxhZ3MucmVzaXplRWRnZSAhPT0gdW5kZWZpbmVkKSB7XHJcbiAgICAgICAgc2V0UmVzaXplKCk7XHJcbiAgICB9IGVsc2UgaWYgKHJpZ2h0Qm9yZGVyICYmIGJvdHRvbUJvcmRlcikgc2V0UmVzaXplKFwic2UtcmVzaXplXCIpO1xyXG4gICAgZWxzZSBpZiAobGVmdEJvcmRlciAmJiBib3R0b21Cb3JkZXIpIHNldFJlc2l6ZShcInN3LXJlc2l6ZVwiKTtcclxuICAgIGVsc2UgaWYgKGxlZnRCb3JkZXIgJiYgdG9wQm9yZGVyKSBzZXRSZXNpemUoXCJudy1yZXNpemVcIik7XHJcbiAgICBlbHNlIGlmICh0b3BCb3JkZXIgJiYgcmlnaHRCb3JkZXIpIHNldFJlc2l6ZShcIm5lLXJlc2l6ZVwiKTtcclxuICAgIGVsc2UgaWYgKGxlZnRCb3JkZXIpIHNldFJlc2l6ZShcInctcmVzaXplXCIpO1xyXG4gICAgZWxzZSBpZiAodG9wQm9yZGVyKSBzZXRSZXNpemUoXCJuLXJlc2l6ZVwiKTtcclxuICAgIGVsc2UgaWYgKGJvdHRvbUJvcmRlcikgc2V0UmVzaXplKFwicy1yZXNpemVcIik7XHJcbiAgICBlbHNlIGlmIChyaWdodEJvcmRlcikgc2V0UmVzaXplKFwiZS1yZXNpemVcIik7XHJcblxyXG59KTtcclxuXHJcbi8vIFNldHVwIGNvbnRleHQgbWVudSBob29rXHJcbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdjb250ZXh0bWVudScsIGZ1bmN0aW9uKGUpIHtcclxuICAgIC8vIGFsd2F5cyBzaG93IHRoZSBjb250ZXh0bWVudSBpbiBkZWJ1ZyAmIGRldlxyXG4gICAgaWYgKERFQlVHKSByZXR1cm47XHJcblxyXG4gICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5kaXNhYmxlRGVmYXVsdENvbnRleHRNZW51KSB7XHJcbiAgICAgICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xyXG4gICAgfSBlbHNlIHtcclxuICAgICAgICBDb250ZXh0TWVudS5wcm9jZXNzRGVmYXVsdENvbnRleHRNZW51KGUpO1xyXG4gICAgfVxyXG59KTtcclxuXHJcbndpbmRvdy5XYWlsc0ludm9rZShcInJ1bnRpbWU6cmVhZHlcIik7Il0sCiAgIm1hcHBpbmdzIjogIjs7Ozs7Ozs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFrQkEsV0FBUyxlQUFlLE9BQU8sU0FBUztBQUl2QyxXQUFPLFlBQVksTUFBTSxRQUFRLE9BQU87QUFBQSxFQUN6QztBQVFPLFdBQVMsU0FBUyxTQUFTO0FBQ2pDLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxTQUFTLFNBQVM7QUFDakMsbUJBQWUsS0FBSyxPQUFPO0FBQUEsRUFDNUI7QUFRTyxXQUFTLFNBQVMsU0FBUztBQUNqQyxtQkFBZSxLQUFLLE9BQU87QUFBQSxFQUM1QjtBQVFPLFdBQVMsUUFBUSxTQUFTO0FBQ2hDLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxXQUFXLFNBQVM7QUFDbkMsbUJBQWUsS0FBSyxPQUFPO0FBQUEsRUFDNUI7QUFRTyxXQUFTLFNBQVMsU0FBUztBQUNqQyxtQkFBZSxLQUFLLE9BQU87QUFBQSxFQUM1QjtBQVFPLFdBQVMsU0FBUyxTQUFTO0FBQ2pDLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxZQUFZLFVBQVU7QUFDckMsbUJBQWUsS0FBSyxRQUFRO0FBQUEsRUFDN0I7QUFHTyxNQUFNLFdBQVc7QUFBQSxJQUN2QixPQUFPO0FBQUEsSUFDUCxPQUFPO0FBQUEsSUFDUCxNQUFNO0FBQUEsSUFDTixTQUFTO0FBQUEsSUFDVCxPQUFPO0FBQUEsRUFDUjs7O0FDOUZBLE1BQU0sV0FBTixNQUFlO0FBQUEsSUFRWCxZQUFZLFdBQVcsVUFBVSxjQUFjO0FBQzNDLFdBQUssWUFBWTtBQUVqQixXQUFLLGVBQWUsZ0JBQWdCO0FBR3BDLFdBQUssV0FBVyxDQUFDLFNBQVM7QUFDdEIsaUJBQVMsTUFBTSxNQUFNLElBQUk7QUFFekIsWUFBSSxLQUFLLGlCQUFpQixJQUFJO0FBQzFCLGlCQUFPO0FBQUEsUUFDWDtBQUVBLGFBQUssZ0JBQWdCO0FBQ3JCLGVBQU8sS0FBSyxpQkFBaUI7QUFBQSxNQUNqQztBQUFBLElBQ0o7QUFBQSxFQUNKO0FBRU8sTUFBTSxpQkFBaUIsQ0FBQztBQVd4QixXQUFTLGlCQUFpQixXQUFXLFVBQVUsY0FBYztBQUNoRSxtQkFBZSxhQUFhLGVBQWUsY0FBYyxDQUFDO0FBQzFELFVBQU0sZUFBZSxJQUFJLFNBQVMsV0FBVyxVQUFVLFlBQVk7QUFDbkUsbUJBQWUsV0FBVyxLQUFLLFlBQVk7QUFDM0MsV0FBTyxNQUFNLFlBQVksWUFBWTtBQUFBLEVBQ3pDO0FBVU8sV0FBUyxTQUFTLFdBQVcsVUFBVTtBQUMxQyxXQUFPLGlCQUFpQixXQUFXLFVBQVUsRUFBRTtBQUFBLEVBQ25EO0FBVU8sV0FBUyxXQUFXLFdBQVcsVUFBVTtBQUM1QyxXQUFPLGlCQUFpQixXQUFXLFVBQVUsQ0FBQztBQUFBLEVBQ2xEO0FBRUEsV0FBUyxnQkFBZ0IsV0FBVztBQUdoQyxRQUFJLFlBQVksVUFBVTtBQUcxQixVQUFNLHVCQUF1QixlQUFlLFlBQVksTUFBTSxLQUFLLENBQUM7QUFHcEUsUUFBSSxxQkFBcUIsUUFBUTtBQUc3QixlQUFTLFFBQVEscUJBQXFCLFNBQVMsR0FBRyxTQUFTLEdBQUcsU0FBUyxHQUFHO0FBR3RFLGNBQU0sV0FBVyxxQkFBcUI7QUFFdEMsWUFBSSxPQUFPLFVBQVU7QUFHckIsY0FBTSxVQUFVLFNBQVMsU0FBUyxJQUFJO0FBQ3RDLFlBQUksU0FBUztBQUVULCtCQUFxQixPQUFPLE9BQU8sQ0FBQztBQUFBLFFBQ3hDO0FBQUEsTUFDSjtBQUdBLFVBQUkscUJBQXFCLFdBQVcsR0FBRztBQUNuQyx1QkFBZSxTQUFTO0FBQUEsTUFDNUIsT0FBTztBQUNILHVCQUFlLGFBQWE7QUFBQSxNQUNoQztBQUFBLElBQ0o7QUFBQSxFQUNKO0FBU08sV0FBUyxhQUFhLGVBQWU7QUFFeEMsUUFBSTtBQUNKLFFBQUk7QUFDQSxnQkFBVSxLQUFLLE1BQU0sYUFBYTtBQUFBLElBQ3RDLFNBQVMsR0FBUDtBQUNFLFlBQU0sUUFBUSxvQ0FBb0M7QUFDbEQsWUFBTSxJQUFJLE1BQU0sS0FBSztBQUFBLElBQ3pCO0FBQ0Esb0JBQWdCLE9BQU87QUFBQSxFQUMzQjtBQVFPLFdBQVMsV0FBVyxXQUFXO0FBRWxDLFVBQU0sVUFBVTtBQUFBLE1BQ1osTUFBTTtBQUFBLE1BQ04sTUFBTSxDQUFDLEVBQUUsTUFBTSxNQUFNLFNBQVMsRUFBRSxNQUFNLENBQUM7QUFBQSxJQUMzQztBQUdBLG9CQUFnQixPQUFPO0FBR3ZCLFdBQU8sWUFBWSxPQUFPLEtBQUssVUFBVSxPQUFPLENBQUM7QUFBQSxFQUNyRDtBQUVBLFdBQVMsZUFBZSxXQUFXO0FBRS9CLFdBQU8sZUFBZTtBQUd0QixXQUFPLFlBQVksT0FBTyxTQUFTO0FBQUEsRUFDdkM7QUFTTyxXQUFTLFVBQVUsY0FBYyxzQkFBc0I7QUFDMUQsbUJBQWUsU0FBUztBQUV4QixRQUFJLHFCQUFxQixTQUFTLEdBQUc7QUFDakMsMkJBQXFCLFFBQVEsQ0FBQUEsZUFBYTtBQUN0Qyx1QkFBZUEsVUFBUztBQUFBLE1BQzVCLENBQUM7QUFBQSxJQUNMO0FBQUEsRUFDSjtBQUtRLFdBQVMsZUFBZTtBQUM1QixVQUFNLGFBQWEsT0FBTyxLQUFLLGNBQWM7QUFDN0MsZUFBVyxRQUFRLGVBQWE7QUFDNUIscUJBQWUsU0FBUztBQUFBLElBQzVCLENBQUM7QUFBQSxFQUNMO0FBT0MsV0FBUyxZQUFZLFVBQVU7QUFDNUIsVUFBTSxZQUFZLFNBQVM7QUFDM0IsUUFBSSxlQUFlLGVBQWU7QUFBVztBQUc3QyxtQkFBZSxhQUFhLGVBQWUsV0FBVyxPQUFPLE9BQUssTUFBTSxRQUFRO0FBR2hGLFFBQUksZUFBZSxXQUFXLFdBQVcsR0FBRztBQUN4QyxxQkFBZSxTQUFTO0FBQUEsSUFDNUI7QUFBQSxFQUNKOzs7QUMxTU8sTUFBTSxZQUFZLENBQUM7QUFPMUIsV0FBUyxlQUFlO0FBQ3ZCLFFBQUksUUFBUSxJQUFJLFlBQVksQ0FBQztBQUM3QixXQUFPLE9BQU8sT0FBTyxnQkFBZ0IsS0FBSyxFQUFFO0FBQUEsRUFDN0M7QUFRQSxXQUFTLGNBQWM7QUFDdEIsV0FBTyxLQUFLLE9BQU8sSUFBSTtBQUFBLEVBQ3hCO0FBR0EsTUFBSTtBQUNKLE1BQUksT0FBTyxRQUFRO0FBQ2xCLGlCQUFhO0FBQUEsRUFDZCxPQUFPO0FBQ04saUJBQWE7QUFBQSxFQUNkO0FBaUJPLFdBQVMsS0FBSyxNQUFNLE1BQU0sU0FBUztBQUd6QyxRQUFJLFdBQVcsTUFBTTtBQUNwQixnQkFBVTtBQUFBLElBQ1g7QUFHQSxXQUFPLElBQUksUUFBUSxTQUFVLFNBQVMsUUFBUTtBQUc3QyxVQUFJO0FBQ0osU0FBRztBQUNGLHFCQUFhLE9BQU8sTUFBTSxXQUFXO0FBQUEsTUFDdEMsU0FBUyxVQUFVO0FBRW5CLFVBQUk7QUFFSixVQUFJLFVBQVUsR0FBRztBQUNoQix3QkFBZ0IsV0FBVyxXQUFZO0FBQ3RDLGlCQUFPLE1BQU0sYUFBYSxPQUFPLDZCQUE2QixVQUFVLENBQUM7QUFBQSxRQUMxRSxHQUFHLE9BQU87QUFBQSxNQUNYO0FBR0EsZ0JBQVUsY0FBYztBQUFBLFFBQ3ZCO0FBQUEsUUFDQTtBQUFBLFFBQ0E7QUFBQSxNQUNEO0FBRUEsVUFBSTtBQUNILGNBQU0sVUFBVTtBQUFBLFVBQ2Y7QUFBQSxVQUNBO0FBQUEsVUFDQTtBQUFBLFFBQ0Q7QUFHUyxlQUFPLFlBQVksTUFBTSxLQUFLLFVBQVUsT0FBTyxDQUFDO0FBQUEsTUFDcEQsU0FBUyxHQUFQO0FBRUUsZ0JBQVEsTUFBTSxDQUFDO0FBQUEsTUFDbkI7QUFBQSxJQUNKLENBQUM7QUFBQSxFQUNMO0FBRUEsU0FBTyxpQkFBaUIsQ0FBQyxJQUFJLE1BQU0sWUFBWTtBQUczQyxRQUFJLFdBQVcsTUFBTTtBQUNqQixnQkFBVTtBQUFBLElBQ2Q7QUFHQSxXQUFPLElBQUksUUFBUSxTQUFVLFNBQVMsUUFBUTtBQUcxQyxVQUFJO0FBQ0osU0FBRztBQUNDLHFCQUFhLEtBQUssTUFBTSxXQUFXO0FBQUEsTUFDdkMsU0FBUyxVQUFVO0FBRW5CLFVBQUk7QUFFSixVQUFJLFVBQVUsR0FBRztBQUNiLHdCQUFnQixXQUFXLFdBQVk7QUFDbkMsaUJBQU8sTUFBTSxvQkFBb0IsS0FBSyw2QkFBNkIsVUFBVSxDQUFDO0FBQUEsUUFDbEYsR0FBRyxPQUFPO0FBQUEsTUFDZDtBQUdBLGdCQUFVLGNBQWM7QUFBQSxRQUNwQjtBQUFBLFFBQ0E7QUFBQSxRQUNBO0FBQUEsTUFDSjtBQUVBLFVBQUk7QUFDQSxjQUFNLFVBQVU7QUFBQSxVQUN4QjtBQUFBLFVBQ0E7QUFBQSxVQUNBO0FBQUEsUUFDRDtBQUdTLGVBQU8sWUFBWSxNQUFNLEtBQUssVUFBVSxPQUFPLENBQUM7QUFBQSxNQUNwRCxTQUFTLEdBQVA7QUFFRSxnQkFBUSxNQUFNLENBQUM7QUFBQSxNQUNuQjtBQUFBLElBQ0osQ0FBQztBQUFBLEVBQ0w7QUFVTyxXQUFTLFNBQVMsaUJBQWlCO0FBRXpDLFFBQUk7QUFDSixRQUFJO0FBQ0gsZ0JBQVUsS0FBSyxNQUFNLGVBQWU7QUFBQSxJQUNyQyxTQUFTLEdBQVA7QUFDRCxZQUFNLFFBQVEsb0NBQW9DLEVBQUUscUJBQXFCO0FBQ3pFLGNBQVEsU0FBUyxLQUFLO0FBQ3RCLFlBQU0sSUFBSSxNQUFNLEtBQUs7QUFBQSxJQUN0QjtBQUNBLFFBQUksYUFBYSxRQUFRO0FBQ3pCLFFBQUksZUFBZSxVQUFVO0FBQzdCLFFBQUksQ0FBQyxjQUFjO0FBQ2xCLFlBQU0sUUFBUSxhQUFhO0FBQzNCLGNBQVEsTUFBTSxLQUFLO0FBQ25CLFlBQU0sSUFBSSxNQUFNLEtBQUs7QUFBQSxJQUN0QjtBQUNBLGlCQUFhLGFBQWEsYUFBYTtBQUV2QyxXQUFPLFVBQVU7QUFFakIsUUFBSSxRQUFRLE9BQU87QUFDbEIsbUJBQWEsT0FBTyxRQUFRLEtBQUs7QUFBQSxJQUNsQyxPQUFPO0FBQ04sbUJBQWEsUUFBUSxRQUFRLE1BQU07QUFBQSxJQUNwQztBQUFBLEVBQ0Q7OztBQzFLQSxTQUFPLEtBQUssQ0FBQztBQUVOLFdBQVMsWUFBWSxhQUFhO0FBQ3hDLFFBQUk7QUFDSCxvQkFBYyxLQUFLLE1BQU0sV0FBVztBQUFBLElBQ3JDLFNBQVMsR0FBUDtBQUNELGNBQVEsTUFBTSxDQUFDO0FBQUEsSUFDaEI7QUFHQSxXQUFPLEtBQUssT0FBTyxNQUFNLENBQUM7QUFHMUIsV0FBTyxLQUFLLFdBQVcsRUFBRSxRQUFRLENBQUMsZ0JBQWdCO0FBR2pELGFBQU8sR0FBRyxlQUFlLE9BQU8sR0FBRyxnQkFBZ0IsQ0FBQztBQUdwRCxhQUFPLEtBQUssWUFBWSxZQUFZLEVBQUUsUUFBUSxDQUFDLGVBQWU7QUFHN0QsZUFBTyxHQUFHLGFBQWEsY0FBYyxPQUFPLEdBQUcsYUFBYSxlQUFlLENBQUM7QUFFNUUsZUFBTyxLQUFLLFlBQVksYUFBYSxXQUFXLEVBQUUsUUFBUSxDQUFDLGVBQWU7QUFFekUsaUJBQU8sR0FBRyxhQUFhLFlBQVksY0FBYyxXQUFZO0FBRzVELGdCQUFJLFVBQVU7QUFHZCxxQkFBUyxVQUFVO0FBQ2xCLG9CQUFNLE9BQU8sQ0FBQyxFQUFFLE1BQU0sS0FBSyxTQUFTO0FBQ3BDLHFCQUFPLEtBQUssQ0FBQyxhQUFhLFlBQVksVUFBVSxFQUFFLEtBQUssR0FBRyxHQUFHLE1BQU0sT0FBTztBQUFBLFlBQzNFO0FBR0Esb0JBQVEsYUFBYSxTQUFVLFlBQVk7QUFDMUMsd0JBQVU7QUFBQSxZQUNYO0FBR0Esb0JBQVEsYUFBYSxXQUFZO0FBQ2hDLHFCQUFPO0FBQUEsWUFDUjtBQUVBLG1CQUFPO0FBQUEsVUFDUixFQUFFO0FBQUEsUUFDSCxDQUFDO0FBQUEsTUFDRixDQUFDO0FBQUEsSUFDRixDQUFDO0FBQUEsRUFDRjs7O0FDbEVBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBZU8sV0FBUyxlQUFlO0FBQzNCLFdBQU8sU0FBUyxPQUFPO0FBQUEsRUFDM0I7QUFFTyxXQUFTLGtCQUFrQjtBQUM5QixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBRU8sV0FBUyw4QkFBOEI7QUFDMUMsV0FBTyxZQUFZLE9BQU87QUFBQSxFQUM5QjtBQUVPLFdBQVMsc0JBQXNCO0FBQ2xDLFdBQU8sWUFBWSxNQUFNO0FBQUEsRUFDN0I7QUFFTyxXQUFTLHFCQUFxQjtBQUNqQyxXQUFPLFlBQVksTUFBTTtBQUFBLEVBQzdCO0FBT08sV0FBUyxlQUFlO0FBQzNCLFdBQU8sWUFBWSxJQUFJO0FBQUEsRUFDM0I7QUFRTyxXQUFTLGVBQWUsT0FBTztBQUNsQyxXQUFPLFlBQVksT0FBTyxLQUFLO0FBQUEsRUFDbkM7QUFPTyxXQUFTLG1CQUFtQjtBQUMvQixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBT08sV0FBUyxxQkFBcUI7QUFDakMsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQVFPLFdBQVMscUJBQXFCO0FBQ2pDLFdBQU8sS0FBSywyQkFBMkI7QUFBQSxFQUMzQztBQVNPLFdBQVMsY0FBYyxPQUFPLFFBQVE7QUFDekMsV0FBTyxZQUFZLFFBQVEsUUFBUSxNQUFNLE1BQU07QUFBQSxFQUNuRDtBQVNPLFdBQVMsZ0JBQWdCO0FBQzVCLFdBQU8sS0FBSyxzQkFBc0I7QUFBQSxFQUN0QztBQVNPLFdBQVMsaUJBQWlCLE9BQU8sUUFBUTtBQUM1QyxXQUFPLFlBQVksUUFBUSxRQUFRLE1BQU0sTUFBTTtBQUFBLEVBQ25EO0FBU08sV0FBUyxpQkFBaUIsT0FBTyxRQUFRO0FBQzVDLFdBQU8sWUFBWSxRQUFRLFFBQVEsTUFBTSxNQUFNO0FBQUEsRUFDbkQ7QUFTTyxXQUFTLHFCQUFxQixHQUFHO0FBRXBDLFdBQU8sWUFBWSxXQUFXLElBQUksTUFBTSxJQUFJO0FBQUEsRUFDaEQ7QUFZTyxXQUFTLGtCQUFrQixHQUFHLEdBQUc7QUFDcEMsV0FBTyxZQUFZLFFBQVEsSUFBSSxNQUFNLENBQUM7QUFBQSxFQUMxQztBQVFPLFdBQVMsb0JBQW9CO0FBQ2hDLFdBQU8sS0FBSyxxQkFBcUI7QUFBQSxFQUNyQztBQU9PLFdBQVMsYUFBYTtBQUN6QixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBT08sV0FBUyxhQUFhO0FBQ3pCLFdBQU8sWUFBWSxJQUFJO0FBQUEsRUFDM0I7QUFPTyxXQUFTLGlCQUFpQjtBQUM3QixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBT08sV0FBUyx1QkFBdUI7QUFDbkMsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQU9PLFdBQVMsbUJBQW1CO0FBQy9CLFdBQU8sWUFBWSxJQUFJO0FBQUEsRUFDM0I7QUFRTyxXQUFTLG9CQUFvQjtBQUNoQyxXQUFPLEtBQUssMEJBQTBCO0FBQUEsRUFDMUM7QUFPTyxXQUFTLGlCQUFpQjtBQUM3QixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBT08sV0FBUyxtQkFBbUI7QUFDL0IsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQVFPLFdBQVMsb0JBQW9CO0FBQ2hDLFdBQU8sS0FBSywwQkFBMEI7QUFBQSxFQUMxQztBQVFPLFdBQVMsaUJBQWlCO0FBQzdCLFdBQU8sS0FBSyx1QkFBdUI7QUFBQSxFQUN2QztBQVdPLFdBQVMsMEJBQTBCLEdBQUcsR0FBRyxHQUFHLEdBQUc7QUFDbEQsUUFBSSxPQUFPLEtBQUssVUFBVSxFQUFDLEdBQUcsS0FBSyxHQUFHLEdBQUcsS0FBSyxHQUFHLEdBQUcsS0FBSyxHQUFHLEdBQUcsS0FBSyxJQUFHLENBQUM7QUFDeEUsV0FBTyxZQUFZLFFBQVEsSUFBSTtBQUFBLEVBQ25DOzs7QUMzUUE7QUFBQTtBQUFBO0FBQUE7QUFzQk8sV0FBUyxlQUFlO0FBQzNCLFdBQU8sS0FBSyxxQkFBcUI7QUFBQSxFQUNyQzs7O0FDeEJBO0FBQUE7QUFBQTtBQUFBO0FBS08sV0FBUyxlQUFlLEtBQUs7QUFDbEMsV0FBTyxZQUFZLFFBQVEsR0FBRztBQUFBLEVBQ2hDOzs7QUNQQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBb0JPLFdBQVMsaUJBQWlCLE1BQU07QUFDbkMsV0FBTyxLQUFLLDJCQUEyQixDQUFDLElBQUksQ0FBQztBQUFBLEVBQ2pEO0FBU08sV0FBUyxtQkFBbUI7QUFDL0IsV0FBTyxLQUFLLHlCQUF5QjtBQUFBLEVBQ3pDOzs7QUNqQ0E7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFjQSxNQUFNLFFBQVE7QUFBQSxJQUNWLFlBQVk7QUFBQSxJQUNaLHNCQUFzQjtBQUFBLElBQ3RCLGVBQWU7QUFBQSxJQUNmLGdCQUFnQjtBQUFBLElBQ2hCLHVCQUF1QjtBQUFBLEVBQzNCO0FBRUEsTUFBTSxxQkFBcUI7QUFRM0IsV0FBUyxxQkFBcUIsT0FBTztBQUNqQyxVQUFNLGVBQWUsTUFBTSxpQkFBaUIsT0FBTyxNQUFNLE1BQU0sZUFBZSxFQUFFLEtBQUs7QUFDckYsUUFBSSxjQUFjO0FBQ2QsVUFBSSxpQkFBaUIsT0FBTyxNQUFNLE1BQU0sY0FBYztBQUNsRCxlQUFPO0FBQUEsTUFDWDtBQUlBLGFBQU87QUFBQSxJQUNYO0FBQ0EsV0FBTztBQUFBLEVBQ1g7QUFPQSxXQUFTLFdBQVcsR0FBRztBQUluQixVQUFNLGFBQWEsRUFBRSxhQUFhLE1BQU0sU0FBUyxPQUFPO0FBR3hELFFBQUksQ0FBQyxZQUFZO0FBQ2I7QUFBQSxJQUNKO0FBR0EsTUFBRSxlQUFlO0FBQ2pCLE1BQUUsYUFBYSxhQUFhO0FBRTVCLFFBQUksQ0FBQyxPQUFPLE1BQU0sTUFBTSx3QkFBd0I7QUFDNUM7QUFBQSxJQUNKO0FBRUEsUUFBSSxDQUFDLE1BQU0sZUFBZTtBQUN0QjtBQUFBLElBQ0o7QUFFQSxVQUFNLFVBQVUsRUFBRTtBQUdsQixRQUFHLE1BQU07QUFBZ0IsWUFBTSxlQUFlO0FBRzlDLFFBQUksQ0FBQyxXQUFXLENBQUMscUJBQXFCLGlCQUFpQixPQUFPLENBQUMsR0FBRztBQUM5RDtBQUFBLElBQ0o7QUFFQSxRQUFJLGlCQUFpQjtBQUNyQixXQUFPLGdCQUFnQjtBQUVuQixVQUFJLHFCQUFxQixpQkFBaUIsY0FBYyxDQUFDLEdBQUc7QUFDeEQsdUJBQWUsVUFBVSxJQUFJLGtCQUFrQjtBQUFBLE1BQ25EO0FBQ0EsdUJBQWlCLGVBQWU7QUFBQSxJQUNwQztBQUFBLEVBQ0o7QUFPQSxXQUFTLFlBQVksR0FBRztBQUVwQixVQUFNLGFBQWEsRUFBRSxhQUFhLE1BQU0sU0FBUyxPQUFPO0FBR3hELFFBQUksQ0FBQyxZQUFZO0FBQ2I7QUFBQSxJQUNKO0FBR0EsTUFBRSxlQUFlO0FBRWpCLFFBQUksQ0FBQyxPQUFPLE1BQU0sTUFBTSx3QkFBd0I7QUFDNUM7QUFBQSxJQUNKO0FBRUEsUUFBSSxDQUFDLE1BQU0sZUFBZTtBQUN0QjtBQUFBLElBQ0o7QUFHQSxRQUFJLENBQUMsRUFBRSxVQUFVLENBQUMscUJBQXFCLGlCQUFpQixFQUFFLE1BQU0sQ0FBQyxHQUFHO0FBQ2hFLGFBQU87QUFBQSxJQUNYO0FBR0EsUUFBRyxNQUFNO0FBQWdCLFlBQU0sZUFBZTtBQUc5QyxVQUFNLGlCQUFpQixNQUFNO0FBRXpCLFlBQU0sS0FBSyxTQUFTLHVCQUF1QixrQkFBa0IsQ0FBQyxFQUFFLFFBQVEsUUFBTSxHQUFHLFVBQVUsT0FBTyxrQkFBa0IsQ0FBQztBQUVySCxZQUFNLGlCQUFpQjtBQUV2QixVQUFJLE1BQU0sdUJBQXVCO0FBQzdCLHFCQUFhLE1BQU0scUJBQXFCO0FBQ3hDLGNBQU0sd0JBQXdCO0FBQUEsTUFDbEM7QUFBQSxJQUNKO0FBR0EsVUFBTSx3QkFBd0IsV0FBVyxNQUFNO0FBQzNDLFVBQUcsTUFBTTtBQUFnQixjQUFNLGVBQWU7QUFBQSxJQUNsRCxHQUFHLEVBQUU7QUFBQSxFQUNUO0FBT0EsV0FBUyxPQUFPLEdBQUc7QUFFZixVQUFNLGFBQWEsRUFBRSxhQUFhLE1BQU0sU0FBUyxPQUFPO0FBR3hELFFBQUksQ0FBQyxZQUFZO0FBQ2I7QUFBQSxJQUNKO0FBR0EsTUFBRSxlQUFlO0FBRWpCLFFBQUksQ0FBQyxPQUFPLE1BQU0sTUFBTSx3QkFBd0I7QUFDNUM7QUFBQSxJQUNKO0FBRUEsUUFBSSxvQkFBb0IsR0FBRztBQUV2QixVQUFJLFFBQVEsQ0FBQztBQUNiLFVBQUksRUFBRSxhQUFhLE9BQU87QUFDdEIsZ0JBQVEsQ0FBQyxHQUFHLEVBQUUsYUFBYSxLQUFLLEVBQUUsSUFBSSxDQUFDLE1BQU0sTUFBTTtBQUMvQyxjQUFJLEtBQUssU0FBUyxRQUFRO0FBQ3RCLG1CQUFPLEtBQUssVUFBVTtBQUFBLFVBQzFCO0FBQUEsUUFDSixDQUFDO0FBQUEsTUFDTCxPQUFPO0FBQ0gsZ0JBQVEsQ0FBQyxHQUFHLEVBQUUsYUFBYSxLQUFLO0FBQUEsTUFDcEM7QUFDQSxhQUFPLFFBQVEsaUJBQWlCLEVBQUUsR0FBRyxFQUFFLEdBQUcsS0FBSztBQUFBLElBQ25EO0FBRUEsUUFBSSxDQUFDLE1BQU0sZUFBZTtBQUN0QjtBQUFBLElBQ0o7QUFHQSxRQUFHLE1BQU07QUFBZ0IsWUFBTSxlQUFlO0FBRzlDLFVBQU0sS0FBSyxTQUFTLHVCQUF1QixrQkFBa0IsQ0FBQyxFQUFFLFFBQVEsUUFBTSxHQUFHLFVBQVUsT0FBTyxrQkFBa0IsQ0FBQztBQUFBLEVBQ3pIO0FBUU8sV0FBUyxzQkFBc0I7QUFDbEMsV0FBTyxPQUFPLFFBQVEsU0FBUyxvQ0FBb0M7QUFBQSxFQUN2RTtBQVVPLFdBQVMsaUJBQWlCLEdBQUcsR0FBRyxPQUFPO0FBRzFDLFFBQUksT0FBTyxRQUFRLFNBQVMsa0NBQWtDO0FBQzFELGFBQU8sUUFBUSxpQ0FBaUMsYUFBYSxLQUFLLEtBQUssS0FBSztBQUFBLElBQ2hGO0FBQUEsRUFDSjtBQW1CTyxXQUFTLFdBQVcsVUFBVSxlQUFlO0FBQ2hELFFBQUksT0FBTyxhQUFhLFlBQVk7QUFDaEMsY0FBUSxNQUFNLHVDQUF1QztBQUNyRDtBQUFBLElBQ0o7QUFFQSxRQUFJLE1BQU0sWUFBWTtBQUNsQjtBQUFBLElBQ0o7QUFDQSxVQUFNLGFBQWE7QUFFbkIsVUFBTSxRQUFRLE9BQU87QUFDckIsVUFBTSxnQkFBZ0IsVUFBVSxlQUFlLFVBQVUsWUFBWSxNQUFNLHVCQUF1QjtBQUNsRyxXQUFPLGlCQUFpQixZQUFZLFVBQVU7QUFDOUMsV0FBTyxpQkFBaUIsYUFBYSxXQUFXO0FBQ2hELFdBQU8saUJBQWlCLFFBQVEsTUFBTTtBQUV0QyxRQUFJLEtBQUs7QUFDVCxRQUFJLE1BQU0sZUFBZTtBQUNyQixXQUFLLFNBQVUsR0FBRyxHQUFHLE9BQU87QUFDeEIsY0FBTSxVQUFVLFNBQVMsaUJBQWlCLEdBQUcsQ0FBQztBQUU5QyxZQUFJLENBQUMsV0FBVyxDQUFDLHFCQUFxQixpQkFBaUIsT0FBTyxDQUFDLEdBQUc7QUFDOUQsaUJBQU87QUFBQSxRQUNYO0FBQ0EsaUJBQVMsR0FBRyxHQUFHLEtBQUs7QUFBQSxNQUN4QjtBQUFBLElBQ0o7QUFFQSxhQUFTLG1CQUFtQixFQUFFO0FBQUEsRUFDbEM7QUFLTyxXQUFTLGdCQUFnQjtBQUM1QixXQUFPLG9CQUFvQixZQUFZLFVBQVU7QUFDakQsV0FBTyxvQkFBb0IsYUFBYSxXQUFXO0FBQ25ELFdBQU8sb0JBQW9CLFFBQVEsTUFBTTtBQUN6QyxjQUFVLGlCQUFpQjtBQUMzQixVQUFNLGFBQWE7QUFBQSxFQUN2Qjs7O0FDNVFPLFdBQVMsMEJBQTBCLE9BQU87QUFFN0MsVUFBTSxVQUFVLE1BQU07QUFDdEIsVUFBTSxnQkFBZ0IsT0FBTyxpQkFBaUIsT0FBTztBQUNyRCxVQUFNLDJCQUEyQixjQUFjLGlCQUFpQix1QkFBdUIsRUFBRSxLQUFLO0FBQzlGLFlBQVEsMEJBQTBCO0FBQUEsTUFDOUIsS0FBSztBQUNEO0FBQUEsTUFDSixLQUFLO0FBQ0QsY0FBTSxlQUFlO0FBQ3JCO0FBQUEsTUFDSjtBQUVJLFlBQUksUUFBUSxtQkFBbUI7QUFDM0I7QUFBQSxRQUNKO0FBR0EsY0FBTSxZQUFZLE9BQU8sYUFBYTtBQUN0QyxjQUFNLGVBQWdCLFVBQVUsU0FBUyxFQUFFLFNBQVM7QUFDcEQsWUFBSSxjQUFjO0FBQ2QsbUJBQVMsSUFBSSxHQUFHLElBQUksVUFBVSxZQUFZLEtBQUs7QUFDM0Msa0JBQU0sUUFBUSxVQUFVLFdBQVcsQ0FBQztBQUNwQyxrQkFBTSxRQUFRLE1BQU0sZUFBZTtBQUNuQyxxQkFBUyxJQUFJLEdBQUcsSUFBSSxNQUFNLFFBQVEsS0FBSztBQUNuQyxvQkFBTSxPQUFPLE1BQU07QUFDbkIsa0JBQUksU0FBUyxpQkFBaUIsS0FBSyxNQUFNLEtBQUssR0FBRyxNQUFNLFNBQVM7QUFDNUQ7QUFBQSxjQUNKO0FBQUEsWUFDSjtBQUFBLFVBQ0o7QUFBQSxRQUNKO0FBRUEsWUFBSSxRQUFRLFlBQVksV0FBVyxRQUFRLFlBQVksWUFBWTtBQUMvRCxjQUFJLGdCQUFpQixDQUFDLFFBQVEsWUFBWSxDQUFDLFFBQVEsVUFBVztBQUMxRDtBQUFBLFVBQ0o7QUFBQSxRQUNKO0FBR0EsY0FBTSxlQUFlO0FBQUEsSUFDN0I7QUFBQSxFQUNKOzs7QUNuQk8sV0FBUyxPQUFPO0FBQ25CLFdBQU8sWUFBWSxHQUFHO0FBQUEsRUFDMUI7QUFFTyxXQUFTLE9BQU87QUFDbkIsV0FBTyxZQUFZLEdBQUc7QUFBQSxFQUMxQjtBQUVPLFdBQVMsT0FBTztBQUNuQixXQUFPLFlBQVksR0FBRztBQUFBLEVBQzFCO0FBRU8sV0FBUyxjQUFjO0FBQzFCLFdBQU8sS0FBSyxvQkFBb0I7QUFBQSxFQUNwQztBQUdBLFNBQU8sVUFBVTtBQUFBLElBQ2IsR0FBRztBQUFBLElBQ0gsR0FBRztBQUFBLElBQ0gsR0FBRztBQUFBLElBQ0gsR0FBRztBQUFBLElBQ0gsR0FBRztBQUFBLElBQ0gsR0FBRztBQUFBLElBQ0g7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxFQUNKO0FBR0EsU0FBTyxRQUFRO0FBQUEsSUFDWDtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBLE9BQU87QUFBQSxNQUNILHNCQUFzQjtBQUFBLE1BQ3RCLDJCQUEyQjtBQUFBLE1BQzNCLGNBQWM7QUFBQSxNQUNkLGVBQWU7QUFBQSxNQUNmLGlCQUFpQjtBQUFBLE1BQ2pCLFlBQVk7QUFBQSxNQUNaLHNCQUFzQjtBQUFBLE1BQ3RCLGlCQUFpQjtBQUFBLE1BQ2pCLGNBQWM7QUFBQSxNQUNkLGlCQUFpQjtBQUFBLE1BQ2pCLGNBQWM7QUFBQSxNQUNkLHdCQUF3QjtBQUFBLElBQzVCO0FBQUEsRUFDSjtBQUdBLE1BQUksT0FBTyxlQUFlO0FBQ3RCLFdBQU8sTUFBTSxZQUFZLE9BQU8sYUFBYTtBQUM3QyxXQUFPLE9BQU8sTUFBTTtBQUFBLEVBQ3hCO0FBR0EsTUFBSSxPQUFRO0FBQ1IsV0FBTyxPQUFPO0FBQUEsRUFDbEI7QUFFQSxNQUFJLFdBQVcsU0FBUyxHQUFHO0FBQ3ZCLFFBQUksTUFBTSxPQUFPLGlCQUFpQixFQUFFLE1BQU0sRUFBRSxpQkFBaUIsT0FBTyxNQUFNLE1BQU0sZUFBZTtBQUMvRixRQUFJLEtBQUs7QUFDTCxZQUFNLElBQUksS0FBSztBQUFBLElBQ25CO0FBRUEsUUFBSSxRQUFRLE9BQU8sTUFBTSxNQUFNLGNBQWM7QUFDekMsYUFBTztBQUFBLElBQ1g7QUFFQSxRQUFJLEVBQUUsWUFBWSxHQUFHO0FBRWpCLGFBQU87QUFBQSxJQUNYO0FBRUEsUUFBSSxFQUFFLFdBQVcsR0FBRztBQUVoQixhQUFPO0FBQUEsSUFDWDtBQUVBLFdBQU87QUFBQSxFQUNYO0FBRUEsU0FBTyxNQUFNLHVCQUF1QixTQUFTLFVBQVUsT0FBTztBQUMxRCxXQUFPLE1BQU0sTUFBTSxrQkFBa0I7QUFDckMsV0FBTyxNQUFNLE1BQU0sZUFBZTtBQUFBLEVBQ3RDO0FBRUEsU0FBTyxNQUFNLHVCQUF1QixTQUFTLFVBQVUsT0FBTztBQUMxRCxXQUFPLE1BQU0sTUFBTSxrQkFBa0I7QUFDckMsV0FBTyxNQUFNLE1BQU0sZUFBZTtBQUFBLEVBQ3RDO0FBRUEsU0FBTyxpQkFBaUIsYUFBYSxDQUFDLE1BQU07QUFFeEMsUUFBSSxPQUFPLE1BQU0sTUFBTSxZQUFZO0FBQy9CLGFBQU8sWUFBWSxZQUFZLE9BQU8sTUFBTSxNQUFNLFVBQVU7QUFDNUQsUUFBRSxlQUFlO0FBQ2pCO0FBQUEsSUFDSjtBQUVBLFFBQUksU0FBUyxDQUFDLEdBQUc7QUFDYixVQUFJLE9BQU8sTUFBTSxNQUFNLHNCQUFzQjtBQUV6QyxZQUFJLEVBQUUsVUFBVSxFQUFFLE9BQU8sZUFBZSxFQUFFLFVBQVUsRUFBRSxPQUFPLGNBQWM7QUFDdkU7QUFBQSxRQUNKO0FBQUEsTUFDSjtBQUNBLFVBQUksT0FBTyxNQUFNLE1BQU0sc0JBQXNCO0FBQ3pDLGVBQU8sTUFBTSxNQUFNLGFBQWE7QUFBQSxNQUNwQyxPQUFPO0FBQ0gsVUFBRSxlQUFlO0FBQ2pCLGVBQU8sWUFBWSxNQUFNO0FBQUEsTUFDN0I7QUFDQTtBQUFBLElBQ0osT0FBTztBQUNILGFBQU8sTUFBTSxNQUFNLGFBQWE7QUFBQSxJQUNwQztBQUFBLEVBQ0osQ0FBQztBQUVELFNBQU8saUJBQWlCLFdBQVcsTUFBTTtBQUNyQyxXQUFPLE1BQU0sTUFBTSxhQUFhO0FBQUEsRUFDcEMsQ0FBQztBQUVELFdBQVMsVUFBVSxRQUFRO0FBQ3ZCLGFBQVMsZ0JBQWdCLE1BQU0sU0FBUyxVQUFVLE9BQU8sTUFBTSxNQUFNO0FBQ3JFLFdBQU8sTUFBTSxNQUFNLGFBQWE7QUFBQSxFQUNwQztBQUVBLFNBQU8saUJBQWlCLGFBQWEsU0FBUyxHQUFHO0FBQzdDLFFBQUksT0FBTyxNQUFNLE1BQU0sWUFBWTtBQUMvQixhQUFPLE1BQU0sTUFBTSxhQUFhO0FBQ2hDLFVBQUksZUFBZSxFQUFFLFlBQVksU0FBWSxFQUFFLFVBQVUsRUFBRTtBQUMzRCxVQUFJLGVBQWUsR0FBRztBQUNsQixlQUFPLFlBQVksTUFBTTtBQUN6QjtBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQ0EsUUFBSSxDQUFDLE9BQU8sTUFBTSxNQUFNLGNBQWM7QUFDbEM7QUFBQSxJQUNKO0FBQ0EsUUFBSSxPQUFPLE1BQU0sTUFBTSxpQkFBaUIsTUFBTTtBQUMxQyxhQUFPLE1BQU0sTUFBTSxnQkFBZ0IsU0FBUyxnQkFBZ0IsTUFBTTtBQUFBLElBQ3RFO0FBQ0EsUUFBSSxPQUFPLGFBQWEsRUFBRSxVQUFVLE9BQU8sTUFBTSxNQUFNLG1CQUFtQixPQUFPLGNBQWMsRUFBRSxVQUFVLE9BQU8sTUFBTSxNQUFNLGlCQUFpQjtBQUMzSSxlQUFTLGdCQUFnQixNQUFNLFNBQVM7QUFBQSxJQUM1QztBQUNBLFFBQUksY0FBYyxPQUFPLGFBQWEsRUFBRSxVQUFVLE9BQU8sTUFBTSxNQUFNO0FBQ3JFLFFBQUksYUFBYSxFQUFFLFVBQVUsT0FBTyxNQUFNLE1BQU07QUFDaEQsUUFBSSxZQUFZLEVBQUUsVUFBVSxPQUFPLE1BQU0sTUFBTTtBQUMvQyxRQUFJLGVBQWUsT0FBTyxjQUFjLEVBQUUsVUFBVSxPQUFPLE1BQU0sTUFBTTtBQUd2RSxRQUFJLENBQUMsY0FBYyxDQUFDLGVBQWUsQ0FBQyxhQUFhLENBQUMsZ0JBQWdCLE9BQU8sTUFBTSxNQUFNLGVBQWUsUUFBVztBQUMzRyxnQkFBVTtBQUFBLElBQ2QsV0FBVyxlQUFlO0FBQWMsZ0JBQVUsV0FBVztBQUFBLGFBQ3BELGNBQWM7QUFBYyxnQkFBVSxXQUFXO0FBQUEsYUFDakQsY0FBYztBQUFXLGdCQUFVLFdBQVc7QUFBQSxhQUM5QyxhQUFhO0FBQWEsZ0JBQVUsV0FBVztBQUFBLGFBQy9DO0FBQVksZ0JBQVUsVUFBVTtBQUFBLGFBQ2hDO0FBQVcsZ0JBQVUsVUFBVTtBQUFBLGFBQy9CO0FBQWMsZ0JBQVUsVUFBVTtBQUFBLGFBQ2xDO0FBQWEsZ0JBQVUsVUFBVTtBQUFBLEVBRTlDLENBQUM7QUFHRCxTQUFPLGlCQUFpQixlQUFlLFNBQVMsR0FBRztBQUUvQyxRQUFJO0FBQU87QUFFWCxRQUFJLE9BQU8sTUFBTSxNQUFNLDJCQUEyQjtBQUM5QyxRQUFFLGVBQWU7QUFBQSxJQUNyQixPQUFPO0FBQ0gsTUFBWSwwQkFBMEIsQ0FBQztBQUFBLElBQzNDO0FBQUEsRUFDSixDQUFDO0FBRUQsU0FBTyxZQUFZLGVBQWU7IiwKICAibmFtZXMiOiBbImV2ZW50TmFtZSJdCn0K
diff --git a/v2/internal/frontend/runtime/runtime_prod_desktop.go b/v2/internal/frontend/runtime/runtime_prod_desktop.go
new file mode 100644
index 000000000..7336f0102
--- /dev/null
+++ b/v2/internal/frontend/runtime/runtime_prod_desktop.go
@@ -0,0 +1,8 @@
+//go:build production && !debug
+
+package runtime
+
+import _ "embed"
+
+//go:embed runtime_prod_desktop.js
+var RuntimeDesktopJS []byte
diff --git a/v2/internal/frontend/runtime/runtime_prod_desktop.js b/v2/internal/frontend/runtime/runtime_prod_desktop.js
new file mode 100644
index 000000000..3d38924f7
--- /dev/null
+++ b/v2/internal/frontend/runtime/runtime_prod_desktop.js
@@ -0,0 +1 @@
+(()=>{var j=Object.defineProperty;var p=(e,t)=>{for(var n in t)j(e,n,{get:t[n],enumerable:!0})};var b={};p(b,{LogDebug:()=>$,LogError:()=>Q,LogFatal:()=>_,LogInfo:()=>Y,LogLevel:()=>K,LogPrint:()=>X,LogTrace:()=>J,LogWarning:()=>q,SetLogLevel:()=>Z});function u(e,t){window.WailsInvoke("L"+e+t)}function J(e){u("T",e)}function X(e){u("P",e)}function $(e){u("D",e)}function Y(e){u("I",e)}function q(e){u("W",e)}function Q(e){u("E",e)}function _(e){u("F",e)}function Z(e){u("S",e)}var K={TRACE:1,DEBUG:2,INFO:3,WARNING:4,ERROR:5};var y=class{constructor(t,n,o){this.eventName=t,this.maxCallbacks=o||-1,this.Callback=i=>(n.apply(null,i),this.maxCallbacks===-1?!1:(this.maxCallbacks-=1,this.maxCallbacks===0))}},w={};function v(e,t,n){w[e]=w[e]||[];let o=new y(e,t,n);return w[e].push(o),()=>ee(o)}function W(e,t){return v(e,t,-1)}function A(e,t){return v(e,t,1)}function P(e){let t=e.name,n=w[t]?.slice()||[];if(n.length){for(let o=n.length-1;o>=0;o-=1){let i=n[o],r=e.data;i.Callback(r)&&n.splice(o,1)}n.length===0?g(t):w[t]=n}}function F(e){let t;try{t=JSON.parse(e)}catch{let o="Invalid JSON passed to Notify: "+e;throw new Error(o)}P(t)}function R(e){let t={name:e,data:[].slice.apply(arguments).slice(1)};P(t),window.WailsInvoke("EE"+JSON.stringify(t))}function g(e){delete w[e],window.WailsInvoke("EX"+e)}function x(e,...t){g(e),t.length>0&&t.forEach(n=>{g(n)})}function M(){Object.keys(w).forEach(t=>{g(t)})}function ee(e){let t=e.eventName;w[t]!==void 0&&(w[t]=w[t].filter(n=>n!==e),w[t].length===0&&g(t))}var c={};function te(){var e=new Uint32Array(1);return window.crypto.getRandomValues(e)[0]}function ne(){return Math.random()*9007199254740991}var D;window.crypto?D=te:D=ne;function a(e,t,n){return n==null&&(n=0),new Promise(function(o,i){var r;do r=e+"-"+D();while(c[r]);var l;n>0&&(l=setTimeout(function(){i(Error("Call to "+e+" timed out. Request ID: "+r))},n)),c[r]={timeoutHandle:l,reject:i,resolve:o};try{let d={name:e,args:t,callbackID:r};window.WailsInvoke("C"+JSON.stringify(d))}catch(d){console.error(d)}})}window.ObfuscatedCall=(e,t,n)=>(n==null&&(n=0),new Promise(function(o,i){var r;do r=e+"-"+D();while(c[r]);var l;n>0&&(l=setTimeout(function(){i(Error("Call to method "+e+" timed out. Request ID: "+r))},n)),c[r]={timeoutHandle:l,reject:i,resolve:o};try{let d={id:e,args:t,callbackID:r};window.WailsInvoke("c"+JSON.stringify(d))}catch(d){console.error(d)}}));function z(e){let t;try{t=JSON.parse(e)}catch(i){let r=`Invalid JSON passed to callback: ${i.message}. Message: ${e}`;throw runtime.LogDebug(r),new Error(r)}let n=t.callbackid,o=c[n];if(!o){let i=`Callback '${n}' not registered!!!`;throw console.error(i),new Error(i)}clearTimeout(o.timeoutHandle),delete c[n],t.error?o.reject(t.error):o.resolve(t.result)}window.go={};function B(e){try{e=JSON.parse(e)}catch(t){console.error(t)}window.go=window.go||{},Object.keys(e).forEach(t=>{window.go[t]=window.go[t]||{},Object.keys(e[t]).forEach(n=>{window.go[t][n]=window.go[t][n]||{},Object.keys(e[t][n]).forEach(o=>{window.go[t][n][o]=function(){let i=0;function r(){let l=[].slice.call(arguments);return a([t,n,o].join("."),l,i)}return r.setTimeout=function(l){i=l},r.getTimeout=function(){return i},r}()})})})}var T={};p(T,{WindowCenter:()=>ae,WindowFullscreen:()=>de,WindowGetPosition:()=>xe,WindowGetSize:()=>pe,WindowHide:()=>De,WindowIsFullscreen:()=>ue,WindowIsMaximised:()=>Te,WindowIsMinimised:()=>Ce,WindowIsNormal:()=>Ie,WindowMaximise:()=>Ee,WindowMinimise:()=>Se,WindowReload:()=>oe,WindowReloadApp:()=>ie,WindowSetAlwaysOnTop:()=>ve,WindowSetBackgroundColour:()=>Oe,WindowSetDarkTheme:()=>le,WindowSetLightTheme:()=>se,WindowSetMaxSize:()=>ge,WindowSetMinSize:()=>me,WindowSetPosition:()=>We,WindowSetSize:()=>ce,WindowSetSystemDefaultTheme:()=>re,WindowSetTitle:()=>we,WindowShow:()=>he,WindowToggleMaximise:()=>be,WindowUnfullscreen:()=>fe,WindowUnmaximise:()=>ye,WindowUnminimise:()=>ke});function oe(){window.location.reload()}function ie(){window.WailsInvoke("WR")}function re(){window.WailsInvoke("WASDT")}function se(){window.WailsInvoke("WALT")}function le(){window.WailsInvoke("WADT")}function ae(){window.WailsInvoke("Wc")}function we(e){window.WailsInvoke("WT"+e)}function de(){window.WailsInvoke("WF")}function fe(){window.WailsInvoke("Wf")}function ue(){return a(":wails:WindowIsFullscreen")}function ce(e,t){window.WailsInvoke("Ws:"+e+":"+t)}function pe(){return a(":wails:WindowGetSize")}function ge(e,t){window.WailsInvoke("WZ:"+e+":"+t)}function me(e,t){window.WailsInvoke("Wz:"+e+":"+t)}function ve(e){window.WailsInvoke("WATP:"+(e?"1":"0"))}function We(e,t){window.WailsInvoke("Wp:"+e+":"+t)}function xe(){return a(":wails:WindowGetPos")}function De(){window.WailsInvoke("WH")}function he(){window.WailsInvoke("WS")}function Ee(){window.WailsInvoke("WM")}function be(){window.WailsInvoke("Wt")}function ye(){window.WailsInvoke("WU")}function Te(){return a(":wails:WindowIsMaximised")}function Se(){window.WailsInvoke("Wm")}function ke(){window.WailsInvoke("Wu")}function Ce(){return a(":wails:WindowIsMinimised")}function Ie(){return a(":wails:WindowIsNormal")}function Oe(e,t,n,o){let i=JSON.stringify({r:e||0,g:t||0,b:n||0,a:o||255});window.WailsInvoke("Wr:"+i)}var S={};p(S,{ScreenGetAll:()=>Le});function Le(){return a(":wails:ScreenGetAll")}var k={};p(k,{BrowserOpenURL:()=>Ae});function Ae(e){window.WailsInvoke("BO:"+e)}var C={};p(C,{ClipboardGetText:()=>Fe,ClipboardSetText:()=>Pe});function Pe(e){return a(":wails:ClipboardSetText",[e])}function Fe(){return a(":wails:ClipboardGetText")}var I={};p(I,{CanResolveFilePaths:()=>V,OnFileDrop:()=>Me,OnFileDropOff:()=>ze,ResolveFilePaths:()=>Re});var s={registered:!1,defaultUseDropTarget:!0,useDropTarget:!0,nextDeactivate:null,nextDeactivateTimeout:null},m="wails-drop-target-active";function h(e){let t=e.getPropertyValue(window.wails.flags.cssDropProperty).trim();return t?t===window.wails.flags.cssDropValue:!1}function G(e){if(!e.dataTransfer.types.includes("Files")||(e.preventDefault(),e.dataTransfer.dropEffect="copy",!window.wails.flags.enableWailsDragAndDrop)||!s.useDropTarget)return;let n=e.target;if(s.nextDeactivate&&s.nextDeactivate(),!n||!h(getComputedStyle(n)))return;let o=n;for(;o;)h(getComputedStyle(o))&&o.classList.add(m),o=o.parentElement}function H(e){if(!!e.dataTransfer.types.includes("Files")&&(e.preventDefault(),!!window.wails.flags.enableWailsDragAndDrop&&!!s.useDropTarget)){if(!e.target||!h(getComputedStyle(e.target)))return null;s.nextDeactivate&&s.nextDeactivate(),s.nextDeactivate=()=>{Array.from(document.getElementsByClassName(m)).forEach(n=>n.classList.remove(m)),s.nextDeactivate=null,s.nextDeactivateTimeout&&(clearTimeout(s.nextDeactivateTimeout),s.nextDeactivateTimeout=null)},s.nextDeactivateTimeout=setTimeout(()=>{s.nextDeactivate&&s.nextDeactivate()},50)}}function U(e){if(!!e.dataTransfer.types.includes("Files")&&(e.preventDefault(),!!window.wails.flags.enableWailsDragAndDrop)){if(V()){let n=[];e.dataTransfer.items?n=[...e.dataTransfer.items].map((o,i)=>{if(o.kind==="file")return o.getAsFile()}):n=[...e.dataTransfer.files],window.runtime.ResolveFilePaths(e.x,e.y,n)}!s.useDropTarget||(s.nextDeactivate&&s.nextDeactivate(),Array.from(document.getElementsByClassName(m)).forEach(n=>n.classList.remove(m)))}}function V(){return window.chrome?.webview?.postMessageWithAdditionalObjects!=null}function Re(e,t,n){window.chrome?.webview?.postMessageWithAdditionalObjects&&chrome.webview.postMessageWithAdditionalObjects(`file:drop:${e}:${t}`,n)}function Me(e,t){if(typeof e!="function"){console.error("DragAndDropCallback is not a function");return}if(s.registered)return;s.registered=!0;let n=typeof t;s.useDropTarget=n==="undefined"||n!=="boolean"?s.defaultUseDropTarget:t,window.addEventListener("dragover",G),window.addEventListener("dragleave",H),window.addEventListener("drop",U);let o=e;s.useDropTarget&&(o=function(i,r,l){let d=document.elementFromPoint(i,r);if(!d||!h(getComputedStyle(d)))return null;e(i,r,l)}),W("wails:file-drop",o)}function ze(){window.removeEventListener("dragover",G),window.removeEventListener("dragleave",H),window.removeEventListener("drop",U),x("wails:file-drop"),s.registered=!1}function N(e){let t=e.target;switch(window.getComputedStyle(t).getPropertyValue("--default-contextmenu").trim()){case"show":return;case"hide":e.preventDefault();return;default:if(t.isContentEditable)return;let i=window.getSelection(),r=i.toString().length>0;if(r)for(let l=0;l{if(window.wails.flags.resizeEdge){window.WailsInvoke("resize:"+window.wails.flags.resizeEdge),e.preventDefault();return}if(Ne(e)){if(window.wails.flags.disableScrollbarDrag&&(e.offsetX>e.target.clientWidth||e.offsetY>e.target.clientHeight))return;window.wails.flags.deferDragToMouseMove?window.wails.flags.shouldDrag=!0:(e.preventDefault(),window.WailsInvoke("drag"));return}else window.wails.flags.shouldDrag=!1});window.addEventListener("mouseup",()=>{window.wails.flags.shouldDrag=!1});function f(e){document.documentElement.style.cursor=e||window.wails.flags.defaultCursor,window.wails.flags.resizeEdge=e}window.addEventListener("mousemove",function(e){if(window.wails.flags.shouldDrag&&(window.wails.flags.shouldDrag=!1,(e.buttons!==void 0?e.buttons:e.which)>0)){window.WailsInvoke("drag");return}if(!window.wails.flags.enableResize)return;window.wails.flags.defaultCursor==null&&(window.wails.flags.defaultCursor=document.documentElement.style.cursor),window.outerWidth-e.clientX",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/wailsapp/wails/issues"
+ },
+ "homepage": "https://github.com/wailsapp/wails#readme"
+}
diff --git a/v2/internal/frontend/runtime/wrapper/runtime.d.ts b/v2/internal/frontend/runtime/wrapper/runtime.d.ts
new file mode 100644
index 000000000..4445dac21
--- /dev/null
+++ b/v2/internal/frontend/runtime/wrapper/runtime.d.ts
@@ -0,0 +1,249 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export interface Size {
+ w: number;
+ h: number;
+}
+
+export interface Screen {
+ isCurrent: boolean;
+ isPrimary: boolean;
+ width : number
+ height : number
+}
+
+// Environment information such as platform, buildtype, ...
+export interface EnvironmentInfo {
+ buildType: string;
+ platform: string;
+ arch: string;
+}
+
+// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
+// emits the given event. Optional data may be passed with the event.
+// This will trigger any event listeners.
+export function EventsEmit(eventName: string, ...data: any): void;
+
+// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
+export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
+// sets up a listener for the given event name, but will only trigger a given number times.
+export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
+
+// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
+// sets up a listener for the given event name, but will only trigger once.
+export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
+// unregisters the listener for the given event name.
+export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
+
+// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
+// unregisters all listeners.
+export function EventsOffAll(): void;
+
+// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
+// logs the given message as a raw message
+export function LogPrint(message: string): void;
+
+// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
+// logs the given message at the `trace` log level.
+export function LogTrace(message: string): void;
+
+// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
+// logs the given message at the `debug` log level.
+export function LogDebug(message: string): void;
+
+// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
+// logs the given message at the `error` log level.
+export function LogError(message: string): void;
+
+// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
+// logs the given message at the `fatal` log level.
+// The application will quit after calling this method.
+export function LogFatal(message: string): void;
+
+// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
+// logs the given message at the `info` log level.
+export function LogInfo(message: string): void;
+
+// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
+// logs the given message at the `warning` log level.
+export function LogWarning(message: string): void;
+
+// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
+// Forces a reload by the main application as well as connected browsers.
+export function WindowReload(): void;
+
+// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
+// Reloads the application frontend.
+export function WindowReloadApp(): void;
+
+// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
+// Sets the window AlwaysOnTop or not on top.
+export function WindowSetAlwaysOnTop(b: boolean): void;
+
+// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
+// *Windows only*
+// Sets window theme to system default (dark/light).
+export function WindowSetSystemDefaultTheme(): void;
+
+// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
+// *Windows only*
+// Sets window to light theme.
+export function WindowSetLightTheme(): void;
+
+// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
+// *Windows only*
+// Sets window to dark theme.
+export function WindowSetDarkTheme(): void;
+
+// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
+// Centers the window on the monitor the window is currently on.
+export function WindowCenter(): void;
+
+// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
+// Sets the text in the window title bar.
+export function WindowSetTitle(title: string): void;
+
+// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
+// Makes the window full screen.
+export function WindowFullscreen(): void;
+
+// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
+// Restores the previous window dimensions and position prior to full screen.
+export function WindowUnfullscreen(): void;
+
+// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
+// Returns the state of the window, i.e. whether the window is in full screen mode or not.
+export function WindowIsFullscreen(): Promise;
+
+// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
+// Sets the width and height of the window.
+export function WindowSetSize(width: number, height: number): void;
+
+// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
+// Gets the width and height of the window.
+export function WindowGetSize(): Promise;
+
+// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
+// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMaxSize(width: number, height: number): void;
+
+// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
+// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMinSize(width: number, height: number): void;
+
+// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
+// Sets the window position relative to the monitor the window is currently on.
+export function WindowSetPosition(x: number, y: number): void;
+
+// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
+// Gets the window position relative to the monitor the window is currently on.
+export function WindowGetPosition(): Promise;
+
+// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
+// Hides the window.
+export function WindowHide(): void;
+
+// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
+// Shows the window, if it is currently hidden.
+export function WindowShow(): void;
+
+// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
+// Maximises the window to fill the screen.
+export function WindowMaximise(): void;
+
+// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
+// Toggles between Maximised and UnMaximised.
+export function WindowToggleMaximise(): void;
+
+// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
+// Restores the window to the dimensions and position prior to maximising.
+export function WindowUnmaximise(): void;
+
+// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
+// Returns the state of the window, i.e. whether the window is maximised or not.
+export function WindowIsMaximised(): Promise;
+
+// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
+// Minimises the window.
+export function WindowMinimise(): void;
+
+// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
+// Restores the window to the dimensions and position prior to minimising.
+export function WindowUnminimise(): void;
+
+// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
+// Returns the state of the window, i.e. whether the window is minimised or not.
+export function WindowIsMinimised(): Promise;
+
+// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
+// Returns the state of the window, i.e. whether the window is normal or not.
+export function WindowIsNormal(): Promise;
+
+// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
+// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
+export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
+
+// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
+// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
+export function ScreenGetAll(): Promise;
+
+// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
+// Opens the given URL in the system browser.
+export function BrowserOpenURL(url: string): void;
+
+// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
+// Returns information about the environment
+export function Environment(): Promise;
+
+// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
+// Quits the application.
+export function Quit(): void;
+
+// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
+// Hides the application.
+export function Hide(): void;
+
+// [Show](https://wails.io/docs/reference/runtime/intro#show)
+// Shows the application.
+export function Show(): void;
+
+// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
+// Returns the current text stored on clipboard
+export function ClipboardGetText(): Promise;
+
+// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
+// Sets a text on the clipboard
+export function ClipboardSetText(text: string): Promise;
+
+// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
+// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
+
+// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
+// OnFileDropOff removes the drag and drop listeners and handlers.
+export function OnFileDropOff() :void
+
+// Check if the file path resolver is available
+export function CanResolveFilePaths(): boolean;
+
+// Resolves file paths for an array of files
+export function ResolveFilePaths(files: File[]): void
\ No newline at end of file
diff --git a/v2/internal/frontend/runtime/wrapper/runtime.js b/v2/internal/frontend/runtime/wrapper/runtime.js
new file mode 100644
index 000000000..7cb89d750
--- /dev/null
+++ b/v2/internal/frontend/runtime/wrapper/runtime.js
@@ -0,0 +1,242 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export function LogPrint(message) {
+ window.runtime.LogPrint(message);
+}
+
+export function LogTrace(message) {
+ window.runtime.LogTrace(message);
+}
+
+export function LogDebug(message) {
+ window.runtime.LogDebug(message);
+}
+
+export function LogInfo(message) {
+ window.runtime.LogInfo(message);
+}
+
+export function LogWarning(message) {
+ window.runtime.LogWarning(message);
+}
+
+export function LogError(message) {
+ window.runtime.LogError(message);
+}
+
+export function LogFatal(message) {
+ window.runtime.LogFatal(message);
+}
+
+export function EventsOnMultiple(eventName, callback, maxCallbacks) {
+ return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
+}
+
+export function EventsOn(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, -1);
+}
+
+export function EventsOff(eventName, ...additionalEventNames) {
+ return window.runtime.EventsOff(eventName, ...additionalEventNames);
+}
+
+export function EventsOffAll() {
+ return window.runtime.EventsOffAll();
+}
+
+export function EventsOnce(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, 1);
+}
+
+export function EventsEmit(eventName) {
+ let args = [eventName].slice.call(arguments);
+ return window.runtime.EventsEmit.apply(null, args);
+}
+
+export function WindowReload() {
+ window.runtime.WindowReload();
+}
+
+export function WindowReloadApp() {
+ window.runtime.WindowReloadApp();
+}
+
+export function WindowSetAlwaysOnTop(b) {
+ window.runtime.WindowSetAlwaysOnTop(b);
+}
+
+export function WindowSetSystemDefaultTheme() {
+ window.runtime.WindowSetSystemDefaultTheme();
+}
+
+export function WindowSetLightTheme() {
+ window.runtime.WindowSetLightTheme();
+}
+
+export function WindowSetDarkTheme() {
+ window.runtime.WindowSetDarkTheme();
+}
+
+export function WindowCenter() {
+ window.runtime.WindowCenter();
+}
+
+export function WindowSetTitle(title) {
+ window.runtime.WindowSetTitle(title);
+}
+
+export function WindowFullscreen() {
+ window.runtime.WindowFullscreen();
+}
+
+export function WindowUnfullscreen() {
+ window.runtime.WindowUnfullscreen();
+}
+
+export function WindowIsFullscreen() {
+ return window.runtime.WindowIsFullscreen();
+}
+
+export function WindowGetSize() {
+ return window.runtime.WindowGetSize();
+}
+
+export function WindowSetSize(width, height) {
+ window.runtime.WindowSetSize(width, height);
+}
+
+export function WindowSetMaxSize(width, height) {
+ window.runtime.WindowSetMaxSize(width, height);
+}
+
+export function WindowSetMinSize(width, height) {
+ window.runtime.WindowSetMinSize(width, height);
+}
+
+export function WindowSetPosition(x, y) {
+ window.runtime.WindowSetPosition(x, y);
+}
+
+export function WindowGetPosition() {
+ return window.runtime.WindowGetPosition();
+}
+
+export function WindowHide() {
+ window.runtime.WindowHide();
+}
+
+export function WindowShow() {
+ window.runtime.WindowShow();
+}
+
+export function WindowMaximise() {
+ window.runtime.WindowMaximise();
+}
+
+export function WindowToggleMaximise() {
+ window.runtime.WindowToggleMaximise();
+}
+
+export function WindowUnmaximise() {
+ window.runtime.WindowUnmaximise();
+}
+
+export function WindowIsMaximised() {
+ return window.runtime.WindowIsMaximised();
+}
+
+export function WindowMinimise() {
+ window.runtime.WindowMinimise();
+}
+
+export function WindowUnminimise() {
+ window.runtime.WindowUnminimise();
+}
+
+export function WindowSetBackgroundColour(R, G, B, A) {
+ window.runtime.WindowSetBackgroundColour(R, G, B, A);
+}
+
+export function ScreenGetAll() {
+ return window.runtime.ScreenGetAll();
+}
+
+export function WindowIsMinimised() {
+ return window.runtime.WindowIsMinimised();
+}
+
+export function WindowIsNormal() {
+ return window.runtime.WindowIsNormal();
+}
+
+export function BrowserOpenURL(url) {
+ window.runtime.BrowserOpenURL(url);
+}
+
+export function Environment() {
+ return window.runtime.Environment();
+}
+
+export function Quit() {
+ window.runtime.Quit();
+}
+
+export function Hide() {
+ window.runtime.Hide();
+}
+
+export function Show() {
+ window.runtime.Show();
+}
+
+export function ClipboardGetText() {
+ return window.runtime.ClipboardGetText();
+}
+
+export function ClipboardSetText(text) {
+ return window.runtime.ClipboardSetText(text);
+}
+
+/**
+ * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ *
+ * @export
+ * @callback OnFileDropCallback
+ * @param {number} x - x coordinate of the drop
+ * @param {number} y - y coordinate of the drop
+ * @param {string[]} paths - A list of file paths.
+ */
+
+/**
+ * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+ *
+ * @export
+ * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
+ */
+export function OnFileDrop(callback, useDropTarget) {
+ return window.runtime.OnFileDrop(callback, useDropTarget);
+}
+
+/**
+ * OnFileDropOff removes the drag and drop listeners and handlers.
+ */
+export function OnFileDropOff() {
+ return window.runtime.OnFileDropOff();
+}
+
+export function CanResolveFilePaths() {
+ return window.runtime.CanResolveFilePaths();
+}
+
+export function ResolveFilePaths(files) {
+ return window.runtime.ResolveFilePaths(files);
+}
\ No newline at end of file
diff --git a/v2/internal/frontend/runtime/wrapper/wrapper.go b/v2/internal/frontend/runtime/wrapper/wrapper.go
new file mode 100644
index 000000000..94853bc7c
--- /dev/null
+++ b/v2/internal/frontend/runtime/wrapper/wrapper.go
@@ -0,0 +1,6 @@
+package wrapper
+
+import "embed"
+
+//go:embed runtime.js runtime.d.ts package.json
+var RuntimeWrapper embed.FS
diff --git a/v2/internal/frontend/utils/urlValidator.go b/v2/internal/frontend/utils/urlValidator.go
new file mode 100644
index 000000000..76ba216ce
--- /dev/null
+++ b/v2/internal/frontend/utils/urlValidator.go
@@ -0,0 +1,58 @@
+package utils
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "regexp"
+ "strings"
+)
+
+func ValidateAndSanitizeURL(rawURL string) (string, error) {
+ // Check for null bytes (can cause truncation issues in some systems)
+ if strings.Contains(rawURL, "\x00") {
+ return "", errors.New("null bytes not allowed in URL")
+ }
+
+ // Parse URL first - this handles most malformed URLs
+ parsedURL, err := url.Parse(rawURL)
+ if err != nil {
+ return "", fmt.Errorf("invalid URL format: %v", err)
+ }
+
+ scheme := strings.ToLower(parsedURL.Scheme)
+
+ if scheme == "javascript" || scheme == "data" || scheme == "file" || scheme == "ftp" || scheme == "" {
+ return "", errors.New("scheme not allowed")
+ }
+
+ // Ensure there's actually a host for http/https URLs
+ if (scheme == "http" || scheme == "https") && parsedURL.Host == "" {
+ return "", fmt.Errorf("missing host for %s URL", scheme)
+ }
+
+ sanitizedURL := parsedURL.String()
+
+ // Check for control characters that might cause issues
+ // (but allow legitimate URL characters like &, ;, etc.)
+ for i, r := range sanitizedURL {
+ // Block control characters except tab, but allow other printable chars
+ if r < 32 && r != 9 { // 9 is tab, which might be legitimate
+ return "", fmt.Errorf("control character at position %d not allowed", i)
+ }
+ }
+
+ // Shell metacharacter check
+ shellDangerous := `[;\|` + "`" + `$\\<>*{}\[\]()~! \t\n\r]`
+ if matched, _ := regexp.MatchString(shellDangerous, sanitizedURL); matched {
+ return "", errors.New("shell metacharacters not allowed")
+ }
+
+ // Unicode danger check
+ unicodeDangerous := "[\u0000-\u001F\u007F\u00A0\u1680\u2000-\u200F\u2028-\u202F\u205F\u2060\u3000\uFEFF]"
+ if matched, _ := regexp.MatchString(unicodeDangerous, sanitizedURL); matched {
+ return "", errors.New("unicode dangerous characters not allowed")
+ }
+
+ return sanitizedURL, nil
+}
diff --git a/v2/internal/frontend/utils/urlValidator_test.go b/v2/internal/frontend/utils/urlValidator_test.go
new file mode 100644
index 000000000..b385ccec1
--- /dev/null
+++ b/v2/internal/frontend/utils/urlValidator_test.go
@@ -0,0 +1,207 @@
+package utils_test
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/wailsapp/wails/v2/internal/frontend/utils"
+)
+
+// Test cases for ValidateAndOpenURL
+func TestValidateURL(t *testing.T) {
+ testCases := []struct {
+ name string
+ url string
+ shouldErr bool
+ errMsg string
+ expected string
+ }{
+ // Valid URLs
+ {
+ name: "valid https URL",
+ url: "https://www.example.com",
+ shouldErr: false,
+ expected: "https://www.example.com",
+ },
+ {
+ name: "valid http URL",
+ url: "http://example.com",
+ shouldErr: false,
+ expected: "http://example.com",
+ },
+ {
+ name: "URL with query parameters",
+ url: "https://example.com/search?q=cats&dogs",
+ shouldErr: false,
+ expected: "https://example.com/search?q=cats&dogs",
+ },
+ {
+ name: "URL with port",
+ url: "https://example.com:8080/path",
+ shouldErr: false,
+ expected: "https://example.com:8080/path",
+ },
+ {
+ name: "URL with fragment",
+ url: "https://example.com/page#section",
+ shouldErr: false,
+ expected: "https://example.com/page#section",
+ },
+ {
+ name: "urlencode params",
+ url: "http://google.com/ ----browser-subprocess-path=C:\\\\Users\\\\Public\\\\test.bat",
+ shouldErr: false,
+ expected: "http://google.com/%20----browser-subprocess-path=C:%5C%5CUsers%5C%5CPublic%5C%5Ctest.bat",
+ },
+
+ // Invalid schemes
+ {
+ name: "javascript scheme",
+ url: "javascript:alert('xss')",
+ shouldErr: true,
+ errMsg: "scheme not allowed",
+ },
+ {
+ name: "data scheme",
+ url: "data:text/html,",
+ shouldErr: true,
+ errMsg: "scheme not allowed",
+ },
+ {
+ name: "file scheme",
+ url: "file:///etc/passwd",
+ shouldErr: true,
+ errMsg: "scheme not allowed",
+ },
+ {
+ name: "ftp scheme",
+ url: "ftp://files.example.com/file.txt",
+ shouldErr: true,
+ errMsg: "scheme not allowed",
+ },
+
+ // Malformed URLs
+ {
+ name: "not a URL",
+ url: "not-a-url",
+ shouldErr: true,
+ errMsg: "scheme not allowed", // will have empty scheme
+ },
+ {
+ name: "missing scheme",
+ url: "example.com",
+ shouldErr: true,
+ errMsg: "scheme not allowed",
+ },
+ {
+ name: "malformed URL",
+ url: "https://",
+ shouldErr: true,
+ errMsg: "missing host",
+ },
+ {
+ name: "empty host",
+ url: "http:///path",
+ shouldErr: true,
+ errMsg: "missing host",
+ },
+
+ // Security issues
+ {
+ name: "null byte in URL",
+ url: "https://example.com\x00/hidden",
+ shouldErr: true,
+ errMsg: "null bytes not allowed",
+ },
+ {
+ name: "control characters",
+ url: "https://example.com\n/path",
+ shouldErr: true,
+ errMsg: "control character",
+ },
+ {
+ name: "carriage return",
+ url: "https://example.com\r/path",
+ shouldErr: true,
+ errMsg: "control character",
+ },
+ {
+ name: "URL with tab character",
+ url: "https://example.com/path?q=hello\tworld",
+ shouldErr: true,
+ errMsg: "control character",
+ },
+ {
+ name: "URL with path parameters",
+ url: "https://example.com/path;param=value",
+ shouldErr: true,
+ errMsg: "shell metacharacters not allowed",
+ },
+ {
+ name: "URL with special characters in query",
+ url: "https://example.com/search?q=hello world&filter=price>100",
+ shouldErr: true,
+ errMsg: "shell metacharacters not allowed",
+ },
+ {
+ name: "URL with special characters in query and params",
+ url: "https://example.com/search?q=hello ----browser-subprocess-path=C:\\\\Users\\\\Public\\\\test.bat",
+ shouldErr: true,
+ errMsg: "shell metacharacters not allowed",
+ },
+ {
+ name: "URL with dollar sign in query",
+ url: "https://example.com/search?price=$100",
+ shouldErr: true,
+ errMsg: "shell metacharacters not allowed",
+ },
+ {
+ name: "URL with parentheses",
+ url: "https://example.com/file(1).html",
+ shouldErr: true,
+ errMsg: "shell metacharacters not allowed",
+ },
+ {
+ name: "URL with unicode",
+ url: "https://example.com/search?q=hello\u2001foo",
+ shouldErr: true,
+ errMsg: "unicode dangerous characters not allowed",
+ },
+
+ // Edge cases
+ {
+ name: "international domain",
+ url: "https://例え.テスト/path",
+ shouldErr: false,
+ expected: "https://%E4%BE%8B%E3%81%88.%E3%83%86%E3%82%B9%E3%83%88/path",
+ },
+ {
+ name: "URL with pipe character",
+ url: "https://example.com/user/123|admin",
+ shouldErr: false,
+ expected: "https://example.com/user/123%7Cadmin",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // We'll test only the validation part to avoid actually opening URLs
+ sanitized, err := utils.ValidateAndSanitizeURL(tc.url)
+
+ if tc.shouldErr {
+ if err == nil {
+ t.Errorf("expected error for URL %q, but got none", tc.url)
+ } else if tc.errMsg != "" && !strings.Contains(err.Error(), tc.errMsg) {
+ t.Errorf("expected error containing %q, got %q", tc.errMsg, err.Error())
+ }
+ } else {
+ if err != nil {
+ t.Errorf("expected no error for URL %q, but got: %v", tc.url, err)
+ }
+ if sanitized != tc.expected {
+ t.Errorf("unexpected sanitized URL for %q: expected %q, got %q", tc.url, tc.expected, sanitized)
+ }
+ }
+ })
+ }
+}
diff --git a/v2/internal/fs/fs.go b/v2/internal/fs/fs.go
new file mode 100644
index 000000000..5662c020c
--- /dev/null
+++ b/v2/internal/fs/fs.go
@@ -0,0 +1,402 @@
+package fs
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "unsafe"
+
+ "github.com/leaanthony/slicer"
+)
+
+// RelativeToCwd returns an absolute path based on the cwd
+// and the given relative path
+func RelativeToCwd(relativePath string) (string, error) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ return "", err
+ }
+
+ return filepath.Join(cwd, relativePath), nil
+}
+
+// Mkdir will create the given directory
+func Mkdir(dirname string) error {
+ return os.Mkdir(dirname, 0o755)
+}
+
+// MkDirs creates the given nested directories.
+// Returns error on failure
+func MkDirs(fullPath string, mode ...os.FileMode) error {
+ var perms os.FileMode
+ perms = 0o755
+ if len(mode) == 1 {
+ perms = mode[0]
+ }
+ return os.MkdirAll(fullPath, perms)
+}
+
+// MoveFile attempts to move the source file to the target
+// Target is a fully qualified path to a file *name*, not a
+// directory
+func MoveFile(source string, target string) error {
+ return os.Rename(source, target)
+}
+
+// DeleteFile will delete the given file
+func DeleteFile(filename string) error {
+ return os.Remove(filename)
+}
+
+// CopyFile from source to target
+func CopyFile(source string, target string) error {
+ s, err := os.Open(source)
+ if err != nil {
+ return err
+ }
+ defer s.Close()
+ d, err := os.Create(target)
+ if err != nil {
+ return err
+ }
+ if _, err := io.Copy(d, s); err != nil {
+ d.Close()
+ return err
+ }
+ return d.Close()
+}
+
+// DirExists - Returns true if the given path resolves to a directory on the filesystem
+func DirExists(path string) bool {
+ fi, err := os.Lstat(path)
+ if err != nil {
+ return false
+ }
+
+ return fi.Mode().IsDir()
+}
+
+// FileExists returns a boolean value indicating whether
+// the given file exists
+func FileExists(path string) bool {
+ fi, err := os.Lstat(path)
+ if err != nil {
+ return false
+ }
+
+ return fi.Mode().IsRegular()
+}
+
+// RelativePath returns a qualified path created by joining the
+// directory of the calling file and the given relative path.
+//
+// Example: RelativePath("..") in *this* file would give you '/path/to/wails2/v2/internal`
+func RelativePath(relativepath string, optionalpaths ...string) string {
+ _, thisFile, _, _ := runtime.Caller(1)
+ localDir := filepath.Dir(thisFile)
+
+ // If we have optional paths, join them to the relativepath
+ if len(optionalpaths) > 0 {
+ paths := []string{relativepath}
+ paths = append(paths, optionalpaths...)
+ relativepath = filepath.Join(paths...)
+ }
+ result, err := filepath.Abs(filepath.Join(localDir, relativepath))
+ if err != nil {
+ // I'm allowing this for 1 reason only: It's fatal if the path
+ // supplied is wrong as it's only used internally in Wails. If we get
+ // that path wrong, we should know about it immediately. The other reason is
+ // that it cuts down a ton of unnecessary error handling.
+ panic(err)
+ }
+ return result
+}
+
+// MustLoadString attempts to load a string and will abort with a fatal message if
+// something goes wrong
+func MustLoadString(filename string) string {
+ data, err := os.ReadFile(filename)
+ if err != nil {
+ fmt.Printf("FATAL: Unable to load file '%s': %s\n", filename, err.Error())
+ os.Exit(1)
+ }
+ return *(*string)(unsafe.Pointer(&data))
+}
+
+// MD5File returns the md5sum of the given file
+func MD5File(filename string) (string, error) {
+ f, err := os.Open(filename)
+ if err != nil {
+ return "", err
+ }
+ defer f.Close()
+
+ h := md5.New()
+ if _, err := io.Copy(h, f); err != nil {
+ return "", err
+ }
+
+ return hex.EncodeToString(h.Sum(nil)), nil
+}
+
+// MustMD5File will call MD5File and abort the program on error
+func MustMD5File(filename string) string {
+ result, err := MD5File(filename)
+ if err != nil {
+ println("FATAL: Unable to MD5Sum file:", err.Error())
+ os.Exit(1)
+ }
+ return result
+}
+
+// MustWriteString will attempt to write the given data to the given filename
+// It will abort the program in the event of a failure
+func MustWriteString(filename string, data string) {
+ err := os.WriteFile(filename, []byte(data), 0o755)
+ if err != nil {
+ fatal("Unable to write file", filename, ":", err.Error())
+ os.Exit(1)
+ }
+}
+
+// fatal will print the optional messages and die
+func fatal(message ...string) {
+ if len(message) > 0 {
+ print("FATAL:")
+ for text := range message {
+ print(text)
+ }
+ }
+ os.Exit(1)
+}
+
+// GetSubdirectories returns a list of subdirectories for the given root directory
+func GetSubdirectories(rootDir string) (*slicer.StringSlicer, error) {
+ var result slicer.StringSlicer
+
+ // Iterate root dir
+ err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ // If we have a directory, save it
+ if info.IsDir() {
+ result.Add(path)
+ }
+ return nil
+ })
+ return &result, err
+}
+
+func DirIsEmpty(dir string) (bool, error) {
+ // CREDIT: https://stackoverflow.com/a/30708914/8325411
+ f, err := os.Open(dir)
+ if err != nil {
+ return false, err
+ }
+ defer f.Close()
+
+ _, err = f.Readdirnames(1) // Or f.Readdir(1)
+ if err == io.EOF {
+ return true, nil
+ }
+ return false, err // Either not empty or error, suits both cases
+}
+
+// CopyDir recursively copies a directory tree, attempting to preserve permissions.
+// Source directory must exist, destination directory must *not* exist.
+// Symlinks are ignored and skipped.
+// Credit: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04
+func CopyDir(src string, dst string) (err error) {
+ src = filepath.Clean(src)
+ dst = filepath.Clean(dst)
+
+ si, err := os.Stat(src)
+ if err != nil {
+ return err
+ }
+ if !si.IsDir() {
+ return fmt.Errorf("source is not a directory")
+ }
+
+ _, err = os.Stat(dst)
+ if err != nil && !os.IsNotExist(err) {
+ return
+ }
+ if err == nil {
+ return fmt.Errorf("destination already exists")
+ }
+
+ err = MkDirs(dst)
+ if err != nil {
+ return
+ }
+
+ entries, err := os.ReadDir(src)
+ if err != nil {
+ return
+ }
+
+ for _, entry := range entries {
+ srcPath := filepath.Join(src, entry.Name())
+ dstPath := filepath.Join(dst, entry.Name())
+
+ if entry.IsDir() {
+ err = CopyDir(srcPath, dstPath)
+ if err != nil {
+ return
+ }
+ } else {
+ // Skip symlinks.
+ if entry.Type()&os.ModeSymlink != 0 {
+ continue
+ }
+
+ err = CopyFile(srcPath, dstPath)
+ if err != nil {
+ return
+ }
+ }
+ }
+
+ return
+}
+
+// SetPermissions recursively sets file permissions on a directory
+func SetPermissions(dir string, perm os.FileMode) error {
+ return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ return os.Chmod(path, perm)
+ })
+}
+
+// CopyDirExtended recursively copies a directory tree, attempting to preserve permissions.
+// Source directory must exist, destination directory must *not* exist. It ignores any files or
+// directories that are given through the ignore parameter.
+// Symlinks are ignored and skipped.
+// Credit: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04
+func CopyDirExtended(src string, dst string, ignore []string) (err error) {
+ ignoreList := slicer.String(ignore)
+ src = filepath.Clean(src)
+ dst = filepath.Clean(dst)
+
+ si, err := os.Stat(src)
+ if err != nil {
+ return err
+ }
+ if !si.IsDir() {
+ return fmt.Errorf("source is not a directory")
+ }
+
+ _, err = os.Stat(dst)
+ if err != nil && !os.IsNotExist(err) {
+ return
+ }
+ if err == nil {
+ return fmt.Errorf("destination already exists")
+ }
+
+ err = MkDirs(dst)
+ if err != nil {
+ return
+ }
+
+ entries, err := os.ReadDir(src)
+ if err != nil {
+ return
+ }
+
+ for _, entry := range entries {
+ if ignoreList.Contains(entry.Name()) {
+ continue
+ }
+ srcPath := filepath.Join(src, entry.Name())
+ dstPath := filepath.Join(dst, entry.Name())
+
+ if entry.IsDir() {
+ err = CopyDir(srcPath, dstPath)
+ if err != nil {
+ return
+ }
+ } else {
+ // Skip symlinks.
+ if entry.Type()&os.ModeSymlink != 0 {
+ continue
+ }
+
+ err = CopyFile(srcPath, dstPath)
+ if err != nil {
+ return
+ }
+ }
+ }
+
+ return
+}
+
+func FindPathToFile(fsys fs.FS, file string) (string, error) {
+ stat, _ := fs.Stat(fsys, file)
+ if stat != nil {
+ return ".", nil
+ }
+ var indexFiles slicer.StringSlicer
+ err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if strings.HasSuffix(path, file) {
+ indexFiles.Add(path)
+ }
+ return nil
+ })
+ if err != nil {
+ return "", err
+ }
+
+ if indexFiles.Length() > 1 {
+ selected := indexFiles.AsSlice()[0]
+ for _, f := range indexFiles.AsSlice() {
+ if len(f) < len(selected) {
+ selected = f
+ }
+ }
+ path, _ := filepath.Split(selected)
+ return path, nil
+ }
+ if indexFiles.Length() > 0 {
+ path, _ := filepath.Split(indexFiles.AsSlice()[0])
+ return path, nil
+ }
+ return "", fmt.Errorf("%s: %w", file, os.ErrNotExist)
+}
+
+// FindFileInParents searches for a file in the current directory and all parent directories.
+// Returns the absolute path to the file if found, otherwise an empty string
+func FindFileInParents(path string, filename string) string {
+ // Check for bad paths
+ if _, err := os.Stat(path); err != nil {
+ return ""
+ }
+
+ var pathToFile string
+ for {
+ pathToFile = filepath.Join(path, filename)
+ if _, err := os.Stat(pathToFile); err == nil {
+ break
+ }
+ parent := filepath.Dir(path)
+ if parent == path {
+ return ""
+ }
+ path = parent
+ }
+ return pathToFile
+}
diff --git a/v2/internal/fs/fs_test.go b/v2/internal/fs/fs_test.go
new file mode 100644
index 000000000..efc4929e6
--- /dev/null
+++ b/v2/internal/fs/fs_test.go
@@ -0,0 +1,90 @@
+package fs
+
+import (
+ "github.com/samber/lo"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/matryer/is"
+)
+
+func TestRelativePath(t *testing.T) {
+
+ i := is.New(t)
+
+ cwd, err := os.Getwd()
+ i.Equal(err, nil)
+
+ // Check current directory
+ actual := RelativePath(".")
+ i.Equal(actual, cwd)
+
+ // Check 2 parameters
+ actual = RelativePath("..", "fs")
+ i.Equal(actual, cwd)
+
+ // Check 3 parameters including filename
+ actual = RelativePath("..", "fs", "fs.go")
+ expected := filepath.Join(cwd, "fs.go")
+ i.Equal(actual, expected)
+
+}
+
+func Test_FindFileInParents(t *testing.T) {
+ tests := []struct {
+ name string
+ setup func() (startDir string, configDir string)
+ wantErr bool
+ }{
+ {
+ name: "should error when no wails.json file is found in local or parent dirs",
+ setup: func() (string, string) {
+ tempDir := os.TempDir()
+ testDir := lo.Must(os.MkdirTemp(tempDir, "projectPath"))
+ _ = os.MkdirAll(testDir, 0755)
+ return testDir, ""
+ },
+ wantErr: true,
+ },
+ {
+ name: "should find wails.json in local path",
+ setup: func() (string, string) {
+ tempDir := os.TempDir()
+ testDir := lo.Must(os.MkdirTemp(tempDir, "projectPath"))
+ _ = os.MkdirAll(testDir, 0755)
+ configFile := filepath.Join(testDir, "wails.json")
+ _ = os.WriteFile(configFile, []byte("{}"), 0755)
+ return testDir, configFile
+ },
+ wantErr: false,
+ },
+ {
+ name: "should find wails.json in parent path",
+ setup: func() (string, string) {
+ tempDir := os.TempDir()
+ testDir := lo.Must(os.MkdirTemp(tempDir, "projectPath"))
+ _ = os.MkdirAll(testDir, 0755)
+ parentDir := filepath.Dir(testDir)
+ configFile := filepath.Join(parentDir, "wails.json")
+ _ = os.WriteFile(configFile, []byte("{}"), 0755)
+ return testDir, configFile
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ path, expectedPath := tt.setup()
+ defer func() {
+ if expectedPath != "" {
+ _ = os.Remove(expectedPath)
+ }
+ }()
+ got := FindFileInParents(path, "wails.json")
+ if got != expectedPath {
+ t.Errorf("FindFileInParents() got = %v, want %v", got, expectedPath)
+ }
+ })
+ }
+}
diff --git a/v2/internal/github/github.go b/v2/internal/github/github.go
new file mode 100644
index 000000000..2aa5e1432
--- /dev/null
+++ b/v2/internal/github/github.go
@@ -0,0 +1,147 @@
+package github
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/charmbracelet/glamour/styles"
+ "io"
+ "net/http"
+ "net/url"
+ "runtime"
+ "sort"
+ "strings"
+
+ "github.com/charmbracelet/glamour"
+)
+
+func GetReleaseNotes(tagVersion string, noColour bool) string {
+ resp, err := http.Get("https://api.github.com/repos/wailsapp/wails/releases/tags/" + url.PathEscape(tagVersion))
+ if err != nil {
+ return "Unable to retrieve release notes. Please check your network connection"
+ }
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "Unable to retrieve release notes. Please check your network connection"
+ }
+
+ data := map[string]interface{}{}
+ err = json.Unmarshal(body, &data)
+ if err != nil {
+ return "Unable to retrieve release notes. Please check your network connection"
+ }
+
+ if data["body"] == nil {
+ return "No release notes found"
+ }
+
+ result := "# Release Notes for " + tagVersion + "\n" + data["body"].(string)
+ var renderer *glamour.TermRenderer
+
+ var termRendererOpts []glamour.TermRendererOption
+
+ if runtime.GOOS == "windows" || noColour {
+ termRendererOpts = append(termRendererOpts, glamour.WithStyles(styles.NoTTYStyleConfig))
+ } else {
+ termRendererOpts = append(termRendererOpts, glamour.WithAutoStyle())
+ }
+
+ renderer, err = glamour.NewTermRenderer(termRendererOpts...)
+ if err != nil {
+ return result
+ }
+ result, err = renderer.Render(result)
+ if err != nil {
+ return err.Error()
+ }
+ return result
+}
+
+// GetVersionTags gets the list of tags on the Wails repo
+// It returns a list of sorted tags in descending order
+func GetVersionTags() ([]*SemanticVersion, error) {
+ result := []*SemanticVersion{}
+ var err error
+
+ resp, err := http.Get("https://api.github.com/repos/wailsapp/wails/tags")
+ if err != nil {
+ return result, err
+ }
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return result, err
+ }
+
+ data := []map[string]interface{}{}
+ err = json.Unmarshal(body, &data)
+ if err != nil {
+ return result, err
+ }
+
+ // Convert tag data to Version structs
+ for _, tag := range data {
+ version := tag["name"].(string)
+ if !strings.HasPrefix(version, "v2") {
+ continue
+ }
+ semver, err := NewSemanticVersion(version)
+ if err != nil {
+ return result, err
+ }
+ result = append(result, semver)
+ }
+
+ // Reverse Sort
+ sort.Sort(sort.Reverse(SemverCollection(result)))
+
+ return result, err
+}
+
+// GetLatestStableRelease gets the latest stable release on GitHub
+func GetLatestStableRelease() (result *SemanticVersion, err error) {
+ tags, err := GetVersionTags()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, tag := range tags {
+ if tag.IsRelease() {
+ return tag, nil
+ }
+ }
+
+ return nil, fmt.Errorf("no release tag found")
+}
+
+// GetLatestPreRelease gets the latest prerelease on GitHub
+func GetLatestPreRelease() (result *SemanticVersion, err error) {
+ tags, err := GetVersionTags()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, tag := range tags {
+ if tag.IsPreRelease() {
+ return tag, nil
+ }
+ }
+
+ return nil, fmt.Errorf("no prerelease tag found")
+}
+
+// IsValidTag returns true if the given string is a valid tag
+func IsValidTag(tagVersion string) (bool, error) {
+ if tagVersion[0] == 'v' {
+ tagVersion = tagVersion[1:]
+ }
+ tags, err := GetVersionTags()
+ if err != nil {
+ return false, err
+ }
+
+ for _, tag := range tags {
+ if tag.String() == tagVersion {
+ return true, nil
+ }
+ }
+ return false, nil
+}
diff --git a/v2/internal/github/semver.go b/v2/internal/github/semver.go
new file mode 100644
index 000000000..1cf5907fa
--- /dev/null
+++ b/v2/internal/github/semver.go
@@ -0,0 +1,106 @@
+package github
+
+import (
+ "fmt"
+
+ "github.com/Masterminds/semver"
+)
+
+// SemanticVersion is a struct containing a semantic version
+type SemanticVersion struct {
+ Version *semver.Version
+}
+
+// NewSemanticVersion creates a new SemanticVersion object with the given version string
+func NewSemanticVersion(version string) (*SemanticVersion, error) {
+ semverVersion, err := semver.NewVersion(version)
+ if err != nil {
+ return nil, err
+ }
+ return &SemanticVersion{
+ Version: semverVersion,
+ }, nil
+}
+
+// IsRelease returns true if it's a release version
+func (s *SemanticVersion) IsRelease() bool {
+ // Limit to v2
+ if s.Version.Major() != 2 {
+ return false
+ }
+ return len(s.Version.Prerelease()) == 0 && len(s.Version.Metadata()) == 0
+}
+
+// IsPreRelease returns true if it's a prerelease version
+func (s *SemanticVersion) IsPreRelease() bool {
+ // Limit to v1
+ if s.Version.Major() != 2 {
+ return false
+ }
+ return len(s.Version.Prerelease()) > 0
+}
+
+func (s *SemanticVersion) String() string {
+ return s.Version.String()
+}
+
+// IsGreaterThan returns true if this version is greater than the given version
+func (s *SemanticVersion) IsGreaterThan(version *SemanticVersion) (bool, error) {
+ // Set up new constraint
+ constraint, err := semver.NewConstraint("> " + version.Version.String())
+ if err != nil {
+ return false, err
+ }
+
+ // Check if the desired one is greater than the requested on
+ success, msgs := constraint.Validate(s.Version)
+ if !success {
+ return false, msgs[0]
+ }
+ return true, nil
+}
+
+// IsGreaterThanOrEqual returns true if this version is greater than or equal the given version
+func (s *SemanticVersion) IsGreaterThanOrEqual(version *SemanticVersion) (bool, error) {
+ // Set up new constraint
+ constraint, err := semver.NewConstraint(">= " + version.Version.String())
+ if err != nil {
+ return false, err
+ }
+
+ // Check if the desired one is greater than the requested on
+ success, msgs := constraint.Validate(s.Version)
+ if !success {
+ return false, msgs[0]
+ }
+ return true, nil
+}
+
+// MainVersion returns the main version of any version+prerelease+metadata
+// EG: MainVersion("1.2.3-pre") => "1.2.3"
+func (s *SemanticVersion) MainVersion() *SemanticVersion {
+ mainVersion := fmt.Sprintf("%d.%d.%d", s.Version.Major(), s.Version.Minor(), s.Version.Patch())
+ result, _ := NewSemanticVersion(mainVersion)
+ return result
+}
+
+// SemverCollection is a collection of SemanticVersion objects
+type SemverCollection []*SemanticVersion
+
+// Len returns the length of a collection. The number of Version instances
+// on the slice.
+func (c SemverCollection) Len() int {
+ return len(c)
+}
+
+// Less is needed for the sort interface to compare two Version objects on the
+// slice. If checks if one is less than the other.
+func (c SemverCollection) Less(i, j int) bool {
+ return c[i].Version.LessThan(c[j].Version)
+}
+
+// Swap is needed for the sort interface to replace the Version objects
+// at two different positions in the slice.
+func (c SemverCollection) Swap(i, j int) {
+ c[i], c[j] = c[j], c[i]
+}
diff --git a/v2/internal/github/semver_test.go b/v2/internal/github/semver_test.go
new file mode 100644
index 000000000..f748a57a0
--- /dev/null
+++ b/v2/internal/github/semver_test.go
@@ -0,0 +1,43 @@
+package github
+
+import (
+ "github.com/matryer/is"
+ "testing"
+)
+
+func TestSemanticVersion_IsGreaterThan(t *testing.T) {
+ is2 := is.New(t)
+
+ alpha1, err := NewSemanticVersion("v2.0.0-alpha.1")
+ is2.NoErr(err)
+
+ beta1, err := NewSemanticVersion("v2.0.0-beta.1")
+ is2.NoErr(err)
+
+ v2, err := NewSemanticVersion("v2.0.0")
+ is2.NoErr(err)
+
+ is2.True(alpha1.IsPreRelease())
+ is2.True(beta1.IsPreRelease())
+ is2.True(!v2.IsPreRelease())
+ is2.True(v2.IsRelease())
+
+ result, err := beta1.IsGreaterThan(alpha1)
+ is2.NoErr(err)
+ is2.True(result)
+
+ result, err = v2.IsGreaterThan(beta1)
+ is2.NoErr(err)
+ is2.True(result)
+
+ beta44, err := NewSemanticVersion("v2.0.0-beta.44.2")
+ is2.NoErr(err)
+
+ rc1, err := NewSemanticVersion("v2.0.0-rc.1")
+ is2.NoErr(err)
+
+ result, err = rc1.IsGreaterThan(beta44)
+ is2.NoErr(err)
+ is2.True(result)
+
+}
diff --git a/licenses/github.com/wailsapp/webview/LICENSE b/v2/internal/go-common-file-dialog/LICENSE
similarity index 96%
rename from licenses/github.com/wailsapp/webview/LICENSE
rename to v2/internal/go-common-file-dialog/LICENSE
index b18604bf4..508b6978e 100644
--- a/licenses/github.com/wailsapp/webview/LICENSE
+++ b/v2/internal/go-common-file-dialog/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2017 Serge Zaitsev
+Copyright (c) 2019 Harry Phillips
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/v2/internal/go-common-file-dialog/README.md b/v2/internal/go-common-file-dialog/README.md
new file mode 100644
index 000000000..1cb5902d1
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/README.md
@@ -0,0 +1,31 @@
+# Common File Dialog bindings for Golang
+
+[Project Home](https://github.com/harry1453/go-common-file-dialog)
+
+This library contains bindings for Windows Vista and
+newer's [Common File Dialogs](https://docs.microsoft.com/en-us/windows/win32/shell/common-file-dialog), which is the
+standard system dialog for selecting files or folders to open or save.
+
+The Common File Dialogs have to be accessed via
+the [COM Interface](https://en.wikipedia.org/wiki/Component_Object_Model), normally via C++ or via bindings (like in C#)
+.
+
+This library contains bindings for Golang. **It does not require CGO**, and contains empty stubs for non-windows
+platforms (so is safe to compile and run on platforms other than windows, but will just return errors at runtime).
+
+This can be very useful if you want to quickly get a file selector in your Golang application. The `cfdutil` package
+contains utility functions with a single call to open and configure a dialog, and then get the result from it. Examples
+for this are in [`_examples/usingutil`](_examples/usingutil). Or, if you want finer control over the dialog's operation,
+you can use the base package. Examples for this are in [`_examples/notusingutil`](_examples/notusingutil).
+
+This library is available under the MIT license.
+
+Currently supported features:
+
+* Open File Dialog (to open a single file)
+* Open Multiple Files Dialog (to open multiple files)
+* Open Folder Dialog
+* Save File Dialog
+* Dialog "roles" to allow Windows to remember different "last locations" for different types of dialog
+* Set dialog Title, Default Folder and Initial Folder
+* Set dialog File Filters
diff --git a/v2/internal/go-common-file-dialog/cfd/CommonFileDialog.go b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog.go
new file mode 100644
index 000000000..58e97aa4e
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog.go
@@ -0,0 +1,72 @@
+// Cross-platform.
+
+// Common File Dialogs
+package cfd
+
+type Dialog interface {
+ // Show the dialog to the user.
+ // Blocks until the user has closed the dialog.
+ Show() error
+ // Sets the dialog's parent window. Use 0 to set the dialog to have no parent window.
+ SetParentWindowHandle(hwnd uintptr)
+ // Show the dialog to the user.
+ // Blocks until the user has closed the dialog and returns their selection.
+ // Returns an error if the user cancelled the dialog.
+ // Do not use for the Open Multiple Files dialog. Use ShowAndGetResults instead.
+ ShowAndGetResult() (string, error)
+ // Sets the title of the dialog window.
+ SetTitle(title string) error
+ // Sets the "role" of the dialog. This is used to derive the dialog's GUID, which the
+ // OS will use to differentiate it from dialogs that are intended for other purposes.
+ // This means that, for example, a dialog with role "Import" will have a different
+ // previous location that it will open to than a dialog with role "Open". Can be any string.
+ SetRole(role string) error
+ // Sets the folder used as a default if there is not a recently used folder value available
+ SetDefaultFolder(defaultFolder string) error
+ // Sets the folder that the dialog always opens to.
+ // If this is set, it will override the "default folder" behaviour and the dialog will always open to this folder.
+ SetFolder(folder string) error
+ // Gets the selected file or folder path, as an absolute path eg. "C:\Folder\file.txt"
+ // Do not use for the Open Multiple Files dialog. Use GetResults instead.
+ GetResult() (string, error)
+ // Sets the file name, I.E. the contents of the file name text box.
+ // For Select Folder Dialog, sets folder name.
+ SetFileName(fileName string) error
+ // Release the resources allocated to this Dialog.
+ // Should be called when the dialog is finished with.
+ Release() error
+}
+
+type FileDialog interface {
+ Dialog
+ // Set the list of file filters that the user can select.
+ SetFileFilters(fileFilter []FileFilter) error
+ // Set the selected item from the list of file filters (set using SetFileFilters) by its index. Defaults to 0 (the first item in the list) if not called.
+ SetSelectedFileFilterIndex(index uint) error
+ // Sets the default extension applied when a user does not provide one as part of the file name.
+ // If the user selects a different file filter, the default extension will be automatically updated to match the new file filter.
+ // For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists.
+ // For Save File Dialog, this extension will be used whenever a user does not specify an extension.
+ SetDefaultExtension(defaultExtension string) error
+}
+
+type OpenFileDialog interface {
+ FileDialog
+}
+
+type OpenMultipleFilesDialog interface {
+ FileDialog
+ // Show the dialog to the user.
+ // Blocks until the user has closed the dialog and returns the selected files.
+ ShowAndGetResults() ([]string, error)
+ // Gets the selected file paths, as absolute paths eg. "C:\Folder\file.txt"
+ GetResults() ([]string, error)
+}
+
+type SelectFolderDialog interface {
+ Dialog
+}
+
+type SaveFileDialog interface { // TODO Properties
+ FileDialog
+}
diff --git a/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_nonWindows.go b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_nonWindows.go
new file mode 100644
index 000000000..3ab969850
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_nonWindows.go
@@ -0,0 +1,28 @@
+//go:build !windows
+// +build !windows
+
+package cfd
+
+import "fmt"
+
+var unsupportedError = fmt.Errorf("common file dialogs are only available on windows")
+
+// TODO doc
+func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) {
+ return nil, unsupportedError
+}
+
+// TODO doc
+func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) {
+ return nil, unsupportedError
+}
+
+// TODO doc
+func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) {
+ return nil, unsupportedError
+}
+
+// TODO doc
+func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) {
+ return nil, unsupportedError
+}
diff --git a/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_windows.go b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_windows.go
new file mode 100644
index 000000000..69f46118e
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/CommonFileDialog_windows.go
@@ -0,0 +1,79 @@
+//go:build windows
+// +build windows
+
+package cfd
+
+import "github.com/go-ole/go-ole"
+
+func initialize() {
+ // Swallow error
+ _ = ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE)
+}
+
+// TODO doc
+func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) {
+ initialize()
+
+ openDialog, err := newIFileOpenDialog()
+ if err != nil {
+ return nil, err
+ }
+ err = config.apply(openDialog)
+ if err != nil {
+ return nil, err
+ }
+ return openDialog, nil
+}
+
+// TODO doc
+func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) {
+ initialize()
+
+ openDialog, err := newIFileOpenDialog()
+ if err != nil {
+ return nil, err
+ }
+ err = config.apply(openDialog)
+ if err != nil {
+ return nil, err
+ }
+ err = openDialog.setIsMultiselect(true)
+ if err != nil {
+ return nil, err
+ }
+ return openDialog, nil
+}
+
+// TODO doc
+func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) {
+ initialize()
+
+ openDialog, err := newIFileOpenDialog()
+ if err != nil {
+ return nil, err
+ }
+ err = config.apply(openDialog)
+ if err != nil {
+ return nil, err
+ }
+ err = openDialog.setPickFolders(true)
+ if err != nil {
+ return nil, err
+ }
+ return openDialog, nil
+}
+
+// TODO doc
+func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) {
+ initialize()
+
+ saveDialog, err := newIFileSaveDialog()
+ if err != nil {
+ return nil, err
+ }
+ err = config.apply(saveDialog)
+ if err != nil {
+ return nil, err
+ }
+ return saveDialog, nil
+}
diff --git a/v2/internal/go-common-file-dialog/cfd/DialogConfig.go b/v2/internal/go-common-file-dialog/cfd/DialogConfig.go
new file mode 100644
index 000000000..9e06fb503
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/DialogConfig.go
@@ -0,0 +1,141 @@
+// Cross-platform.
+
+package cfd
+
+import (
+ "fmt"
+ "os"
+ "reflect"
+)
+
+type FileFilter struct {
+ // The display name of the filter (That is shown to the user)
+ DisplayName string
+ // The filter pattern. Eg. "*.txt;*.png" to select all txt and png files, "*.*" to select any files, etc.
+ Pattern string
+}
+
+// Never obfuscate the FileFilter type.
+var _ = reflect.TypeOf(FileFilter{})
+
+type DialogConfig struct {
+ // The title of the dialog
+ Title string
+ // The role of the dialog. This is used to derive the dialog's GUID, which the
+ // OS will use to differentiate it from dialogs that are intended for other purposes.
+ // This means that, for example, a dialog with role "Import" will have a different
+ // previous location that it will open to than a dialog with role "Open". Can be any string.
+ Role string
+ // The default folder - the folder that is used the first time the user opens it
+ // (after the first time their last used location is used).
+ DefaultFolder string
+ // The initial folder - the folder that the dialog always opens to if not empty.
+ // If this is not empty, it will override the "default folder" behaviour and
+ // the dialog will always open to this folder.
+ Folder string
+ // The file filters that restrict which types of files the dialog is able to choose.
+ // Ignored by Select Folder Dialog.
+ FileFilters []FileFilter
+ // Sets the initially selected file filter. This is an index of FileFilters.
+ // Ignored by Select Folder Dialog.
+ SelectedFileFilterIndex uint
+ // The initial name of the file (I.E. the text in the file name text box) when the user opens the dialog.
+ // For the Select Folder Dialog, this sets the initial folder name.
+ FileName string
+ // The default extension applied when a user does not provide one as part of the file name.
+ // If the user selects a different file filter, the default extension will be automatically updated to match the new file filter.
+ // For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists.
+ // For Save File Dialog, this extension will be used whenever a user does not specify an extension.
+ // Ignored by Select Folder Dialog.
+ DefaultExtension string
+ // ParentWindowHandle is the handle (HWND) to the parent window of the dialog.
+ // If left as 0 / nil, the dialog will have no parent window.
+ ParentWindowHandle uintptr
+}
+
+var defaultFilters = []FileFilter{
+ {
+ DisplayName: "All Files (*.*)",
+ Pattern: "*.*",
+ },
+}
+
+func (config *DialogConfig) apply(dialog Dialog) (err error) {
+ if config.Title != "" {
+ err = dialog.SetTitle(config.Title)
+ if err != nil {
+ return
+ }
+ }
+
+ if config.Role != "" {
+ err = dialog.SetRole(config.Role)
+ if err != nil {
+ return
+ }
+ }
+
+ if config.Folder != "" {
+ _, err = os.Stat(config.Folder)
+ if err != nil {
+ return
+ }
+ err = dialog.SetFolder(config.Folder)
+ if err != nil {
+ return
+ }
+ }
+
+ if config.DefaultFolder != "" {
+ _, err = os.Stat(config.DefaultFolder)
+ if err != nil {
+ return
+ }
+ err = dialog.SetDefaultFolder(config.DefaultFolder)
+ if err != nil {
+ return
+ }
+ }
+
+ if config.FileName != "" {
+ err = dialog.SetFileName(config.FileName)
+ if err != nil {
+ return
+ }
+ }
+
+ dialog.SetParentWindowHandle(config.ParentWindowHandle)
+
+ if dialog, ok := dialog.(FileDialog); ok {
+ var fileFilters []FileFilter
+ if config.FileFilters != nil && len(config.FileFilters) > 0 {
+ fileFilters = config.FileFilters
+ } else {
+ fileFilters = defaultFilters
+ }
+ err = dialog.SetFileFilters(fileFilters)
+ if err != nil {
+ return
+ }
+
+ if config.SelectedFileFilterIndex != 0 {
+ if config.SelectedFileFilterIndex > uint(len(fileFilters)) {
+ err = fmt.Errorf("selected file filter index out of range")
+ return
+ }
+ err = dialog.SetSelectedFileFilterIndex(config.SelectedFileFilterIndex)
+ if err != nil {
+ return
+ }
+ }
+
+ if config.DefaultExtension != "" {
+ err = dialog.SetDefaultExtension(config.DefaultExtension)
+ if err != nil {
+ return
+ }
+ }
+ }
+
+ return
+}
diff --git a/v2/internal/go-common-file-dialog/cfd/errors.go b/v2/internal/go-common-file-dialog/cfd/errors.go
new file mode 100644
index 000000000..4ca3300b9
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/errors.go
@@ -0,0 +1,9 @@
+package cfd
+
+import "errors"
+
+var (
+ ErrCancelled = errors.New("cancelled by user")
+ ErrInvalidGUID = errors.New("guid cannot be nil")
+ ErrEmptyFilters = errors.New("must specify at least one filter")
+)
diff --git a/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go b/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go
new file mode 100644
index 000000000..b1be23fcf
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go
@@ -0,0 +1,200 @@
+//go:build windows
+// +build windows
+
+package cfd
+
+import (
+ "github.com/go-ole/go-ole"
+ "github.com/google/uuid"
+ "syscall"
+ "unsafe"
+)
+
+var (
+ fileOpenDialogCLSID = ole.NewGUID("{DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7}")
+ fileOpenDialogIID = ole.NewGUID("{d57c7288-d4ad-4768-be02-9d969532d960}")
+)
+
+type iFileOpenDialog struct {
+ vtbl *iFileOpenDialogVtbl
+ parentWindowHandle uintptr
+}
+
+type iFileOpenDialogVtbl struct {
+ iFileDialogVtbl
+
+ GetResults uintptr // func (ppenum **IShellItemArray) HRESULT
+ GetSelectedItems uintptr
+}
+
+func newIFileOpenDialog() (*iFileOpenDialog, error) {
+ if unknown, err := ole.CreateInstance(fileOpenDialogCLSID, fileOpenDialogIID); err == nil {
+ return (*iFileOpenDialog)(unsafe.Pointer(unknown)), nil
+ } else {
+ return nil, err
+ }
+}
+
+func (fileOpenDialog *iFileOpenDialog) Show() error {
+ return fileOpenDialog.vtbl.show(unsafe.Pointer(fileOpenDialog), fileOpenDialog.parentWindowHandle)
+}
+
+func (fileOpenDialog *iFileOpenDialog) SetParentWindowHandle(hwnd uintptr) {
+ fileOpenDialog.parentWindowHandle = hwnd
+}
+
+func (fileOpenDialog *iFileOpenDialog) ShowAndGetResult() (string, error) {
+ isMultiselect, err := fileOpenDialog.isMultiselect()
+ if err != nil {
+ return "", err
+ }
+ if isMultiselect {
+ // We should panic as this error is caused by the developer using the library
+ panic("use ShowAndGetResults for open multiple files dialog")
+ }
+ if err := fileOpenDialog.Show(); err != nil {
+ return "", err
+ }
+ return fileOpenDialog.GetResult()
+}
+
+func (fileOpenDialog *iFileOpenDialog) ShowAndGetResults() ([]string, error) {
+ isMultiselect, err := fileOpenDialog.isMultiselect()
+ if err != nil {
+ return nil, err
+ }
+ if !isMultiselect {
+ // We should panic as this error is caused by the developer using the library
+ panic("use ShowAndGetResult for open single file dialog")
+ }
+ if err := fileOpenDialog.Show(); err != nil {
+ return nil, err
+ }
+ return fileOpenDialog.GetResults()
+}
+
+func (fileOpenDialog *iFileOpenDialog) SetTitle(title string) error {
+ return fileOpenDialog.vtbl.setTitle(unsafe.Pointer(fileOpenDialog), title)
+}
+
+func (fileOpenDialog *iFileOpenDialog) GetResult() (string, error) {
+ isMultiselect, err := fileOpenDialog.isMultiselect()
+ if err != nil {
+ return "", err
+ }
+ if isMultiselect {
+ // We should panic as this error is caused by the developer using the library
+ panic("use GetResults for open multiple files dialog")
+ }
+ return fileOpenDialog.vtbl.getResultString(unsafe.Pointer(fileOpenDialog))
+}
+
+func (fileOpenDialog *iFileOpenDialog) Release() error {
+ return fileOpenDialog.vtbl.release(unsafe.Pointer(fileOpenDialog))
+}
+
+func (fileOpenDialog *iFileOpenDialog) SetDefaultFolder(defaultFolderPath string) error {
+ return fileOpenDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath)
+}
+
+func (fileOpenDialog *iFileOpenDialog) SetFolder(defaultFolderPath string) error {
+ return fileOpenDialog.vtbl.setFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath)
+}
+
+func (fileOpenDialog *iFileOpenDialog) SetFileFilters(filter []FileFilter) error {
+ return fileOpenDialog.vtbl.setFileTypes(unsafe.Pointer(fileOpenDialog), filter)
+}
+
+func (fileOpenDialog *iFileOpenDialog) SetRole(role string) error {
+ return fileOpenDialog.vtbl.setClientGuid(unsafe.Pointer(fileOpenDialog), StringToUUID(role))
+}
+
+// This should only be callable when the user asks for a multi select because
+// otherwise they will be given the Dialog interface which does not expose this function.
+func (fileOpenDialog *iFileOpenDialog) GetResults() ([]string, error) {
+ isMultiselect, err := fileOpenDialog.isMultiselect()
+ if err != nil {
+ return nil, err
+ }
+ if !isMultiselect {
+ // We should panic as this error is caused by the developer using the library
+ panic("use GetResult for open single file dialog")
+ }
+ return fileOpenDialog.vtbl.getResultsStrings(unsafe.Pointer(fileOpenDialog))
+}
+
+func (fileOpenDialog *iFileOpenDialog) SetDefaultExtension(defaultExtension string) error {
+ return fileOpenDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileOpenDialog), defaultExtension)
+}
+
+func (fileOpenDialog *iFileOpenDialog) SetFileName(initialFileName string) error {
+ return fileOpenDialog.vtbl.setFileName(unsafe.Pointer(fileOpenDialog), initialFileName)
+}
+
+func (fileOpenDialog *iFileOpenDialog) SetSelectedFileFilterIndex(index uint) error {
+ return fileOpenDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileOpenDialog), index)
+}
+
+func (fileOpenDialog *iFileOpenDialog) setPickFolders(pickFolders bool) error {
+ const FosPickfolders = 0x20
+ if pickFolders {
+ return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosPickfolders)
+ } else {
+ return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosPickfolders)
+ }
+}
+
+const FosAllowMultiselect = 0x200
+
+func (fileOpenDialog *iFileOpenDialog) isMultiselect() (bool, error) {
+ options, err := fileOpenDialog.vtbl.getOptions(unsafe.Pointer(fileOpenDialog))
+ if err != nil {
+ return false, err
+ }
+ return options&FosAllowMultiselect != 0, nil
+}
+
+func (fileOpenDialog *iFileOpenDialog) setIsMultiselect(isMultiselect bool) error {
+ if isMultiselect {
+ return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect)
+ } else {
+ return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect)
+ }
+}
+
+func (vtbl *iFileOpenDialogVtbl) getResults(objPtr unsafe.Pointer) (*iShellItemArray, error) {
+ var shellItemArray *iShellItemArray
+ ret, _, _ := syscall.SyscallN(vtbl.GetResults,
+ uintptr(objPtr),
+ uintptr(unsafe.Pointer(&shellItemArray)),
+ 0)
+ return shellItemArray, hresultToError(ret)
+}
+
+func (vtbl *iFileOpenDialogVtbl) getResultsStrings(objPtr unsafe.Pointer) ([]string, error) {
+ shellItemArray, err := vtbl.getResults(objPtr)
+ if err != nil {
+ return nil, err
+ }
+ if shellItemArray == nil {
+ return nil, ErrCancelled
+ }
+ defer shellItemArray.vtbl.release(unsafe.Pointer(shellItemArray))
+ count, err := shellItemArray.vtbl.getCount(unsafe.Pointer(shellItemArray))
+ if err != nil {
+ return nil, err
+ }
+ var results []string
+ for i := uintptr(0); i < count; i++ {
+ newItem, err := shellItemArray.vtbl.getItemAt(unsafe.Pointer(shellItemArray), i)
+ if err != nil {
+ return nil, err
+ }
+ results = append(results, newItem)
+ }
+ return results, nil
+}
+
+func StringToUUID(str string) *ole.GUID {
+ return ole.NewGUID(uuid.NewSHA1(uuid.Nil, []byte(str)).String())
+}
diff --git a/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go b/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go
new file mode 100644
index 000000000..ddee7b246
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go
@@ -0,0 +1,92 @@
+//go:build windows
+// +build windows
+
+package cfd
+
+import (
+ "github.com/go-ole/go-ole"
+ "unsafe"
+)
+
+var (
+ saveFileDialogCLSID = ole.NewGUID("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}")
+ saveFileDialogIID = ole.NewGUID("{84bccd23-5fde-4cdb-aea4-af64b83d78ab}")
+)
+
+type iFileSaveDialog struct {
+ vtbl *iFileSaveDialogVtbl
+ parentWindowHandle uintptr
+}
+
+type iFileSaveDialogVtbl struct {
+ iFileDialogVtbl
+
+ SetSaveAsItem uintptr
+ SetProperties uintptr
+ SetCollectedProperties uintptr
+ GetProperties uintptr
+ ApplyProperties uintptr
+}
+
+func newIFileSaveDialog() (*iFileSaveDialog, error) {
+ if unknown, err := ole.CreateInstance(saveFileDialogCLSID, saveFileDialogIID); err == nil {
+ return (*iFileSaveDialog)(unsafe.Pointer(unknown)), nil
+ } else {
+ return nil, err
+ }
+}
+
+func (fileSaveDialog *iFileSaveDialog) Show() error {
+ return fileSaveDialog.vtbl.show(unsafe.Pointer(fileSaveDialog), fileSaveDialog.parentWindowHandle)
+}
+
+func (fileSaveDialog *iFileSaveDialog) SetParentWindowHandle(hwnd uintptr) {
+ fileSaveDialog.parentWindowHandle = hwnd
+}
+
+func (fileSaveDialog *iFileSaveDialog) ShowAndGetResult() (string, error) {
+ if err := fileSaveDialog.Show(); err != nil {
+ return "", err
+ }
+ return fileSaveDialog.GetResult()
+}
+
+func (fileSaveDialog *iFileSaveDialog) SetTitle(title string) error {
+ return fileSaveDialog.vtbl.setTitle(unsafe.Pointer(fileSaveDialog), title)
+}
+
+func (fileSaveDialog *iFileSaveDialog) GetResult() (string, error) {
+ return fileSaveDialog.vtbl.getResultString(unsafe.Pointer(fileSaveDialog))
+}
+
+func (fileSaveDialog *iFileSaveDialog) Release() error {
+ return fileSaveDialog.vtbl.release(unsafe.Pointer(fileSaveDialog))
+}
+
+func (fileSaveDialog *iFileSaveDialog) SetDefaultFolder(defaultFolderPath string) error {
+ return fileSaveDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath)
+}
+
+func (fileSaveDialog *iFileSaveDialog) SetFolder(defaultFolderPath string) error {
+ return fileSaveDialog.vtbl.setFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath)
+}
+
+func (fileSaveDialog *iFileSaveDialog) SetFileFilters(filter []FileFilter) error {
+ return fileSaveDialog.vtbl.setFileTypes(unsafe.Pointer(fileSaveDialog), filter)
+}
+
+func (fileSaveDialog *iFileSaveDialog) SetRole(role string) error {
+ return fileSaveDialog.vtbl.setClientGuid(unsafe.Pointer(fileSaveDialog), StringToUUID(role))
+}
+
+func (fileSaveDialog *iFileSaveDialog) SetDefaultExtension(defaultExtension string) error {
+ return fileSaveDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileSaveDialog), defaultExtension)
+}
+
+func (fileSaveDialog *iFileSaveDialog) SetFileName(initialFileName string) error {
+ return fileSaveDialog.vtbl.setFileName(unsafe.Pointer(fileSaveDialog), initialFileName)
+}
+
+func (fileSaveDialog *iFileSaveDialog) SetSelectedFileFilterIndex(index uint) error {
+ return fileSaveDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileSaveDialog), index)
+}
diff --git a/v2/internal/go-common-file-dialog/cfd/iShellItem.go b/v2/internal/go-common-file-dialog/cfd/iShellItem.go
new file mode 100644
index 000000000..080115345
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/iShellItem.go
@@ -0,0 +1,56 @@
+//go:build windows
+// +build windows
+
+package cfd
+
+import (
+ "github.com/go-ole/go-ole"
+ "syscall"
+ "unsafe"
+)
+
+var (
+ procSHCreateItemFromParsingName = syscall.NewLazyDLL("Shell32.dll").NewProc("SHCreateItemFromParsingName")
+ iidShellItem = ole.NewGUID("43826d1e-e718-42ee-bc55-a1e261c37bfe")
+)
+
+type iShellItem struct {
+ vtbl *iShellItemVtbl
+}
+
+type iShellItemVtbl struct {
+ iUnknownVtbl
+ BindToHandler uintptr
+ GetParent uintptr
+ GetDisplayName uintptr // func (sigdnName SIGDN, ppszName *LPWSTR) HRESULT
+ GetAttributes uintptr
+ Compare uintptr
+}
+
+func newIShellItem(path string) (*iShellItem, error) {
+ var shellItem *iShellItem
+ pathPtr := ole.SysAllocString(path)
+ defer func(v *int16) {
+ _ = ole.SysFreeString(v)
+ }(pathPtr)
+
+ ret, _, _ := procSHCreateItemFromParsingName.Call(
+ uintptr(unsafe.Pointer(pathPtr)),
+ 0,
+ uintptr(unsafe.Pointer(iidShellItem)),
+ uintptr(unsafe.Pointer(&shellItem)))
+ return shellItem, hresultToError(ret)
+}
+
+func (vtbl *iShellItemVtbl) getDisplayName(objPtr unsafe.Pointer) (string, error) {
+ var ptr *uint16
+ ret, _, _ := syscall.SyscallN(vtbl.GetDisplayName,
+ uintptr(objPtr),
+ 0x80058000, // SIGDN_FILESYSPATH,
+ uintptr(unsafe.Pointer(&ptr)))
+ if err := hresultToError(ret); err != nil {
+ return "", err
+ }
+ defer ole.CoTaskMemFree(uintptr(unsafe.Pointer(ptr)))
+ return ole.LpOleStrToString(ptr), nil
+}
diff --git a/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go b/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go
new file mode 100644
index 000000000..c548160d1
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go
@@ -0,0 +1,64 @@
+//go:build windows
+// +build windows
+
+package cfd
+
+import (
+ "github.com/go-ole/go-ole"
+ "syscall"
+ "unsafe"
+)
+
+const (
+ iidShellItemArrayGUID = "{b63ea76d-1f85-456f-a19c-48159efa858b}"
+)
+
+var (
+ iidShellItemArray *ole.GUID
+)
+
+func init() {
+ iidShellItemArray, _ = ole.IIDFromString(iidShellItemArrayGUID)
+}
+
+type iShellItemArray struct {
+ vtbl *iShellItemArrayVtbl
+}
+
+type iShellItemArrayVtbl struct {
+ iUnknownVtbl
+ BindToHandler uintptr
+ GetPropertyStore uintptr
+ GetPropertyDescriptionList uintptr
+ GetAttributes uintptr
+ GetCount uintptr // func (pdwNumItems *DWORD) HRESULT
+ GetItemAt uintptr // func (dwIndex DWORD, ppsi **IShellItem) HRESULT
+ EnumItems uintptr
+}
+
+func (vtbl *iShellItemArrayVtbl) getCount(objPtr unsafe.Pointer) (uintptr, error) {
+ var count uintptr
+ ret, _, _ := syscall.SyscallN(vtbl.GetCount,
+ uintptr(objPtr),
+ uintptr(unsafe.Pointer(&count)))
+ if err := hresultToError(ret); err != nil {
+ return 0, err
+ }
+ return count, nil
+}
+
+func (vtbl *iShellItemArrayVtbl) getItemAt(objPtr unsafe.Pointer, index uintptr) (string, error) {
+ var shellItem *iShellItem
+ ret, _, _ := syscall.SyscallN(vtbl.GetItemAt,
+ uintptr(objPtr),
+ index,
+ uintptr(unsafe.Pointer(&shellItem)))
+ if err := hresultToError(ret); err != nil {
+ return "", err
+ }
+ if shellItem == nil {
+ return "", ErrCancelled
+ }
+ defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
+ return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem))
+}
diff --git a/v2/internal/go-common-file-dialog/cfd/vtblCommon.go b/v2/internal/go-common-file-dialog/cfd/vtblCommon.go
new file mode 100644
index 000000000..21015c27c
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/vtblCommon.go
@@ -0,0 +1,48 @@
+//go:build windows
+// +build windows
+
+package cfd
+
+type comDlgFilterSpec struct {
+ pszName *int16
+ pszSpec *int16
+}
+
+type iUnknownVtbl struct {
+ QueryInterface uintptr
+ AddRef uintptr
+ Release uintptr
+}
+
+type iModalWindowVtbl struct {
+ iUnknownVtbl
+ Show uintptr // func (hwndOwner HWND) HRESULT
+}
+
+type iFileDialogVtbl struct {
+ iModalWindowVtbl
+ SetFileTypes uintptr // func (cFileTypes UINT, rgFilterSpec *COMDLG_FILTERSPEC) HRESULT
+ SetFileTypeIndex uintptr // func(iFileType UINT) HRESULT
+ GetFileTypeIndex uintptr
+ Advise uintptr
+ Unadvise uintptr
+ SetOptions uintptr // func (fos FILEOPENDIALOGOPTIONS) HRESULT
+ GetOptions uintptr // func (pfos *FILEOPENDIALOGOPTIONS) HRESULT
+ SetDefaultFolder uintptr // func (psi *IShellItem) HRESULT
+ SetFolder uintptr // func (psi *IShellItem) HRESULT
+ GetFolder uintptr
+ GetCurrentSelection uintptr
+ SetFileName uintptr // func (pszName LPCWSTR) HRESULT
+ GetFileName uintptr
+ SetTitle uintptr // func(pszTitle LPCWSTR) HRESULT
+ SetOkButtonLabel uintptr
+ SetFileNameLabel uintptr
+ GetResult uintptr // func (ppsi **IShellItem) HRESULT
+ AddPlace uintptr
+ SetDefaultExtension uintptr // func (pszDefaultExtension LPCWSTR) HRESULT
+ // This can only be used from a callback.
+ Close uintptr
+ SetClientGuid uintptr // func (guid REFGUID) HRESULT
+ ClearClientData uintptr
+ SetFilter uintptr
+}
diff --git a/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go b/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go
new file mode 100644
index 000000000..581a7b25c
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go
@@ -0,0 +1,224 @@
+//go:build windows
+
+package cfd
+
+import (
+ "github.com/go-ole/go-ole"
+ "strings"
+ "syscall"
+ "unsafe"
+)
+
+func hresultToError(hr uintptr) error {
+ if hr < 0 {
+ return ole.NewError(hr)
+ }
+ return nil
+}
+
+func (vtbl *iUnknownVtbl) release(objPtr unsafe.Pointer) error {
+ ret, _, _ := syscall.SyscallN(vtbl.Release,
+ uintptr(objPtr),
+ 0)
+ return hresultToError(ret)
+}
+
+func (vtbl *iModalWindowVtbl) show(objPtr unsafe.Pointer, hwnd uintptr) error {
+ ret, _, _ := syscall.SyscallN(vtbl.Show,
+ uintptr(objPtr),
+ hwnd)
+ return hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) setFileTypes(objPtr unsafe.Pointer, filters []FileFilter) error {
+ cFileTypes := len(filters)
+ if cFileTypes < 0 {
+ return ErrEmptyFilters
+ }
+ comDlgFilterSpecs := make([]comDlgFilterSpec, cFileTypes)
+ for i := 0; i < cFileTypes; i++ {
+ filter := &filters[i]
+ comDlgFilterSpecs[i] = comDlgFilterSpec{
+ pszName: ole.SysAllocString(filter.DisplayName),
+ pszSpec: ole.SysAllocString(filter.Pattern),
+ }
+ }
+
+ // Ensure memory is freed after use
+ defer func() {
+ for _, spec := range comDlgFilterSpecs {
+ ole.SysFreeString(spec.pszName)
+ ole.SysFreeString(spec.pszSpec)
+ }
+ }()
+
+ ret, _, _ := syscall.SyscallN(vtbl.SetFileTypes,
+ uintptr(objPtr),
+ uintptr(cFileTypes),
+ uintptr(unsafe.Pointer(&comDlgFilterSpecs[0])))
+ return hresultToError(ret)
+}
+
+// Options are:
+// FOS_OVERWRITEPROMPT = 0x2,
+// FOS_STRICTFILETYPES = 0x4,
+// FOS_NOCHANGEDIR = 0x8,
+// FOS_PICKFOLDERS = 0x20,
+// FOS_FORCEFILESYSTEM = 0x40,
+// FOS_ALLNONSTORAGEITEMS = 0x80,
+// FOS_NOVALIDATE = 0x100,
+// FOS_ALLOWMULTISELECT = 0x200,
+// FOS_PATHMUSTEXIST = 0x800,
+// FOS_FILEMUSTEXIST = 0x1000,
+// FOS_CREATEPROMPT = 0x2000,
+// FOS_SHAREAWARE = 0x4000,
+// FOS_NOREADONLYRETURN = 0x8000,
+// FOS_NOTESTFILECREATE = 0x10000,
+// FOS_HIDEMRUPLACES = 0x20000,
+// FOS_HIDEPINNEDPLACES = 0x40000,
+// FOS_NODEREFERENCELINKS = 0x100000,
+// FOS_OKBUTTONNEEDSINTERACTION = 0x200000,
+// FOS_DONTADDTORECENT = 0x2000000,
+// FOS_FORCESHOWHIDDEN = 0x10000000,
+// FOS_DEFAULTNOMINIMODE = 0x20000000,
+// FOS_FORCEPREVIEWPANEON = 0x40000000,
+// FOS_SUPPORTSTREAMABLEITEMS = 0x80000000
+func (vtbl *iFileDialogVtbl) setOptions(objPtr unsafe.Pointer, options uint32) error {
+ ret, _, _ := syscall.SyscallN(vtbl.SetOptions,
+ uintptr(objPtr),
+ uintptr(options))
+ return hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) getOptions(objPtr unsafe.Pointer) (uint32, error) {
+ var options uint32
+ ret, _, _ := syscall.SyscallN(vtbl.GetOptions,
+ uintptr(objPtr),
+ uintptr(unsafe.Pointer(&options)))
+ return options, hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) addOption(objPtr unsafe.Pointer, option uint32) error {
+ if options, err := vtbl.getOptions(objPtr); err == nil {
+ return vtbl.setOptions(objPtr, options|option)
+ } else {
+ return err
+ }
+}
+
+func (vtbl *iFileDialogVtbl) removeOption(objPtr unsafe.Pointer, option uint32) error {
+ if options, err := vtbl.getOptions(objPtr); err == nil {
+ return vtbl.setOptions(objPtr, options&^option)
+ } else {
+ return err
+ }
+}
+
+func (vtbl *iFileDialogVtbl) setDefaultFolder(objPtr unsafe.Pointer, path string) error {
+ shellItem, err := newIShellItem(path)
+ if err != nil {
+ return err
+ }
+ defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
+ ret, _, _ := syscall.SyscallN(vtbl.SetDefaultFolder,
+ uintptr(objPtr),
+ uintptr(unsafe.Pointer(shellItem)))
+ return hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) setFolder(objPtr unsafe.Pointer, path string) error {
+ shellItem, err := newIShellItem(path)
+ if err != nil {
+ return err
+ }
+ defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
+ ret, _, _ := syscall.SyscallN(vtbl.SetFolder,
+ uintptr(objPtr),
+ uintptr(unsafe.Pointer(shellItem)))
+ return hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) setTitle(objPtr unsafe.Pointer, title string) error {
+ titlePtr := ole.SysAllocString(title)
+ defer ole.SysFreeString(titlePtr) // Ensure the string is freed
+ ret, _, _ := syscall.SyscallN(vtbl.SetTitle,
+ uintptr(objPtr),
+ uintptr(unsafe.Pointer(titlePtr)))
+ return hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) close(objPtr unsafe.Pointer) error {
+ ret, _, _ := syscall.SyscallN(vtbl.Close,
+ uintptr(objPtr))
+ return hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) getResult(objPtr unsafe.Pointer) (*iShellItem, error) {
+ var shellItem *iShellItem
+ ret, _, _ := syscall.SyscallN(vtbl.GetResult,
+ uintptr(objPtr),
+ uintptr(unsafe.Pointer(&shellItem)))
+ return shellItem, hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) getResultString(objPtr unsafe.Pointer) (string, error) {
+ shellItem, err := vtbl.getResult(objPtr)
+ if err != nil {
+ return "", err
+ }
+ if shellItem == nil {
+ return "", ErrCancelled
+ }
+ defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
+ return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem))
+}
+
+func (vtbl *iFileDialogVtbl) setClientGuid(objPtr unsafe.Pointer, guid *ole.GUID) error {
+ // Ensure the GUID is not nil
+ if guid == nil {
+ return ErrInvalidGUID
+ }
+
+ // Call the SetClientGuid method
+ ret, _, _ := syscall.SyscallN(vtbl.SetClientGuid,
+ uintptr(objPtr),
+ uintptr(unsafe.Pointer(guid)))
+
+ // Convert the HRESULT to a Go error
+ return hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) setDefaultExtension(objPtr unsafe.Pointer, defaultExtension string) error {
+ // Ensure the string is not empty before accessing the first character
+ if len(defaultExtension) > 0 && defaultExtension[0] == '.' {
+ defaultExtension = strings.TrimPrefix(defaultExtension, ".")
+ }
+
+ // Allocate memory for the default extension string
+ defaultExtensionPtr := ole.SysAllocString(defaultExtension)
+ defer ole.SysFreeString(defaultExtensionPtr) // Ensure the string is freed
+
+ // Call the SetDefaultExtension method
+ ret, _, _ := syscall.SyscallN(vtbl.SetDefaultExtension,
+ uintptr(objPtr),
+ uintptr(unsafe.Pointer(defaultExtensionPtr)))
+
+ // Convert the HRESULT to a Go error
+ return hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) setFileName(objPtr unsafe.Pointer, fileName string) error {
+ fileNamePtr := ole.SysAllocString(fileName)
+ defer ole.SysFreeString(fileNamePtr) // Ensure the string is freed
+ ret, _, _ := syscall.SyscallN(vtbl.SetFileName,
+ uintptr(objPtr),
+ uintptr(unsafe.Pointer(fileNamePtr)))
+ return hresultToError(ret)
+}
+
+func (vtbl *iFileDialogVtbl) setSelectedFileFilterIndex(objPtr unsafe.Pointer, index uint) error {
+ ret, _, _ := syscall.SyscallN(vtbl.SetFileTypeIndex,
+ uintptr(objPtr),
+ uintptr(index+1)) // SetFileTypeIndex counts from 1
+ return hresultToError(ret)
+}
diff --git a/v2/internal/go-common-file-dialog/cfdutil/CFDUtil.go b/v2/internal/go-common-file-dialog/cfdutil/CFDUtil.go
new file mode 100644
index 000000000..bde52d743
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/cfdutil/CFDUtil.go
@@ -0,0 +1,45 @@
+package cfdutil
+
+import (
+ "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/cfd"
+)
+
+// TODO doc
+func ShowOpenFileDialog(config cfd.DialogConfig) (string, error) {
+ dialog, err := cfd.NewOpenFileDialog(config)
+ if err != nil {
+ return "", err
+ }
+ defer dialog.Release()
+ return dialog.ShowAndGetResult()
+}
+
+// TODO doc
+func ShowOpenMultipleFilesDialog(config cfd.DialogConfig) ([]string, error) {
+ dialog, err := cfd.NewOpenMultipleFilesDialog(config)
+ if err != nil {
+ return nil, err
+ }
+ defer dialog.Release()
+ return dialog.ShowAndGetResults()
+}
+
+// TODO doc
+func ShowPickFolderDialog(config cfd.DialogConfig) (string, error) {
+ dialog, err := cfd.NewSelectFolderDialog(config)
+ if err != nil {
+ return "", err
+ }
+ defer dialog.Release()
+ return dialog.ShowAndGetResult()
+}
+
+// TODO doc
+func ShowSaveFileDialog(config cfd.DialogConfig) (string, error) {
+ dialog, err := cfd.NewSaveFileDialog(config)
+ if err != nil {
+ return "", err
+ }
+ defer dialog.Release()
+ return dialog.ShowAndGetResult()
+}
diff --git a/v2/internal/go-common-file-dialog/util/util.go b/v2/internal/go-common-file-dialog/util/util.go
new file mode 100644
index 000000000..723fbedc0
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/util/util.go
@@ -0,0 +1,10 @@
+package util
+
+import (
+ "github.com/go-ole/go-ole"
+ "github.com/google/uuid"
+)
+
+func StringToUUID(str string) *ole.GUID {
+ return ole.NewGUID(uuid.NewSHA1(uuid.Nil, []byte(str)).String())
+}
diff --git a/v2/internal/go-common-file-dialog/util/util_test.go b/v2/internal/go-common-file-dialog/util/util_test.go
new file mode 100644
index 000000000..2e8ffeb05
--- /dev/null
+++ b/v2/internal/go-common-file-dialog/util/util_test.go
@@ -0,0 +1,14 @@
+package util
+
+import (
+ "github.com/go-ole/go-ole"
+ "testing"
+)
+
+func TestStringToUUID(t *testing.T) {
+ generated := *StringToUUID("TestTestTest")
+ expected := *ole.NewGUID("7933985F-2C87-5A5B-A26E-5D0326829AC2")
+ if generated != expected {
+ t.Errorf("not equal. expected %s, found %s", expected.String(), generated.String())
+ }
+}
diff --git a/v2/internal/gomod/gomod.go b/v2/internal/gomod/gomod.go
new file mode 100644
index 000000000..c38e60f0b
--- /dev/null
+++ b/v2/internal/gomod/gomod.go
@@ -0,0 +1,114 @@
+package gomod
+
+import (
+ "fmt"
+
+ "github.com/Masterminds/semver"
+ "golang.org/x/mod/modfile"
+)
+
+func GetWailsVersionFromModFile(goModText []byte) (*semver.Version, error) {
+ file, err := modfile.Parse("", goModText, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, req := range file.Require {
+ if req.Syntax == nil {
+ continue
+ }
+ tokenPosition := 0
+ if !req.Syntax.InBlock {
+ tokenPosition = 1
+ }
+ if req.Syntax.Token[tokenPosition] == "github.com/wailsapp/wails/v2" {
+ version := req.Syntax.Token[tokenPosition+1]
+ return semver.NewVersion(version)
+ }
+ }
+
+ return nil, nil
+}
+
+func GoModOutOfSync(goModData []byte, currentVersion string) (bool, error) {
+ gomodversion, err := GetWailsVersionFromModFile(goModData)
+ if err != nil {
+ return false, err
+ }
+ if gomodversion == nil {
+ return false, fmt.Errorf("Unable to find Wails in go.mod")
+ }
+
+ result, err := semver.NewVersion(currentVersion)
+ if err != nil || result == nil {
+ return false, fmt.Errorf("Unable to parse Wails version: %s", currentVersion)
+ }
+
+ return !gomodversion.Equal(result), nil
+}
+
+func UpdateGoModVersion(goModText []byte, currentVersion string) ([]byte, error) {
+ file, err := modfile.Parse("", goModText, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ err = file.AddRequire("github.com/wailsapp/wails/v2", currentVersion)
+ if err != nil {
+ return nil, err
+ }
+
+ // Replace
+ if len(file.Replace) == 0 {
+ return file.Format()
+ }
+
+ for _, req := range file.Replace {
+ if req.Syntax == nil {
+ continue
+ }
+ tokenPosition := 0
+ if !req.Syntax.InBlock {
+ tokenPosition = 1
+ }
+ if req.Syntax.Token[tokenPosition] == "github.com/wailsapp/wails/v2" {
+ version := req.Syntax.Token[tokenPosition+1]
+ _, err := semver.NewVersion(version)
+ if err == nil {
+ req.Syntax.Token[tokenPosition+1] = currentVersion
+ }
+ }
+ }
+
+ return file.Format()
+}
+
+func SyncGoVersion(goModText []byte, goVersion string) ([]byte, bool, error) {
+ file, err := modfile.Parse("", goModText, nil)
+ if err != nil {
+ return nil, false, err
+ }
+
+ modVersion, err := semver.NewVersion(file.Go.Version)
+ if err != nil {
+ return nil, false, fmt.Errorf("Unable to parse Go version from go mod file: %s", err)
+ }
+
+ targetVersion, err := semver.NewVersion(goVersion)
+ if err != nil {
+ return nil, false, fmt.Errorf("Unable to parse Go version: %s", targetVersion)
+ }
+
+ if !targetVersion.GreaterThan(modVersion) {
+ return goModText, false, nil
+ }
+
+ file.Go.Version = goVersion
+ file.Go.Syntax.Token[1] = goVersion
+ goModText, err = file.Format()
+ if err != nil {
+ return nil, false, err
+ }
+
+ return goModText, true, nil
+}
diff --git a/v2/internal/gomod/gomod_data_unix.go b/v2/internal/gomod/gomod_data_unix.go
new file mode 100644
index 000000000..c6004f486
--- /dev/null
+++ b/v2/internal/gomod/gomod_data_unix.go
@@ -0,0 +1,139 @@
+//go:build darwin || linux
+
+package gomod
+
+const basic string = `module changeme
+
+go 1.17
+
+require github.com/wailsapp/wails/v2 v2.0.0-beta.7
+
+//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2
+`
+
+const basicUpdated string = `module changeme
+
+go 1.17
+
+require github.com/wailsapp/wails/v2 v2.0.0-beta.20
+
+//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2
+`
+
+const multilineRequire = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7
+)
+
+//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2
+`
+
+const multilineReplace = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7
+)
+
+replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2
+`
+
+const multilineReplaceNoVersion = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7
+)
+
+replace github.com/wailsapp/wails/v2 => /home/lea/wails/v2
+`
+
+const multilineReplaceNoVersionBlock = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7
+)
+
+replace (
+ github.com/wailsapp/wails/v2 => /home/lea/wails/v2
+)
+`
+
+const multilineReplaceBlock = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7
+)
+
+replace (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2
+)
+`
+
+const multilineRequireUpdated = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20
+)
+
+//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2
+`
+
+const multilineReplaceUpdated = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20
+)
+
+replace github.com/wailsapp/wails/v2 v2.0.0-beta.20 => /home/lea/wails/v2
+`
+
+const multilineReplaceNoVersionUpdated = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20
+)
+
+replace github.com/wailsapp/wails/v2 => /home/lea/wails/v2
+`
+
+const multilineReplaceNoVersionBlockUpdated = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20
+)
+
+replace (
+ github.com/wailsapp/wails/v2 => /home/lea/wails/v2
+)
+`
+
+const multilineReplaceBlockUpdated = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20
+)
+
+replace (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20 => /home/lea/wails/v2
+)
+`
diff --git a/v2/internal/gomod/gomod_data_windows.go b/v2/internal/gomod/gomod_data_windows.go
new file mode 100644
index 000000000..691129c78
--- /dev/null
+++ b/v2/internal/gomod/gomod_data_windows.go
@@ -0,0 +1,135 @@
+//go:build windows
+
+package gomod
+
+const basic string = `module changeme
+
+go 1.17
+
+require github.com/wailsapp/wails/v2 v2.0.0-beta.7
+
+//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+`
+const basicUpdated string = `module changeme
+
+go 1.17
+
+require github.com/wailsapp/wails/v2 v2.0.0-beta.20
+
+//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+`
+
+const multilineRequire = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7
+)
+
+//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+`
+const multilineReplace = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7
+)
+
+replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+`
+
+const multilineReplaceNoVersion = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7
+)
+
+replace github.com/wailsapp/wails/v2 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+`
+
+const multilineReplaceNoVersionBlock = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7
+)
+
+replace (
+ github.com/wailsapp/wails/v2 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+)
+`
+
+const multilineReplaceBlock = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7
+)
+
+replace (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+)
+`
+
+const multilineRequireUpdated = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20
+)
+
+//replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+`
+
+const multilineReplaceUpdated = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20
+)
+
+replace github.com/wailsapp/wails/v2 v2.0.0-beta.20 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+`
+const multilineReplaceNoVersionUpdated = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20
+)
+
+replace github.com/wailsapp/wails/v2 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+`
+const multilineReplaceNoVersionBlockUpdated = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20
+)
+
+replace (
+ github.com/wailsapp/wails/v2 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+)
+`
+
+const multilineReplaceBlockUpdated = `module changeme
+
+go 1.17
+
+require (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20
+)
+
+replace (
+ github.com/wailsapp/wails/v2 v2.0.0-beta.20 => C:\Users\leaan\Documents\wails-v2-beta\wails\v2
+)
+`
diff --git a/v2/internal/gomod/gomod_test.go b/v2/internal/gomod/gomod_test.go
new file mode 100644
index 000000000..eeafd0f9a
--- /dev/null
+++ b/v2/internal/gomod/gomod_test.go
@@ -0,0 +1,139 @@
+package gomod
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/Masterminds/semver"
+ "github.com/matryer/is"
+)
+
+func TestGetWailsVersion(t *testing.T) {
+ tests := []struct {
+ name string
+ goModText []byte
+ want *semver.Version
+ wantErr bool
+ }{
+ {"basic", []byte(basic), semver.MustParse("v2.0.0-beta.7"), false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := GetWailsVersionFromModFile(tt.goModText)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("GetWailsVersion() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("GetWailsVersion() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestUpdateGoModVersion(t *testing.T) {
+ is2 := is.New(t)
+
+ type args struct {
+ goModText []byte
+ currentVersion string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []byte
+ wantErr bool
+ }{
+ {"basic", args{[]byte(basic), "v2.0.0-beta.20"}, []byte(basicUpdated), false},
+ {"basicmultiline", args{[]byte(multilineRequire), "v2.0.0-beta.20"}, []byte(multilineRequireUpdated), false},
+ {"basicmultilinereplace", args{[]byte(multilineReplace), "v2.0.0-beta.20"}, []byte(multilineReplaceUpdated), false},
+ {"basicmultilinereplaceblock", args{[]byte(multilineReplaceBlock), "v2.0.0-beta.20"}, []byte(multilineReplaceBlockUpdated), false},
+ {"basicmultilinereplacenoversion", args{[]byte(multilineReplaceNoVersion), "v2.0.0-beta.20"}, []byte(multilineReplaceNoVersionUpdated), false},
+ {"basicmultilinereplacenoversionblock", args{[]byte(multilineReplaceNoVersionBlock), "v2.0.0-beta.20"}, []byte(multilineReplaceNoVersionBlockUpdated), false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := UpdateGoModVersion(tt.args.goModText, tt.args.currentVersion)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("UpdateGoModVersion() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ is2.Equal(string(got), string(tt.want))
+ })
+ }
+}
+
+func TestGoModOutOfSync(t *testing.T) {
+ is2 := is.New(t)
+
+ type args struct {
+ goModData []byte
+ currentVersion string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ wantErr bool
+ }{
+ {"basic", args{[]byte(basic), "v2.0.0-beta.20"}, true, false},
+ {"basicmultiline", args{[]byte(multilineRequire), "v2.0.0-beta.20"}, true, false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := GoModOutOfSync(tt.args.goModData, tt.args.currentVersion)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("GoModOutOfSync() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ is2.Equal(got, tt.want)
+ })
+ }
+}
+
+const basicGo118 string = `module changeme
+
+go 1.18
+
+require github.com/wailsapp/wails/v2 v2.0.0-beta.7
+`
+
+const basicGo119 string = `module changeme
+
+go 1.19
+
+require github.com/wailsapp/wails/v2 v2.0.0-beta.7
+`
+
+func TestUpdateGoModGoVersion(t *testing.T) {
+ is2 := is.New(t)
+
+ type args struct {
+ goModText []byte
+ currentVersion string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []byte
+ updated bool
+ }{
+ {"basic1.18", args{[]byte(basicGo118), "1.18"}, []byte(basicGo118), false},
+ {"basic1.19", args{[]byte(basicGo119), "1.17"}, []byte(basicGo119), false},
+ {"basic1.19", args{[]byte(basicGo119), "1.18"}, []byte(basicGo119), false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, updated, err := SyncGoVersion(tt.args.goModText, tt.args.currentVersion)
+ if err != nil {
+ t.Errorf("UpdateGoModVersion() error = %v", err)
+ return
+ }
+ if updated != tt.updated {
+ t.Errorf("UpdateGoModVersion() updated = %t, want = %t", updated, tt.updated)
+ return
+ }
+ is2.Equal(got, tt.want)
+ })
+ }
+}
diff --git a/v2/internal/goversion/build_constraint.go b/v2/internal/goversion/build_constraint.go
new file mode 100644
index 000000000..5e1b9fcf5
--- /dev/null
+++ b/v2/internal/goversion/build_constraint.go
@@ -0,0 +1,10 @@
+//go:build !go1.18
+// +build !go1.18
+
+package goversion
+
+const MinGoVersionRequired = "You need Go " + MinRequirement + " or newer to compile this program"
+
+func init() {
+ MinGoVersionRequired
+}
diff --git a/v2/internal/goversion/min.go b/v2/internal/goversion/min.go
new file mode 100644
index 000000000..8c057b3c2
--- /dev/null
+++ b/v2/internal/goversion/min.go
@@ -0,0 +1,3 @@
+package goversion
+
+const MinRequirement string = "1.20"
diff --git a/v2/internal/logger/custom_logger.go b/v2/internal/logger/custom_logger.go
new file mode 100644
index 000000000..51e07c0fc
--- /dev/null
+++ b/v2/internal/logger/custom_logger.go
@@ -0,0 +1,95 @@
+package logger
+
+import (
+ "fmt"
+)
+
+// CustomLogger defines what a user can do with a logger
+type CustomLogger interface {
+ // Writeln writes directly to the output with no log level plus line ending
+ Writeln(message string)
+
+ // Write writes directly to the output with no log level
+ Write(message string)
+
+ // Trace level logging. Works like Sprintf.
+ Trace(format string, args ...interface{})
+
+ // Debug level logging. Works like Sprintf.
+ Debug(format string, args ...interface{})
+
+ // Info level logging. Works like Sprintf.
+ Info(format string, args ...interface{})
+
+ // Warning level logging. Works like Sprintf.
+ Warning(format string, args ...interface{})
+
+ // Error level logging. Works like Sprintf.
+ Error(format string, args ...interface{})
+
+ // Fatal level logging. Works like Sprintf.
+ Fatal(format string, args ...interface{})
+}
+
+// customLogger is a utlility to log messages to a number of destinations
+type customLogger struct {
+ logger *Logger
+ name string
+}
+
+// New creates a new customLogger. You may pass in a number of `io.Writer`s that
+// are the targets for the logs
+func newcustomLogger(logger *Logger, name string) *customLogger {
+ result := &customLogger{
+ name: name,
+ logger: logger,
+ }
+ return result
+}
+
+// Writeln writes directly to the output with no log level
+// Appends a carriage return to the message
+func (l *customLogger) Writeln(message string) {
+ l.logger.Writeln(message)
+}
+
+// Write writes directly to the output with no log level
+func (l *customLogger) Write(message string) {
+ l.logger.Write(message)
+}
+
+// Trace level logging. Works like Sprintf.
+func (l *customLogger) Trace(format string, args ...interface{}) {
+ format = fmt.Sprintf("%s | %s", l.name, format)
+ l.logger.Trace(format, args...)
+}
+
+// Debug level logging. Works like Sprintf.
+func (l *customLogger) Debug(format string, args ...interface{}) {
+ format = fmt.Sprintf("%s | %s", l.name, format)
+ l.logger.Debug(format, args...)
+}
+
+// Info level logging. Works like Sprintf.
+func (l *customLogger) Info(format string, args ...interface{}) {
+ format = fmt.Sprintf("%s | %s", l.name, format)
+ l.logger.Info(format, args...)
+}
+
+// Warning level logging. Works like Sprintf.
+func (l *customLogger) Warning(format string, args ...interface{}) {
+ format = fmt.Sprintf("%s | %s", l.name, format)
+ l.logger.Warning(format, args...)
+}
+
+// Error level logging. Works like Sprintf.
+func (l *customLogger) Error(format string, args ...interface{}) {
+ format = fmt.Sprintf("%s | %s", l.name, format)
+ l.logger.Error(format, args...)
+}
+
+// Fatal level logging. Works like Sprintf.
+func (l *customLogger) Fatal(format string, args ...interface{}) {
+ format = fmt.Sprintf("%s | %s", l.name, format)
+ l.logger.Fatal(format, args...)
+}
diff --git a/v2/internal/logger/default_logger.go b/v2/internal/logger/default_logger.go
new file mode 100644
index 000000000..5c72ae209
--- /dev/null
+++ b/v2/internal/logger/default_logger.go
@@ -0,0 +1,107 @@
+package logger
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/wailsapp/wails/v2/pkg/logger"
+)
+
+// LogLevel is an alias for the public LogLevel
+type LogLevel = logger.LogLevel
+
+// Logger is a utlility to log messages to a number of destinations
+type Logger struct {
+ output logger.Logger
+ logLevel LogLevel
+ showLevelInLog bool
+}
+
+// New creates a new Logger. You may pass in a number of `io.Writer`s that
+// are the targets for the logs
+func New(output logger.Logger) *Logger {
+ if output == nil {
+ output = logger.NewDefaultLogger()
+ }
+ result := &Logger{
+ logLevel: logger.INFO,
+ showLevelInLog: true,
+ output: output,
+ }
+
+ return result
+}
+
+// CustomLogger creates a new custom logger that prints out a name/id
+// before the messages
+func (l *Logger) CustomLogger(name string) CustomLogger {
+ return newcustomLogger(l, name)
+}
+
+// HideLogLevel removes the loglevel text from the start of each logged line
+func (l *Logger) HideLogLevel() {
+ l.showLevelInLog = true
+}
+
+// SetLogLevel sets the minimum level of logs that will be output
+func (l *Logger) SetLogLevel(level LogLevel) {
+ l.logLevel = level
+}
+
+// Writeln writes directly to the output with no log level
+// Appends a carriage return to the message
+func (l *Logger) Writeln(message string) {
+ l.output.Print(message)
+}
+
+// Write writes directly to the output with no log level
+func (l *Logger) Write(message string) {
+ l.output.Print(message)
+}
+
+// Print writes directly to the output with no log level
+// Appends a carriage return to the message
+func (l *Logger) Print(message string) {
+ l.Write(message)
+}
+
+// Trace level logging. Works like Sprintf.
+func (l *Logger) Trace(format string, args ...interface{}) {
+ if l.logLevel <= logger.TRACE {
+ l.output.Trace(fmt.Sprintf(format, args...))
+ }
+}
+
+// Debug level logging. Works like Sprintf.
+func (l *Logger) Debug(format string, args ...interface{}) {
+ if l.logLevel <= logger.DEBUG {
+ l.output.Debug(fmt.Sprintf(format, args...))
+ }
+}
+
+// Info level logging. Works like Sprintf.
+func (l *Logger) Info(format string, args ...interface{}) {
+ if l.logLevel <= logger.INFO {
+ l.output.Info(fmt.Sprintf(format, args...))
+ }
+}
+
+// Warning level logging. Works like Sprintf.
+func (l *Logger) Warning(format string, args ...interface{}) {
+ if l.logLevel <= logger.WARNING {
+ l.output.Warning(fmt.Sprintf(format, args...))
+ }
+}
+
+// Error level logging. Works like Sprintf.
+func (l *Logger) Error(format string, args ...interface{}) {
+ if l.logLevel <= logger.ERROR {
+ l.output.Error(fmt.Sprintf(format, args...))
+ }
+}
+
+// Fatal level logging. Works like Sprintf.
+func (l *Logger) Fatal(format string, args ...interface{}) {
+ l.output.Fatal(fmt.Sprintf(format, args...))
+ os.Exit(1)
+}
diff --git a/v2/internal/menumanager/applicationmenu.go b/v2/internal/menumanager/applicationmenu.go
new file mode 100644
index 000000000..4446a00cb
--- /dev/null
+++ b/v2/internal/menumanager/applicationmenu.go
@@ -0,0 +1,49 @@
+package menumanager
+
+import "github.com/wailsapp/wails/v2/pkg/menu"
+
+func (m *Manager) SetApplicationMenu(applicationMenu *menu.Menu) error {
+ if applicationMenu == nil {
+ return nil
+ }
+
+ m.applicationMenu = applicationMenu
+
+ // Reset the menu map
+ m.applicationMenuItemMap = NewMenuItemMap()
+
+ // Add the menu to the menu map
+ m.applicationMenuItemMap.AddMenu(applicationMenu)
+
+ return m.processApplicationMenu()
+}
+
+func (m *Manager) GetApplicationMenuJSON() string {
+ return m.applicationMenuJSON
+}
+
+func (m *Manager) GetProcessedApplicationMenu() *WailsMenu {
+ return m.processedApplicationMenu
+}
+
+// UpdateApplicationMenu reprocesses the application menu to pick up structure
+// changes etc
+// Returns the JSON representation of the updated menu
+func (m *Manager) UpdateApplicationMenu() (string, error) {
+ m.applicationMenuItemMap = NewMenuItemMap()
+ m.applicationMenuItemMap.AddMenu(m.applicationMenu)
+ err := m.processApplicationMenu()
+ return m.applicationMenuJSON, err
+}
+
+func (m *Manager) processApplicationMenu() error {
+ // Process the menu
+ m.processedApplicationMenu = NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu)
+ m.processRadioGroups(m.processedApplicationMenu, m.applicationMenuItemMap)
+ applicationMenuJSON, err := m.processedApplicationMenu.AsJSON()
+ if err != nil {
+ return err
+ }
+ m.applicationMenuJSON = applicationMenuJSON
+ return nil
+}
diff --git a/v2/internal/menumanager/contextmenu.go b/v2/internal/menumanager/contextmenu.go
new file mode 100644
index 000000000..f05bcdc49
--- /dev/null
+++ b/v2/internal/menumanager/contextmenu.go
@@ -0,0 +1,59 @@
+package menumanager
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/wailsapp/wails/v2/pkg/menu"
+)
+
+type ContextMenu struct {
+ ID string
+ ProcessedMenu *WailsMenu
+ menuItemMap *MenuItemMap
+ menu *menu.Menu
+}
+
+func (t *ContextMenu) AsJSON() (string, error) {
+ data, err := json.Marshal(t)
+ if err != nil {
+ return "", err
+ }
+ return string(data), nil
+}
+
+func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu {
+ result := &ContextMenu{
+ ID: contextMenu.ID,
+ menu: contextMenu.Menu,
+ menuItemMap: NewMenuItemMap(),
+ }
+
+ result.menuItemMap.AddMenu(contextMenu.Menu)
+ result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
+
+ return result
+}
+
+func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) {
+ newContextMenu := NewContextMenu(contextMenu)
+
+ // Save the references
+ m.contextMenus[contextMenu.ID] = newContextMenu
+ m.contextMenuPointers[contextMenu] = contextMenu.ID
+}
+
+func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) {
+ contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu]
+ if !contextMenuKnown {
+ return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID)
+ }
+
+ // Create the updated context menu
+ updatedContextMenu := NewContextMenu(contextMenu)
+
+ // Save the reference
+ m.contextMenus[contextMenuID] = updatedContextMenu
+
+ return updatedContextMenu.AsJSON()
+}
diff --git a/v2/internal/menumanager/menuitemmap.go b/v2/internal/menumanager/menuitemmap.go
new file mode 100644
index 000000000..e4e291be6
--- /dev/null
+++ b/v2/internal/menumanager/menuitemmap.go
@@ -0,0 +1,76 @@
+package menumanager
+
+import (
+ "fmt"
+ "strconv"
+ "sync"
+
+ "github.com/wailsapp/wails/v2/pkg/menu"
+)
+
+// MenuItemMap holds a mapping between menuIDs and menu items
+type MenuItemMap struct {
+ idToMenuItemMap map[string]*menu.MenuItem
+ menuItemToIDMap map[*menu.MenuItem]string
+
+ // We use a simple counter to keep track of unique menu IDs
+ menuIDCounter int64
+ menuIDCounterMutex sync.Mutex
+}
+
+func NewMenuItemMap() *MenuItemMap {
+ result := &MenuItemMap{
+ idToMenuItemMap: make(map[string]*menu.MenuItem),
+ menuItemToIDMap: make(map[*menu.MenuItem]string),
+ }
+
+ return result
+}
+
+func (m *MenuItemMap) AddMenu(menu *menu.Menu) {
+ if menu == nil {
+ return
+ }
+ for _, item := range menu.Items {
+ m.processMenuItem(item)
+ }
+}
+
+func (m *MenuItemMap) Dump() {
+ println("idToMenuItemMap:")
+ for key, value := range m.idToMenuItemMap {
+ fmt.Printf(" %s\t%p\n", key, value)
+ }
+ println("\nmenuItemToIDMap")
+ for key, value := range m.menuItemToIDMap {
+ fmt.Printf(" %p\t%s\n", key, value)
+ }
+}
+
+// GenerateMenuID returns a unique string ID for a menu item
+func (m *MenuItemMap) generateMenuID() string {
+ m.menuIDCounterMutex.Lock()
+ result := strconv.FormatInt(m.menuIDCounter, 10)
+ m.menuIDCounter++
+ m.menuIDCounterMutex.Unlock()
+ return result
+}
+
+func (m *MenuItemMap) processMenuItem(item *menu.MenuItem) {
+ if item.SubMenu != nil {
+ for _, submenuitem := range item.SubMenu.Items {
+ m.processMenuItem(submenuitem)
+ }
+ }
+
+ // Create a unique ID for this menu item
+ menuID := m.generateMenuID()
+
+ // Store references
+ m.idToMenuItemMap[menuID] = item
+ m.menuItemToIDMap[item] = menuID
+}
+
+func (m *MenuItemMap) getMenuItemByID(menuId string) *menu.MenuItem {
+ return m.idToMenuItemMap[menuId]
+}
diff --git a/v2/internal/menumanager/menumanager.go b/v2/internal/menumanager/menumanager.go
new file mode 100644
index 000000000..0c6be0df2
--- /dev/null
+++ b/v2/internal/menumanager/menumanager.go
@@ -0,0 +1,115 @@
+package menumanager
+
+import (
+ "fmt"
+
+ "github.com/wailsapp/wails/v2/pkg/menu"
+)
+
+type Manager struct {
+ // The application menu.
+ applicationMenu *menu.Menu
+ applicationMenuJSON string
+ processedApplicationMenu *WailsMenu
+
+ // Our application menu mappings
+ applicationMenuItemMap *MenuItemMap
+
+ // Context menus
+ contextMenus map[string]*ContextMenu
+ contextMenuPointers map[*menu.ContextMenu]string
+
+ // Tray menu stores
+ trayMenus map[string]*TrayMenu
+ trayMenuPointers map[*menu.TrayMenu]string
+
+ // Radio groups
+ radioGroups map[*menu.MenuItem][]*menu.MenuItem
+}
+
+func NewManager() *Manager {
+ return &Manager{
+ applicationMenuItemMap: NewMenuItemMap(),
+ contextMenus: make(map[string]*ContextMenu),
+ contextMenuPointers: make(map[*menu.ContextMenu]string),
+ trayMenus: make(map[string]*TrayMenu),
+ trayMenuPointers: make(map[*menu.TrayMenu]string),
+ radioGroups: make(map[*menu.MenuItem][]*menu.MenuItem),
+ }
+}
+
+func (m *Manager) getMenuItemByID(menuMap *MenuItemMap, menuId string) *menu.MenuItem {
+ return menuMap.idToMenuItemMap[menuId]
+}
+
+func (m *Manager) ProcessClick(menuID string, data string, menuType string, parentID string) error {
+ var menuItemMap *MenuItemMap
+
+ switch menuType {
+ case "ApplicationMenu":
+ menuItemMap = m.applicationMenuItemMap
+ case "ContextMenu":
+ contextMenu := m.contextMenus[parentID]
+ if contextMenu == nil {
+ return fmt.Errorf("unknown context menu: %s", parentID)
+ }
+ menuItemMap = contextMenu.menuItemMap
+ case "TrayMenu":
+ trayMenu := m.trayMenus[parentID]
+ if trayMenu == nil {
+ return fmt.Errorf("unknown tray menu: %s", parentID)
+ }
+ menuItemMap = trayMenu.menuItemMap
+ default:
+ return fmt.Errorf("unknown menutype: %s", menuType)
+ }
+
+ // Get the menu item
+ menuItem := menuItemMap.getMenuItemByID(menuID)
+ if menuItem == nil {
+ return fmt.Errorf("Cannot process menuid %s - unknown", menuID)
+ }
+
+ // Is the menu item a checkbox?
+ if menuItem.Type == menu.CheckboxType {
+ // Toggle state
+ menuItem.Checked = !menuItem.Checked
+ }
+
+ if menuItem.Type == menu.RadioType {
+ println("Toggle radio")
+ // Get my radio group
+ for _, radioMenuItem := range m.radioGroups[menuItem] {
+ radioMenuItem.Checked = (radioMenuItem == menuItem)
+ }
+ }
+
+ if menuItem.Click == nil {
+ // No callback
+ return fmt.Errorf("No callback for menu '%s'", menuItem.Label)
+ }
+
+ // Create new Callback struct
+ callbackData := &menu.CallbackData{
+ MenuItem: menuItem,
+ // ContextData: data,
+ }
+
+ // Call back!
+ go menuItem.Click(callbackData)
+
+ return nil
+}
+
+func (m *Manager) processRadioGroups(processedMenu *WailsMenu, itemMap *MenuItemMap) {
+ for _, group := range processedMenu.RadioGroups {
+ radioGroupMenuItems := []*menu.MenuItem{}
+ for _, member := range group.Members {
+ item := m.getMenuItemByID(itemMap, member)
+ radioGroupMenuItems = append(radioGroupMenuItems, item)
+ }
+ for _, radioGroupMenuItem := range radioGroupMenuItems {
+ m.radioGroups[radioGroupMenuItem] = radioGroupMenuItems
+ }
+ }
+}
diff --git a/v2/internal/menumanager/processedMenu.go b/v2/internal/menumanager/processedMenu.go
new file mode 100644
index 000000000..c87646ccb
--- /dev/null
+++ b/v2/internal/menumanager/processedMenu.go
@@ -0,0 +1,185 @@
+package menumanager
+
+import (
+ "encoding/json"
+
+ "github.com/wailsapp/wails/v2/pkg/menu"
+ "github.com/wailsapp/wails/v2/pkg/menu/keys"
+)
+
+type ProcessedMenuItem struct {
+ ID string
+ // Label is what appears as the menu text
+ Label string `json:",omitempty"`
+ // Role is a predefined menu type
+ // Role menu.Role `json:",omitempty"`
+ // Accelerator holds a representation of a key binding
+ Accelerator *keys.Accelerator `json:",omitempty"`
+ // Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu
+ Type menu.Type
+ // Disabled makes the item unselectable
+ Disabled bool `json:",omitempty"`
+ // Hidden ensures that the item is not shown in the menu
+ Hidden bool `json:",omitempty"`
+ // Checked indicates if the item is selected (used by Checkbox and Radio types only)
+ Checked bool `json:",omitempty"`
+ // SubMenu contains a list of menu items that will be shown as a submenu
+ // SubMenu []*MenuItem `json:"SubMenu,omitempty"`
+ SubMenu *ProcessedMenu `json:",omitempty"`
+ /*
+ // Colour
+ RGBA string `json:",omitempty"`
+
+ // Font
+ FontSize int `json:",omitempty"`
+ FontName string `json:",omitempty"`
+
+ // Image - base64 image data
+ Image string `json:",omitempty"`
+ MacTemplateImage bool `json:", omitempty"`
+ MacAlternate bool `json:", omitempty"`
+
+ // Tooltip
+ Tooltip string `json:",omitempty"`
+
+ // Styled label
+ StyledLabel []*ansi.StyledText `json:",omitempty"`
+ */
+}
+
+func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem {
+ ID := menuItemMap.menuItemToIDMap[menuItem]
+
+ // Parse ANSI text
+ //var styledLabel []*ansi.StyledText
+ //tempLabel := menuItem.Label
+ //if strings.Contains(tempLabel, "\033[") {
+ // parsedLabel, err := ansi.Parse(menuItem.Label)
+ // if err == nil {
+ // styledLabel = parsedLabel
+ // }
+ //}
+
+ result := &ProcessedMenuItem{
+ ID: ID,
+ Label: menuItem.Label,
+ // Role: menuItem.Role,
+ Accelerator: menuItem.Accelerator,
+ Type: menuItem.Type,
+ Disabled: menuItem.Disabled,
+ Hidden: menuItem.Hidden,
+ Checked: menuItem.Checked,
+ SubMenu: nil,
+ // BackgroundColour: menuItem.BackgroundColour,
+ // FontSize: menuItem.FontSize,
+ // FontName: menuItem.FontName,
+ // Image: menuItem.Image,
+ // MacTemplateImage: menuItem.MacTemplateImage,
+ // MacAlternate: menuItem.MacAlternate,
+ // Tooltip: menuItem.Tooltip,
+ // StyledLabel: styledLabel,
+ }
+
+ if menuItem.SubMenu != nil {
+ result.SubMenu = NewProcessedMenu(menuItemMap, menuItem.SubMenu)
+ }
+
+ return result
+}
+
+type ProcessedMenu struct {
+ Items []*ProcessedMenuItem
+}
+
+func NewProcessedMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *ProcessedMenu {
+ result := &ProcessedMenu{}
+ if menu != nil {
+ for _, item := range menu.Items {
+ processedMenuItem := NewProcessedMenuItem(menuItemMap, item)
+ result.Items = append(result.Items, processedMenuItem)
+ }
+ }
+
+ return result
+}
+
+// WailsMenu is the original menu with the addition
+// of radio groups extracted from the menu data
+type WailsMenu struct {
+ Menu *ProcessedMenu
+ RadioGroups []*RadioGroup
+ currentRadioGroup []string
+}
+
+// RadioGroup holds all the members of the same radio group
+type RadioGroup struct {
+ Members []string
+ Length int
+}
+
+func NewWailsMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *WailsMenu {
+ result := &WailsMenu{}
+
+ // Process the menus
+ result.Menu = NewProcessedMenu(menuItemMap, menu)
+
+ // Process the radio groups
+ result.processRadioGroups()
+
+ return result
+}
+
+func (w *WailsMenu) AsJSON() (string, error) {
+ menuAsJSON, err := json.Marshal(w)
+ if err != nil {
+ return "", err
+ }
+ return string(menuAsJSON), nil
+}
+
+func (w *WailsMenu) processRadioGroups() {
+ // Loop over top level menus
+ for _, item := range w.Menu.Items {
+ // Process MenuItem
+ w.processMenuItem(item)
+ }
+
+ w.finaliseRadioGroup()
+}
+
+func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) {
+ switch item.Type {
+
+ // We need to recurse submenus
+ case menu.SubmenuType:
+
+ // Finalise any current radio groups as they don't trickle down to submenus
+ w.finaliseRadioGroup()
+
+ // Process each submenu item
+ for _, subitem := range item.SubMenu.Items {
+ w.processMenuItem(subitem)
+ }
+ case menu.RadioType:
+ // Add the item to the radio group
+ w.currentRadioGroup = append(w.currentRadioGroup, item.ID)
+ default:
+ w.finaliseRadioGroup()
+ }
+}
+
+func (w *WailsMenu) finaliseRadioGroup() {
+ // If we were processing a radio group, fix up the references
+ if len(w.currentRadioGroup) > 0 {
+
+ // Create new radiogroup
+ group := &RadioGroup{
+ Members: w.currentRadioGroup,
+ Length: len(w.currentRadioGroup),
+ }
+ w.RadioGroups = append(w.RadioGroups, group)
+
+ // Empty the radio group
+ w.currentRadioGroup = []string{}
+ }
+}
diff --git a/v2/internal/menumanager/traymenu.go b/v2/internal/menumanager/traymenu.go
new file mode 100644
index 000000000..5efc4a861
--- /dev/null
+++ b/v2/internal/menumanager/traymenu.go
@@ -0,0 +1,222 @@
+package menumanager
+
+import (
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/leaanthony/go-ansi-parser"
+
+ "github.com/pkg/errors"
+ "github.com/wailsapp/wails/v2/pkg/menu"
+)
+
+var (
+ trayMenuID int
+ trayMenuIDMutex sync.Mutex
+)
+
+func generateTrayID() string {
+ var idStr string
+ trayMenuIDMutex.Lock()
+ idStr = strconv.Itoa(trayMenuID)
+ trayMenuID++
+ trayMenuIDMutex.Unlock()
+ return idStr
+}
+
+type TrayMenu struct {
+ ID string
+ Label string
+ FontSize int
+ FontName string
+ Disabled bool
+ Tooltip string `json:",omitempty"`
+ Image string
+ MacTemplateImage bool
+ RGBA string
+ menuItemMap *MenuItemMap
+ menu *menu.Menu
+ ProcessedMenu *WailsMenu
+ trayMenu *menu.TrayMenu
+ StyledLabel []*ansi.StyledText `json:",omitempty"`
+}
+
+func (t *TrayMenu) AsJSON() (string, error) {
+ data, err := json.Marshal(t)
+ if err != nil {
+ return "", err
+ }
+ return string(data), nil
+}
+
+func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
+ // Parse ANSI text
+ var styledLabel []*ansi.StyledText
+ tempLabel := trayMenu.Label
+ if strings.Contains(tempLabel, "\033[") {
+ parsedLabel, err := ansi.Parse(tempLabel)
+ if err == nil {
+ styledLabel = parsedLabel
+ }
+ }
+
+ result := &TrayMenu{
+ Label: trayMenu.Label,
+ FontName: trayMenu.FontName,
+ FontSize: trayMenu.FontSize,
+ Disabled: trayMenu.Disabled,
+ Tooltip: trayMenu.Tooltip,
+ Image: trayMenu.Image,
+ MacTemplateImage: trayMenu.MacTemplateImage,
+ menu: trayMenu.Menu,
+ RGBA: trayMenu.RGBA,
+ menuItemMap: NewMenuItemMap(),
+ trayMenu: trayMenu,
+ StyledLabel: styledLabel,
+ }
+
+ result.menuItemMap.AddMenu(trayMenu.Menu)
+ result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
+
+ return result
+}
+
+func (m *Manager) OnTrayMenuOpen(id string) {
+ trayMenu, ok := m.trayMenus[id]
+ if !ok {
+ return
+ }
+ if trayMenu.trayMenu.OnOpen == nil {
+ return
+ }
+ go trayMenu.trayMenu.OnOpen()
+}
+
+func (m *Manager) OnTrayMenuClose(id string) {
+ trayMenu, ok := m.trayMenus[id]
+ if !ok {
+ return
+ }
+ if trayMenu.trayMenu.OnClose == nil {
+ return
+ }
+ go trayMenu.trayMenu.OnClose()
+}
+
+func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
+ newTrayMenu := NewTrayMenu(trayMenu)
+
+ // Hook up a new ID
+ trayID := generateTrayID()
+ newTrayMenu.ID = trayID
+
+ // Save the references
+ m.trayMenus[trayID] = newTrayMenu
+ m.trayMenuPointers[trayMenu] = trayID
+
+ return newTrayMenu.AsJSON()
+}
+
+func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) {
+ trayID, exists := m.trayMenuPointers[trayMenu]
+ if !exists {
+ return "", fmt.Errorf("Unable to find menu ID for tray menu!")
+ }
+ return trayID, nil
+}
+
+// SetTrayMenu updates or creates a menu
+func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
+ trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
+ if !trayMenuKnown {
+ return m.AddTrayMenu(trayMenu)
+ }
+
+ // Create the updated tray menu
+ updatedTrayMenu := NewTrayMenu(trayMenu)
+ updatedTrayMenu.ID = trayID
+
+ // Save the reference
+ m.trayMenus[trayID] = updatedTrayMenu
+
+ return updatedTrayMenu.AsJSON()
+}
+
+func (m *Manager) GetTrayMenus() ([]string, error) {
+ result := []string{}
+ for _, trayMenu := range m.trayMenus {
+ JSON, err := trayMenu.AsJSON()
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, JSON)
+ }
+
+ return result, nil
+}
+
+func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
+ trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
+ if !trayMenuKnown {
+ return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label)
+ }
+
+ type LabelUpdate struct {
+ ID string
+ Label string `json:",omitempty"`
+ FontName string `json:",omitempty"`
+ FontSize int
+ RGBA string `json:",omitempty"`
+ Disabled bool
+ Tooltip string `json:",omitempty"`
+ Image string `json:",omitempty"`
+ MacTemplateImage bool
+ StyledLabel []*ansi.StyledText `json:",omitempty"`
+ }
+
+ // Parse ANSI text
+ var styledLabel []*ansi.StyledText
+ tempLabel := trayMenu.Label
+ if strings.Contains(tempLabel, "\033[") {
+ parsedLabel, err := ansi.Parse(tempLabel)
+ if err == nil {
+ styledLabel = parsedLabel
+ }
+ }
+
+ update := &LabelUpdate{
+ ID: trayID,
+ Label: trayMenu.Label,
+ FontName: trayMenu.FontName,
+ FontSize: trayMenu.FontSize,
+ Disabled: trayMenu.Disabled,
+ Tooltip: trayMenu.Tooltip,
+ Image: trayMenu.Image,
+ MacTemplateImage: trayMenu.MacTemplateImage,
+ RGBA: trayMenu.RGBA,
+ StyledLabel: styledLabel,
+ }
+
+ data, err := json.Marshal(update)
+ if err != nil {
+ return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ")
+ }
+
+ return string(data), nil
+}
+
+func (m *Manager) GetContextMenus() ([]string, error) {
+ result := []string{}
+ for _, contextMenu := range m.contextMenus {
+ JSON, err := contextMenu.AsJSON()
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, JSON)
+ }
+
+ return result, nil
+}
diff --git a/v2/internal/platform/menu/manager.go b/v2/internal/platform/menu/manager.go
new file mode 100644
index 000000000..0ddbc9dde
--- /dev/null
+++ b/v2/internal/platform/menu/manager.go
@@ -0,0 +1,147 @@
+//go:build windows
+
+package menu
+
+import (
+ "github.com/wailsapp/wails/v2/pkg/menu"
+)
+
+// MenuManager manages the menus for the application
+var MenuManager = NewManager()
+
+type radioGroup []*menu.MenuItem
+
+// Click updates the radio group state based on the item clicked
+func (g *radioGroup) Click(item *menu.MenuItem) {
+ for _, radioGroupItem := range *g {
+ if radioGroupItem != item {
+ radioGroupItem.Checked = false
+ }
+ }
+}
+
+type processedMenu struct {
+
+ // the menu we processed
+ menu *menu.Menu
+
+ // updateMenuItemCallback is called when the menu item needs to be updated in the UI
+ updateMenuItemCallback func(*menu.MenuItem)
+
+ // items is a map of all menu items in this menu
+ items map[*menu.MenuItem]struct{}
+
+ // radioGroups tracks which radiogroup a menu item belongs to
+ radioGroups map[*menu.MenuItem][]*radioGroup
+}
+
+func newProcessedMenu(topLevelMenu *menu.Menu, updateMenuItemCallback func(*menu.MenuItem)) *processedMenu {
+ result := &processedMenu{
+ updateMenuItemCallback: updateMenuItemCallback,
+ menu: topLevelMenu,
+ items: make(map[*menu.MenuItem]struct{}),
+ radioGroups: make(map[*menu.MenuItem][]*radioGroup),
+ }
+ result.process(topLevelMenu.Items)
+ return result
+}
+
+func (p *processedMenu) process(items []*menu.MenuItem) {
+ var currentRadioGroup radioGroup
+ for index, item := range items {
+ // Save the reference to the top level menu for this item
+ p.items[item] = struct{}{}
+
+ // If this is a radio item, add it to the radio group
+ if item.Type == menu.RadioType {
+ currentRadioGroup = append(currentRadioGroup, item)
+ }
+
+ // If this is not a radio item, or we are processing the last item in the menu,
+ // then we need to add the current radio group to the map if it has items
+ if item.Type != menu.RadioType || index == len(items)-1 {
+ if len(currentRadioGroup) > 0 {
+ p.addRadioGroup(currentRadioGroup)
+ currentRadioGroup = nil
+ }
+ }
+
+ // Process the submenu
+ if item.SubMenu != nil {
+ p.process(item.SubMenu.Items)
+ }
+ }
+}
+
+func (p *processedMenu) processClick(item *menu.MenuItem) {
+ // If this item is not in our menu, then we can't process it
+ if _, ok := p.items[item]; !ok {
+ return
+ }
+
+ // If this is a radio item, then we need to update the radio group
+ if item.Type == menu.RadioType {
+ // Get the radio groups for this item
+ radioGroups := p.radioGroups[item]
+ // Iterate each radio group this item belongs to and set the checked state
+ // of all items apart from the one that was clicked to false
+ for _, thisRadioGroup := range radioGroups {
+ thisRadioGroup.Click(item)
+ for _, thisRadioGroupItem := range *thisRadioGroup {
+ p.updateMenuItemCallback(thisRadioGroupItem)
+ }
+ }
+ }
+
+ if item.Type == menu.CheckboxType {
+ p.updateMenuItemCallback(item)
+ }
+
+}
+
+func (p *processedMenu) addRadioGroup(r radioGroup) {
+ for _, item := range r {
+ p.radioGroups[item] = append(p.radioGroups[item], &r)
+ }
+}
+
+type Manager struct {
+ menus map[*menu.Menu]*processedMenu
+}
+
+func NewManager() *Manager {
+ return &Manager{
+ menus: make(map[*menu.Menu]*processedMenu),
+ }
+}
+
+func (m *Manager) AddMenu(menu *menu.Menu, updateMenuItemCallback func(*menu.MenuItem)) {
+ m.menus[menu] = newProcessedMenu(menu, updateMenuItemCallback)
+}
+
+func (m *Manager) ProcessClick(item *menu.MenuItem) {
+
+ // if menuitem is a checkbox, then we need to toggle the state
+ if item.Type == menu.CheckboxType {
+ item.Checked = !item.Checked
+ }
+
+ // Set the radio item to checked
+ if item.Type == menu.RadioType {
+ item.Checked = true
+ }
+
+ for _, thisMenu := range m.menus {
+ thisMenu.processClick(item)
+ }
+
+ if item.Click != nil {
+ item.Click(&menu.CallbackData{
+ MenuItem: item,
+ })
+ }
+}
+
+func (m *Manager) RemoveMenu(data *menu.Menu) {
+ delete(m.menus, data)
+}
diff --git a/v2/internal/platform/menu/manager_test.go b/v2/internal/platform/menu/manager_test.go
new file mode 100644
index 000000000..9e014b3ee
--- /dev/null
+++ b/v2/internal/platform/menu/manager_test.go
@@ -0,0 +1,297 @@
+//go:build windows
+
+package menu_test
+
+import (
+ "github.com/stretchr/testify/require"
+ platformMenu "github.com/wailsapp/wails/v2/internal/platform/menu"
+ "github.com/wailsapp/wails/v2/pkg/menu"
+ "testing"
+)
+
+func TestManager_ProcessClick_Checkbox(t *testing.T) {
+
+ checkbox := menu.Label("Checkbox").SetChecked(false)
+ menu1 := &menu.Menu{
+ Items: []*menu.MenuItem{
+ checkbox,
+ },
+ }
+ menu2 := &menu.Menu{
+ Items: []*menu.MenuItem{
+ checkbox,
+ },
+ }
+ menuWithNoCheckbox := &menu.Menu{
+ Items: []*menu.MenuItem{
+ menu.Label("No Checkbox"),
+ },
+ }
+ clicked := false
+
+ tests := []struct {
+ name string
+ inputs []*menu.Menu
+ startState bool
+ expectedState bool
+ expectedMenuUpdates map[*menu.Menu][]*menu.MenuItem
+ click func(*menu.CallbackData)
+ }{
+ {
+ name: "should callback menu checkbox state when clicked (false -> true)",
+ inputs: []*menu.Menu{menu1},
+ expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
+ menu1: {checkbox},
+ },
+ startState: false,
+ expectedState: true,
+ },
+ {
+ name: "should callback multiple menus when checkbox state when clicked (false -> true)",
+ inputs: []*menu.Menu{menu1, menu2},
+ startState: false,
+ expectedState: true,
+ expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
+ menu1: {checkbox},
+ menu2: {checkbox},
+ },
+ },
+ {
+ name: "should callback only for the menus that the checkbox is in (false -> true)",
+ inputs: []*menu.Menu{menu1, menuWithNoCheckbox},
+ startState: false,
+ expectedState: true,
+ expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
+ menu1: {checkbox},
+ },
+ },
+ {
+ name: "should callback menu checkbox state when clicked (true->false)",
+ inputs: []*menu.Menu{menu1},
+ expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
+ menu1: {checkbox},
+ },
+ startState: true,
+ expectedState: false,
+ },
+ {
+ name: "should callback multiple menus when checkbox state when clicked (true->false)",
+ inputs: []*menu.Menu{menu1, menu2},
+ startState: true,
+ expectedState: false,
+ expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
+ menu1: {checkbox},
+ menu2: {checkbox},
+ },
+ },
+ {
+ name: "should callback only for the menus that the checkbox is in (true->false)",
+ inputs: []*menu.Menu{menu1, menuWithNoCheckbox},
+ startState: true,
+ expectedState: false,
+ expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
+ menu1: {checkbox},
+ },
+ },
+ {
+ name: "should callback no menus if checkbox not in them",
+ inputs: []*menu.Menu{menuWithNoCheckbox},
+ startState: false,
+ expectedState: false,
+ expectedMenuUpdates: nil,
+ },
+ {
+ name: "should call Click on the checkbox",
+ inputs: []*menu.Menu{menu1, menu2},
+ startState: false,
+ expectedState: true,
+ expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
+ menu1: {checkbox},
+ menu2: {checkbox},
+ },
+ click: func(data *menu.CallbackData) {
+ clicked = true
+ },
+ },
+ }
+ for _, tt := range tests {
+
+ menusUpdated := map[*menu.Menu][]*menu.MenuItem{}
+ clicked = false
+
+ var checkMenuItemStateInMenu func(menu *menu.Menu)
+
+ checkMenuItemStateInMenu = func(menu *menu.Menu) {
+ for _, item := range menusUpdated[menu] {
+ if item == checkbox {
+ require.Equal(t, tt.expectedState, item.Checked)
+ }
+ if item.SubMenu != nil {
+ checkMenuItemStateInMenu(item.SubMenu)
+ }
+ }
+ }
+
+ t.Run(tt.name, func(t *testing.T) {
+ m := platformMenu.NewManager()
+ checkbox.SetChecked(tt.startState)
+ checkbox.Click = tt.click
+ for _, thisMenu := range tt.inputs {
+ thisMenu := thisMenu
+ m.AddMenu(thisMenu, func(menuItem *menu.MenuItem) {
+ menusUpdated[thisMenu] = append(menusUpdated[thisMenu], menuItem)
+ })
+ }
+ m.ProcessClick(checkbox)
+
+ // Check the item has the correct state in all the menus
+ for thisMenu := range menusUpdated {
+ require.EqualValues(t, tt.expectedMenuUpdates[thisMenu], menusUpdated[thisMenu])
+ }
+
+ if tt.click != nil {
+ require.Equal(t, true, clicked)
+ }
+ })
+ }
+}
+
+func TestManager_ProcessClick_RadioGroups(t *testing.T) {
+
+ radio1 := menu.Radio("Radio1", false, nil, nil)
+ radio2 := menu.Radio("Radio2", false, nil, nil)
+ radio3 := menu.Radio("Radio3", false, nil, nil)
+ radio4 := menu.Radio("Radio4", false, nil, nil)
+ radio5 := menu.Radio("Radio5", false, nil, nil)
+ radio6 := menu.Radio("Radio6", false, nil, nil)
+
+ radioGroupOne := &menu.Menu{
+ Items: []*menu.MenuItem{
+ radio1,
+ radio2,
+ radio3,
+ },
+ }
+
+ radioGroupTwo := &menu.Menu{
+ Items: []*menu.MenuItem{
+ radio4,
+ radio5,
+ radio6,
+ },
+ }
+
+ radioGroupThree := &menu.Menu{
+ Items: []*menu.MenuItem{
+ radio1,
+ radio2,
+ radio3,
+ },
+ }
+
+ clicked := false
+
+ tests := []struct {
+ name string
+ inputs []*menu.Menu
+ startState map[*menu.MenuItem]bool
+ selected *menu.MenuItem
+ expectedMenuUpdates map[*menu.Menu][]*menu.MenuItem
+ click func(*menu.CallbackData)
+ expectedState map[*menu.MenuItem]bool
+ }{
+ {
+ name: "should only set the clicked radio item",
+ inputs: []*menu.Menu{radioGroupOne},
+ expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
+ radioGroupOne: {radio1, radio2, radio3},
+ },
+ startState: map[*menu.MenuItem]bool{
+ radio1: true,
+ radio2: false,
+ radio3: false,
+ },
+ selected: radio2,
+ expectedState: map[*menu.MenuItem]bool{
+ radio1: false,
+ radio2: true,
+ radio3: false,
+ },
+ },
+ {
+ name: "should not affect other radio groups or menus",
+ inputs: []*menu.Menu{radioGroupOne, radioGroupTwo},
+ expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
+ radioGroupOne: {radio1, radio2, radio3},
+ },
+ startState: map[*menu.MenuItem]bool{
+ radio1: true,
+ radio2: false,
+ radio3: false,
+ radio4: true,
+ radio5: false,
+ radio6: false,
+ },
+ selected: radio2,
+ expectedState: map[*menu.MenuItem]bool{
+ radio1: false,
+ radio2: true,
+ radio3: false,
+ radio4: true,
+ radio5: false,
+ radio6: false,
+ },
+ },
+ {
+ name: "menus with the same radio group should be updated",
+ inputs: []*menu.Menu{radioGroupOne, radioGroupThree},
+ expectedMenuUpdates: map[*menu.Menu][]*menu.MenuItem{
+ radioGroupOne: {radio1, radio2, radio3},
+ radioGroupThree: {radio1, radio2, radio3},
+ },
+ startState: map[*menu.MenuItem]bool{
+ radio1: true,
+ radio2: false,
+ radio3: false,
+ },
+ selected: radio2,
+ expectedState: map[*menu.MenuItem]bool{
+ radio1: false,
+ radio2: true,
+ radio3: false,
+ },
+ },
+ }
+ for _, tt := range tests {
+
+ menusUpdated := map[*menu.Menu][]*menu.MenuItem{}
+ clicked = false
+
+ t.Run(tt.name, func(t *testing.T) {
+ m := platformMenu.NewManager()
+
+ for item, value := range tt.startState {
+ item.SetChecked(value)
+ }
+
+ tt.selected.Click = tt.click
+ for _, thisMenu := range tt.inputs {
+ thisMenu := thisMenu
+ m.AddMenu(thisMenu, func(menuItem *menu.MenuItem) {
+ menusUpdated[thisMenu] = append(menusUpdated[thisMenu], menuItem)
+ })
+ }
+ m.ProcessClick(tt.selected)
+ require.Equal(t, tt.expectedMenuUpdates, menusUpdated)
+
+ // Check the items have the correct state in all the menus
+ for item, expectedValue := range tt.expectedState {
+ require.Equal(t, expectedValue, item.Checked)
+ }
+
+ if tt.click != nil {
+ require.Equal(t, true, clicked)
+ }
+ })
+ }
+}
diff --git a/v2/internal/platform/menu/windows.go b/v2/internal/platform/menu/windows.go
new file mode 100644
index 000000000..68ebbcb49
--- /dev/null
+++ b/v2/internal/platform/menu/windows.go
@@ -0,0 +1,9 @@
+//go:build windows
+
+package menu
+
+import "github.com/wailsapp/wails/v2/internal/platform/win32"
+
+type Menu struct {
+ menu win32.HMENU
+}
diff --git a/v2/internal/platform/win32/consts.go b/v2/internal/platform/win32/consts.go
new file mode 100644
index 000000000..43149b036
--- /dev/null
+++ b/v2/internal/platform/win32/consts.go
@@ -0,0 +1,859 @@
+//go:build windows
+
+package win32
+
+import (
+ "fmt"
+ "syscall"
+ "unsafe"
+
+ "github.com/wailsapp/wails/v2/internal/system/operatingsystem"
+ "golang.org/x/sys/windows"
+)
+
+var (
+ modKernel32 = syscall.NewLazyDLL("kernel32.dll")
+ procGetModuleHandle = modKernel32.NewProc("GetModuleHandleW")
+
+ moduser32 = syscall.NewLazyDLL("user32.dll")
+ procRegisterClassEx = moduser32.NewProc("RegisterClassExW")
+ procLoadIcon = moduser32.NewProc("LoadIconW")
+ procLoadCursor = moduser32.NewProc("LoadCursorW")
+ procCreateWindowEx = moduser32.NewProc("CreateWindowExW")
+ procPostMessage = moduser32.NewProc("PostMessageW")
+ procGetCursorPos = moduser32.NewProc("GetCursorPos")
+ procSetForegroundWindow = moduser32.NewProc("SetForegroundWindow")
+ procCreatePopupMenu = moduser32.NewProc("CreatePopupMenu")
+ procTrackPopupMenu = moduser32.NewProc("TrackPopupMenu")
+ procDestroyMenu = moduser32.NewProc("DestroyMenu")
+ procAppendMenuW = moduser32.NewProc("AppendMenuW")
+ procCheckMenuItem = moduser32.NewProc("CheckMenuItem")
+ procCheckMenuRadioItem = moduser32.NewProc("CheckMenuRadioItem")
+ procCreateIconFromResourceEx = moduser32.NewProc("CreateIconFromResourceEx")
+ procGetMessageW = moduser32.NewProc("GetMessageW")
+ procIsDialogMessage = moduser32.NewProc("IsDialogMessageW")
+ procTranslateMessage = moduser32.NewProc("TranslateMessage")
+ procDispatchMessage = moduser32.NewProc("DispatchMessageW")
+ procPostQuitMessage = moduser32.NewProc("PostQuitMessage")
+ procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW")
+ procSetWindowCompositionAttribute = moduser32.NewProc("SetWindowCompositionAttribute")
+ procGetKeyState = moduser32.NewProc("GetKeyState")
+ procCreateAcceleratorTable = moduser32.NewProc("CreateAcceleratorTableW")
+ procTranslateAccelerator = moduser32.NewProc("TranslateAcceleratorW")
+
+ modshell32 = syscall.NewLazyDLL("shell32.dll")
+ procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW")
+
+ moddwmapi = syscall.NewLazyDLL("dwmapi.dll")
+ procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute")
+
+ moduxtheme = syscall.NewLazyDLL("uxtheme.dll")
+ procSetWindowTheme = moduxtheme.NewProc("SetWindowTheme")
+
+ AllowDarkModeForWindow func(HWND, bool) uintptr
+ SetPreferredAppMode func(int32) uintptr
+)
+
+type PreferredAppMode = int32
+
+const (
+ PreferredAppModeDefault PreferredAppMode = iota
+ PreferredAppModeAllowDark
+ PreferredAppModeForceDark
+ PreferredAppModeForceLight
+ PreferredAppModeMax
+)
+
+/*
+RtlGetNtVersionNumbers = void (LPDWORD major, LPDWORD minor, LPDWORD build) // 1809 17763
+ShouldAppsUseDarkMode = bool () // ordinal 132
+AllowDarkModeForWindow = bool (HWND hWnd, bool allow) // ordinal 133
+AllowDarkModeForApp = bool (bool allow) // ordinal 135, removed since 18334
+FlushMenuThemes = void () // ordinal 136
+RefreshImmersiveColorPolicyState = void () // ordinal 104
+IsDarkModeAllowedForWindow = bool (HWND hWnd) // ordinal 137
+GetIsImmersiveColorUsingHighContrast = bool (IMMERSIVE_HC_CACHE_MODE mode) // ordinal 106
+OpenNcThemeData = HTHEME (HWND hWnd, LPCWSTR pszClassList) // ordinal 49
+// Insider 18290
+ShouldSystemUseDarkMode = bool () // ordinal 138
+// Insider 18334
+SetPreferredAppMode = PreferredAppMode (PreferredAppMode appMode) // ordinal 135, since 18334
+IsDarkModeAllowedForApp = bool () // ordinal 139
+*/
+func Init() {
+ if IsWindowsVersionAtLeast(10, 0, 18334) {
+
+ // AllowDarkModeForWindow is only available on Windows 10+
+ uxtheme, err := windows.LoadLibrary("uxtheme.dll")
+ if err == nil {
+ procAllowDarkModeForWindow, err := windows.GetProcAddressByOrdinal(uxtheme, uintptr(133))
+ if err == nil {
+ AllowDarkModeForWindow = func(hwnd HWND, allow bool) uintptr {
+ var allowInt int32
+ if allow {
+ allowInt = 1
+ }
+ ret, _, _ := syscall.SyscallN(procAllowDarkModeForWindow, uintptr(hwnd), uintptr(allowInt))
+ return ret
+ }
+ }
+ }
+
+ // SetPreferredAppMode is only available on Windows 10+
+ procSetPreferredAppMode, err := windows.GetProcAddressByOrdinal(uxtheme, uintptr(135))
+ if err == nil {
+ SetPreferredAppMode = func(mode int32) uintptr {
+ ret, _, _ := syscall.SyscallN(procSetPreferredAppMode, uintptr(mode))
+ return ret
+ }
+ SetPreferredAppMode(PreferredAppModeAllowDark)
+ }
+ }
+
+}
+
+type HANDLE uintptr
+type HINSTANCE = HANDLE
+type HICON = HANDLE
+type HCURSOR = HANDLE
+type HBRUSH = HANDLE
+type HWND = HANDLE
+type HMENU = HANDLE
+type DWORD = uint32
+type ATOM uint16
+type MenuID uint16
+
+const (
+ WM_APP = 32768
+ WM_ACTIVATE = 6
+ WM_ACTIVATEAPP = 28
+ WM_AFXFIRST = 864
+ WM_AFXLAST = 895
+ WM_ASKCBFORMATNAME = 780
+ WM_CANCELJOURNAL = 75
+ WM_CANCELMODE = 31
+ WM_CAPTURECHANGED = 533
+ WM_CHANGECBCHAIN = 781
+ WM_CHAR = 258
+ WM_CHARTOITEM = 47
+ WM_CHILDACTIVATE = 34
+ WM_CLEAR = 771
+ WM_CLOSE = 16
+ WM_COMMAND = 273
+ WM_COMMNOTIFY = 68 /* OBSOLETE */
+ WM_COMPACTING = 65
+ WM_COMPAREITEM = 57
+ WM_CONTEXTMENU = 123
+ WM_COPY = 769
+ WM_COPYDATA = 74
+ WM_CREATE = 1
+ WM_CTLCOLORBTN = 309
+ WM_CTLCOLORDLG = 310
+ WM_CTLCOLOREDIT = 307
+ WM_CTLCOLORLISTBOX = 308
+ WM_CTLCOLORMSGBOX = 306
+ WM_CTLCOLORSCROLLBAR = 311
+ WM_CTLCOLORSTATIC = 312
+ WM_CUT = 768
+ WM_DEADCHAR = 259
+ WM_DELETEITEM = 45
+ WM_DESTROY = 2
+ WM_DESTROYCLIPBOARD = 775
+ WM_DEVICECHANGE = 537
+ WM_DEVMODECHANGE = 27
+ WM_DISPLAYCHANGE = 126
+ WM_DRAWCLIPBOARD = 776
+ WM_DRAWITEM = 43
+ WM_DROPFILES = 563
+ WM_ENABLE = 10
+ WM_ENDSESSION = 22
+ WM_ENTERIDLE = 289
+ WM_ENTERMENULOOP = 529
+ WM_ENTERSIZEMOVE = 561
+ WM_ERASEBKGND = 20
+ WM_EXITMENULOOP = 530
+ WM_EXITSIZEMOVE = 562
+ WM_FONTCHANGE = 29
+ WM_GETDLGCODE = 135
+ WM_GETFONT = 49
+ WM_GETHOTKEY = 51
+ WM_GETICON = 127
+ WM_GETMINMAXINFO = 36
+ WM_GETTEXT = 13
+ WM_GETTEXTLENGTH = 14
+ WM_HANDHELDFIRST = 856
+ WM_HANDHELDLAST = 863
+ WM_HELP = 83
+ WM_HOTKEY = 786
+ WM_HSCROLL = 276
+ WM_HSCROLLCLIPBOARD = 782
+ WM_ICONERASEBKGND = 39
+ WM_INITDIALOG = 272
+ WM_INITMENU = 278
+ WM_INITMENUPOPUP = 279
+ WM_INPUT = 0x00FF
+ WM_INPUTLANGCHANGE = 81
+ WM_INPUTLANGCHANGEREQUEST = 80
+ WM_KEYDOWN = 256
+ WM_KEYUP = 257
+ WM_KILLFOCUS = 8
+ WM_MDIACTIVATE = 546
+ WM_MDICASCADE = 551
+ WM_MDICREATE = 544
+ WM_MDIDESTROY = 545
+ WM_MDIGETACTIVE = 553
+ WM_MDIICONARRANGE = 552
+ WM_MDIMAXIMIZE = 549
+ WM_MDINEXT = 548
+ WM_MDIREFRESHMENU = 564
+ WM_MDIRESTORE = 547
+ WM_MDISETMENU = 560
+ WM_MDITILE = 550
+ WM_MEASUREITEM = 44
+ WM_GETOBJECT = 0x003D
+ WM_CHANGEUISTATE = 0x0127
+ WM_UPDATEUISTATE = 0x0128
+ WM_QUERYUISTATE = 0x0129
+ WM_UNINITMENUPOPUP = 0x0125
+ WM_MENURBUTTONUP = 290
+ WM_MENUCOMMAND = 0x0126
+ WM_MENUGETOBJECT = 0x0124
+ WM_MENUDRAG = 0x0123
+ WM_APPCOMMAND = 0x0319
+ WM_MENUCHAR = 288
+ WM_MENUSELECT = 287
+ WM_MOVE = 3
+ WM_MOVING = 534
+ WM_NCACTIVATE = 134
+ WM_NCCALCSIZE = 131
+ WM_NCCREATE = 129
+ WM_NCDESTROY = 130
+ WM_NCHITTEST = 132
+ WM_NCLBUTTONDBLCLK = 163
+ WM_NCLBUTTONDOWN = 161
+ WM_NCLBUTTONUP = 162
+ WM_NCMBUTTONDBLCLK = 169
+ WM_NCMBUTTONDOWN = 167
+ WM_NCMBUTTONUP = 168
+ WM_NCXBUTTONDOWN = 171
+ WM_NCXBUTTONUP = 172
+ WM_NCXBUTTONDBLCLK = 173
+ WM_NCMOUSEHOVER = 0x02A0
+ WM_NCMOUSELEAVE = 0x02A2
+ WM_NCMOUSEMOVE = 160
+ WM_NCPAINT = 133
+ WM_NCRBUTTONDBLCLK = 166
+ WM_NCRBUTTONDOWN = 164
+ WM_NCRBUTTONUP = 165
+ WM_NEXTDLGCTL = 40
+ WM_NEXTMENU = 531
+ WM_NOTIFY = 78
+ WM_NOTIFYFORMAT = 85
+ WM_NULL = 0
+ WM_PAINT = 15
+ WM_PAINTCLIPBOARD = 777
+ WM_PAINTICON = 38
+ WM_PALETTECHANGED = 785
+ WM_PALETTEISCHANGING = 784
+ WM_PARENTNOTIFY = 528
+ WM_PASTE = 770
+ WM_PENWINFIRST = 896
+ WM_PENWINLAST = 911
+ WM_POWER = 72
+ WM_PRINT = 791
+ WM_PRINTCLIENT = 792
+ WM_QUERYDRAGICON = 55
+ WM_QUERYENDSESSION = 17
+ WM_QUERYNEWPALETTE = 783
+ WM_QUERYOPEN = 19
+ WM_QUEUESYNC = 35
+ WM_QUIT = 18
+ WM_RENDERALLFORMATS = 774
+ WM_RENDERFORMAT = 773
+ WM_SETCURSOR = 32
+ WM_SETFOCUS = 7
+ WM_SETFONT = 48
+ WM_SETHOTKEY = 50
+ WM_SETICON = 128
+ WM_SETREDRAW = 11
+ WM_SETTEXT = 12
+ WM_SETTINGCHANGE = 26
+ WM_SHOWWINDOW = 24
+ WM_SIZE = 5
+ WM_SIZECLIPBOARD = 779
+ WM_SIZING = 532
+ WM_SPOOLERSTATUS = 42
+ WM_STYLECHANGED = 125
+ WM_STYLECHANGING = 124
+ WM_SYSCHAR = 262
+ WM_SYSCOLORCHANGE = 21
+ WM_SYSCOMMAND = 274
+ WM_SYSDEADCHAR = 263
+ WM_SYSKEYDOWN = 260
+ WM_SYSKEYUP = 261
+ WM_TCARD = 82
+ WM_THEMECHANGED = 794
+ WM_TIMECHANGE = 30
+ WM_TIMER = 275
+ WM_UNDO = 772
+ WM_USER = 1024
+ WM_USERCHANGED = 84
+ WM_VKEYTOITEM = 46
+ WM_VSCROLL = 277
+ WM_VSCROLLCLIPBOARD = 778
+ WM_WINDOWPOSCHANGED = 71
+ WM_WINDOWPOSCHANGING = 70
+ WM_WININICHANGE = 26
+ WM_KEYFIRST = 256
+ WM_KEYLAST = 264
+ WM_SYNCPAINT = 136
+ WM_MOUSEACTIVATE = 33
+ WM_MOUSEMOVE = 512
+ WM_LBUTTONDOWN = 513
+ WM_LBUTTONUP = 514
+ WM_LBUTTONDBLCLK = 515
+ WM_RBUTTONDOWN = 516
+ WM_RBUTTONUP = 517
+ WM_RBUTTONDBLCLK = 518
+ WM_MBUTTONDOWN = 519
+ WM_MBUTTONUP = 520
+ WM_MBUTTONDBLCLK = 521
+ WM_MOUSEWHEEL = 522
+ WM_MOUSEFIRST = 512
+ WM_XBUTTONDOWN = 523
+ WM_XBUTTONUP = 524
+ WM_XBUTTONDBLCLK = 525
+ WM_MOUSELAST = 525
+ WM_MOUSEHOVER = 0x2A1
+ WM_MOUSELEAVE = 0x2A3
+ WM_CLIPBOARDUPDATE = 0x031D
+
+ WS_EX_APPWINDOW = 0x00040000
+ WS_OVERLAPPEDWINDOW = 0x00000000 | 0x00C00000 | 0x00080000 | 0x00040000 | 0x00020000 | 0x00010000
+ WS_EX_NOREDIRECTIONBITMAP = 0x00200000
+ CW_USEDEFAULT = ^0x7fffffff
+
+ NIM_ADD = 0x00000000
+ NIM_MODIFY = 0x00000001
+ NIM_DELETE = 0x00000002
+ NIM_SETVERSION = 0x00000004
+
+ NIF_MESSAGE = 0x00000001
+ NIF_ICON = 0x00000002
+ NIF_TIP = 0x00000004
+ NIF_STATE = 0x00000008
+ NIF_INFO = 0x00000010
+
+ NIS_HIDDEN = 0x00000001
+
+ NIIF_NONE = 0x00000000
+ NIIF_INFO = 0x00000001
+ NIIF_WARNING = 0x00000002
+ NIIF_ERROR = 0x00000003
+ NIIF_USER = 0x00000004
+ NIIF_NOSOUND = 0x00000010
+ NIIF_LARGE_ICON = 0x00000020
+ NIIF_RESPECT_QUIET_TIME = 0x00000080
+ NIIF_ICON_MASK = 0x0000000F
+
+ IMAGE_BITMAP = 0
+ IMAGE_ICON = 1
+ LR_LOADFROMFILE = 0x00000010
+ LR_DEFAULTSIZE = 0x00000040
+
+ IDC_ARROW = 32512
+ COLOR_WINDOW = 5
+ COLOR_BTNFACE = 15
+
+ GWLP_USERDATA = -21
+ WS_CLIPSIBLINGS = 0x04000000
+ WS_EX_CONTROLPARENT = 0x00010000
+
+ HWND_MESSAGE = ^HWND(2)
+ NOTIFYICON_VERSION = 4
+
+ IDI_APPLICATION = 32512
+
+ MenuItemMsgID = WM_APP + 1024
+ NotifyIconMessageId = WM_APP + iota
+
+ MF_STRING = 0x00000000
+ MF_ENABLED = 0x00000000
+ MF_GRAYED = 0x00000001
+ MF_DISABLED = 0x00000002
+ MF_SEPARATOR = 0x00000800
+ MF_UNCHECKED = 0x00000000
+ MF_CHECKED = 0x00000008
+ MF_POPUP = 0x00000010
+ MF_MENUBARBREAK = 0x00000020
+ MF_BYCOMMAND = 0x00000000
+
+ TPM_LEFTALIGN = 0x0000
+
+ CS_VREDRAW = 0x0001
+ CS_HREDRAW = 0x0002
+)
+
+func WMMessageToString(msg uintptr) string {
+ // Convert windows message to string
+ switch msg {
+ case WM_APP:
+ return "WM_APP"
+ case WM_ACTIVATE:
+ return "WM_ACTIVATE"
+ case WM_ACTIVATEAPP:
+ return "WM_ACTIVATEAPP"
+ case WM_AFXFIRST:
+ return "WM_AFXFIRST"
+ case WM_AFXLAST:
+ return "WM_AFXLAST"
+ case WM_ASKCBFORMATNAME:
+ return "WM_ASKCBFORMATNAME"
+ case WM_CANCELJOURNAL:
+ return "WM_CANCELJOURNAL"
+ case WM_CANCELMODE:
+ return "WM_CANCELMODE"
+ case WM_CAPTURECHANGED:
+ return "WM_CAPTURECHANGED"
+ case WM_CHANGECBCHAIN:
+ return "WM_CHANGECBCHAIN"
+ case WM_CHAR:
+ return "WM_CHAR"
+ case WM_CHARTOITEM:
+ return "WM_CHARTOITEM"
+ case WM_CHILDACTIVATE:
+ return "WM_CHILDACTIVATE"
+ case WM_CLEAR:
+ return "WM_CLEAR"
+ case WM_CLOSE:
+ return "WM_CLOSE"
+ case WM_COMMAND:
+ return "WM_COMMAND"
+ case WM_COMMNOTIFY /* OBSOLETE */ :
+ return "WM_COMMNOTIFY"
+ case WM_COMPACTING:
+ return "WM_COMPACTING"
+ case WM_COMPAREITEM:
+ return "WM_COMPAREITEM"
+ case WM_CONTEXTMENU:
+ return "WM_CONTEXTMENU"
+ case WM_COPY:
+ return "WM_COPY"
+ case WM_COPYDATA:
+ return "WM_COPYDATA"
+ case WM_CREATE:
+ return "WM_CREATE"
+ case WM_CTLCOLORBTN:
+ return "WM_CTLCOLORBTN"
+ case WM_CTLCOLORDLG:
+ return "WM_CTLCOLORDLG"
+ case WM_CTLCOLOREDIT:
+ return "WM_CTLCOLOREDIT"
+ case WM_CTLCOLORLISTBOX:
+ return "WM_CTLCOLORLISTBOX"
+ case WM_CTLCOLORMSGBOX:
+ return "WM_CTLCOLORMSGBOX"
+ case WM_CTLCOLORSCROLLBAR:
+ return "WM_CTLCOLORSCROLLBAR"
+ case WM_CTLCOLORSTATIC:
+ return "WM_CTLCOLORSTATIC"
+ case WM_CUT:
+ return "WM_CUT"
+ case WM_DEADCHAR:
+ return "WM_DEADCHAR"
+ case WM_DELETEITEM:
+ return "WM_DELETEITEM"
+ case WM_DESTROY:
+ return "WM_DESTROY"
+ case WM_DESTROYCLIPBOARD:
+ return "WM_DESTROYCLIPBOARD"
+ case WM_DEVICECHANGE:
+ return "WM_DEVICECHANGE"
+ case WM_DEVMODECHANGE:
+ return "WM_DEVMODECHANGE"
+ case WM_DISPLAYCHANGE:
+ return "WM_DISPLAYCHANGE"
+ case WM_DRAWCLIPBOARD:
+ return "WM_DRAWCLIPBOARD"
+ case WM_DRAWITEM:
+ return "WM_DRAWITEM"
+ case WM_DROPFILES:
+ return "WM_DROPFILES"
+ case WM_ENABLE:
+ return "WM_ENABLE"
+ case WM_ENDSESSION:
+ return "WM_ENDSESSION"
+ case WM_ENTERIDLE:
+ return "WM_ENTERIDLE"
+ case WM_ENTERMENULOOP:
+ return "WM_ENTERMENULOOP"
+ case WM_ENTERSIZEMOVE:
+ return "WM_ENTERSIZEMOVE"
+ case WM_ERASEBKGND:
+ return "WM_ERASEBKGND"
+ case WM_EXITMENULOOP:
+ return "WM_EXITMENULOOP"
+ case WM_EXITSIZEMOVE:
+ return "WM_EXITSIZEMOVE"
+ case WM_FONTCHANGE:
+ return "WM_FONTCHANGE"
+ case WM_GETDLGCODE:
+ return "WM_GETDLGCODE"
+ case WM_GETFONT:
+ return "WM_GETFONT"
+ case WM_GETHOTKEY:
+ return "WM_GETHOTKEY"
+ case WM_GETICON:
+ return "WM_GETICON"
+ case WM_GETMINMAXINFO:
+ return "WM_GETMINMAXINFO"
+ case WM_GETTEXT:
+ return "WM_GETTEXT"
+ case WM_GETTEXTLENGTH:
+ return "WM_GETTEXTLENGTH"
+ case WM_HANDHELDFIRST:
+ return "WM_HANDHELDFIRST"
+ case WM_HANDHELDLAST:
+ return "WM_HANDHELDLAST"
+ case WM_HELP:
+ return "WM_HELP"
+ case WM_HOTKEY:
+ return "WM_HOTKEY"
+ case WM_HSCROLL:
+ return "WM_HSCROLL"
+ case WM_HSCROLLCLIPBOARD:
+ return "WM_HSCROLLCLIPBOARD"
+ case WM_ICONERASEBKGND:
+ return "WM_ICONERASEBKGND"
+ case WM_INITDIALOG:
+ return "WM_INITDIALOG"
+ case WM_INITMENU:
+ return "WM_INITMENU"
+ case WM_INITMENUPOPUP:
+ return "WM_INITMENUPOPUP"
+ case WM_INPUT:
+ return "WM_INPUT"
+ case WM_INPUTLANGCHANGE:
+ return "WM_INPUTLANGCHANGE"
+ case WM_INPUTLANGCHANGEREQUEST:
+ return "WM_INPUTLANGCHANGEREQUEST"
+ case WM_KEYDOWN:
+ return "WM_KEYDOWN"
+ case WM_KEYUP:
+ return "WM_KEYUP"
+ case WM_KILLFOCUS:
+ return "WM_KILLFOCUS"
+ case WM_MDIACTIVATE:
+ return "WM_MDIACTIVATE"
+ case WM_MDICASCADE:
+ return "WM_MDICASCADE"
+ case WM_MDICREATE:
+ return "WM_MDICREATE"
+ case WM_MDIDESTROY:
+ return "WM_MDIDESTROY"
+ case WM_MDIGETACTIVE:
+ return "WM_MDIGETACTIVE"
+ case WM_MDIICONARRANGE:
+ return "WM_MDIICONARRANGE"
+ case WM_MDIMAXIMIZE:
+ return "WM_MDIMAXIMIZE"
+ case WM_MDINEXT:
+ return "WM_MDINEXT"
+ case WM_MDIREFRESHMENU:
+ return "WM_MDIREFRESHMENU"
+ case WM_MDIRESTORE:
+ return "WM_MDIRESTORE"
+ case WM_MDISETMENU:
+ return "WM_MDISETMENU"
+ case WM_MDITILE:
+ return "WM_MDITILE"
+ case WM_MEASUREITEM:
+ return "WM_MEASUREITEM"
+ case WM_GETOBJECT:
+ return "WM_GETOBJECT"
+ case WM_CHANGEUISTATE:
+ return "WM_CHANGEUISTATE"
+ case WM_UPDATEUISTATE:
+ return "WM_UPDATEUISTATE"
+ case WM_QUERYUISTATE:
+ return "WM_QUERYUISTATE"
+ case WM_UNINITMENUPOPUP:
+ return "WM_UNINITMENUPOPUP"
+ case WM_MENURBUTTONUP:
+ return "WM_MENURBUTTONUP"
+ case WM_MENUCOMMAND:
+ return "WM_MENUCOMMAND"
+ case WM_MENUGETOBJECT:
+ return "WM_MENUGETOBJECT"
+ case WM_MENUDRAG:
+ return "WM_MENUDRAG"
+ case WM_APPCOMMAND:
+ return "WM_APPCOMMAND"
+ case WM_MENUCHAR:
+ return "WM_MENUCHAR"
+ case WM_MENUSELECT:
+ return "WM_MENUSELECT"
+ case WM_MOVE:
+ return "WM_MOVE"
+ case WM_MOVING:
+ return "WM_MOVING"
+ case WM_NCACTIVATE:
+ return "WM_NCACTIVATE"
+ case WM_NCCALCSIZE:
+ return "WM_NCCALCSIZE"
+ case WM_NCCREATE:
+ return "WM_NCCREATE"
+ case WM_NCDESTROY:
+ return "WM_NCDESTROY"
+ case WM_NCHITTEST:
+ return "WM_NCHITTEST"
+ case WM_NCLBUTTONDBLCLK:
+ return "WM_NCLBUTTONDBLCLK"
+ case WM_NCLBUTTONDOWN:
+ return "WM_NCLBUTTONDOWN"
+ case WM_NCLBUTTONUP:
+ return "WM_NCLBUTTONUP"
+ case WM_NCMBUTTONDBLCLK:
+ return "WM_NCMBUTTONDBLCLK"
+ case WM_NCMBUTTONDOWN:
+ return "WM_NCMBUTTONDOWN"
+ case WM_NCMBUTTONUP:
+ return "WM_NCMBUTTONUP"
+ case WM_NCXBUTTONDOWN:
+ return "WM_NCXBUTTONDOWN"
+ case WM_NCXBUTTONUP:
+ return "WM_NCXBUTTONUP"
+ case WM_NCXBUTTONDBLCLK:
+ return "WM_NCXBUTTONDBLCLK"
+ case WM_NCMOUSEHOVER:
+ return "WM_NCMOUSEHOVER"
+ case WM_NCMOUSELEAVE:
+ return "WM_NCMOUSELEAVE"
+ case WM_NCMOUSEMOVE:
+ return "WM_NCMOUSEMOVE"
+ case WM_NCPAINT:
+ return "WM_NCPAINT"
+ case WM_NCRBUTTONDBLCLK:
+ return "WM_NCRBUTTONDBLCLK"
+ case WM_NCRBUTTONDOWN:
+ return "WM_NCRBUTTONDOWN"
+ case WM_NCRBUTTONUP:
+ return "WM_NCRBUTTONUP"
+ case WM_NEXTDLGCTL:
+ return "WM_NEXTDLGCTL"
+ case WM_NEXTMENU:
+ return "WM_NEXTMENU"
+ case WM_NOTIFY:
+ return "WM_NOTIFY"
+ case WM_NOTIFYFORMAT:
+ return "WM_NOTIFYFORMAT"
+ case WM_NULL:
+ return "WM_NULL"
+ case WM_PAINT:
+ return "WM_PAINT"
+ case WM_PAINTCLIPBOARD:
+ return "WM_PAINTCLIPBOARD"
+ case WM_PAINTICON:
+ return "WM_PAINTICON"
+ case WM_PALETTECHANGED:
+ return "WM_PALETTECHANGED"
+ case WM_PALETTEISCHANGING:
+ return "WM_PALETTEISCHANGING"
+ case WM_PARENTNOTIFY:
+ return "WM_PARENTNOTIFY"
+ case WM_PASTE:
+ return "WM_PASTE"
+ case WM_PENWINFIRST:
+ return "WM_PENWINFIRST"
+ case WM_PENWINLAST:
+ return "WM_PENWINLAST"
+ case WM_POWER:
+ return "WM_POWER"
+ case WM_PRINT:
+ return "WM_PRINT"
+ case WM_PRINTCLIENT:
+ return "WM_PRINTCLIENT"
+ case WM_QUERYDRAGICON:
+ return "WM_QUERYDRAGICON"
+ case WM_QUERYENDSESSION:
+ return "WM_QUERYENDSESSION"
+ case WM_QUERYNEWPALETTE:
+ return "WM_QUERYNEWPALETTE"
+ case WM_QUERYOPEN:
+ return "WM_QUERYOPEN"
+ case WM_QUEUESYNC:
+ return "WM_QUEUESYNC"
+ case WM_QUIT:
+ return "WM_QUIT"
+ case WM_RENDERALLFORMATS:
+ return "WM_RENDERALLFORMATS"
+ case WM_RENDERFORMAT:
+ return "WM_RENDERFORMAT"
+ case WM_SETCURSOR:
+ return "WM_SETCURSOR"
+ case WM_SETFOCUS:
+ return "WM_SETFOCUS"
+ case WM_SETFONT:
+ return "WM_SETFONT"
+ case WM_SETHOTKEY:
+ return "WM_SETHOTKEY"
+ case WM_SETICON:
+ return "WM_SETICON"
+ case WM_SETREDRAW:
+ return "WM_SETREDRAW"
+ case WM_SETTEXT:
+ return "WM_SETTEXT"
+ case WM_SETTINGCHANGE:
+ return "WM_SETTINGCHANGE"
+ case WM_SHOWWINDOW:
+ return "WM_SHOWWINDOW"
+ case WM_SIZE:
+ return "WM_SIZE"
+ case WM_SIZECLIPBOARD:
+ return "WM_SIZECLIPBOARD"
+ case WM_SIZING:
+ return "WM_SIZING"
+ case WM_SPOOLERSTATUS:
+ return "WM_SPOOLERSTATUS"
+ case WM_STYLECHANGED:
+ return "WM_STYLECHANGED"
+ case WM_STYLECHANGING:
+ return "WM_STYLECHANGING"
+ case WM_SYSCHAR:
+ return "WM_SYSCHAR"
+ case WM_SYSCOLORCHANGE:
+ return "WM_SYSCOLORCHANGE"
+ case WM_SYSCOMMAND:
+ return "WM_SYSCOMMAND"
+ case WM_SYSDEADCHAR:
+ return "WM_SYSDEADCHAR"
+ case WM_SYSKEYDOWN:
+ return "WM_SYSKEYDOWN"
+ case WM_SYSKEYUP:
+ return "WM_SYSKEYUP"
+ case WM_TCARD:
+ return "WM_TCARD"
+ case WM_THEMECHANGED:
+ return "WM_THEMECHANGED"
+ case WM_TIMECHANGE:
+ return "WM_TIMECHANGE"
+ case WM_TIMER:
+ return "WM_TIMER"
+ case WM_UNDO:
+ return "WM_UNDO"
+ case WM_USER:
+ return "WM_USER"
+ case WM_USERCHANGED:
+ return "WM_USERCHANGED"
+ case WM_VKEYTOITEM:
+ return "WM_VKEYTOITEM"
+ case WM_VSCROLL:
+ return "WM_VSCROLL"
+ case WM_VSCROLLCLIPBOARD:
+ return "WM_VSCROLLCLIPBOARD"
+ case WM_WINDOWPOSCHANGED:
+ return "WM_WINDOWPOSCHANGED"
+ case WM_WINDOWPOSCHANGING:
+ return "WM_WINDOWPOSCHANGING"
+ case WM_KEYLAST:
+ return "WM_KEYLAST"
+ case WM_SYNCPAINT:
+ return "WM_SYNCPAINT"
+ case WM_MOUSEACTIVATE:
+ return "WM_MOUSEACTIVATE"
+ case WM_MOUSEMOVE:
+ return "WM_MOUSEMOVE"
+ case WM_LBUTTONDOWN:
+ return "WM_LBUTTONDOWN"
+ case WM_LBUTTONUP:
+ return "WM_LBUTTONUP"
+ case WM_LBUTTONDBLCLK:
+ return "WM_LBUTTONDBLCLK"
+ case WM_RBUTTONDOWN:
+ return "WM_RBUTTONDOWN"
+ case WM_RBUTTONUP:
+ return "WM_RBUTTONUP"
+ case WM_RBUTTONDBLCLK:
+ return "WM_RBUTTONDBLCLK"
+ case WM_MBUTTONDOWN:
+ return "WM_MBUTTONDOWN"
+ case WM_MBUTTONUP:
+ return "WM_MBUTTONUP"
+ case WM_MBUTTONDBLCLK:
+ return "WM_MBUTTONDBLCLK"
+ case WM_MOUSEWHEEL:
+ return "WM_MOUSEWHEEL"
+ case WM_XBUTTONDOWN:
+ return "WM_XBUTTONDOWN"
+ case WM_XBUTTONUP:
+ return "WM_XBUTTONUP"
+ case WM_MOUSELAST:
+ return "WM_MOUSELAST"
+ case WM_MOUSEHOVER:
+ return "WM_MOUSEHOVER"
+ case WM_MOUSELEAVE:
+ return "WM_MOUSELEAVE"
+ case WM_CLIPBOARDUPDATE:
+ return "WM_CLIPBOARDUPDATE"
+ default:
+ return fmt.Sprintf("0x%08x", msg)
+ }
+}
+
+var windowsVersion, _ = operatingsystem.GetWindowsVersionInfo()
+
+func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool {
+ return windowsVersion.Major >= major &&
+ windowsVersion.Minor >= minor &&
+ windowsVersion.Build >= buildNumber
+}
+
+type WindowProc func(hwnd HWND, msg uint32, wparam, lparam uintptr) uintptr
+
+func GetModuleHandle(value uintptr) uintptr {
+ result, _, _ := procGetModuleHandle.Call(value)
+ return result
+}
+
+func GetMessage(msg *MSG) uintptr {
+ rt, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
+ return rt
+}
+
+func PostMessage(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
+ ret, _, _ := procPostMessage.Call(
+ uintptr(hwnd),
+ uintptr(msg),
+ wParam,
+ lParam)
+
+ return ret
+}
+
+func ShellNotifyIcon(cmd uintptr, nid *NOTIFYICONDATA) bool {
+ ret, _, _ := procShellNotifyIcon.Call(cmd, uintptr(unsafe.Pointer(nid)))
+ return ret == 1
+}
+
+func IsDialogMessage(hwnd HWND, msg *MSG) uintptr {
+ ret, _, _ := procIsDialogMessage.Call(uintptr(hwnd), uintptr(unsafe.Pointer(msg)))
+ return ret
+}
+
+func TranslateMessage(msg *MSG) uintptr {
+ ret, _, _ := procTranslateMessage.Call(uintptr(unsafe.Pointer(msg)))
+ return ret
+}
+
+func DispatchMessage(msg *MSG) uintptr {
+ ret, _, _ := procDispatchMessage.Call(uintptr(unsafe.Pointer(msg)))
+ return ret
+}
+
+func PostQuitMessage(exitCode int32) {
+ procPostQuitMessage.Call(uintptr(exitCode))
+}
+
+func LoHiWords(input uint32) (uint16, uint16) {
+ return uint16(input & 0xffff), uint16(input >> 16 & 0xffff)
+}
diff --git a/v2/internal/platform/win32/cursor.go b/v2/internal/platform/win32/cursor.go
new file mode 100644
index 000000000..04449a91b
--- /dev/null
+++ b/v2/internal/platform/win32/cursor.go
@@ -0,0 +1,11 @@
+//go:build windows
+
+package win32
+
+import "unsafe"
+
+func GetCursorPos() (x, y int, ok bool) {
+ pt := POINT{}
+ ret, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
+ return int(pt.X), int(pt.Y), ret != 0
+}
diff --git a/v2/internal/platform/win32/icon.go b/v2/internal/platform/win32/icon.go
new file mode 100644
index 000000000..916b92d44
--- /dev/null
+++ b/v2/internal/platform/win32/icon.go
@@ -0,0 +1,41 @@
+//go:build windows
+
+package win32
+
+import (
+ "unsafe"
+)
+
+func CreateIconFromResourceEx(presbits uintptr, dwResSize uint32, isIcon bool, version uint32, cxDesired int, cyDesired int, flags uint) (uintptr, error) {
+ icon := 0
+ if isIcon {
+ icon = 1
+ }
+ r, _, err := procCreateIconFromResourceEx.Call(
+ presbits,
+ uintptr(dwResSize),
+ uintptr(icon),
+ uintptr(version),
+ uintptr(cxDesired),
+ uintptr(cyDesired),
+ uintptr(flags),
+ )
+
+ if r == 0 {
+ return 0, err
+ }
+ return r, nil
+}
+
+// CreateHIconFromPNG creates a HICON from a PNG file
+func CreateHIconFromPNG(pngData []byte) (HICON, error) {
+ icon, err := CreateIconFromResourceEx(
+ uintptr(unsafe.Pointer(&pngData[0])),
+ uint32(len(pngData)),
+ true,
+ 0x00030000,
+ 0,
+ 0,
+ LR_DEFAULTSIZE)
+ return HICON(icon), err
+}
diff --git a/v2/internal/platform/win32/keyboard.go b/v2/internal/platform/win32/keyboard.go
new file mode 100644
index 000000000..7a86d6643
--- /dev/null
+++ b/v2/internal/platform/win32/keyboard.go
@@ -0,0 +1,810 @@
+//go:build windows
+
+/*
+ * Copyright (C) 2019 The Winc Authors. All Rights Reserved.
+ * Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
+ */
+
+package win32
+
+import (
+ "bytes"
+ "github.com/wailsapp/wails/v2/pkg/menu/keys"
+ "strings"
+ "unsafe"
+)
+
+type Key uint16
+
+func (k Key) String() string {
+ return key2string[k]
+}
+
+// Virtual key codes
+const (
+ VK_LBUTTON = 1
+ VK_RBUTTON = 2
+ VK_CANCEL = 3
+ VK_MBUTTON = 4
+ VK_XBUTTON1 = 5
+ VK_XBUTTON2 = 6
+ VK_BACK = 8
+ VK_TAB = 9
+ VK_CLEAR = 12
+ VK_RETURN = 13
+ VK_SHIFT = 16
+ VK_CONTROL = 17
+ VK_MENU = 18
+ VK_PAUSE = 19
+ VK_CAPITAL = 20
+ VK_KANA = 0x15
+ VK_HANGEUL = 0x15
+ VK_HANGUL = 0x15
+ VK_JUNJA = 0x17
+ VK_FINAL = 0x18
+ VK_HANJA = 0x19
+ VK_KANJI = 0x19
+ VK_ESCAPE = 0x1B
+ VK_CONVERT = 0x1C
+ VK_NONCONVERT = 0x1D
+ VK_ACCEPT = 0x1E
+ VK_MODECHANGE = 0x1F
+ VK_SPACE = 32
+ VK_PRIOR = 33
+ VK_NEXT = 34
+ VK_END = 35
+ VK_HOME = 36
+ VK_LEFT = 37
+ VK_UP = 38
+ VK_RIGHT = 39
+ VK_DOWN = 40
+ VK_SELECT = 41
+ VK_PRINT = 42
+ VK_EXECUTE = 43
+ VK_SNAPSHOT = 44
+ VK_INSERT = 45
+ VK_DELETE = 46
+ VK_HELP = 47
+ VK_LWIN = 0x5B
+ VK_RWIN = 0x5C
+ VK_APPS = 0x5D
+ VK_SLEEP = 0x5F
+ VK_NUMPAD0 = 0x60
+ VK_NUMPAD1 = 0x61
+ VK_NUMPAD2 = 0x62
+ VK_NUMPAD3 = 0x63
+ VK_NUMPAD4 = 0x64
+ VK_NUMPAD5 = 0x65
+ VK_NUMPAD6 = 0x66
+ VK_NUMPAD7 = 0x67
+ VK_NUMPAD8 = 0x68
+ VK_NUMPAD9 = 0x69
+ VK_MULTIPLY = 0x6A
+ VK_ADD = 0x6B
+ VK_SEPARATOR = 0x6C
+ VK_SUBTRACT = 0x6D
+ VK_DECIMAL = 0x6E
+ VK_DIVIDE = 0x6F
+ VK_F1 = 0x70
+ VK_F2 = 0x71
+ VK_F3 = 0x72
+ VK_F4 = 0x73
+ VK_F5 = 0x74
+ VK_F6 = 0x75
+ VK_F7 = 0x76
+ VK_F8 = 0x77
+ VK_F9 = 0x78
+ VK_F10 = 0x79
+ VK_F11 = 0x7A
+ VK_F12 = 0x7B
+ VK_F13 = 0x7C
+ VK_F14 = 0x7D
+ VK_F15 = 0x7E
+ VK_F16 = 0x7F
+ VK_F17 = 0x80
+ VK_F18 = 0x81
+ VK_F19 = 0x82
+ VK_F20 = 0x83
+ VK_F21 = 0x84
+ VK_F22 = 0x85
+ VK_F23 = 0x86
+ VK_F24 = 0x87
+ VK_NUMLOCK = 0x90
+ VK_SCROLL = 0x91
+ VK_LSHIFT = 0xA0
+ VK_RSHIFT = 0xA1
+ VK_LCONTROL = 0xA2
+ VK_RCONTROL = 0xA3
+ VK_LMENU = 0xA4
+ VK_RMENU = 0xA5
+ VK_BROWSER_BACK = 0xA6
+ VK_BROWSER_FORWARD = 0xA7
+ VK_BROWSER_REFRESH = 0xA8
+ VK_BROWSER_STOP = 0xA9
+ VK_BROWSER_SEARCH = 0xAA
+ VK_BROWSER_FAVORITES = 0xAB
+ VK_BROWSER_HOME = 0xAC
+ VK_VOLUME_MUTE = 0xAD
+ VK_VOLUME_DOWN = 0xAE
+ VK_VOLUME_UP = 0xAF
+ VK_MEDIA_NEXT_TRACK = 0xB0
+ VK_MEDIA_PREV_TRACK = 0xB1
+ VK_MEDIA_STOP = 0xB2
+ VK_MEDIA_PLAY_PAUSE = 0xB3
+ VK_LAUNCH_MAIL = 0xB4
+ VK_LAUNCH_MEDIA_SELECT = 0xB5
+ VK_LAUNCH_APP1 = 0xB6
+ VK_LAUNCH_APP2 = 0xB7
+ VK_OEM_1 = 0xBA
+ VK_OEM_PLUS = 0xBB
+ VK_OEM_COMMA = 0xBC
+ VK_OEM_MINUS = 0xBD
+ VK_OEM_PERIOD = 0xBE
+ VK_OEM_2 = 0xBF
+ VK_OEM_3 = 0xC0
+ VK_OEM_4 = 0xDB
+ VK_OEM_5 = 0xDC
+ VK_OEM_6 = 0xDD
+ VK_OEM_7 = 0xDE
+ VK_OEM_8 = 0xDF
+ VK_OEM_102 = 0xE2
+ VK_PROCESSKEY = 0xE5
+ VK_PACKET = 0xE7
+ VK_ATTN = 0xF6
+ VK_CRSEL = 0xF7
+ VK_EXSEL = 0xF8
+ VK_EREOF = 0xF9
+ VK_PLAY = 0xFA
+ VK_ZOOM = 0xFB
+ VK_NONAME = 0xFC
+ VK_PA1 = 0xFD
+ VK_OEM_CLEAR = 0xFE
+)
+
+const (
+ KeyLButton Key = VK_LBUTTON
+ KeyRButton Key = VK_RBUTTON
+ KeyCancel Key = VK_CANCEL
+ KeyMButton Key = VK_MBUTTON
+ KeyXButton1 Key = VK_XBUTTON1
+ KeyXButton2 Key = VK_XBUTTON2
+ KeyBack Key = VK_BACK
+ KeyTab Key = VK_TAB
+ KeyClear Key = VK_CLEAR
+ KeyReturn Key = VK_RETURN
+ KeyShift Key = VK_SHIFT
+ KeyControl Key = VK_CONTROL
+ KeyAlt Key = VK_MENU
+ KeyMenu Key = VK_MENU
+ KeyPause Key = VK_PAUSE
+ KeyCapital Key = VK_CAPITAL
+ KeyKana Key = VK_KANA
+ KeyHangul Key = VK_HANGUL
+ KeyJunja Key = VK_JUNJA
+ KeyFinal Key = VK_FINAL
+ KeyHanja Key = VK_HANJA
+ KeyKanji Key = VK_KANJI
+ KeyEscape Key = VK_ESCAPE
+ KeyConvert Key = VK_CONVERT
+ KeyNonconvert Key = VK_NONCONVERT
+ KeyAccept Key = VK_ACCEPT
+ KeyModeChange Key = VK_MODECHANGE
+ KeySpace Key = VK_SPACE
+ KeyPrior Key = VK_PRIOR
+ KeyNext Key = VK_NEXT
+ KeyEnd Key = VK_END
+ KeyHome Key = VK_HOME
+ KeyLeft Key = VK_LEFT
+ KeyUp Key = VK_UP
+ KeyRight Key = VK_RIGHT
+ KeyDown Key = VK_DOWN
+ KeySelect Key = VK_SELECT
+ KeyPrint Key = VK_PRINT
+ KeyExecute Key = VK_EXECUTE
+ KeySnapshot Key = VK_SNAPSHOT
+ KeyInsert Key = VK_INSERT
+ KeyDelete Key = VK_DELETE
+ KeyHelp Key = VK_HELP
+ Key0 Key = 0x30
+ Key1 Key = 0x31
+ Key2 Key = 0x32
+ Key3 Key = 0x33
+ Key4 Key = 0x34
+ Key5 Key = 0x35
+ Key6 Key = 0x36
+ Key7 Key = 0x37
+ Key8 Key = 0x38
+ Key9 Key = 0x39
+ KeyA Key = 0x41
+ KeyB Key = 0x42
+ KeyC Key = 0x43
+ KeyD Key = 0x44
+ KeyE Key = 0x45
+ KeyF Key = 0x46
+ KeyG Key = 0x47
+ KeyH Key = 0x48
+ KeyI Key = 0x49
+ KeyJ Key = 0x4A
+ KeyK Key = 0x4B
+ KeyL Key = 0x4C
+ KeyM Key = 0x4D
+ KeyN Key = 0x4E
+ KeyO Key = 0x4F
+ KeyP Key = 0x50
+ KeyQ Key = 0x51
+ KeyR Key = 0x52
+ KeyS Key = 0x53
+ KeyT Key = 0x54
+ KeyU Key = 0x55
+ KeyV Key = 0x56
+ KeyW Key = 0x57
+ KeyX Key = 0x58
+ KeyY Key = 0x59
+ KeyZ Key = 0x5A
+ KeyLWIN Key = VK_LWIN
+ KeyRWIN Key = VK_RWIN
+ KeyApps Key = VK_APPS
+ KeySleep Key = VK_SLEEP
+ KeyNumpad0 Key = VK_NUMPAD0
+ KeyNumpad1 Key = VK_NUMPAD1
+ KeyNumpad2 Key = VK_NUMPAD2
+ KeyNumpad3 Key = VK_NUMPAD3
+ KeyNumpad4 Key = VK_NUMPAD4
+ KeyNumpad5 Key = VK_NUMPAD5
+ KeyNumpad6 Key = VK_NUMPAD6
+ KeyNumpad7 Key = VK_NUMPAD7
+ KeyNumpad8 Key = VK_NUMPAD8
+ KeyNumpad9 Key = VK_NUMPAD9
+ KeyMultiply Key = VK_MULTIPLY
+ KeyAdd Key = VK_ADD
+ KeySeparator Key = VK_SEPARATOR
+ KeySubtract Key = VK_SUBTRACT
+ KeyDecimal Key = VK_DECIMAL
+ KeyDivide Key = VK_DIVIDE
+ KeyF1 Key = VK_F1
+ KeyF2 Key = VK_F2
+ KeyF3 Key = VK_F3
+ KeyF4 Key = VK_F4
+ KeyF5 Key = VK_F5
+ KeyF6 Key = VK_F6
+ KeyF7 Key = VK_F7
+ KeyF8 Key = VK_F8
+ KeyF9 Key = VK_F9
+ KeyF10 Key = VK_F10
+ KeyF11 Key = VK_F11
+ KeyF12 Key = VK_F12
+ KeyF13 Key = VK_F13
+ KeyF14 Key = VK_F14
+ KeyF15 Key = VK_F15
+ KeyF16 Key = VK_F16
+ KeyF17 Key = VK_F17
+ KeyF18 Key = VK_F18
+ KeyF19 Key = VK_F19
+ KeyF20 Key = VK_F20
+ KeyF21 Key = VK_F21
+ KeyF22 Key = VK_F22
+ KeyF23 Key = VK_F23
+ KeyF24 Key = VK_F24
+ KeyNumlock Key = VK_NUMLOCK
+ KeyScroll Key = VK_SCROLL
+ KeyLShift Key = VK_LSHIFT
+ KeyRShift Key = VK_RSHIFT
+ KeyLControl Key = VK_LCONTROL
+ KeyRControl Key = VK_RCONTROL
+ KeyLAlt Key = VK_LMENU
+ KeyLMenu Key = VK_LMENU
+ KeyRAlt Key = VK_RMENU
+ KeyRMenu Key = VK_RMENU
+ KeyBrowserBack Key = VK_BROWSER_BACK
+ KeyBrowserForward Key = VK_BROWSER_FORWARD
+ KeyBrowserRefresh Key = VK_BROWSER_REFRESH
+ KeyBrowserStop Key = VK_BROWSER_STOP
+ KeyBrowserSearch Key = VK_BROWSER_SEARCH
+ KeyBrowserFavorites Key = VK_BROWSER_FAVORITES
+ KeyBrowserHome Key = VK_BROWSER_HOME
+ KeyVolumeMute Key = VK_VOLUME_MUTE
+ KeyVolumeDown Key = VK_VOLUME_DOWN
+ KeyVolumeUp Key = VK_VOLUME_UP
+ KeyMediaNextTrack Key = VK_MEDIA_NEXT_TRACK
+ KeyMediaPrevTrack Key = VK_MEDIA_PREV_TRACK
+ KeyMediaStop Key = VK_MEDIA_STOP
+ KeyMediaPlayPause Key = VK_MEDIA_PLAY_PAUSE
+ KeyLaunchMail Key = VK_LAUNCH_MAIL
+ KeyLaunchMediaSelect Key = VK_LAUNCH_MEDIA_SELECT
+ KeyLaunchApp1 Key = VK_LAUNCH_APP1
+ KeyLaunchApp2 Key = VK_LAUNCH_APP2
+ KeyOEM1 Key = VK_OEM_1
+ KeyOEMPlus Key = VK_OEM_PLUS
+ KeyOEMComma Key = VK_OEM_COMMA
+ KeyOEMMinus Key = VK_OEM_MINUS
+ KeyOEMPeriod Key = VK_OEM_PERIOD
+ KeyOEM2 Key = VK_OEM_2
+ KeyOEM3 Key = VK_OEM_3
+ KeyOEM4 Key = VK_OEM_4
+ KeyOEM5 Key = VK_OEM_5
+ KeyOEM6 Key = VK_OEM_6
+ KeyOEM7 Key = VK_OEM_7
+ KeyOEM8 Key = VK_OEM_8
+ KeyOEM102 Key = VK_OEM_102
+ KeyProcessKey Key = VK_PROCESSKEY
+ KeyPacket Key = VK_PACKET
+ KeyAttn Key = VK_ATTN
+ KeyCRSel Key = VK_CRSEL
+ KeyEXSel Key = VK_EXSEL
+ KeyErEOF Key = VK_EREOF
+ KeyPlay Key = VK_PLAY
+ KeyZoom Key = VK_ZOOM
+ KeyNoName Key = VK_NONAME
+ KeyPA1 Key = VK_PA1
+ KeyOEMClear Key = VK_OEM_CLEAR
+)
+
+var key2string = map[Key]string{
+ KeyLButton: "LButton",
+ KeyRButton: "RButton",
+ KeyCancel: "Cancel",
+ KeyMButton: "MButton",
+ KeyXButton1: "XButton1",
+ KeyXButton2: "XButton2",
+ KeyBack: "Back",
+ KeyTab: "Tab",
+ KeyClear: "Clear",
+ KeyReturn: "Return",
+ KeyShift: "Shift",
+ KeyControl: "Control",
+ KeyAlt: "Alt / Menu",
+ KeyPause: "Pause",
+ KeyCapital: "Capital",
+ KeyKana: "Kana / Hangul",
+ KeyJunja: "Junja",
+ KeyFinal: "Final",
+ KeyHanja: "Hanja / Kanji",
+ KeyEscape: "Escape",
+ KeyConvert: "Convert",
+ KeyNonconvert: "Nonconvert",
+ KeyAccept: "Accept",
+ KeyModeChange: "ModeChange",
+ KeySpace: "Space",
+ KeyPrior: "Prior",
+ KeyNext: "Next",
+ KeyEnd: "End",
+ KeyHome: "Home",
+ KeyLeft: "Left",
+ KeyUp: "Up",
+ KeyRight: "Right",
+ KeyDown: "Down",
+ KeySelect: "Select",
+ KeyPrint: "Print",
+ KeyExecute: "Execute",
+ KeySnapshot: "Snapshot",
+ KeyInsert: "Insert",
+ KeyDelete: "Delete",
+ KeyHelp: "Help",
+ Key0: "0",
+ Key1: "1",
+ Key2: "2",
+ Key3: "3",
+ Key4: "4",
+ Key5: "5",
+ Key6: "6",
+ Key7: "7",
+ Key8: "8",
+ Key9: "9",
+ KeyA: "A",
+ KeyB: "B",
+ KeyC: "C",
+ KeyD: "D",
+ KeyE: "E",
+ KeyF: "F",
+ KeyG: "G",
+ KeyH: "H",
+ KeyI: "I",
+ KeyJ: "J",
+ KeyK: "K",
+ KeyL: "L",
+ KeyM: "M",
+ KeyN: "N",
+ KeyO: "O",
+ KeyP: "P",
+ KeyQ: "Q",
+ KeyR: "R",
+ KeyS: "S",
+ KeyT: "T",
+ KeyU: "U",
+ KeyV: "V",
+ KeyW: "W",
+ KeyX: "X",
+ KeyY: "Y",
+ KeyZ: "Z",
+ KeyLWIN: "LWIN",
+ KeyRWIN: "RWIN",
+ KeyApps: "Apps",
+ KeySleep: "Sleep",
+ KeyNumpad0: "Numpad0",
+ KeyNumpad1: "Numpad1",
+ KeyNumpad2: "Numpad2",
+ KeyNumpad3: "Numpad3",
+ KeyNumpad4: "Numpad4",
+ KeyNumpad5: "Numpad5",
+ KeyNumpad6: "Numpad6",
+ KeyNumpad7: "Numpad7",
+ KeyNumpad8: "Numpad8",
+ KeyNumpad9: "Numpad9",
+ KeyMultiply: "Multiply",
+ KeyAdd: "Add",
+ KeySeparator: "Separator",
+ KeySubtract: "Subtract",
+ KeyDecimal: "Decimal",
+ KeyDivide: "Divide",
+ KeyF1: "F1",
+ KeyF2: "F2",
+ KeyF3: "F3",
+ KeyF4: "F4",
+ KeyF5: "F5",
+ KeyF6: "F6",
+ KeyF7: "F7",
+ KeyF8: "F8",
+ KeyF9: "F9",
+ KeyF10: "F10",
+ KeyF11: "F11",
+ KeyF12: "F12",
+ KeyF13: "F13",
+ KeyF14: "F14",
+ KeyF15: "F15",
+ KeyF16: "F16",
+ KeyF17: "F17",
+ KeyF18: "F18",
+ KeyF19: "F19",
+ KeyF20: "F20",
+ KeyF21: "F21",
+ KeyF22: "F22",
+ KeyF23: "F23",
+ KeyF24: "F24",
+ KeyNumlock: "Numlock",
+ KeyScroll: "Scroll",
+ KeyLShift: "LShift",
+ KeyRShift: "RShift",
+ KeyLControl: "LControl",
+ KeyRControl: "RControl",
+ KeyLMenu: "LMenu",
+ KeyRMenu: "RMenu",
+ KeyBrowserBack: "BrowserBack",
+ KeyBrowserForward: "BrowserForward",
+ KeyBrowserRefresh: "BrowserRefresh",
+ KeyBrowserStop: "BrowserStop",
+ KeyBrowserSearch: "BrowserSearch",
+ KeyBrowserFavorites: "BrowserFavorites",
+ KeyBrowserHome: "BrowserHome",
+ KeyVolumeMute: "VolumeMute",
+ KeyVolumeDown: "VolumeDown",
+ KeyVolumeUp: "VolumeUp",
+ KeyMediaNextTrack: "MediaNextTrack",
+ KeyMediaPrevTrack: "MediaPrevTrack",
+ KeyMediaStop: "MediaStop",
+ KeyMediaPlayPause: "MediaPlayPause",
+ KeyLaunchMail: "LaunchMail",
+ KeyLaunchMediaSelect: "LaunchMediaSelect",
+ KeyLaunchApp1: "LaunchApp1",
+ KeyLaunchApp2: "LaunchApp2",
+ KeyOEM1: "OEM1",
+ KeyOEMPlus: "OEMPlus",
+ KeyOEMComma: "OEMComma",
+ KeyOEMMinus: "OEMMinus",
+ KeyOEMPeriod: "OEMPeriod",
+ KeyOEM2: "OEM2",
+ KeyOEM3: "OEM3",
+ KeyOEM4: "OEM4",
+ KeyOEM5: "OEM5",
+ KeyOEM6: "OEM6",
+ KeyOEM7: "OEM7",
+ KeyOEM8: "OEM8",
+ KeyOEM102: "OEM102",
+ KeyProcessKey: "ProcessKey",
+ KeyPacket: "Packet",
+ KeyAttn: "Attn",
+ KeyCRSel: "CRSel",
+ KeyEXSel: "EXSel",
+ KeyErEOF: "ErEOF",
+ KeyPlay: "Play",
+ KeyZoom: "Zoom",
+ KeyNoName: "NoName",
+ KeyPA1: "PA1",
+ KeyOEMClear: "OEMClear",
+}
+
+type Modifiers byte
+
+func (m Modifiers) String() string {
+ return modifiers2string[m]
+}
+
+var modifiers2string = map[Modifiers]string{
+ ModShift: "Shift",
+ ModControl: "Ctrl",
+ ModControl | ModShift: "Ctrl+Shift",
+ ModAlt: "Alt",
+ ModAlt | ModShift: "Alt+Shift",
+ ModAlt | ModControl | ModShift: "Alt+Ctrl+Shift",
+}
+
+const (
+ ModShift Modifiers = 1 << iota
+ ModControl
+ ModAlt
+)
+
+func ModifiersDown() Modifiers {
+ var m Modifiers
+
+ if ShiftDown() {
+ m |= ModShift
+ }
+ if ControlDown() {
+ m |= ModControl
+ }
+ if AltDown() {
+ m |= ModAlt
+ }
+
+ return m
+}
+
+type Shortcut struct {
+ Modifiers Modifiers
+ Key Key
+}
+
+func (s Shortcut) String() string {
+ m := s.Modifiers.String()
+ if m == "" {
+ return s.Key.String()
+ }
+
+ b := new(bytes.Buffer)
+
+ b.WriteString(m)
+ b.WriteRune('+')
+ b.WriteString(s.Key.String())
+
+ return b.String()
+}
+
+func GetKeyState(nVirtKey int32) int16 {
+ ret, _, _ := procGetKeyState.Call(
+ uintptr(nVirtKey),
+ )
+
+ return int16(ret)
+}
+
+func AltDown() bool {
+ return GetKeyState(int32(KeyAlt))>>15 != 0
+}
+
+func ControlDown() bool {
+ return GetKeyState(int32(KeyControl))>>15 != 0
+}
+
+func ShiftDown() bool {
+ return GetKeyState(int32(KeyShift))>>15 != 0
+}
+
+var ModifierMap = map[keys.Modifier]Modifiers{
+ keys.ShiftKey: ModShift,
+ keys.ControlKey: ModControl,
+ keys.OptionOrAltKey: ModAlt,
+ keys.CmdOrCtrlKey: ModControl,
+}
+
+var NoShortcut = Shortcut{}
+
+func AcceleratorToShortcut(accelerator *keys.Accelerator) Shortcut {
+
+ if accelerator == nil {
+ return NoShortcut
+ }
+ inKey := strings.ToUpper(accelerator.Key)
+ key, exists := KeyMap[inKey]
+ if !exists {
+ return NoShortcut
+ }
+ var modifiers Modifiers
+ if _, exists := shiftMap[inKey]; exists {
+ modifiers = ModShift
+ }
+ for _, mod := range accelerator.Modifiers {
+ modifiers |= ModifierMap[mod]
+ }
+ return Shortcut{
+ Modifiers: modifiers,
+ Key: key,
+ }
+}
+
+var shiftMap = map[string]struct{}{
+ "~": {},
+ ")": {},
+ "!": {},
+ "@": {},
+ "#": {},
+ "$": {},
+ "%": {},
+ "^": {},
+ "&": {},
+ "*": {},
+ "(": {},
+ "_": {},
+ "PLUS": {},
+ "<": {},
+ ">": {},
+ "?": {},
+ ":": {},
+ `"`: {},
+ "{": {},
+ "}": {},
+ "|": {},
+}
+
+var KeyMap = map[string]Key{
+ "0": Key0,
+ "1": Key1,
+ "2": Key2,
+ "3": Key3,
+ "4": Key4,
+ "5": Key5,
+ "6": Key6,
+ "7": Key7,
+ "8": Key8,
+ "9": Key9,
+ "A": KeyA,
+ "B": KeyB,
+ "C": KeyC,
+ "D": KeyD,
+ "E": KeyE,
+ "F": KeyF,
+ "G": KeyG,
+ "H": KeyH,
+ "I": KeyI,
+ "J": KeyJ,
+ "K": KeyK,
+ "L": KeyL,
+ "M": KeyM,
+ "N": KeyN,
+ "O": KeyO,
+ "P": KeyP,
+ "Q": KeyQ,
+ "R": KeyR,
+ "S": KeyS,
+ "T": KeyT,
+ "U": KeyU,
+ "V": KeyV,
+ "W": KeyW,
+ "X": KeyX,
+ "Y": KeyY,
+ "Z": KeyZ,
+ "F1": KeyF1,
+ "F2": KeyF2,
+ "F3": KeyF3,
+ "F4": KeyF4,
+ "F5": KeyF5,
+ "F6": KeyF6,
+ "F7": KeyF7,
+ "F8": KeyF8,
+ "F9": KeyF9,
+ "F10": KeyF10,
+ "F11": KeyF11,
+ "F12": KeyF12,
+ "F13": KeyF13,
+ "F14": KeyF14,
+ "F15": KeyF15,
+ "F16": KeyF16,
+ "F17": KeyF17,
+ "F18": KeyF18,
+ "F19": KeyF19,
+ "F20": KeyF20,
+ "F21": KeyF21,
+ "F22": KeyF22,
+ "F23": KeyF23,
+ "F24": KeyF24,
+
+ "`": KeyOEM3,
+ ",": KeyOEMComma,
+ ".": KeyOEMPeriod,
+ "/": KeyOEM2,
+ ";": KeyOEM1,
+ "'": KeyOEM7,
+ "[": KeyOEM4,
+ "]": KeyOEM6,
+ `\`: KeyOEM5,
+ "~": KeyOEM3,
+ ")": Key0,
+ "!": Key1,
+ "@": Key2,
+ "#": Key3,
+ "$": Key4,
+ "%": Key5,
+ "^": Key6,
+ "&": Key7,
+ "*": Key8,
+ "(": Key9,
+ "_": KeyOEMMinus,
+ "PLUS": KeyOEMPlus,
+ "<": KeyOEMComma,
+ ">": KeyOEMPeriod,
+ "?": KeyOEM2,
+ ":": KeyOEM1,
+ `"`: KeyOEM7,
+ "{": KeyOEM4,
+ "}": KeyOEM6,
+ "|": KeyOEM5,
+
+ "SPACE": KeySpace,
+ "TAB": KeyTab,
+ "CAPSLOCK": KeyCapital,
+ "NUMLOCK": KeyNumlock,
+ "SCROLLLOCK": KeyScroll,
+ "BACKSPACE": KeyBack,
+ "DELETE": KeyDelete,
+ "INSERT": KeyInsert,
+ "RETURN": KeyReturn,
+ "ENTER": KeyReturn,
+ "UP": KeyUp,
+ "DOWN": KeyDown,
+ "LEFT": KeyLeft,
+ "RIGHT": KeyRight,
+ "HOME": KeyHome,
+ "END": KeyEnd,
+ "PAGEUP": KeyPrior,
+ "PAGEDOWN": KeyNext,
+ "ESCAPE": KeyEscape,
+ "ESC": KeyEscape,
+ "VOLUMEUP": KeyVolumeUp,
+ "VOLUMEDOWN": KeyVolumeDown,
+ "VOLUMEMUTE": KeyVolumeMute,
+ "MEDIANEXTTRACK": KeyMediaNextTrack,
+ "MEDIAPREVIOUSTRACK": KeyMediaPrevTrack,
+ "MEDIASTOP": KeyMediaStop,
+ "MEDIAPLAYPAUSE": KeyMediaPlayPause,
+ "PRINTSCREEN": KeyPrint,
+ "NUM0": KeyNumpad0,
+ "NUM1": KeyNumpad1,
+ "NUM2": KeyNumpad2,
+ "NUM3": KeyNumpad3,
+ "NUM4": KeyNumpad4,
+ "NUM5": KeyNumpad5,
+ "NUM6": KeyNumpad6,
+ "NUM7": KeyNumpad7,
+ "NUM8": KeyNumpad8,
+ "NUM9": KeyNumpad9,
+ "nummult": KeyMultiply,
+ "numadd": KeyAdd,
+ "numsub": KeySubtract,
+ "numdec": KeyDecimal,
+ "numdiv": KeyDivide,
+}
+
+type Accelerator struct {
+ Virtual byte
+ Key uint16
+ Cmd uint16
+}
+
+func CreateAcceleratorTable(acc []Accelerator) uintptr {
+ if len(acc) == 0 {
+ return 0
+ }
+ ret, _, _ := procCreateAcceleratorTable.Call(
+ uintptr(unsafe.Pointer(&acc[0])),
+ uintptr(len(acc)),
+ )
+ return ret
+}
+
+func TranslateAccelerator(hwnd HWND, hAccTable uintptr, lpMsg *MSG) bool {
+ ret, _, _ := procTranslateAccelerator.Call(
+ uintptr(hwnd),
+ hAccTable,
+ uintptr(unsafe.Pointer(lpMsg)),
+ )
+ return ret != 0
+}
diff --git a/v2/internal/platform/win32/menu.go b/v2/internal/platform/win32/menu.go
new file mode 100644
index 000000000..f05886414
--- /dev/null
+++ b/v2/internal/platform/win32/menu.go
@@ -0,0 +1,82 @@
+//go:build windows
+
+package win32
+
+type Menu HMENU
+type PopupMenu Menu
+
+func CreatePopupMenu() PopupMenu {
+ ret, _, _ := procCreatePopupMenu.Call(0, 0, 0, 0)
+ return PopupMenu(ret)
+}
+
+func (m Menu) Destroy() bool {
+ ret, _, _ := procDestroyMenu.Call(uintptr(m))
+ return ret != 0
+}
+
+func (p PopupMenu) Destroy() bool {
+ return Menu(p).Destroy()
+}
+
+func (p PopupMenu) Track(flags uint, x, y int, wnd HWND) bool {
+ ret, _, _ := procTrackPopupMenu.Call(
+ uintptr(p),
+ uintptr(flags),
+ uintptr(x),
+ uintptr(y),
+ 0,
+ uintptr(wnd),
+ 0,
+ )
+ return ret != 0
+}
+
+func (p PopupMenu) Append(flags uintptr, id uintptr, text string) bool {
+ return Menu(p).Append(flags, id, text)
+}
+
+func (m Menu) Append(flags uintptr, id uintptr, text string) bool {
+ ret, _, _ := procAppendMenuW.Call(
+ uintptr(m),
+ flags,
+ id,
+ MustStringToUTF16uintptr(text),
+ )
+ return ret != 0
+}
+
+func (p PopupMenu) Check(id uintptr, checked bool) bool {
+ return Menu(p).Check(id, checked)
+}
+
+func (m Menu) Check(id uintptr, check bool) bool {
+ var checkState uint = MF_UNCHECKED
+ if check {
+ checkState = MF_CHECKED
+ }
+ return CheckMenuItem(HMENU(m), id, checkState) != 0
+}
+
+func (m Menu) CheckRadio(startID int, endID int, selectedID int) bool {
+ ret, _, _ := procCheckMenuRadioItem.Call(
+ uintptr(m),
+ uintptr(startID),
+ uintptr(endID),
+ uintptr(selectedID),
+ MF_BYCOMMAND)
+ return ret != 0
+}
+
+func CheckMenuItem(menu HMENU, id uintptr, flags uint) uint {
+ ret, _, _ := procCheckMenuItem.Call(
+ uintptr(menu),
+ id,
+ uintptr(flags),
+ )
+ return uint(ret)
+}
+
+func (p PopupMenu) CheckRadio(startID, endID, selectedID int) bool {
+ return Menu(p).CheckRadio(startID, endID, selectedID)
+}
diff --git a/v2/internal/platform/win32/structs.go b/v2/internal/platform/win32/structs.go
new file mode 100644
index 000000000..3f79d8585
--- /dev/null
+++ b/v2/internal/platform/win32/structs.go
@@ -0,0 +1,51 @@
+//go:build windows
+
+package win32
+
+import "golang.org/x/sys/windows"
+
+type NOTIFYICONDATA struct {
+ CbSize uint32
+ HWnd HWND
+ UID uint32
+ UFlags uint32
+ UCallbackMessage uint32
+ HIcon HICON
+ SzTip [128]uint16
+ DwState uint32
+ DwStateMask uint32
+ SzInfo [256]uint16
+ UVersion uint32
+ SzInfoTitle [64]uint16
+ DwInfoFlags uint32
+ GuidItem windows.GUID
+ HBalloonIcon HICON
+}
+
+type WNDCLASSEX struct {
+ CbSize uint32
+ Style uint32
+ LpfnWndProc uintptr
+ CbClsExtra int32
+ CbWndExtra int32
+ HInstance HINSTANCE
+ HIcon HICON
+ HCursor HCURSOR
+ HbrBackground HBRUSH
+ LpszMenuName *uint16
+ LpszClassName *uint16
+ HIconSm HICON
+}
+
+type MSG struct {
+ HWnd HWND
+ Message uint32
+ WParam uintptr
+ LParam uintptr
+ Time uint32
+ Pt POINT
+}
+
+type POINT struct {
+ X, Y int32
+}
diff --git a/v2/internal/platform/win32/theme.go b/v2/internal/platform/win32/theme.go
new file mode 100644
index 000000000..ad29b1201
--- /dev/null
+++ b/v2/internal/platform/win32/theme.go
@@ -0,0 +1,191 @@
+//go:build windows
+
+package win32
+
+import (
+ "golang.org/x/sys/windows/registry"
+ "unsafe"
+)
+
+type DWMWINDOWATTRIBUTE int32
+
+const DwmwaUseImmersiveDarkModeBefore20h1 DWMWINDOWATTRIBUTE = 19
+const DwmwaUseImmersiveDarkMode DWMWINDOWATTRIBUTE = 20
+const DwmwaBorderColor DWMWINDOWATTRIBUTE = 34
+const DwmwaCaptionColor DWMWINDOWATTRIBUTE = 35
+const DwmwaTextColor DWMWINDOWATTRIBUTE = 36
+const DwmwaSystemBackdropType DWMWINDOWATTRIBUTE = 38
+
+const SPI_GETHIGHCONTRAST = 0x0042
+const HCF_HIGHCONTRASTON = 0x00000001
+const WCA_ACCENT_POLICY WINDOWCOMPOSITIONATTRIB = 19
+
+type ACCENT_STATE DWORD
+
+const (
+ ACCENT_DISABLED ACCENT_STATE = 0
+ ACCENT_ENABLE_GRADIENT ACCENT_STATE = 1
+ ACCENT_ENABLE_TRANSPARENTGRADIENT ACCENT_STATE = 2
+ ACCENT_ENABLE_BLURBEHIND ACCENT_STATE = 3
+ ACCENT_ENABLE_ACRYLICBLURBEHIND ACCENT_STATE = 4 // RS4 1803
+ ACCENT_ENABLE_HOSTBACKDROP ACCENT_STATE = 5 // RS5 1809
+ ACCENT_INVALID_STATE ACCENT_STATE = 6
+)
+
+type ACCENT_POLICY struct {
+ AccentState ACCENT_STATE
+ AccentFlags DWORD
+ GradientColor DWORD
+ AnimationId DWORD
+}
+
+type WINDOWCOMPOSITIONATTRIBDATA struct {
+ Attrib WINDOWCOMPOSITIONATTRIB
+ PvData unsafe.Pointer
+ CbData uintptr
+}
+
+type WINDOWCOMPOSITIONATTRIB DWORD
+
+// BackdropType defines the type of translucency we wish to use
+type BackdropType int32
+
+const (
+ BackdropTypeAuto BackdropType = 0
+ BackdropTypeNone BackdropType = 1
+ BackdropTypeMica BackdropType = 2
+ BackdropTypeAcrylic BackdropType = 3
+ BackdropTypeTabbed BackdropType = 4
+)
+
+func dwmSetWindowAttribute(hwnd HWND, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) {
+ ret, _, err := procDwmSetWindowAttribute.Call(
+ uintptr(hwnd),
+ uintptr(dwAttribute),
+ uintptr(pvAttribute),
+ cbAttribute)
+ if ret != 0 {
+ _ = err
+ // println(err.Error())
+ }
+}
+
+func SupportsThemes() bool {
+ // We can't support Windows versions before 17763
+ return IsWindowsVersionAtLeast(10, 0, 17763)
+}
+
+func SupportsCustomThemes() bool {
+ return IsWindowsVersionAtLeast(10, 0, 17763)
+}
+
+func SupportsBackdropTypes() bool {
+ return IsWindowsVersionAtLeast(10, 0, 22621)
+}
+
+func SupportsImmersiveDarkMode() bool {
+ return IsWindowsVersionAtLeast(10, 0, 18985)
+}
+
+func SetTheme(hwnd HWND, useDarkMode bool) {
+ if SupportsThemes() {
+ attr := DwmwaUseImmersiveDarkModeBefore20h1
+ if SupportsImmersiveDarkMode() {
+ attr = DwmwaUseImmersiveDarkMode
+ }
+ var winDark int32
+ if useDarkMode {
+ winDark = 1
+ }
+ dwmSetWindowAttribute(hwnd, attr, unsafe.Pointer(&winDark), unsafe.Sizeof(winDark))
+ }
+}
+
+func EnableBlurBehind(hwnd HWND) {
+ var accent = ACCENT_POLICY{
+ AccentState: ACCENT_ENABLE_ACRYLICBLURBEHIND,
+ AccentFlags: 0x2,
+ }
+ var data WINDOWCOMPOSITIONATTRIBDATA
+ data.Attrib = WCA_ACCENT_POLICY
+ data.PvData = unsafe.Pointer(&accent)
+ data.CbData = unsafe.Sizeof(accent)
+
+ SetWindowCompositionAttribute(hwnd, &data)
+}
+
+func SetWindowCompositionAttribute(hwnd HWND, data *WINDOWCOMPOSITIONATTRIBDATA) bool {
+ if procSetWindowCompositionAttribute != nil {
+ ret, _, _ := procSetWindowCompositionAttribute.Call(
+ uintptr(hwnd),
+ uintptr(unsafe.Pointer(data)),
+ )
+ return ret != 0
+ }
+ return false
+}
+
+func EnableTranslucency(hwnd HWND, backdrop BackdropType) {
+ if SupportsBackdropTypes() {
+ dwmSetWindowAttribute(hwnd, DwmwaSystemBackdropType, unsafe.Pointer(&backdrop), unsafe.Sizeof(backdrop))
+ } else {
+ println("Warning: Translucency type unavailable on Windows < 22621")
+ }
+}
+
+func SetTitleBarColour(hwnd HWND, titleBarColour int32) {
+ dwmSetWindowAttribute(hwnd, DwmwaCaptionColor, unsafe.Pointer(&titleBarColour), unsafe.Sizeof(titleBarColour))
+}
+
+func SetTitleTextColour(hwnd HWND, titleTextColour int32) {
+ dwmSetWindowAttribute(hwnd, DwmwaTextColor, unsafe.Pointer(&titleTextColour), unsafe.Sizeof(titleTextColour))
+}
+
+func SetBorderColour(hwnd HWND, titleBorderColour int32) {
+ dwmSetWindowAttribute(hwnd, DwmwaBorderColor, unsafe.Pointer(&titleBorderColour), unsafe.Sizeof(titleBorderColour))
+}
+
+func SetWindowTheme(hwnd HWND, appName string, subIdList string) uintptr {
+ var subID uintptr
+ if subIdList != "" {
+ subID = MustStringToUTF16uintptr(subIdList)
+ }
+ ret, _, _ := procSetWindowTheme.Call(
+ uintptr(hwnd),
+ MustStringToUTF16uintptr(appName),
+ subID,
+ )
+
+ return ret
+}
+func IsCurrentlyDarkMode() bool {
+ key, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE)
+ if err != nil {
+ return false
+ }
+ defer key.Close()
+
+ AppsUseLightTheme, _, err := key.GetIntegerValue("AppsUseLightTheme")
+ if err != nil {
+ return false
+ }
+ return AppsUseLightTheme == 0
+}
+
+type highContrast struct {
+ CbSize uint32
+ DwFlags uint32
+ LpszDefaultScheme *int16
+}
+
+func IsCurrentlyHighContrastMode() bool {
+ var result highContrast
+ result.CbSize = uint32(unsafe.Sizeof(result))
+ res, _, err := procSystemParametersInfo.Call(SPI_GETHIGHCONTRAST, uintptr(result.CbSize), uintptr(unsafe.Pointer(&result)), 0)
+ if res == 0 {
+ _ = err
+ return false
+ }
+ r := result.DwFlags&HCF_HIGHCONTRASTON == HCF_HIGHCONTRASTON
+ return r
+}
diff --git a/v2/internal/platform/win32/window.go b/v2/internal/platform/win32/window.go
new file mode 100644
index 000000000..0ca31ecee
--- /dev/null
+++ b/v2/internal/platform/win32/window.go
@@ -0,0 +1,139 @@
+//go:build windows
+
+package win32
+
+import (
+ "fmt"
+ "github.com/samber/lo"
+ "golang.org/x/sys/windows"
+ "syscall"
+ "unsafe"
+)
+
+func LoadIconWithResourceID(instance HINSTANCE, res uintptr) HICON {
+ ret, _, _ := procLoadIcon.Call(
+ uintptr(instance),
+ res)
+
+ return HICON(ret)
+}
+
+func LoadCursorWithResourceID(instance HINSTANCE, res uintptr) HCURSOR {
+ ret, _, _ := procLoadCursor.Call(
+ uintptr(instance),
+ res)
+
+ return HCURSOR(ret)
+}
+
+func RegisterClassEx(wndClassEx *WNDCLASSEX) ATOM {
+ ret, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(wndClassEx)))
+ return ATOM(ret)
+}
+
+func RegisterClass(className string, wndproc uintptr, instance HINSTANCE) error {
+ classNamePtr, err := syscall.UTF16PtrFromString(className)
+ if err != nil {
+ return err
+ }
+ icon := LoadIconWithResourceID(instance, IDI_APPLICATION)
+
+ var wc WNDCLASSEX
+ wc.CbSize = uint32(unsafe.Sizeof(wc))
+ wc.Style = CS_HREDRAW | CS_VREDRAW
+ wc.LpfnWndProc = wndproc
+ wc.HInstance = instance
+ wc.HbrBackground = COLOR_WINDOW + 1
+ wc.HIcon = icon
+ wc.HCursor = LoadCursorWithResourceID(0, IDC_ARROW)
+ wc.LpszClassName = classNamePtr
+ wc.LpszMenuName = nil
+ wc.HIconSm = icon
+
+ if ret := RegisterClassEx(&wc); ret == 0 {
+ return syscall.GetLastError()
+ }
+
+ return nil
+}
+
+func CreateWindow(className string, instance HINSTANCE, parent HWND, exStyle, style uint) HWND {
+
+ classNamePtr := lo.Must(syscall.UTF16PtrFromString(className))
+
+ result := CreateWindowEx(
+ exStyle,
+ classNamePtr,
+ nil,
+ style,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ parent,
+ 0,
+ instance,
+ nil)
+
+ if result == 0 {
+ errStr := fmt.Sprintf("Error occurred in CreateWindow(%s, %v, %d, %d)", className, parent, exStyle, style)
+ panic(errStr)
+ }
+
+ return result
+}
+
+func CreateWindowEx(exStyle uint, className, windowName *uint16,
+ style uint, x, y, width, height int, parent HWND, menu HMENU,
+ instance HINSTANCE, param unsafe.Pointer) HWND {
+ ret, _, _ := procCreateWindowEx.Call(
+ uintptr(exStyle),
+ uintptr(unsafe.Pointer(className)),
+ uintptr(unsafe.Pointer(windowName)),
+ uintptr(style),
+ uintptr(x),
+ uintptr(y),
+ uintptr(width),
+ uintptr(height),
+ uintptr(parent),
+ uintptr(menu),
+ uintptr(instance),
+ uintptr(param))
+
+ return HWND(ret)
+}
+
+func MustStringToUTF16Ptr(input string) *uint16 {
+ ret, err := syscall.UTF16PtrFromString(input)
+ if err != nil {
+ panic(err)
+ }
+ return ret
+}
+
+func MustStringToUTF16uintptr(input string) uintptr {
+ ret, err := syscall.UTF16PtrFromString(input)
+ if err != nil {
+ panic(err)
+ }
+ return uintptr(unsafe.Pointer(ret))
+}
+
+func MustUTF16FromString(input string) []uint16 {
+ ret, err := syscall.UTF16FromString(input)
+ if err != nil {
+ panic(err)
+ }
+ return ret
+}
+
+func UTF16PtrToString(input uintptr) string {
+ return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(input)))
+}
+
+func SetForegroundWindow(wnd HWND) bool {
+ ret, _, _ := procSetForegroundWindow.Call(
+ uintptr(wnd),
+ )
+ return ret != 0
+}
diff --git a/v2/internal/process/process.go b/v2/internal/process/process.go
new file mode 100644
index 000000000..18c9f45da
--- /dev/null
+++ b/v2/internal/process/process.go
@@ -0,0 +1,74 @@
+package process
+
+import (
+ "os"
+ "os/exec"
+)
+
+// Process defines a process that can be executed
+type Process struct {
+ cmd *exec.Cmd
+ exitChannel chan bool
+ Running bool
+}
+
+// NewProcess creates a new process struct
+func NewProcess(cmd string, args ...string) *Process {
+ result := &Process{
+ cmd: exec.Command(cmd, args...),
+ exitChannel: make(chan bool, 1),
+ }
+ result.cmd.Stdout = os.Stdout
+ result.cmd.Stderr = os.Stderr
+ return result
+}
+
+// Start the process
+func (p *Process) Start(exitCodeChannel chan int) error {
+ err := p.cmd.Start()
+ if err != nil {
+ return err
+ }
+
+ p.Running = true
+
+ go func(cmd *exec.Cmd, running *bool, exitChannel chan bool, exitCodeChannel chan int) {
+ err := cmd.Wait()
+ if err == nil {
+ exitCodeChannel <- 0
+ }
+ *running = false
+ exitChannel <- true
+ }(p.cmd, &p.Running, p.exitChannel, exitCodeChannel)
+
+ return nil
+}
+
+// Kill the process
+func (p *Process) Kill() error {
+ if !p.Running {
+ return nil
+ }
+ err := p.cmd.Process.Kill()
+ if err != nil {
+ return err
+ }
+ err = p.cmd.Process.Release()
+ if err != nil {
+ return err
+ }
+
+ // Wait for command to exit properly
+ <-p.exitChannel
+
+ return err
+}
+
+// PID returns the process PID
+func (p *Process) PID() int {
+ return p.cmd.Process.Pid
+}
+
+func (p *Process) SetDir(dir string) {
+ p.cmd.Dir = dir
+}
diff --git a/v2/internal/project/project.go b/v2/internal/project/project.go
new file mode 100644
index 000000000..2df99bdfa
--- /dev/null
+++ b/v2/internal/project/project.go
@@ -0,0 +1,283 @@
+package project
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/samber/lo"
+)
+
+// Project holds the data related to a Wails project
+type Project struct {
+ /*** Application Data ***/
+ Name string `json:"name"`
+ AssetDirectory string `json:"assetdir,omitempty"`
+
+ ReloadDirectories string `json:"reloaddirs,omitempty"`
+
+ BuildCommand string `json:"frontend:build"`
+ InstallCommand string `json:"frontend:install"`
+
+ // Commands used in `wails dev`
+ DevCommand string `json:"frontend:dev"`
+ DevBuildCommand string `json:"frontend:dev:build"`
+ DevInstallCommand string `json:"frontend:dev:install"`
+ DevWatcherCommand string `json:"frontend:dev:watcher"`
+ // The url of the external wails dev server. If this is set, this server is used for the frontend. Default ""
+ FrontendDevServerURL string `json:"frontend:dev:serverUrl"`
+
+ // Directory to generate the API Module
+ WailsJSDir string `json:"wailsjsdir"`
+
+ Version string `json:"version"`
+
+ /*** Internal Data ***/
+
+ // The path to the project directory
+ Path string `json:"projectdir"`
+
+ // Build directory
+ BuildDir string `json:"build:dir"`
+
+ // BuildTags Extra tags to process during build
+ BuildTags string `json:"build:tags"`
+
+ // The output filename
+ OutputFilename string `json:"outputfilename"`
+
+ // The type of application. EG: Desktop, Server, etc
+ OutputType string
+
+ // The platform to target
+ Platform string
+
+ // RunNonNativeBuildHooks will run build hooks though they are defined for a GOOS which is not equal to the host os
+ RunNonNativeBuildHooks bool `json:"runNonNativeBuildHooks"`
+
+ // Build hooks for different targets, the hooks are executed in the following order
+ // Key: GOOS/GOARCH - Executed at build level before/after a build of the specific platform and arch
+ // Key: GOOS/* - Executed at build level before/after a build of the specific platform
+ // Key: */* - Executed at build level before/after a build
+ // The following keys are not yet supported.
+ // Key: GOOS - Executed at platform level before/after all builds of the specific platform
+ // Key: * - Executed at platform level before/after all builds of a platform
+ // Key: [empty] - Executed at global level before/after all builds of all platforms
+ PostBuildHooks map[string]string `json:"postBuildHooks"`
+ PreBuildHooks map[string]string `json:"preBuildHooks"`
+
+ // The application author
+ Author Author
+
+ // The application information
+ Info Info
+
+ // Fully qualified filename
+ filename string
+
+ // The debounce time for hot-reload of the built-in dev server. Default 100
+ DebounceMS int `json:"debounceMS"`
+
+ // The address to bind the wails dev server to. Default "localhost:34115"
+ DevServer string `json:"devServer"`
+
+ // Arguments that are forward to the application in dev mode
+ AppArgs string `json:"appargs"`
+
+ // NSISType to be build
+ NSISType string `json:"nsisType"`
+
+ // Garble
+ Obfuscated bool `json:"obfuscated"`
+ GarbleArgs string `json:"garbleargs"`
+
+ // Frontend directory
+ FrontendDir string `json:"frontend:dir"`
+
+ // The timeout in seconds for Vite server detection. Default 10
+ ViteServerTimeout int `json:"viteServerTimeout"`
+
+ Bindings Bindings `json:"bindings"`
+}
+
+func (p *Project) GetFrontendDir() string {
+ if filepath.IsAbs(p.FrontendDir) {
+ return p.FrontendDir
+ }
+ return filepath.Join(p.Path, p.FrontendDir)
+}
+
+func (p *Project) GetWailsJSDir() string {
+ if filepath.IsAbs(p.WailsJSDir) {
+ return p.WailsJSDir
+ }
+ return filepath.Join(p.Path, p.WailsJSDir)
+}
+
+func (p *Project) GetBuildDir() string {
+ if filepath.IsAbs(p.BuildDir) {
+ return p.BuildDir
+ }
+ return filepath.Join(p.Path, p.BuildDir)
+}
+
+func (p *Project) GetDevBuildCommand() string {
+ if p.DevBuildCommand != "" {
+ return p.DevBuildCommand
+ }
+ if p.DevCommand != "" {
+ return p.DevCommand
+ }
+ return p.BuildCommand
+}
+
+func (p *Project) GetDevInstallerCommand() string {
+ if p.DevInstallCommand != "" {
+ return p.DevInstallCommand
+ }
+ return p.InstallCommand
+}
+
+func (p *Project) IsFrontendDevServerURLAutoDiscovery() bool {
+ return p.FrontendDevServerURL == "auto"
+}
+
+func (p *Project) Save() error {
+ data, err := json.MarshalIndent(p, "", " ")
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(p.filename, data, 0o755)
+}
+
+func (p *Project) setDefaults() {
+ if p.Path == "" {
+ p.Path = lo.Must(os.Getwd())
+ }
+ if p.Version == "" {
+ p.Version = "2"
+ }
+ // Create default name if not given
+ if p.Name == "" {
+ p.Name = "wailsapp"
+ }
+ if p.OutputFilename == "" {
+ p.OutputFilename = p.Name
+ }
+ if p.FrontendDir == "" {
+ p.FrontendDir = "frontend"
+ }
+ if p.WailsJSDir == "" {
+ p.WailsJSDir = p.FrontendDir
+ }
+ if p.BuildDir == "" {
+ p.BuildDir = "build"
+ }
+ if p.DebounceMS == 0 {
+ p.DebounceMS = 100
+ }
+ if p.DevServer == "" {
+ p.DevServer = "localhost:34115"
+ }
+ if p.ViteServerTimeout == 0 {
+ p.ViteServerTimeout = 10
+ }
+ if p.NSISType == "" {
+ p.NSISType = "multiple"
+ }
+ if p.Info.CompanyName == "" {
+ p.Info.CompanyName = p.Name
+ }
+ if p.Info.ProductName == "" {
+ p.Info.ProductName = p.Name
+ }
+ if p.Info.ProductVersion == "" {
+ p.Info.ProductVersion = "1.0.0"
+ }
+ if p.Info.Copyright == nil {
+ v := "Copyright........."
+ p.Info.Copyright = &v
+ }
+ if p.Info.Comments == nil {
+ v := "Built using Wails (https://wails.io)"
+ p.Info.Comments = &v
+ }
+
+ // Fix up OutputFilename
+ switch runtime.GOOS {
+ case "windows":
+ if !strings.HasSuffix(p.OutputFilename, ".exe") {
+ p.OutputFilename += ".exe"
+ }
+ case "darwin", "linux":
+ p.OutputFilename = strings.TrimSuffix(p.OutputFilename, ".exe")
+ }
+}
+
+// Author stores details about the application author
+type Author struct {
+ Name string `json:"name"`
+ Email string `json:"email"`
+}
+
+type Info struct {
+ CompanyName string `json:"companyName"`
+ ProductName string `json:"productName"`
+ ProductVersion string `json:"productVersion"`
+ Copyright *string `json:"copyright"`
+ Comments *string `json:"comments"`
+ FileAssociations []FileAssociation `json:"fileAssociations"`
+ Protocols []Protocol `json:"protocols"`
+}
+
+type FileAssociation struct {
+ Ext string `json:"ext"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ IconName string `json:"iconName"`
+ Role string `json:"role"`
+}
+
+type Protocol struct {
+ Scheme string `json:"scheme"`
+ Description string `json:"description"`
+ Role string `json:"role"`
+}
+
+type Bindings struct {
+ TsGeneration TsGeneration `json:"ts_generation"`
+}
+
+type TsGeneration struct {
+ Prefix string `json:"prefix"`
+ Suffix string `json:"suffix"`
+ OutputType string `json:"outputType"`
+}
+
+// Parse the given JSON data into a Project struct
+func Parse(projectData []byte) (*Project, error) {
+ project := &Project{}
+ err := json.Unmarshal(projectData, project)
+ if err != nil {
+ return nil, err
+ }
+ project.setDefaults()
+ return project, nil
+}
+
+// Load the project from the current working directory
+func Load(projectPath string) (*Project, error) {
+ projectFile := filepath.Join(projectPath, "wails.json")
+ rawBytes, err := os.ReadFile(projectFile)
+ if err != nil {
+ return nil, err
+ }
+ result, err := Parse(rawBytes)
+ if err != nil {
+ return nil, err
+ }
+ result.filename = projectFile
+ return result, nil
+}
diff --git a/v2/internal/project/project_test.go b/v2/internal/project/project_test.go
new file mode 100644
index 000000000..8c080307b
--- /dev/null
+++ b/v2/internal/project/project_test.go
@@ -0,0 +1,142 @@
+package project_test
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "github.com/samber/lo"
+ "github.com/wailsapp/wails/v2/internal/project"
+)
+
+func TestProject_GetFrontendDir(t *testing.T) {
+ cwd := lo.Must(os.Getwd())
+ tests := []struct {
+ name string
+ inputJSON string
+ want string
+ wantError bool
+ }{
+ {
+ name: "Should use 'frontend' by default",
+ inputJSON: "{}",
+ want: filepath.ToSlash(filepath.Join(cwd, "frontend")),
+ wantError: false,
+ },
+ {
+ name: "Should resolve a relative path with no project path",
+ inputJSON: `{"frontend:dir": "./frontend"}`,
+ want: filepath.ToSlash(filepath.Join(cwd, "frontend")),
+ wantError: false,
+ },
+ {
+ name: "Should resolve a relative path with project path set",
+ inputJSON: func() string {
+ if runtime.GOOS == "windows" {
+ return `{"frontend:dir": "./frontend", "projectdir": "C:\\project"}`
+ } else {
+ return `{"frontend:dir": "./frontend", "projectdir": "/home/user/project"}`
+ }
+ }(),
+ want: func() string {
+ if runtime.GOOS == "windows" {
+ return `C:/project/frontend`
+ } else {
+ return `/home/user/project/frontend`
+ }
+ }(),
+ wantError: false,
+ },
+ {
+ name: "Should honour an absolute path",
+ inputJSON: func() string {
+ if runtime.GOOS == "windows" {
+ return `{"frontend:dir": "C:\\frontend", "projectdir": "C:\\project"}`
+ } else {
+ return `{"frontend:dir": "/home/myproject/frontend", "projectdir": "/home/user/project"}`
+ }
+ }(),
+ want: func() string {
+ if runtime.GOOS == "windows" {
+ return `C:/frontend`
+ } else {
+ return `/home/myproject/frontend`
+ }
+ }(),
+ wantError: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ proj, err := project.Parse([]byte(tt.inputJSON))
+ if err != nil && !tt.wantError {
+ t.Errorf("Error parsing project: %s", err)
+ }
+ got := proj.GetFrontendDir()
+ got = filepath.ToSlash(got)
+ if got != tt.want {
+ t.Errorf("GetFrontendDir() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+func TestProject_GetBuildDir(t *testing.T) {
+ cwd := lo.Must(os.Getwd())
+ tests := []struct {
+ name string
+ inputJSON string
+ want string
+ wantError bool
+ }{
+ {
+ name: "Should use 'build' by default",
+ inputJSON: "{}",
+ want: filepath.ToSlash(filepath.Join(cwd, "build")),
+ wantError: false,
+ },
+ {
+ name: "Should resolve a relative path with no project path",
+ inputJSON: `{"build:dir": "./build"}`,
+ want: filepath.ToSlash(filepath.Join(cwd, "build")),
+ wantError: false,
+ },
+ {
+ name: "Should resolve a relative path with project path set",
+ inputJSON: `{"build:dir": "./build", "projectdir": "/home/user/project"}`,
+ want: "/home/user/project/build",
+ wantError: false,
+ },
+ {
+ name: "Should honour an absolute path",
+ inputJSON: func() string {
+ if runtime.GOOS == "windows" {
+ return `{"build:dir": "C:\\build", "projectdir": "C:\\project"}`
+ } else {
+ return `{"build:dir": "/home/myproject/build", "projectdir": "/home/user/project"}`
+ }
+ }(),
+ want: func() string {
+ if runtime.GOOS == "windows" {
+ return `C:/build`
+ } else {
+ return `/home/myproject/build`
+ }
+ }(),
+ wantError: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ proj, err := project.Parse([]byte(tt.inputJSON))
+ if err != nil && !tt.wantError {
+ t.Errorf("Error parsing project: %s", err)
+ }
+ got := proj.GetBuildDir()
+ got = filepath.ToSlash(got)
+ if got != tt.want {
+ t.Errorf("GetFrontendDir() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/v2/internal/s/s.go b/v2/internal/s/s.go
new file mode 100644
index 000000000..adb304178
--- /dev/null
+++ b/v2/internal/s/s.go
@@ -0,0 +1,312 @@
+package s
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/bitfield/script"
+)
+
+var (
+ Output io.Writer = io.Discard
+ IndentSize int
+ originalOutput io.Writer
+ currentIndent int
+)
+
+func checkError(err error) {
+ if err != nil {
+ println("\nERROR:", err.Error())
+ os.Exit(1)
+ }
+}
+
+func mute() {
+ originalOutput = Output
+ Output = io.Discard
+}
+
+func unmute() {
+ Output = originalOutput
+}
+
+func indent() {
+ currentIndent += IndentSize
+}
+
+func unindent() {
+ currentIndent -= IndentSize
+}
+
+func log(message string, args ...interface{}) {
+ indent := strings.Repeat(" ", currentIndent)
+ _, err := fmt.Fprintf(Output, indent+message+"\n", args...)
+ checkError(err)
+}
+
+// RENAME a file or directory
+func RENAME(source string, target string) {
+ log("RENAME %s -> %s", source, target)
+ err := os.Rename(source, target)
+ checkError(err)
+}
+
+// MUSTDELETE a file.
+func MUSTDELETE(filename string) {
+ log("DELETE %s", filename)
+ err := os.Remove(filepath.Join(CWD(), filename))
+ checkError(err)
+}
+
+// DELETE a file.
+func DELETE(filename string) {
+ log("DELETE %s", filename)
+ _ = os.Remove(filepath.Join(CWD(), filename))
+}
+
+func CD(dir string) {
+ err := os.Chdir(dir)
+ checkError(err)
+ log("CD %s [%s]", dir, CWD())
+}
+
+func MKDIR(path string, mode ...os.FileMode) {
+ var perms os.FileMode
+ perms = 0o755
+ if len(mode) == 1 {
+ perms = mode[0]
+ }
+ log("MKDIR %s (perms: %v)", path, perms)
+ err := os.MkdirAll(path, perms)
+ checkError(err)
+}
+
+// ENDIR ensures that the path gets created if it doesn't exist
+func ENDIR(path string, mode ...os.FileMode) {
+ var perms os.FileMode
+ perms = 0o755
+ if len(mode) == 1 {
+ perms = mode[0]
+ }
+ _ = os.MkdirAll(path, perms)
+}
+
+// COPYDIR recursively copies a directory tree, attempting to preserve permissions.
+// Source directory must exist, destination directory must *not* exist.
+// Symlinks are ignored and skipped.
+// Credit: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04
+func COPYDIR(src string, dst string) {
+ log("COPYDIR %s -> %s", src, dst)
+ src = filepath.Clean(src)
+ dst = filepath.Clean(dst)
+
+ si, err := os.Stat(src)
+ checkError(err)
+ if !si.IsDir() {
+ checkError(fmt.Errorf("source is not a directory"))
+ }
+
+ _, err = os.Stat(dst)
+ if err != nil && !os.IsNotExist(err) {
+ checkError(err)
+ }
+ if err == nil {
+ checkError(fmt.Errorf("destination already exists"))
+ }
+
+ indent()
+ MKDIR(dst)
+
+ entries, err := os.ReadDir(src)
+ checkError(err)
+
+ for _, entry := range entries {
+ srcPath := filepath.Join(src, entry.Name())
+ dstPath := filepath.Join(dst, entry.Name())
+
+ if entry.IsDir() {
+ COPYDIR(srcPath, dstPath)
+ } else {
+ // Skip symlinks.
+ if entry.Type()&os.ModeSymlink != 0 {
+ continue
+ }
+
+ COPY(srcPath, dstPath)
+ }
+ }
+ unindent()
+}
+
+// COPY file from source to target
+func COPY(source string, target string) {
+ log("COPY %s -> %s", source, target)
+ src, err := os.Open(source)
+ checkError(err)
+ defer closefile(src)
+ d, err := os.Create(target)
+ checkError(err)
+ defer closefile(d)
+ _, err = io.Copy(d, src)
+ checkError(err)
+}
+
+func CWD() string {
+ result, err := os.Getwd()
+ checkError(err)
+ log("CWD [%s]", result)
+ return result
+}
+
+func RMDIR(target string) {
+ log("RMDIR %s", target)
+ err := os.RemoveAll(target)
+ checkError(err)
+}
+
+func RM(target string) {
+ log("RM %s", target)
+ err := os.Remove(target)
+ checkError(err)
+}
+
+func ECHO(message string) {
+ println(message)
+}
+
+func TOUCH(filepath string) {
+ log("TOUCH %s", filepath)
+ f, err := os.Create(filepath)
+ checkError(err)
+ closefile(f)
+}
+
+func EXEC(command string) {
+ log("EXEC %s", command)
+ gen := script.Exec(command)
+ gen.Wait()
+ checkError(gen.Error())
+}
+
+// EXISTS - Returns true if the given path exists
+func EXISTS(path string) bool {
+ _, err := os.Lstat(path)
+ log("EXISTS %s (%T)", path, err == nil)
+ return err == nil
+}
+
+// ISDIR returns true if the given directory exists
+func ISDIR(path string) bool {
+ fi, err := os.Lstat(path)
+ if err != nil {
+ return false
+ }
+
+ return fi.Mode().IsDir()
+}
+
+// ISDIREMPTY returns true if the given directory is empty
+func ISDIREMPTY(dir string) bool {
+ // CREDIT: https://stackoverflow.com/a/30708914/8325411
+ f, err := os.Open(dir)
+ checkError(err)
+ defer closefile(f)
+
+ _, err = f.Readdirnames(1) // Or f.Readdir(1)
+ return err == io.EOF
+}
+
+// ISFILE returns true if the given file exists
+func ISFILE(path string) bool {
+ fi, err := os.Lstat(path)
+ if err != nil {
+ return false
+ }
+
+ return fi.Mode().IsRegular()
+}
+
+// SUBDIRS returns a list of subdirectories for the given directory
+func SUBDIRS(rootDir string) []string {
+ var result []string
+
+ // Iterate root dir
+ err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
+ checkError(err)
+ // If we have a directory, save it
+ if info.IsDir() {
+ result = append(result, path)
+ }
+ return nil
+ })
+ checkError(err)
+ return result
+}
+
+// SAVESTRING will create a file with the given string
+func SAVESTRING(filename string, data string) {
+ log("SAVESTRING %s", filename)
+ mute()
+ SAVEBYTES(filename, []byte(data))
+ unmute()
+}
+
+// LOADSTRING returns the contents of the given filename as a string
+func LOADSTRING(filename string) string {
+ log("LOADSTRING %s", filename)
+ mute()
+ data := LOADBYTES(filename)
+ unmute()
+ return string(data)
+}
+
+// SAVEBYTES will create a file with the given string
+func SAVEBYTES(filename string, data []byte) {
+ log("SAVEBYTES %s", filename)
+ err := os.WriteFile(filename, data, 0o755)
+ checkError(err)
+}
+
+// LOADBYTES returns the contents of the given filename as a string
+func LOADBYTES(filename string) []byte {
+ log("LOADBYTES %s", filename)
+ data, err := os.ReadFile(filename)
+ checkError(err)
+ return data
+}
+
+func closefile(f *os.File) {
+ err := f.Close()
+ checkError(err)
+}
+
+// MD5FILE returns the md5sum of the given file
+func MD5FILE(filename string) string {
+ f, err := os.Open(filename)
+ checkError(err)
+ defer closefile(f)
+
+ h := md5.New()
+ _, err = io.Copy(h, f)
+ checkError(err)
+
+ return hex.EncodeToString(h.Sum(nil))
+}
+
+// Sub is the substitution type
+type Sub map[string]string
+
+// REPLACEALL replaces all substitution keys with associated values in the given file
+func REPLACEALL(filename string, substitutions Sub) {
+ log("REPLACEALL %s (%v)", filename, substitutions)
+ data := LOADSTRING(filename)
+ for old, newText := range substitutions {
+ data = strings.ReplaceAll(data, old, newText)
+ }
+ SAVESTRING(filename, data)
+}
diff --git a/v2/internal/shell/env.go b/v2/internal/shell/env.go
new file mode 100644
index 000000000..ad6a64360
--- /dev/null
+++ b/v2/internal/shell/env.go
@@ -0,0 +1,40 @@
+package shell
+
+import (
+ "fmt"
+ "strings"
+)
+
+func UpsertEnv(env []string, key string, update func(v string) string) []string {
+ newEnv := make([]string, len(env), len(env)+1)
+ found := false
+ for i := range env {
+ if strings.HasPrefix(env[i], key+"=") {
+ eqIndex := strings.Index(env[i], "=")
+ val := env[i][eqIndex+1:]
+ newEnv[i] = fmt.Sprintf("%s=%v", key, update(val))
+ found = true
+ continue
+ }
+ newEnv[i] = env[i]
+ }
+ if !found {
+ newEnv = append(newEnv, fmt.Sprintf("%s=%v", key, update("")))
+ }
+ return newEnv
+}
+
+func RemoveEnv(env []string, key string) []string {
+ newEnv := make([]string, 0, len(env))
+ for _, e := range env {
+ if strings.HasPrefix(e, key+"=") {
+ continue
+ }
+ newEnv = append(newEnv, e)
+ }
+ return newEnv
+}
+
+func SetEnv(env []string, key string, value string) []string {
+ return UpsertEnv(env, key, func(_ string) string { return value })
+}
diff --git a/v2/internal/shell/env_test.go b/v2/internal/shell/env_test.go
new file mode 100644
index 000000000..ca41c84dc
--- /dev/null
+++ b/v2/internal/shell/env_test.go
@@ -0,0 +1,67 @@
+package shell
+
+import "testing"
+
+func TestUpdateEnv(t *testing.T) {
+
+ env := []string{"one=1", "two=a=b", "three="}
+ newEnv := UpsertEnv(env, "two", func(v string) string {
+ return v + "+added"
+ })
+ newEnv = UpsertEnv(newEnv, "newVar", func(v string) string {
+ return "added"
+ })
+ newEnv = UpsertEnv(newEnv, "three", func(v string) string {
+ return "3"
+ })
+ newEnv = UpsertEnv(newEnv, "GOARCH", func(v string) string {
+ return "amd64"
+ })
+
+ if len(newEnv) != 5 {
+ t.Errorf("expected: 5, got: %d", len(newEnv))
+ }
+ if newEnv[1] != "two=a=b+added" {
+ t.Errorf("expected: \"two=a=b+added\", got: %q", newEnv[1])
+ }
+ if newEnv[2] != "three=3" {
+ t.Errorf("expected: \"three=3\", got: %q", newEnv[2])
+ }
+ if newEnv[3] != "newVar=added" {
+ t.Errorf("expected: \"newVar=added\", got: %q", newEnv[3])
+ }
+ if newEnv[4] != "GOARCH=amd64" {
+ t.Errorf("expected: \"newVar=added\", got: %q", newEnv[4])
+ }
+}
+
+func TestSetEnv(t *testing.T) {
+ env := []string{"one=1", "two=a=b", "three="}
+ newEnv := SetEnv(env, "two", "set")
+ newEnv = SetEnv(newEnv, "newVar", "added")
+
+ if len(newEnv) != 4 {
+ t.Errorf("expected: 4, got: %d", len(newEnv))
+ }
+ if newEnv[1] != "two=set" {
+ t.Errorf("expected: \"two=set\", got: %q", newEnv[1])
+ }
+ if newEnv[3] != "newVar=added" {
+ t.Errorf("expected: \"newVar=added\", got: %q", newEnv[3])
+ }
+}
+
+func TestRemoveEnv(t *testing.T) {
+ env := []string{"one=1", "two=a=b", "three=3"}
+ newEnv := RemoveEnv(env, "two")
+
+ if len(newEnv) != 2 {
+ t.Errorf("expected: 2, got: %d", len(newEnv))
+ }
+ if newEnv[0] != "one=1" {
+ t.Errorf("expected: \"one=1\", got: %q", newEnv[1])
+ }
+ if newEnv[1] != "three=3" {
+ t.Errorf("expected: \"three=3\", got: %q", newEnv[3])
+ }
+}
diff --git a/v2/internal/shell/shell.go b/v2/internal/shell/shell.go
new file mode 100644
index 000000000..349e27bff
--- /dev/null
+++ b/v2/internal/shell/shell.go
@@ -0,0 +1,98 @@
+package shell
+
+import (
+ "bytes"
+ "os"
+ "os/exec"
+)
+
+type Command struct {
+ command string
+ args []string
+ env []string
+ dir string
+ stdo, stde bytes.Buffer
+}
+
+func NewCommand(command string) *Command {
+ return &Command{
+ command: command,
+ env: os.Environ(),
+ }
+}
+
+func (c *Command) Dir(dir string) {
+ c.dir = dir
+}
+
+func (c *Command) Env(name string, value string) {
+ c.env = append(c.env, name+"="+value)
+}
+
+func (c *Command) Run() error {
+ cmd := exec.Command(c.command, c.args...)
+ if c.dir != "" {
+ cmd.Dir = c.dir
+ }
+ cmd.Stdout = &c.stdo
+ cmd.Stderr = &c.stde
+ return cmd.Run()
+}
+
+func (c *Command) Stdout() string {
+ return c.stdo.String()
+}
+
+func (c *Command) Stderr() string {
+ return c.stde.String()
+}
+
+func (c *Command) AddArgs(args []string) {
+ c.args = append(c.args, args...)
+}
+
+// CreateCommand returns a *Cmd struct that when run, will run the given command + args in the given directory
+func CreateCommand(directory string, command string, args ...string) *exec.Cmd {
+ cmd := exec.Command(command, args...)
+ cmd.Dir = directory
+ return cmd
+}
+
+// RunCommand will run the given command + args in the given directory
+// Will return stdout, stderr and error
+func RunCommand(directory string, command string, args ...string) (string, string, error) {
+ return RunCommandWithEnv(nil, directory, command, args...)
+}
+
+// RunCommandWithEnv will run the given command + args in the given directory and using the specified env.
+//
+// Env specifies the environment of the process. Each entry is of the form "key=value".
+// If Env is nil, the new process uses the current process's environment.
+//
+// Will return stdout, stderr and error
+func RunCommandWithEnv(env []string, directory string, command string, args ...string) (string, string, error) {
+ cmd := CreateCommand(directory, command, args...)
+ cmd.Env = env
+
+ var stdo, stde bytes.Buffer
+ cmd.Stdout = &stdo
+ cmd.Stderr = &stde
+ err := cmd.Run()
+ return stdo.String(), stde.String(), err
+}
+
+// RunCommandVerbose will run the given command + args in the given directory
+// Will return an error if one occurs
+func RunCommandVerbose(directory string, command string, args ...string) error {
+ cmd := CreateCommand(directory, command, args...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err := cmd.Run()
+ return err
+}
+
+// CommandExists returns true if the given command can be found on the shell
+func CommandExists(name string) bool {
+ _, err := exec.LookPath(name)
+ return err == nil
+}
diff --git a/v2/internal/signal/signal.go b/v2/internal/signal/signal.go
new file mode 100644
index 000000000..fa797453f
--- /dev/null
+++ b/v2/internal/signal/signal.go
@@ -0,0 +1,38 @@
+package signal
+
+import (
+ "os"
+ gosignal "os/signal"
+ "sync"
+ "syscall"
+)
+
+var signalChannel = make(chan os.Signal, 2)
+
+var (
+ callbacks []func()
+ lock sync.Mutex
+)
+
+func OnShutdown(callback func()) {
+ lock.Lock()
+ defer lock.Unlock()
+ callbacks = append(callbacks, callback)
+}
+
+// Start the Signal Manager
+func Start() {
+ // Hook into interrupts
+ gosignal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
+
+ // Spin off signal listener and wait for either a cancellation
+ // or signal
+ go func() {
+ <-signalChannel
+ println("")
+ println("Ctrl+C detected. Shutting down...")
+ for _, callback := range callbacks {
+ callback()
+ }
+ }()
+}
diff --git a/v2/internal/staticanalysis/staticanalysis.go b/v2/internal/staticanalysis/staticanalysis.go
new file mode 100644
index 000000000..cde436633
--- /dev/null
+++ b/v2/internal/staticanalysis/staticanalysis.go
@@ -0,0 +1,84 @@
+package staticanalysis
+
+import (
+ "go/ast"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/tools/go/packages"
+)
+
+type EmbedDetails struct {
+ BaseDir string
+ EmbedPath string
+ All bool
+}
+
+func (e *EmbedDetails) GetFullPath() string {
+ return filepath.Join(e.BaseDir, e.EmbedPath)
+}
+
+func GetEmbedDetails(sourcePath string) ([]*EmbedDetails, error) {
+ // read in project files and determine which directories are used for embedding
+ // return a list of directories
+
+ absPath, err := filepath.Abs(sourcePath)
+ if err != nil {
+ return nil, err
+ }
+ pkgs, err := packages.Load(&packages.Config{
+ Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedCompiledGoFiles,
+ Dir: absPath,
+ }, "./...")
+ if err != nil {
+ return nil, err
+ }
+ var result []*EmbedDetails
+ for _, pkg := range pkgs {
+ for index, file := range pkg.Syntax {
+ baseDir := filepath.Dir(pkg.CompiledGoFiles[index])
+ embedPaths := GetEmbedDetailsForFile(file, baseDir)
+ if len(embedPaths) > 0 {
+ result = append(result, embedPaths...)
+ }
+ }
+ }
+ return result, nil
+}
+
+func GetEmbedDetailsForFile(file *ast.File, baseDir string) []*EmbedDetails {
+ var result []*EmbedDetails
+ for _, comment := range file.Comments {
+ for _, c := range comment.List {
+ if strings.HasPrefix(c.Text, "//go:embed") {
+ sl := strings.Split(c.Text, " ")
+ if len(sl) == 1 {
+ continue
+ }
+ // support for multiple paths in one comment
+ for _, arg := range sl[1:] {
+ embedPath := strings.TrimSpace(arg)
+ // ignores all pattern matching characters except escape sequence
+ if strings.Contains(embedPath, "*") || strings.Contains(embedPath, "?") || strings.Contains(embedPath, "[") {
+ continue
+ }
+ if strings.HasPrefix(embedPath, "all:") {
+ result = append(result, &EmbedDetails{
+ EmbedPath: strings.TrimPrefix(embedPath, "all:"),
+ All: true,
+ BaseDir: baseDir,
+ })
+ } else {
+ result = append(result, &EmbedDetails{
+ EmbedPath: embedPath,
+ All: false,
+ BaseDir: baseDir,
+ })
+ }
+
+ }
+ }
+ }
+ }
+ return result
+}
diff --git a/v2/internal/staticanalysis/staticanalysis_test.go b/v2/internal/staticanalysis/staticanalysis_test.go
new file mode 100644
index 000000000..77ad2fa6c
--- /dev/null
+++ b/v2/internal/staticanalysis/staticanalysis_test.go
@@ -0,0 +1,51 @@
+package staticanalysis
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetEmbedDetails(t *testing.T) {
+ type args struct {
+ sourcePath string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []*EmbedDetails
+ wantErr bool
+ }{
+ {
+ name: "GetEmbedDetails",
+ args: args{
+ sourcePath: "test/standard",
+ },
+ want: []*EmbedDetails{
+ {
+ EmbedPath: "frontend/dist",
+ All: true,
+ },
+ {
+ EmbedPath: "frontend/static",
+ All: false,
+ },
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := GetEmbedDetails(tt.args.sourcePath)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("GetEmbedDetails() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ require.Equal(t, len(tt.want), len(got))
+ for index, g := range got {
+ require.Equal(t, tt.want[index].EmbedPath, g.EmbedPath)
+ require.Equal(t, tt.want[index].All, g.All)
+ }
+ })
+ }
+}
diff --git a/v2/internal/staticanalysis/test/standard/.gitignore b/v2/internal/staticanalysis/test/standard/.gitignore
new file mode 100644
index 000000000..d44c22f8c
--- /dev/null
+++ b/v2/internal/staticanalysis/test/standard/.gitignore
@@ -0,0 +1,3 @@
+build/bin
+node_modules
+frontend/dist
\ No newline at end of file
diff --git a/v2/internal/staticanalysis/test/standard/README.md b/v2/internal/staticanalysis/test/standard/README.md
new file mode 100644
index 000000000..397b08b92
--- /dev/null
+++ b/v2/internal/staticanalysis/test/standard/README.md
@@ -0,0 +1,19 @@
+# README
+
+## About
+
+This is the official Wails Vanilla template.
+
+You can configure the project by editing `wails.json`. More information about the project settings can be found
+here: https://wails.io/docs/reference/project-config
+
+## Live Development
+
+To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
+server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
+and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
+to this in your browser, and you can call your Go code from devtools.
+
+## Building
+
+To build a redistributable, production mode package, use `wails build`.
diff --git a/v2/internal/staticanalysis/test/standard/app.go b/v2/internal/staticanalysis/test/standard/app.go
new file mode 100644
index 000000000..af53038a1
--- /dev/null
+++ b/v2/internal/staticanalysis/test/standard/app.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "context"
+ "fmt"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called when the app starts. The context is saved
+// so we can call the runtime methods
+func (a *App) startup(ctx context.Context) {
+ a.ctx = ctx
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+ return fmt.Sprintf("Hello %s, It's show time!", name)
+}
diff --git a/v2/internal/staticanalysis/test/standard/go.mod b/v2/internal/staticanalysis/test/standard/go.mod
new file mode 100644
index 000000000..c9fe1fb52
--- /dev/null
+++ b/v2/internal/staticanalysis/test/standard/go.mod
@@ -0,0 +1,35 @@
+module changeme
+
+go 1.18
+
+require github.com/wailsapp/wails/v2 v2.3.1
+
+require (
+ github.com/bep/debounce v1.2.1 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/imdario/mergo v0.3.13 // indirect
+ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
+ github.com/labstack/echo/v4 v4.9.1 // indirect
+ github.com/labstack/gommon v0.4.0 // indirect
+ github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
+ github.com/leaanthony/gosod v1.0.3 // indirect
+ github.com/leaanthony/slicer v1.6.0 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.16 // indirect
+ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rivo/uniseg v0.4.2 // indirect
+ github.com/samber/lo v1.27.1 // indirect
+ github.com/tkrajina/go-reflector v0.5.6 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasttemplate v1.2.1 // indirect
+ github.com/wailsapp/mimetype v1.4.1 // indirect
+ golang.org/x/crypto v0.21.0 // indirect
+ golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
+ golang.org/x/net v0.23.0 // indirect
+ golang.org/x/sys v0.18.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+)
+
+// replace github.com/wailsapp/wails/v2 v2.0.0 => C:\Users\leaan
diff --git a/v2/internal/staticanalysis/test/standard/go.sum b/v2/internal/staticanalysis/test/standard/go.sum
new file mode 100644
index 000000000..2cd0cf773
--- /dev/null
+++ b/v2/internal/staticanalysis/test/standard/go.sum
@@ -0,0 +1,87 @@
+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y=
+github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo=
+github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
+github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
+github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
+github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
+github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
+github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
+github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
+github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
+github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
+github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
+github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/samber/lo v1.27.1 h1:sTXwkRiIFIQG+G0HeAvOEnGjqWeWtI9cg5/n51KrxPg=
+github.com/samber/lo v1.27.1/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
+github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
+github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
+github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
+github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
+github.com/wailsapp/wails/v2 v2.3.1 h1:ZJz+pyIBKyASkgO8JO31NuHO1gTTHmvwiHYHwei1CqM=
+github.com/wailsapp/wails/v2 v2.3.1/go.mod h1:zlNLI0E2c2qA6miiuAHtp0Bac8FaGH0tlhA19OssR/8=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
+golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/v2/internal/staticanalysis/test/standard/main.go b/v2/internal/staticanalysis/test/standard/main.go
new file mode 100644
index 000000000..2b6ab33b6
--- /dev/null
+++ b/v2/internal/staticanalysis/test/standard/main.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "embed"
+
+ "github.com/wailsapp/wails/v2"
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+//go:embed all:frontend/dist frontend/static
+var assets embed.FS
+
+//go:embed frontend/src/*.json
+var srcjson embed.FS
+
+func main() {
+ // Create an instance of the app structure
+ app := NewApp()
+
+ // Create application with options
+ err := wails.Run(&options.App{
+ Title: "staticanalysis",
+ Width: 1024,
+ Height: 768,
+ AssetServer: &assetserver.Options{
+ Assets: assets,
+ },
+ BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+ OnStartup: app.startup,
+ Bind: []interface{}{
+ app,
+ },
+ })
+
+ if err != nil {
+ println("Error:", err.Error())
+ }
+}
diff --git a/v2/internal/staticanalysis/test/standard/wails.json b/v2/internal/staticanalysis/test/standard/wails.json
new file mode 100644
index 000000000..5ab7f3600
--- /dev/null
+++ b/v2/internal/staticanalysis/test/standard/wails.json
@@ -0,0 +1,12 @@
+{
+ "name": "staticanalysis",
+ "outputfilename": "staticanalysis",
+ "frontend:install": "npm install",
+ "frontend:build": "npm run build",
+ "frontend:dev:watcher": "npm run dev",
+ "frontend:dev:serverUrl": "auto",
+ "author": {
+ "name": "Lea Anthony",
+ "email": "lea.anthony@gmail.com"
+ }
+}
diff --git a/v2/internal/system/operatingsystem/os.go b/v2/internal/system/operatingsystem/os.go
new file mode 100644
index 000000000..028a97b2e
--- /dev/null
+++ b/v2/internal/system/operatingsystem/os.go
@@ -0,0 +1,14 @@
+package operatingsystem
+
+// OS contains information about the operating system
+type OS struct {
+ ID string
+ Name string
+ Version string
+ Branding string
+}
+
+// Info retrieves information about the current platform
+func Info() (*OS, error) {
+ return platformInfo()
+}
diff --git a/v2/internal/system/operatingsystem/os_darwin.go b/v2/internal/system/operatingsystem/os_darwin.go
new file mode 100644
index 000000000..8083e1aed
--- /dev/null
+++ b/v2/internal/system/operatingsystem/os_darwin.go
@@ -0,0 +1,49 @@
+package operatingsystem
+
+import (
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+func getSysctlValue(key string) (string, error) {
+ stdout, _, err := shell.RunCommand(".", "sysctl", key)
+ if err != nil {
+ return "", err
+ }
+ version := strings.TrimPrefix(stdout, key+": ")
+ return strings.TrimSpace(version), nil
+}
+
+func platformInfo() (*OS, error) {
+ // Default value
+ var result OS
+ result.ID = "Unknown"
+ result.Name = "MacOS"
+ result.Version = "Unknown"
+
+ version, err := getSysctlValue("kern.osproductversion")
+ if err != nil {
+ return nil, err
+ }
+ result.Version = version
+ ID, err := getSysctlValue("kern.osversion")
+ if err != nil {
+ return nil, err
+ }
+ result.ID = ID
+
+ // cmd := CreateCommand(directory, command, args...)
+ // var stdo, stde bytes.Buffer
+ // cmd.Stdout = &stdo
+ // cmd.Stderr = &stde
+ // err := cmd.Run()
+ // return stdo.String(), stde.String(), err
+ // }
+ // sysctl := shell.NewCommand("sysctl")
+ // kern.ostype: Darwin
+ // kern.osrelease: 20.1.0
+ // kern.osrevision: 199506
+
+ return &result, nil
+}
diff --git a/v2/internal/system/operatingsystem/os_linux.go b/v2/internal/system/operatingsystem/os_linux.go
new file mode 100644
index 000000000..49e00c02c
--- /dev/null
+++ b/v2/internal/system/operatingsystem/os_linux.go
@@ -0,0 +1,51 @@
+//go:build linux
+// +build linux
+
+package operatingsystem
+
+import (
+ "fmt"
+ "os"
+ "strings"
+)
+
+// platformInfo is the platform specific method to get system information
+func platformInfo() (*OS, error) {
+ _, err := os.Stat("/etc/os-release")
+ if os.IsNotExist(err) {
+ return nil, fmt.Errorf("unable to read system information")
+ }
+
+ osRelease, _ := os.ReadFile("/etc/os-release")
+ return parseOsRelease(string(osRelease)), nil
+}
+
+func parseOsRelease(osRelease string) *OS {
+
+ // Default value
+ var result OS
+ result.ID = "Unknown"
+ result.Name = "Unknown"
+ result.Version = "Unknown"
+
+ // Split into lines
+ lines := strings.Split(osRelease, "\n")
+ // Iterate lines
+ for _, line := range lines {
+ // Split each line by the equals char
+ splitLine := strings.SplitN(line, "=", 2)
+ // Check we have
+ if len(splitLine) != 2 {
+ continue
+ }
+ switch splitLine[0] {
+ case "ID":
+ result.ID = strings.ToLower(strings.Trim(splitLine[1], "\""))
+ case "NAME":
+ result.Name = strings.Trim(splitLine[1], "\"")
+ case "VERSION_ID":
+ result.Version = strings.Trim(splitLine[1], "\"")
+ }
+ }
+ return &result
+}
diff --git a/v2/internal/system/operatingsystem/os_windows.go b/v2/internal/system/operatingsystem/os_windows.go
new file mode 100644
index 000000000..a9aa05a92
--- /dev/null
+++ b/v2/internal/system/operatingsystem/os_windows.go
@@ -0,0 +1,67 @@
+//go:build windows
+
+package operatingsystem
+
+import (
+ "fmt"
+ "strings"
+ "syscall"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+ "golang.org/x/sys/windows/registry"
+)
+
+func stripNulls(str string) string {
+ // Split the string into substrings at each null character
+ substrings := strings.Split(str, "\x00")
+
+ // Join the substrings back into a single string
+ strippedStr := strings.Join(substrings, "")
+
+ return strippedStr
+}
+
+func mustStringToUTF16Ptr(input string) *uint16 {
+ input = stripNulls(input)
+ result, err := syscall.UTF16PtrFromString(input)
+ if err != nil {
+ panic(err)
+ }
+ return result
+}
+
+func getBranding() string {
+ var modBranding = syscall.NewLazyDLL("winbrand.dll")
+ var brandingFormatString = modBranding.NewProc("BrandingFormatString")
+
+ windowsLong := mustStringToUTF16Ptr("%WINDOWS_LONG%\x00")
+ ret, _, _ := brandingFormatString.Call(
+ uintptr(unsafe.Pointer(windowsLong)),
+ )
+ return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ret)))
+}
+
+func platformInfo() (*OS, error) {
+ // Default value
+ var result OS
+ result.ID = "Unknown"
+ result.Name = "Windows"
+ result.Version = "Unknown"
+
+ // Credit: https://stackoverflow.com/a/33288328
+ // Ignore errors as it isn't a showstopper
+ key, _ := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
+
+ productName, _, _ := key.GetStringValue("ProductName")
+ currentBuild, _, _ := key.GetStringValue("CurrentBuildNumber")
+ displayVersion, _, _ := key.GetStringValue("DisplayVersion")
+ releaseId, _, _ := key.GetStringValue("ReleaseId")
+
+ result.Name = productName
+ result.Version = fmt.Sprintf("%s (Build: %s)", releaseId, currentBuild)
+ result.ID = displayVersion
+ result.Branding = getBranding()
+
+ return &result, key.Close()
+}
diff --git a/v2/internal/system/operatingsystem/version_windows.go b/v2/internal/system/operatingsystem/version_windows.go
new file mode 100644
index 000000000..a8f53d134
--- /dev/null
+++ b/v2/internal/system/operatingsystem/version_windows.go
@@ -0,0 +1,62 @@
+//go:build windows
+
+package operatingsystem
+
+import (
+ "strconv"
+
+ "golang.org/x/sys/windows/registry"
+)
+
+type WindowsVersionInfo struct {
+ Major int
+ Minor int
+ Build int
+ DisplayVersion string
+}
+
+func (w *WindowsVersionInfo) IsWindowsVersionAtLeast(major, minor, buildNumber int) bool {
+ return w.Major >= major && w.Minor >= minor && w.Build >= buildNumber
+}
+
+func GetWindowsVersionInfo() (*WindowsVersionInfo, error) {
+ key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
+ if err != nil {
+ return nil, err
+ }
+
+ return &WindowsVersionInfo{
+ Major: regDWORDKeyAsInt(key, "CurrentMajorVersionNumber"),
+ Minor: regDWORDKeyAsInt(key, "CurrentMinorVersionNumber"),
+ Build: regStringKeyAsInt(key, "CurrentBuildNumber"),
+ DisplayVersion: regKeyAsString(key, "DisplayVersion"),
+ }, nil
+}
+
+func regDWORDKeyAsInt(key registry.Key, name string) int {
+ result, _, err := key.GetIntegerValue(name)
+ if err != nil {
+ return -1
+ }
+ return int(result)
+}
+
+func regStringKeyAsInt(key registry.Key, name string) int {
+ resultStr, _, err := key.GetStringValue(name)
+ if err != nil {
+ return -1
+ }
+ result, err := strconv.Atoi(resultStr)
+ if err != nil {
+ return -1
+ }
+ return result
+}
+
+func regKeyAsString(key registry.Key, name string) string {
+ resultStr, _, err := key.GetStringValue(name)
+ if err != nil {
+ return ""
+ }
+ return resultStr
+}
diff --git a/v2/internal/system/packagemanager/apt.go b/v2/internal/system/packagemanager/apt.go
new file mode 100644
index 000000000..806d08f2d
--- /dev/null
+++ b/v2/internal/system/packagemanager/apt.go
@@ -0,0 +1,109 @@
+//go:build linux
+// +build linux
+
+package packagemanager
+
+import (
+ "bytes"
+ "os"
+ "os/exec"
+ "regexp"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+// Apt represents the Apt manager
+type Apt struct {
+ name string
+ osid string
+}
+
+// NewApt creates a new Apt instance
+func NewApt(osid string) *Apt {
+ return &Apt{
+ name: "apt",
+ osid: osid,
+ }
+}
+
+// Packages returns the libraries that we need for Wails to compile
+// They will potentially differ on different distributions or versions
+func (a *Apt) Packages() packagemap {
+ return packagemap{
+ "libgtk-3": []*Package{
+ {Name: "libgtk-3-dev", SystemPackage: true, Library: true},
+ },
+ "libwebkit": []*Package{
+ {Name: "libwebkit2gtk-4.0-dev", SystemPackage: true, Library: true},
+ },
+ "gcc": []*Package{
+ {Name: "build-essential", SystemPackage: true},
+ },
+ "pkg-config": []*Package{
+ {Name: "pkg-config", SystemPackage: true},
+ },
+ "npm": []*Package{
+ {Name: "npm", SystemPackage: true},
+ },
+ "docker": []*Package{
+ {Name: "docker.io", SystemPackage: true, Optional: true},
+ },
+ "nsis": []*Package{
+ {Name: "nsis", SystemPackage: true, Optional: true},
+ },
+ }
+}
+
+// Name returns the name of the package manager
+func (a *Apt) Name() string {
+ return a.name
+}
+
+// PackageInstalled tests if the given package name is installed
+func (a *Apt) PackageInstalled(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ cmd := exec.Command("apt", "list", "-qq", pkg.Name)
+ var stdo, stde bytes.Buffer
+ cmd.Stdout = &stdo
+ cmd.Stderr = &stde
+ cmd.Env = append(os.Environ(), "LANGUAGE=en")
+ err := cmd.Run()
+ return strings.Contains(stdo.String(), "[installed]"), err
+}
+
+// PackageAvailable tests if the given package is available for installation
+func (a *Apt) PackageAvailable(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ stdout, _, err := shell.RunCommand(".", "apt", "list", "-qq", pkg.Name)
+ // We add a space to ensure we get a full match, not partial match
+ output := a.removeEscapeSequences(stdout)
+ installed := strings.HasPrefix(output, pkg.Name)
+ a.getPackageVersion(pkg, output)
+ return installed, err
+}
+
+// InstallCommand returns the package manager specific command to install a package
+func (a *Apt) InstallCommand(pkg *Package) string {
+ if pkg.SystemPackage == false {
+ return pkg.InstallCommand[a.osid]
+ }
+ return "sudo apt install " + pkg.Name
+}
+
+func (a *Apt) removeEscapeSequences(in string) string {
+ escapechars, _ := regexp.Compile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`)
+ return escapechars.ReplaceAllString(in, "")
+}
+
+func (a *Apt) getPackageVersion(pkg *Package, output string) {
+
+ splitOutput := strings.Split(output, " ")
+ if len(splitOutput) > 1 {
+ pkg.Version = splitOutput[1]
+ }
+}
diff --git a/v2/internal/system/packagemanager/dnf.go b/v2/internal/system/packagemanager/dnf.go
new file mode 100644
index 000000000..fec676f11
--- /dev/null
+++ b/v2/internal/system/packagemanager/dnf.go
@@ -0,0 +1,133 @@
+//go:build linux
+// +build linux
+
+package packagemanager
+
+import (
+ "os/exec"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+// Dnf represents the Dnf manager
+type Dnf struct {
+ name string
+ osid string
+}
+
+// NewDnf creates a new Dnf instance
+func NewDnf(osid string) *Dnf {
+ return &Dnf{
+ name: "dnf",
+ osid: osid,
+ }
+}
+
+// Packages returns the libraries that we need for Wails to compile
+// They will potentially differ on different distributions or versions
+func (y *Dnf) Packages() packagemap {
+ return packagemap{
+ "libgtk-3": []*Package{
+ {Name: "gtk3-devel", SystemPackage: true, Library: true},
+ },
+ "libwebkit": []*Package{
+ {Name: "webkit2gtk4.0-devel", SystemPackage: true, Library: true},
+ {Name: "webkit2gtk3-devel", SystemPackage: true, Library: true},
+ // {Name: "webkitgtk3-devel", SystemPackage: true, Library: true},
+ },
+ "gcc": []*Package{
+ {Name: "gcc-c++", SystemPackage: true},
+ },
+ "pkg-config": []*Package{
+ {Name: "pkgconf-pkg-config", SystemPackage: true},
+ },
+ "npm": []*Package{
+ {Name: "npm", SystemPackage: true},
+ {Name: "nodejs-npm", SystemPackage: true},
+ },
+ "upx": []*Package{
+ {Name: "upx", SystemPackage: true, Optional: true},
+ },
+ "docker": []*Package{
+ {
+ SystemPackage: false,
+ Optional: true,
+ InstallCommand: map[string]string{
+ "centos": "Follow the guide: https://docs.docker.com/engine/install/centos/",
+ "fedora": "Follow the guide: https://docs.docker.com/engine/install/fedora/",
+ },
+ },
+ {Name: "moby-engine", SystemPackage: true, Optional: true},
+ },
+ }
+}
+
+// Name returns the name of the package manager
+func (y *Dnf) Name() string {
+ return y.name
+}
+
+// PackageInstalled tests if the given package name is installed
+func (y *Dnf) PackageInstalled(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ stdout, _, err := shell.RunCommand(".", "dnf", "info", "installed", pkg.Name)
+ if err != nil {
+ _, ok := err.(*exec.ExitError)
+ if ok {
+ return false, nil
+ }
+ return false, err
+ }
+
+ splitoutput := strings.Split(stdout, "\n")
+ for _, line := range splitoutput {
+ if strings.HasPrefix(line, "Version") {
+ splitline := strings.Split(line, ":")
+ pkg.Version = strings.TrimSpace(splitline[1])
+ }
+ }
+
+ return true, err
+}
+
+// PackageAvailable tests if the given package is available for installation
+func (y *Dnf) PackageAvailable(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ stdout, _, err := shell.RunCommand(".", "dnf", "info", pkg.Name)
+ // We add a space to ensure we get a full match, not partial match
+ if err != nil {
+ _, ok := err.(*exec.ExitError)
+ if ok {
+ return false, nil
+ }
+ return false, err
+ }
+ splitoutput := strings.Split(stdout, "\n")
+ for _, line := range splitoutput {
+ if strings.HasPrefix(line, "Version") {
+ splitline := strings.Split(line, ":")
+ pkg.Version = strings.TrimSpace(splitline[1])
+ }
+ }
+ return true, nil
+}
+
+// InstallCommand returns the package manager specific command to install a package
+func (y *Dnf) InstallCommand(pkg *Package) string {
+ if pkg.SystemPackage == false {
+ return pkg.InstallCommand[y.osid]
+ }
+ return "sudo dnf install " + pkg.Name
+}
+
+func (y *Dnf) getPackageVersion(pkg *Package, output string) {
+ splitOutput := strings.Split(output, " ")
+ if len(splitOutput) > 0 {
+ pkg.Version = splitOutput[1]
+ }
+}
diff --git a/v2/internal/system/packagemanager/emerge.go b/v2/internal/system/packagemanager/emerge.go
new file mode 100644
index 000000000..7497d580a
--- /dev/null
+++ b/v2/internal/system/packagemanager/emerge.go
@@ -0,0 +1,118 @@
+//go:build linux
+// +build linux
+
+package packagemanager
+
+import (
+ "os/exec"
+ "regexp"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+// Emerge represents the Emerge package manager
+type Emerge struct {
+ name string
+ osid string
+}
+
+// NewEmerge creates a new Emerge instance
+func NewEmerge(osid string) *Emerge {
+ return &Emerge{
+ name: "emerge",
+ osid: osid,
+ }
+}
+
+// Packages returns the libraries that we need for Wails to compile
+// They will potentially differ on different distributions or versions
+func (e *Emerge) Packages() packagemap {
+ return packagemap{
+ "libgtk-3": []*Package{
+ {Name: "x11-libs/gtk+", SystemPackage: true, Library: true},
+ },
+ "libwebkit": []*Package{
+ {Name: "net-libs/webkit-gtk", SystemPackage: true, Library: true},
+ },
+ "gcc": []*Package{
+ {Name: "sys-devel/gcc", SystemPackage: true},
+ },
+ "pkg-config": []*Package{
+ {Name: "dev-util/pkgconf", SystemPackage: true},
+ },
+ "npm": []*Package{
+ {Name: "net-libs/nodejs", SystemPackage: true},
+ },
+ "docker": []*Package{
+ {Name: "app-emulation/docker", SystemPackage: true, Optional: true},
+ },
+ }
+}
+
+// Name returns the name of the package manager
+func (e *Emerge) Name() string {
+ return e.name
+}
+
+// PackageInstalled tests if the given package name is installed
+func (e *Emerge) PackageInstalled(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ stdout, _, err := shell.RunCommand(".", "emerge", "-s", pkg.Name+"$")
+ if err != nil {
+ _, ok := err.(*exec.ExitError)
+ if ok {
+ return false, nil
+ }
+ return false, err
+ }
+
+ regex := `.*\*\s+` + regexp.QuoteMeta(pkg.Name) + `\n(?:\S|\s)+?Latest version installed: (.*)`
+ installedRegex := regexp.MustCompile(regex)
+ matches := installedRegex.FindStringSubmatch(stdout)
+ pkg.Version = ""
+ noOfMatches := len(matches)
+ installed := false
+ if noOfMatches > 1 && matches[1] != "[ Not Installed ]" {
+ installed = true
+ pkg.Version = strings.TrimSpace(matches[1])
+ }
+ return installed, err
+}
+
+// PackageAvailable tests if the given package is available for installation
+func (e *Emerge) PackageAvailable(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ stdout, _, err := shell.RunCommand(".", "emerge", "-s", pkg.Name+"$")
+ // We add a space to ensure we get a full match, not partial match
+ if err != nil {
+ _, ok := err.(*exec.ExitError)
+ if ok {
+ return false, nil
+ }
+ return false, err
+ }
+
+ installedRegex := regexp.MustCompile(`.*\*\s+` + regexp.QuoteMeta(pkg.Name) + `\n(?:\S|\s)+?Latest version available: (.*)`)
+ matches := installedRegex.FindStringSubmatch(stdout)
+ pkg.Version = ""
+ noOfMatches := len(matches)
+ available := false
+ if noOfMatches > 1 {
+ available = true
+ pkg.Version = strings.TrimSpace(matches[1])
+ }
+ return available, nil
+}
+
+// InstallCommand returns the package manager specific command to install a package
+func (e *Emerge) InstallCommand(pkg *Package) string {
+ if pkg.SystemPackage == false {
+ return pkg.InstallCommand[e.osid]
+ }
+ return "sudo emerge " + pkg.Name
+}
diff --git a/v2/internal/system/packagemanager/eopkg.go b/v2/internal/system/packagemanager/eopkg.go
new file mode 100644
index 000000000..936127eac
--- /dev/null
+++ b/v2/internal/system/packagemanager/eopkg.go
@@ -0,0 +1,115 @@
+//go:build linux
+// +build linux
+
+package packagemanager
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+// Eopkg represents the Eopkg manager
+type Eopkg struct {
+ name string
+ osid string
+}
+
+// NewEopkg creates a new Eopkg instance
+func NewEopkg(osid string) *Eopkg {
+ result := &Eopkg{
+ name: "eopkg",
+ osid: osid,
+ }
+ result.intialiseName()
+ return result
+}
+
+// Packages returns the packages that we need for Wails to compile
+// They will potentially differ on different distributions or versions
+func (e *Eopkg) Packages() packagemap {
+ return packagemap{
+ "libgtk-3": []*Package{
+ {Name: "libgtk-3-devel", SystemPackage: true, Library: true},
+ },
+ "libwebkit": []*Package{
+ {Name: "libwebkit-gtk-devel", SystemPackage: true, Library: true},
+ },
+ "gcc": []*Package{
+ {Name: "gcc", SystemPackage: true},
+ },
+ "pkg-config": []*Package{
+ {Name: "pkgconf", SystemPackage: true},
+ },
+ "npm": []*Package{
+ {Name: "nodejs", SystemPackage: true},
+ },
+ "docker": []*Package{
+ {Name: "docker", SystemPackage: true, Optional: true},
+ },
+ }
+}
+
+// Name returns the name of the package manager
+func (e *Eopkg) Name() string {
+ return e.name
+}
+
+// PackageInstalled tests if the given package is installed
+func (e *Eopkg) PackageInstalled(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ stdout, _, err := shell.RunCommand(".", "eopkg", "info", pkg.Name)
+ return strings.HasPrefix(stdout, "Installed"), err
+}
+
+// PackageAvailable tests if the given package is available for installation
+func (e *Eopkg) PackageAvailable(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ stdout, _, err := shell.RunCommand(".", "eopkg", "info", pkg.Name)
+ // We add a space to ensure we get a full match, not partial match
+ output := e.removeEscapeSequences(stdout)
+ installed := strings.Contains(output, "Package found in Solus repository")
+ e.getPackageVersion(pkg, output)
+ return installed, err
+}
+
+// InstallCommand returns the package manager specific command to install a package
+func (e *Eopkg) InstallCommand(pkg *Package) string {
+ if pkg.SystemPackage == false {
+ return pkg.InstallCommand[e.osid]
+ }
+ return "sudo eopkg it " + pkg.Name
+}
+
+func (e *Eopkg) removeEscapeSequences(in string) string {
+ escapechars, _ := regexp.Compile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`)
+ return escapechars.ReplaceAllString(in, "")
+}
+
+func (e *Eopkg) intialiseName() {
+ result := "eopkg"
+ stdout, _, err := shell.RunCommand(".", "eopkg", "--version")
+ if err == nil {
+ result = strings.TrimSpace(stdout)
+ }
+ e.name = result
+}
+
+func (e *Eopkg) getPackageVersion(pkg *Package, output string) {
+
+ versionRegex := regexp.MustCompile(`.*Name.*version:\s+(.*)+, release: (.*)`)
+ matches := versionRegex.FindStringSubmatch(output)
+ pkg.Version = ""
+ noOfMatches := len(matches)
+ if noOfMatches > 1 {
+ pkg.Version = matches[1]
+ if noOfMatches > 2 {
+ pkg.Version += " (r" + matches[2] + ")"
+ }
+ }
+}
diff --git a/v2/internal/system/packagemanager/nixpkgs.go b/v2/internal/system/packagemanager/nixpkgs.go
new file mode 100644
index 000000000..360473d24
--- /dev/null
+++ b/v2/internal/system/packagemanager/nixpkgs.go
@@ -0,0 +1,159 @@
+//go:build linux
+// +build linux
+
+package packagemanager
+
+import (
+ "encoding/json"
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+// Nixpkgs represents the Nixpkgs manager
+type Nixpkgs struct {
+ name string
+ osid string
+}
+
+type NixPackageDetail struct {
+ Name string
+ Pname string
+ Version string
+}
+
+var available map[string]NixPackageDetail
+
+// NewNixpkgs creates a new Nixpkgs instance
+func NewNixpkgs(osid string) *Nixpkgs {
+ available = map[string]NixPackageDetail{}
+
+ return &Nixpkgs{
+ name: "nixpkgs",
+ osid: osid,
+ }
+}
+
+// Packages returns the libraries that we need for Wails to compile
+// They will potentially differ on different distributions or versions
+func (n *Nixpkgs) Packages() packagemap {
+ // Currently, only support checking the default channel.
+ channel := "nixpkgs"
+ if n.osid == "nixos" {
+ channel = "nixos"
+ }
+
+ return packagemap{
+ "libgtk-3": []*Package{
+ {Name: channel + ".gtk3", SystemPackage: true, Library: true},
+ },
+ "libwebkit": []*Package{
+ {Name: channel + ".webkitgtk", SystemPackage: true, Library: true},
+ },
+ "gcc": []*Package{
+ {Name: channel + ".gcc", SystemPackage: true},
+ },
+ "pkg-config": []*Package{
+ {Name: channel + ".pkg-config", SystemPackage: true},
+ },
+ "npm": []*Package{
+ {Name: channel + ".nodejs", SystemPackage: true},
+ },
+ "upx": []*Package{
+ {Name: channel + ".upx", SystemPackage: true, Optional: true},
+ },
+ "docker": []*Package{
+ {Name: channel + ".docker", SystemPackage: true, Optional: true},
+ },
+ "nsis": []*Package{
+ {Name: channel + ".nsis", SystemPackage: true, Optional: true},
+ },
+ }
+}
+
+// Name returns the name of the package manager
+func (n *Nixpkgs) Name() string {
+ return n.name
+}
+
+// PackageInstalled tests if the given package name is installed
+func (n *Nixpkgs) PackageInstalled(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+
+ stdout, _, err := shell.RunCommand(".", "nix-env", "--json", "-qA", pkg.Name)
+ if err != nil {
+ return false, nil
+ }
+
+ var attributes map[string]NixPackageDetail
+ err = json.Unmarshal([]byte(stdout), &attributes)
+ if err != nil {
+ return false, err
+ }
+
+ // Did we get one?
+ installed := false
+ for attribute, detail := range attributes {
+ if attribute == pkg.Name {
+ installed = true
+ pkg.Version = detail.Version
+ }
+ break
+ }
+
+ // If on NixOS, package may be installed via system config, so check the nix store.
+ detail, ok := available[pkg.Name]
+ if !installed && n.osid == "nixos" && ok {
+ cmd := "nix-store --query --requisites /run/current-system | cut -d- -f2- | sort | uniq | grep '^" + detail.Pname + "'"
+
+ if pkg.Library {
+ cmd += " | grep 'dev$'"
+ }
+
+ stdout, _, err = shell.RunCommand(".", "sh", "-c", cmd)
+ if err != nil {
+ return false, nil
+ }
+
+ if len(stdout) > 0 {
+ installed = true
+ }
+ }
+
+ return installed, nil
+}
+
+// PackageAvailable tests if the given package is available for installation
+func (n *Nixpkgs) PackageAvailable(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+
+ stdout, _, err := shell.RunCommand(".", "nix-env", "--json", "-qaA", pkg.Name)
+ if err != nil {
+ return false, nil
+ }
+
+ var attributes map[string]NixPackageDetail
+ err = json.Unmarshal([]byte(stdout), &attributes)
+ if err != nil {
+ return false, err
+ }
+
+ // Grab first version.
+ for attribute, detail := range attributes {
+ pkg.Version = detail.Version
+ available[attribute] = detail
+ break
+ }
+
+ return len(pkg.Version) > 0, nil
+}
+
+// InstallCommand returns the package manager specific command to install a package
+func (n *Nixpkgs) InstallCommand(pkg *Package) string {
+ if pkg.SystemPackage == false {
+ return pkg.InstallCommand[n.osid]
+ }
+ return "nix-env -iA " + pkg.Name
+}
diff --git a/v2/internal/system/packagemanager/packagemanager.go b/v2/internal/system/packagemanager/packagemanager.go
new file mode 100644
index 000000000..043c8e3cf
--- /dev/null
+++ b/v2/internal/system/packagemanager/packagemanager.go
@@ -0,0 +1,162 @@
+//go:build linux
+// +build linux
+
+package packagemanager
+
+import (
+ "sort"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+// A list of package manager commands
+var pmcommands = []string{
+ "eopkg",
+ "apt",
+ "dnf",
+ "pacman",
+ "emerge",
+ "zypper",
+ "nix-env",
+}
+
+// Find will attempt to find the system package manager
+func Find(osid string) PackageManager {
+
+ // Loop over pmcommands
+ for _, pmname := range pmcommands {
+ if shell.CommandExists(pmname) {
+ return newPackageManager(pmname, osid)
+ }
+ }
+ return nil
+}
+
+func newPackageManager(pmname string, osid string) PackageManager {
+ switch pmname {
+ case "eopkg":
+ return NewEopkg(osid)
+ case "apt":
+ return NewApt(osid)
+ case "dnf":
+ return NewDnf(osid)
+ case "pacman":
+ return NewPacman(osid)
+ case "emerge":
+ return NewEmerge(osid)
+ case "zypper":
+ return NewZypper(osid)
+ case "nix-env":
+ return NewNixpkgs(osid)
+ }
+ return nil
+}
+
+// Dependencies scans the system for required dependencies
+// Returns a list of dependencies search for, whether they were found
+// and whether they were installed
+func Dependencies(p PackageManager) (DependencyList, error) {
+
+ var dependencies DependencyList
+
+ for name, packages := range p.Packages() {
+ dependency := &Dependency{Name: name}
+ for _, pkg := range packages {
+ dependency.Optional = pkg.Optional
+ dependency.External = !pkg.SystemPackage
+ dependency.InstallCommand = p.InstallCommand(pkg)
+ packageavailable, err := p.PackageAvailable(pkg)
+ if err != nil {
+ return nil, err
+ }
+ if packageavailable {
+ dependency.Version = pkg.Version
+ dependency.PackageName = pkg.Name
+ installed, err := p.PackageInstalled(pkg)
+ if err != nil {
+ return nil, err
+ }
+ if installed {
+ dependency.Installed = true
+ dependency.Version = pkg.Version
+ if !pkg.SystemPackage {
+ dependency.Version = AppVersion(name)
+ }
+ } else {
+ dependency.InstallCommand = p.InstallCommand(pkg)
+ }
+ break
+ }
+ }
+ dependencies = append(dependencies, dependency)
+ }
+
+ // Sort dependencies
+ sort.Slice(dependencies, func(i, j int) bool {
+ return dependencies[i].Name < dependencies[j].Name
+ })
+
+ return dependencies, nil
+}
+
+// AppVersion returns the version for application related to the given package
+func AppVersion(name string) string {
+
+ if name == "gcc" {
+ return gccVersion()
+ }
+
+ if name == "pkg-config" {
+ return pkgConfigVersion()
+ }
+
+ if name == "npm" {
+ return npmVersion()
+ }
+
+ if name == "docker" {
+ return dockerVersion()
+ }
+
+ return ""
+
+}
+
+func gccVersion() string {
+
+ var version string
+ var err error
+
+ // Try "-dumpfullversion"
+ version, _, err = shell.RunCommand(".", "gcc", "-dumpfullversion")
+ if err != nil {
+
+ // Try -dumpversion
+ // We ignore the error as this function is not for testing whether the
+ // application exists, only that we can get the version number
+ dumpversion, _, err := shell.RunCommand(".", "gcc", "-dumpversion")
+ if err == nil {
+ version = dumpversion
+ }
+ }
+ return strings.TrimSpace(version)
+}
+
+func pkgConfigVersion() string {
+ version, _, _ := shell.RunCommand(".", "pkg-config", "--version")
+ return strings.TrimSpace(version)
+}
+
+func npmVersion() string {
+ version, _, _ := shell.RunCommand(".", "npm", "--version")
+ return strings.TrimSpace(version)
+}
+
+func dockerVersion() string {
+ version, _, _ := shell.RunCommand(".", "docker", "--version")
+ version = strings.TrimPrefix(version, "Docker version ")
+ version = strings.ReplaceAll(version, ", build ", " (")
+ version = strings.TrimSpace(version) + ")"
+ return version
+}
diff --git a/v2/internal/system/packagemanager/pacman.go b/v2/internal/system/packagemanager/pacman.go
new file mode 100644
index 000000000..1fbecf781
--- /dev/null
+++ b/v2/internal/system/packagemanager/pacman.go
@@ -0,0 +1,115 @@
+//go:build linux
+// +build linux
+
+package packagemanager
+
+import (
+ "os/exec"
+ "regexp"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+// Pacman represents the Pacman package manager
+type Pacman struct {
+ name string
+ osid string
+}
+
+// NewPacman creates a new Pacman instance
+func NewPacman(osid string) *Pacman {
+ return &Pacman{
+ name: "pacman",
+ osid: osid,
+ }
+}
+
+// Packages returns the libraries that we need for Wails to compile
+// They will potentially differ on different distributions or versions
+func (p *Pacman) Packages() packagemap {
+ return packagemap{
+ "libgtk-3": []*Package{
+ {Name: "gtk3", SystemPackage: true, Library: true},
+ },
+ "libwebkit": []*Package{
+ {Name: "webkit2gtk", SystemPackage: true, Library: true},
+ },
+ "gcc": []*Package{
+ {Name: "gcc", SystemPackage: true},
+ },
+ "pkg-config": []*Package{
+ {Name: "pkgconf", SystemPackage: true},
+ },
+ "npm": []*Package{
+ {Name: "npm", SystemPackage: true},
+ },
+ "docker": []*Package{
+ {Name: "docker", SystemPackage: true, Optional: true},
+ },
+ }
+}
+
+// Name returns the name of the package manager
+func (p *Pacman) Name() string {
+ return p.name
+}
+
+// PackageInstalled tests if the given package name is installed
+func (p *Pacman) PackageInstalled(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ stdout, _, err := shell.RunCommand(".", "pacman", "-Q", pkg.Name)
+ if err != nil {
+ _, ok := err.(*exec.ExitError)
+ if ok {
+ return false, nil
+ }
+ return false, err
+ }
+
+ splitoutput := strings.Split(stdout, "\n")
+ for _, line := range splitoutput {
+ if strings.HasPrefix(line, pkg.Name) {
+ splitline := strings.Split(line, " ")
+ pkg.Version = strings.TrimSpace(splitline[1])
+ }
+ }
+
+ return true, err
+}
+
+// PackageAvailable tests if the given package is available for installation
+func (p *Pacman) PackageAvailable(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ output, _, err := shell.RunCommand(".", "pacman", "-Si", pkg.Name)
+ // We add a space to ensure we get a full match, not partial match
+ if err != nil {
+ _, ok := err.(*exec.ExitError)
+ if ok {
+ return false, nil
+ }
+ return false, err
+ }
+
+ reg := regexp.MustCompile(`.*Version.*?:\s+(.*)`)
+ matches := reg.FindStringSubmatch(output)
+ pkg.Version = ""
+ noOfMatches := len(matches)
+ if noOfMatches > 1 {
+ pkg.Version = strings.TrimSpace(matches[1])
+ }
+
+ return true, nil
+}
+
+// InstallCommand returns the package manager specific command to install a package
+func (p *Pacman) InstallCommand(pkg *Package) string {
+ if pkg.SystemPackage == false {
+ return pkg.InstallCommand[p.osid]
+ }
+ return "sudo pacman -S " + pkg.Name
+}
diff --git a/v2/internal/system/packagemanager/pm.go b/v2/internal/system/packagemanager/pm.go
new file mode 100644
index 000000000..bba45cd05
--- /dev/null
+++ b/v2/internal/system/packagemanager/pm.go
@@ -0,0 +1,60 @@
+package packagemanager
+
+// Package contains information about a system package
+type Package struct {
+ Name string
+ Version string
+ InstallCommand map[string]string
+ SystemPackage bool
+ Library bool
+ Optional bool
+}
+
+type packagemap = map[string][]*Package
+
+// PackageManager is a common interface across all package managers
+type PackageManager interface {
+ Name() string
+ Packages() packagemap
+ PackageInstalled(pkg *Package) (bool, error)
+ PackageAvailable(pkg *Package) (bool, error)
+ InstallCommand(pkg *Package) string
+}
+
+// Dependency represents a system package that we require
+type Dependency struct {
+ Name string
+ PackageName string
+ Installed bool
+ InstallCommand string
+ Version string
+ Optional bool
+ External bool
+}
+
+// DependencyList is a list of Dependency instances
+type DependencyList []*Dependency
+
+// InstallAllRequiredCommand returns the command you need to use to install all required dependencies
+func (d DependencyList) InstallAllRequiredCommand() string {
+ result := ""
+ for _, dependency := range d {
+ if !dependency.Installed && !dependency.Optional {
+ result += " - " + dependency.Name + ": " + dependency.InstallCommand + "\n"
+ }
+ }
+
+ return result
+}
+
+// InstallAllOptionalCommand returns the command you need to use to install all optional dependencies
+func (d DependencyList) InstallAllOptionalCommand() string {
+ result := ""
+ for _, dependency := range d {
+ if !dependency.Installed && dependency.Optional {
+ result += " - " + dependency.Name + ": " + dependency.InstallCommand + "\n"
+ }
+ }
+
+ return result
+}
diff --git a/v2/internal/system/packagemanager/zypper.go b/v2/internal/system/packagemanager/zypper.go
new file mode 100644
index 000000000..efaeb0b1b
--- /dev/null
+++ b/v2/internal/system/packagemanager/zypper.go
@@ -0,0 +1,128 @@
+//go:build linux
+// +build linux
+
+package packagemanager
+
+import (
+ "os/exec"
+ "regexp"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+// Zypper represents the Zypper package manager
+type Zypper struct {
+ name string
+ osid string
+}
+
+// NewZypper creates a new Zypper instance
+func NewZypper(osid string) *Zypper {
+ return &Zypper{
+ name: "zypper",
+ osid: osid,
+ }
+}
+
+// Packages returns the libraries that we need for Wails to compile
+// They will potentially differ on different distributions or versions
+func (z *Zypper) Packages() packagemap {
+ return packagemap{
+ "libgtk-3": []*Package{
+ {Name: "gtk3-devel", SystemPackage: true, Library: true},
+ },
+ "libwebkit": []*Package{
+ {Name: "webkit2gtk3-soup2-devel", SystemPackage: true, Library: true},
+ {Name: "webkit2gtk3-devel", SystemPackage: true, Library: true},
+ },
+ "gcc": []*Package{
+ {Name: "gcc-c++", SystemPackage: true},
+ },
+ "pkg-config": []*Package{
+ {Name: "pkg-config", SystemPackage: true},
+ {Name: "pkgconf-pkg-config", SystemPackage: true},
+ },
+ "npm": []*Package{
+ {Name: "npm10", SystemPackage: true},
+ {Name: "npm20", SystemPackage: true},
+ },
+ "docker": []*Package{
+ {Name: "docker", SystemPackage: true, Optional: true},
+ },
+ }
+}
+
+// Name returns the name of the package manager
+func (z *Zypper) Name() string {
+ return z.name
+}
+
+// PackageInstalled tests if the given package name is installed
+func (z *Zypper) PackageInstalled(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ var env []string
+ env = shell.SetEnv(env, "LANGUAGE", "en_US.utf-8")
+ stdout, _, err := shell.RunCommandWithEnv(env, ".", "zypper", "info", pkg.Name)
+ if err != nil {
+ _, ok := err.(*exec.ExitError)
+ if ok {
+ return false, nil
+ }
+ return false, err
+ }
+ reg := regexp.MustCompile(`.*Installed\s*:\s*(Yes)\s*`)
+ matches := reg.FindStringSubmatch(stdout)
+ pkg.Version = ""
+ noOfMatches := len(matches)
+ if noOfMatches > 1 {
+ z.getPackageVersion(pkg, stdout)
+ }
+ return noOfMatches > 1, err
+}
+
+// PackageAvailable tests if the given package is available for installation
+func (z *Zypper) PackageAvailable(pkg *Package) (bool, error) {
+ if pkg.SystemPackage == false {
+ return false, nil
+ }
+ var env []string
+ env = shell.SetEnv(env, "LANGUAGE", "en_US.utf-8")
+ stdout, _, err := shell.RunCommandWithEnv(env, ".", "zypper", "info", pkg.Name)
+ // We add a space to ensure we get a full match, not partial match
+ if err != nil {
+ _, ok := err.(*exec.ExitError)
+ if ok {
+ return false, nil
+ }
+ return false, err
+ }
+
+ available := strings.Contains(stdout, "Information for package")
+ if available {
+ z.getPackageVersion(pkg, stdout)
+ }
+
+ return available, nil
+}
+
+// InstallCommand returns the package manager specific command to install a package
+func (z *Zypper) InstallCommand(pkg *Package) string {
+ if pkg.SystemPackage == false {
+ return pkg.InstallCommand[z.osid]
+ }
+ return "sudo zypper in " + pkg.Name
+}
+
+func (z *Zypper) getPackageVersion(pkg *Package, output string) {
+
+ reg := regexp.MustCompile(`.*Version.*:(.*)`)
+ matches := reg.FindStringSubmatch(output)
+ pkg.Version = ""
+ noOfMatches := len(matches)
+ if noOfMatches > 1 {
+ pkg.Version = strings.TrimSpace(matches[1])
+ }
+}
diff --git a/v2/internal/system/system.go b/v2/internal/system/system.go
new file mode 100644
index 000000000..67453538f
--- /dev/null
+++ b/v2/internal/system/system.go
@@ -0,0 +1,169 @@
+package system
+
+import (
+ "os/exec"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+ "github.com/wailsapp/wails/v2/internal/system/operatingsystem"
+ "github.com/wailsapp/wails/v2/internal/system/packagemanager"
+)
+
+var IsAppleSilicon bool
+
+// Info holds information about the current operating system,
+// package manager and required dependencies
+type Info struct {
+ OS *operatingsystem.OS
+ PM packagemanager.PackageManager
+ Dependencies packagemanager.DependencyList
+}
+
+// GetInfo scans the system for operating system details,
+// the system package manager and the status of required
+// dependencies.
+func GetInfo() (*Info, error) {
+ var result Info
+ err := result.discover()
+ if err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func checkNodejs() *packagemanager.Dependency {
+ // Check for Nodejs
+ output, err := exec.Command("node", "-v").Output()
+ installed := true
+ version := ""
+ if err != nil {
+ installed = false
+ } else {
+ if len(output) > 0 {
+ version = strings.TrimSpace(strings.Split(string(output), "\n")[0])[1:]
+ }
+ }
+ return &packagemanager.Dependency{
+ Name: "Nodejs",
+ PackageName: "N/A",
+ Installed: installed,
+ InstallCommand: "Available at https://nodejs.org/en/download/",
+ Version: version,
+ Optional: false,
+ External: false,
+ }
+}
+
+func checkNPM() *packagemanager.Dependency {
+ // Check for npm
+ output, err := exec.Command("npm", "-version").Output()
+ installed := true
+ version := ""
+ if err != nil {
+ installed = false
+ } else {
+ version = strings.TrimSpace(strings.Split(string(output), "\n")[0])
+ }
+ return &packagemanager.Dependency{
+ Name: "npm ",
+ PackageName: "N/A",
+ Installed: installed,
+ InstallCommand: "Available at https://nodejs.org/en/download/",
+ Version: version,
+ Optional: false,
+ External: false,
+ }
+}
+
+func checkUPX() *packagemanager.Dependency {
+ // Check for npm
+ output, err := exec.Command("upx", "-V").Output()
+ installed := true
+ version := ""
+ if err != nil {
+ installed = false
+ } else {
+ version = strings.TrimSpace(strings.Split(string(output), "\n")[0])
+ }
+ return &packagemanager.Dependency{
+ Name: "upx ",
+ PackageName: "N/A",
+ Installed: installed,
+ InstallCommand: "Available at https://upx.github.io/",
+ Version: version,
+ Optional: true,
+ External: false,
+ }
+}
+
+func checkNSIS() *packagemanager.Dependency {
+ // Check for nsis installer
+ output, err := exec.Command("makensis", "-VERSION").Output()
+ installed := true
+ version := ""
+ if err != nil {
+ installed = false
+ } else {
+ version = strings.TrimSpace(strings.Split(string(output), "\n")[0])
+ }
+ return &packagemanager.Dependency{
+ Name: "nsis ",
+ PackageName: "N/A",
+ Installed: installed,
+ InstallCommand: "More info at https://wails.io/docs/guides/windows-installer/",
+ Version: version,
+ Optional: true,
+ External: false,
+ }
+}
+
+func checkLibrary(name string) func() *packagemanager.Dependency {
+ return func() *packagemanager.Dependency {
+ output, _, _ := shell.RunCommand(".", "pkg-config", "--cflags", name)
+ installed := len(strings.TrimSpace(output)) > 0
+
+ return &packagemanager.Dependency{
+ Name: "lib" + name + " ",
+ PackageName: "N/A",
+ Installed: installed,
+ InstallCommand: "Install via your package manager",
+ Version: "N/A",
+ Optional: false,
+ External: false,
+ }
+ }
+}
+
+func checkDocker() *packagemanager.Dependency {
+ // Check for npm
+ output, err := exec.Command("docker", "version").Output()
+ installed := true
+ version := ""
+
+ // Docker errors if it is not running so check for that
+ if len(output) == 0 && err != nil {
+ installed = false
+ } else {
+ // Version is in a line like: " Version: 20.10.5"
+ versionOutput := strings.Split(string(output), "\n")
+ for _, line := range versionOutput[1:] {
+ splitLine := strings.Split(line, ":")
+ if len(splitLine) > 1 {
+ key := strings.TrimSpace(splitLine[0])
+ if key == "Version" {
+ version = strings.TrimSpace(splitLine[1])
+ break
+ }
+ }
+ }
+ }
+ return &packagemanager.Dependency{
+ Name: "docker ",
+ PackageName: "N/A",
+ Installed: installed,
+ InstallCommand: "Available at https://www.docker.com/products/docker-desktop",
+ Version: version,
+ Optional: true,
+ External: false,
+ }
+}
diff --git a/v2/internal/system/system_darwin.go b/v2/internal/system/system_darwin.go
new file mode 100644
index 000000000..16dfd8873
--- /dev/null
+++ b/v2/internal/system/system_darwin.go
@@ -0,0 +1,91 @@
+//go:build darwin
+// +build darwin
+
+package system
+
+import (
+ "fmt"
+ "os/exec"
+ "strings"
+ "syscall"
+
+ "github.com/wailsapp/wails/v2/internal/system/operatingsystem"
+ "github.com/wailsapp/wails/v2/internal/system/packagemanager"
+)
+
+// Determine if the app is running on Apple Silicon
+// Credit: https://www.yellowduck.be/posts/detecting-apple-silicon-via-go/
+func init() {
+ r, err := syscall.Sysctl("sysctl.proc_translated")
+ if err != nil {
+ return
+ }
+
+ IsAppleSilicon = r == "\x00\x00\x00" || r == "\x01\x00\x00"
+}
+
+func (i *Info) discover() error {
+ var err error
+ osinfo, err := operatingsystem.Info()
+ if err != nil {
+ return err
+ }
+ i.OS = osinfo
+
+ i.Dependencies = append(i.Dependencies, checkXCodeSelect())
+ i.Dependencies = append(i.Dependencies, checkNodejs())
+ i.Dependencies = append(i.Dependencies, checkNPM())
+ i.Dependencies = append(i.Dependencies, checkXCodeBuild())
+ i.Dependencies = append(i.Dependencies, checkUPX())
+ i.Dependencies = append(i.Dependencies, checkNSIS())
+ return nil
+}
+
+func checkXCodeSelect() *packagemanager.Dependency {
+ // Check for xcode command line tools
+ output, err := exec.Command("xcode-select", "-v").Output()
+ installed := true
+ version := ""
+ if err != nil {
+ installed = false
+ } else {
+ version = strings.TrimPrefix(string(output), "xcode-select version ")
+ version = strings.TrimSpace(version)
+ version = strings.TrimSuffix(version, ".")
+ }
+ return &packagemanager.Dependency{
+ Name: "Xcode command line tools ",
+ PackageName: "N/A",
+ Installed: installed,
+ InstallCommand: "xcode-select --install",
+ Version: version,
+ Optional: false,
+ External: false,
+ }
+}
+
+func checkXCodeBuild() *packagemanager.Dependency {
+ // Check for xcode
+ output, err := exec.Command("xcodebuild", "-version").Output()
+ installed := true
+ version := ""
+ if err != nil {
+ installed = false
+ } else if l := strings.Split(string(output), "\n"); len(l) >= 2 {
+ version = fmt.Sprintf("%s (%s)",
+ strings.TrimPrefix(l[0], "Xcode "),
+ strings.TrimPrefix(l[1], "Build version "))
+ } else {
+ version = "N/A"
+ }
+
+ return &packagemanager.Dependency{
+ Name: "Xcode",
+ PackageName: "N/A",
+ Installed: installed,
+ InstallCommand: "Available at https://apps.apple.com/us/app/xcode/id497799835",
+ Version: version,
+ Optional: true,
+ External: false,
+ }
+}
diff --git a/v2/internal/system/system_linux.go b/v2/internal/system/system_linux.go
new file mode 100644
index 000000000..703e978eb
--- /dev/null
+++ b/v2/internal/system/system_linux.go
@@ -0,0 +1,94 @@
+//go:build linux
+// +build linux
+
+package system
+
+import (
+ "github.com/wailsapp/wails/v2/internal/system/operatingsystem"
+ "github.com/wailsapp/wails/v2/internal/system/packagemanager"
+)
+
+func checkGCC() *packagemanager.Dependency {
+
+ version := packagemanager.AppVersion("gcc")
+
+ return &packagemanager.Dependency{
+ Name: "gcc ",
+ PackageName: "N/A",
+ Installed: version != "",
+ InstallCommand: "Install via your package manager",
+ Version: version,
+ Optional: false,
+ External: false,
+ }
+}
+
+func checkPkgConfig() *packagemanager.Dependency {
+
+ version := packagemanager.AppVersion("pkg-config")
+
+ return &packagemanager.Dependency{
+ Name: "pkg-config ",
+ PackageName: "N/A",
+ Installed: version != "",
+ InstallCommand: "Install via your package manager",
+ Version: version,
+ Optional: false,
+ External: false,
+ }
+}
+
+func checkLocallyInstalled(checker func() *packagemanager.Dependency, dependency *packagemanager.Dependency) {
+ if !dependency.Installed {
+ locallyInstalled := checker()
+ if locallyInstalled.Installed {
+ dependency.Installed = true
+ dependency.Version = locallyInstalled.Version
+ }
+ }
+}
+
+var checkerFunctions = map[string]func() *packagemanager.Dependency{
+ "Nodejs": checkNodejs,
+ "npm": checkNPM,
+ "docker": checkDocker,
+ "upx": checkUPX,
+ "gcc": checkGCC,
+ "pkg-config": checkPkgConfig,
+ "libgtk-3": checkLibrary("libgtk-3"),
+ "libwebkit": checkLibrary("libwebkit"),
+}
+
+func (i *Info) discover() error {
+
+ var err error
+ osinfo, err := operatingsystem.Info()
+ if err != nil {
+ return err
+ }
+ i.OS = osinfo
+
+ i.PM = packagemanager.Find(osinfo.ID)
+ if i.PM != nil {
+ dependencies, err := packagemanager.Dependencies(i.PM)
+ if err != nil {
+ return err
+ }
+ for _, dep := range dependencies {
+ checker := checkerFunctions[dep.Name]
+ if checker != nil {
+ checkLocallyInstalled(checker, dep)
+ }
+ if dep.Name == "nsis" {
+ locallyInstalled := checkNSIS()
+ if locallyInstalled.Installed {
+ dep.Installed = true
+ dep.Version = locallyInstalled.Version
+ }
+ }
+ }
+ i.Dependencies = dependencies
+ }
+
+ return nil
+}
diff --git a/v2/internal/system/system_windows.go b/v2/internal/system/system_windows.go
new file mode 100644
index 000000000..40b8f0340
--- /dev/null
+++ b/v2/internal/system/system_windows.go
@@ -0,0 +1,45 @@
+//go:build windows
+// +build windows
+
+package system
+
+import (
+ "github.com/wailsapp/go-webview2/webviewloader"
+ "github.com/wailsapp/wails/v2/internal/system/operatingsystem"
+ "github.com/wailsapp/wails/v2/internal/system/packagemanager"
+)
+
+func (i *Info) discover() error {
+
+ var err error
+ osinfo, err := operatingsystem.Info()
+ if err != nil {
+ return err
+ }
+ i.OS = osinfo
+
+ i.Dependencies = append(i.Dependencies, checkWebView2())
+ i.Dependencies = append(i.Dependencies, checkNodejs())
+ i.Dependencies = append(i.Dependencies, checkNPM())
+ i.Dependencies = append(i.Dependencies, checkUPX())
+ i.Dependencies = append(i.Dependencies, checkNSIS())
+ // i.Dependencies = append(i.Dependencies, checkDocker())
+
+ return nil
+}
+
+func checkWebView2() *packagemanager.Dependency {
+ version, _ := webviewloader.GetAvailableCoreWebView2BrowserVersionString("")
+ installed := version != ""
+
+ return &packagemanager.Dependency{
+ Name: "WebView2 ",
+ PackageName: "N/A",
+ Installed: installed,
+ InstallCommand: "Available at https://developer.microsoft.com/en-us/microsoft-edge/webview2/",
+ Version: version,
+ Optional: false,
+ External: true,
+ }
+
+}
diff --git a/cmd/templates/vuebasic/frontend/src/assets/fonts/LICENSE.txt b/v2/internal/typescriptify/LICENSE.txt
similarity index 97%
rename from cmd/templates/vuebasic/frontend/src/assets/fonts/LICENSE.txt
rename to v2/internal/typescriptify/LICENSE.txt
index 75b52484e..fa6e64ac4 100644
--- a/cmd/templates/vuebasic/frontend/src/assets/fonts/LICENSE.txt
+++ b/v2/internal/typescriptify/LICENSE.txt
@@ -1,202 +1,202 @@
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2015-] [Tomo Krajina]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/v2/internal/typescriptify/README.md b/v2/internal/typescriptify/README.md
new file mode 100644
index 000000000..b5c961835
--- /dev/null
+++ b/v2/internal/typescriptify/README.md
@@ -0,0 +1,2 @@
+Based on: https://github.com/tkrajina/typescriptify-golang-structs
+License: LICENSE.txt
\ No newline at end of file
diff --git a/v2/internal/typescriptify/js-reserved-keywords.go b/v2/internal/typescriptify/js-reserved-keywords.go
new file mode 100644
index 000000000..4f9aa09f4
--- /dev/null
+++ b/v2/internal/typescriptify/js-reserved-keywords.go
@@ -0,0 +1,69 @@
+package typescriptify
+
+var jsReservedKeywords []string = []string{
+ "abstract",
+ "arguments",
+ "await",
+ "boolean",
+ "break",
+ "byte",
+ "case",
+ "catch",
+ "char",
+ "class",
+ "const",
+ "continue",
+ "debugger",
+ "default",
+ "delete",
+ "do",
+ "double",
+ "else",
+ "enum",
+ "eval",
+ "export",
+ "extends",
+ "false",
+ "final",
+ "finally",
+ "float",
+ "for",
+ "function",
+ "goto",
+ "if",
+ "implements",
+ "import",
+ "in",
+ "instanceof",
+ "int",
+ "interface",
+ "let",
+ "long",
+ "native",
+ "new",
+ "null",
+ "package",
+ "private",
+ "protected",
+ "public",
+ "return",
+ "short",
+ "static",
+ "super",
+ "switch",
+ "synchronized",
+ "this",
+ "throw",
+ "throws",
+ "transient",
+ "true",
+ "try",
+ "typeof",
+ "var",
+ "void",
+ "volatile",
+ "while",
+ "with",
+ "yield",
+ "object",
+}
diff --git a/v2/internal/typescriptify/typescriptify.go b/v2/internal/typescriptify/typescriptify.go
new file mode 100644
index 000000000..e732c5976
--- /dev/null
+++ b/v2/internal/typescriptify/typescriptify.go
@@ -0,0 +1,1011 @@
+package typescriptify
+
+import (
+ "bufio"
+ "cmp"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path"
+ "reflect"
+ "regexp"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/leaanthony/slicer"
+
+ "github.com/tkrajina/go-reflector/reflector"
+)
+
+const (
+ tsTransformTag = "ts_transform"
+ tsType = "ts_type"
+ tsConvertValuesFunc = `convertValues(a: any, classs: any, asMap: boolean = false): any {
+ if (!a) {
+ return a;
+ }
+ if (a.slice && a.map) {
+ return (a as any[]).map(elem => this.convertValues(elem, classs));
+ } else if ("object" === typeof a) {
+ if (asMap) {
+ for (const key of Object.keys(a)) {
+ a[key] = new classs(a[key]);
+ }
+ return a;
+ }
+ return new classs(a);
+ }
+ return a;
+}`
+ jsVariableNameRegex = `^([A-Z]|[a-z]|\$|_)([A-Z]|[a-z]|[0-9]|\$|_)*$`
+)
+
+var jsVariableUnsafeChars = regexp.MustCompile(`[^A-Za-z0-9_]`)
+
+func nameTypeOf(typeOf reflect.Type) string {
+ tname := typeOf.Name()
+ gidx := strings.IndexRune(tname, '[')
+ if gidx > 0 { // its a generic type
+ rem := strings.SplitN(tname, "[", 2)
+ tname = rem[0] + "_" + jsVariableUnsafeChars.ReplaceAllLiteralString(rem[1], "_")
+ }
+ return tname
+}
+
+// TypeOptions overrides options set by `ts_*` tags.
+type TypeOptions struct {
+ TSType string
+ TSTransform string
+}
+
+// StructType stores settings for transforming one Golang struct.
+type StructType struct {
+ Type reflect.Type
+ FieldOptions map[reflect.Type]TypeOptions
+}
+
+func NewStruct(i interface{}) *StructType {
+ return &StructType{
+ Type: reflect.TypeOf(i),
+ }
+}
+
+func (st *StructType) WithFieldOpts(i interface{}, opts TypeOptions) *StructType {
+ if st.FieldOptions == nil {
+ st.FieldOptions = map[reflect.Type]TypeOptions{}
+ }
+ var typ reflect.Type
+ if ty, is := i.(reflect.Type); is {
+ typ = ty
+ } else {
+ typ = reflect.TypeOf(i)
+ }
+ st.FieldOptions[typ] = opts
+ return st
+}
+
+type EnumType struct {
+ Type reflect.Type
+}
+
+type enumElement struct {
+ value interface{}
+ name string
+}
+
+type TypeScriptify struct {
+ Prefix string
+ Suffix string
+ Indent string
+ CreateFromMethod bool
+ CreateConstructor bool
+ BackupDir string // If empty no backup
+ DontExport bool
+ CreateInterface bool
+ customImports []string
+
+ structTypes []StructType
+ enumTypes []EnumType
+ enums map[reflect.Type][]enumElement
+ kinds map[reflect.Kind]string
+
+ fieldTypeOptions map[reflect.Type]TypeOptions
+
+ // throwaway, used when converting
+ alreadyConverted map[string]bool
+
+ Namespace string
+ KnownStructs *slicer.StringSlicer
+ KnownEnums *slicer.StringSlicer
+}
+
+func New() *TypeScriptify {
+ result := new(TypeScriptify)
+ result.Indent = "\t"
+ result.BackupDir = "."
+
+ kinds := make(map[reflect.Kind]string)
+
+ kinds[reflect.Bool] = "boolean"
+ kinds[reflect.Interface] = "any"
+
+ kinds[reflect.Int] = "number"
+ kinds[reflect.Int8] = "number"
+ kinds[reflect.Int16] = "number"
+ kinds[reflect.Int32] = "number"
+ kinds[reflect.Int64] = "number"
+ kinds[reflect.Uint] = "number"
+ kinds[reflect.Uint8] = "number"
+ kinds[reflect.Uint16] = "number"
+ kinds[reflect.Uint32] = "number"
+ kinds[reflect.Uint64] = "number"
+ kinds[reflect.Float32] = "number"
+ kinds[reflect.Float64] = "number"
+
+ kinds[reflect.String] = "string"
+
+ result.kinds = kinds
+
+ result.Indent = " "
+ result.CreateFromMethod = true
+ result.CreateConstructor = true
+
+ return result
+}
+
+func (t *TypeScriptify) deepFields(typeOf reflect.Type) []reflect.StructField {
+ fields := make([]reflect.StructField, 0)
+
+ if typeOf.Kind() == reflect.Ptr {
+ typeOf = typeOf.Elem()
+ }
+
+ if typeOf.Kind() != reflect.Struct {
+ return fields
+ }
+
+ for i := 0; i < typeOf.NumField(); i++ {
+ f := typeOf.Field(i)
+ kind := f.Type.Kind()
+ isPointer := kind == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct
+ if f.Anonymous && kind == reflect.Struct {
+ // fmt.Println(v.Interface())
+ fields = append(fields, t.deepFields(f.Type)...)
+ } else if f.Anonymous && isPointer {
+ // fmt.Println(v.Interface())
+ fields = append(fields, t.deepFields(f.Type.Elem())...)
+ } else {
+ // Check we have a json tag
+ jsonTag := t.getJSONFieldName(f, isPointer)
+ if jsonTag != "" {
+ fields = append(fields, f)
+ }
+ }
+ }
+
+ return fields
+}
+
+func (ts TypeScriptify) logf(depth int, s string, args ...interface{}) {
+ fmt.Printf(strings.Repeat(" ", depth)+s+"\n", args...)
+}
+
+// ManageType can define custom options for fields of a specified type.
+//
+// This can be used instead of setting ts_type and ts_transform for all fields of a certain type.
+func (t *TypeScriptify) ManageType(fld interface{}, opts TypeOptions) *TypeScriptify {
+ var typ reflect.Type
+ switch t := fld.(type) {
+ case reflect.Type:
+ typ = t
+ default:
+ typ = reflect.TypeOf(fld)
+ }
+ if t.fieldTypeOptions == nil {
+ t.fieldTypeOptions = map[reflect.Type]TypeOptions{}
+ }
+ t.fieldTypeOptions[typ] = opts
+ return t
+}
+
+func (t *TypeScriptify) GetGeneratedStructs() []string {
+ var result []string
+ for key := range t.alreadyConverted {
+ result = append(result, key)
+ }
+ return result
+}
+
+func (t *TypeScriptify) WithCreateFromMethod(b bool) *TypeScriptify {
+ t.CreateFromMethod = b
+ return t
+}
+
+func (t *TypeScriptify) WithInterface(b bool) *TypeScriptify {
+ t.CreateInterface = b
+ return t
+}
+
+func (t *TypeScriptify) WithConstructor(b bool) *TypeScriptify {
+ t.CreateConstructor = b
+ return t
+}
+
+func (t *TypeScriptify) WithIndent(i string) *TypeScriptify {
+ t.Indent = i
+ return t
+}
+
+func (t *TypeScriptify) WithBackupDir(b string) *TypeScriptify {
+ t.BackupDir = b
+ return t
+}
+
+func (t *TypeScriptify) WithPrefix(p string) *TypeScriptify {
+ t.Prefix = p
+ return t
+}
+
+func (t *TypeScriptify) WithSuffix(s string) *TypeScriptify {
+ t.Suffix = s
+ return t
+}
+
+func (t *TypeScriptify) Add(obj interface{}) *TypeScriptify {
+ switch ty := obj.(type) {
+ case StructType:
+ t.structTypes = append(t.structTypes, ty)
+ case *StructType:
+ t.structTypes = append(t.structTypes, *ty)
+ case reflect.Type:
+ t.AddType(ty)
+ default:
+ t.AddType(reflect.TypeOf(obj))
+ }
+ return t
+}
+
+func (t *TypeScriptify) AddType(typeOf reflect.Type) *TypeScriptify {
+ t.structTypes = append(t.structTypes, StructType{Type: typeOf})
+ return t
+}
+
+func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField) {
+ keyType := field.Type.Key()
+ valueType := field.Type.Elem()
+ valueTypeName := nameTypeOf(valueType)
+ valueTypeSuffix := ""
+ valueTypePrefix := ""
+ if valueType.Kind() == reflect.Ptr {
+ valueType = valueType.Elem()
+ valueTypeName = nameTypeOf(valueType)
+ }
+ if valueType.Kind() == reflect.Array || valueType.Kind() == reflect.Slice {
+ arrayDepth := 1
+ for valueType.Elem().Kind() == reflect.Array || valueType.Elem().Kind() == reflect.Slice {
+ valueType = valueType.Elem()
+ arrayDepth++
+ }
+ valueType = valueType.Elem()
+ valueTypeName = nameTypeOf(valueType)
+ valueTypeSuffix = strings.Repeat(">", arrayDepth)
+ valueTypePrefix = strings.Repeat("Array<", arrayDepth)
+ }
+ if valueType.Kind() == reflect.Ptr {
+ valueType = valueType.Elem()
+ valueTypeName = nameTypeOf(valueType)
+ }
+ if name, ok := t.types[valueType.Kind()]; ok {
+ valueTypeName = name
+ }
+ if valueType.Kind() == reflect.Map {
+ // TODO: support nested maps
+ valueTypeName = "any" // valueType.Elem().Name()
+ }
+ if valueType.Kind() == reflect.Struct && differentNamespaces(t.namespace, valueType) {
+ valueTypeName = valueType.String()
+ }
+ strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
+ isOptional := strings.HasSuffix(fieldName, "?")
+
+ keyTypeStr := ""
+ // Key should always be a JS primitive. JS will read it as a string either way.
+ if typeStr, isSimple := t.types[keyType.Kind()]; isSimple {
+ keyTypeStr = typeStr
+ } else {
+ keyTypeStr = t.types[reflect.String]
+ }
+
+ var dotField string
+ if regexp.MustCompile(jsVariableNameRegex).Match([]byte(strippedFieldName)) {
+ dotField = fmt.Sprintf(".%s", strippedFieldName)
+ } else {
+ dotField = fmt.Sprintf(`["%s"]`, strippedFieldName)
+ if isOptional {
+ fieldName = fmt.Sprintf(`"%s"?`, strippedFieldName)
+ }
+ }
+ t.fields = append(t.fields, fmt.Sprintf("%s%s: Record<%s, %s>;", t.indent, fieldName, keyTypeStr, valueTypePrefix+valueTypeName+valueTypeSuffix))
+ if valueType.Kind() == reflect.Struct {
+ t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = this.convertValues(source[\"%s\"], %s, true);",
+ t.indent, t.indent, dotField, strippedFieldName, t.prefix+valueTypePrefix+valueTypeName+valueTypeSuffix+t.suffix))
+ } else {
+ t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = source[\"%s\"];",
+ t.indent, t.indent, dotField, strippedFieldName))
+ }
+}
+
+func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify {
+ if t.enums == nil {
+ t.enums = map[reflect.Type][]enumElement{}
+ }
+ items := reflect.ValueOf(values)
+ if items.Kind() != reflect.Slice {
+ panic(fmt.Sprintf("Values for %T isn't a slice", values))
+ }
+
+ var elements []enumElement
+ for i := 0; i < items.Len(); i++ {
+ item := items.Index(i)
+
+ var el enumElement
+ if item.Kind() == reflect.Struct {
+ r := reflector.New(item.Interface())
+ val, err := r.Field("Value").Get()
+ if err != nil {
+ panic(fmt.Sprint("missing Type field in ", item.Type().String()))
+ }
+ name, err := r.Field("TSName").Get()
+ if err != nil {
+ panic(fmt.Sprint("missing TSName field in ", item.Type().String()))
+ }
+ el.value = val
+ el.name = name.(string)
+ } else {
+ el.value = item.Interface()
+ if tsNamer, is := item.Interface().(TSNamer); is {
+ el.name = tsNamer.TSName()
+ } else {
+ panic(fmt.Sprint(item.Type().String(), " has no TSName method"))
+ }
+ }
+
+ elements = append(elements, el)
+ }
+ slices.SortFunc(elements, func(a, b enumElement) int {
+ return cmp.Compare(a.name, b.name)
+ })
+ ty := reflect.TypeOf(elements[0].value)
+ t.enums[ty] = elements
+ t.enumTypes = append(t.enumTypes, EnumType{Type: ty})
+
+ return t
+}
+
+// AddEnumValues is deprecated, use `AddEnum()`
+func (t *TypeScriptify) AddEnumValues(typeOf reflect.Type, values interface{}) *TypeScriptify {
+ t.AddEnum(values)
+ return t
+}
+
+func (t *TypeScriptify) Convert(customCode map[string]string) (string, error) {
+ t.alreadyConverted = make(map[string]bool)
+ depth := 0
+
+ result := ""
+ if len(t.customImports) > 0 {
+ // Put the custom imports, i.e.: `import Decimal from 'decimal.js'`
+ for _, cimport := range t.customImports {
+ result += cimport + "\n"
+ }
+ }
+
+ for _, enumTyp := range t.enumTypes {
+ elements := t.enums[enumTyp.Type]
+ typeScriptCode, err := t.convertEnum(depth, enumTyp.Type, elements)
+ if err != nil {
+ return "", err
+ }
+ result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n")
+ }
+
+ for _, strctTyp := range t.structTypes {
+ typeScriptCode, err := t.convertType(depth, strctTyp.Type, customCode)
+ if err != nil {
+ return "", err
+ }
+ result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n")
+ }
+ return result, nil
+}
+
+func loadCustomCode(fileName string) (map[string]string, error) {
+ result := make(map[string]string)
+ f, err := os.Open(fileName)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return result, nil
+ }
+ return result, err
+ }
+ defer f.Close()
+
+ bytes, err := io.ReadAll(f)
+ if err != nil {
+ return result, err
+ }
+
+ var currentName string
+ var currentValue string
+ lines := strings.Split(string(bytes), "\n")
+ for _, line := range lines {
+ trimmedLine := strings.TrimSpace(line)
+ if strings.HasPrefix(trimmedLine, "//[") && strings.HasSuffix(trimmedLine, ":]") {
+ currentName = strings.Replace(strings.Replace(trimmedLine, "//[", "", -1), ":]", "", -1)
+ currentValue = ""
+ } else if trimmedLine == "//[end]" {
+ result[currentName] = strings.TrimRight(currentValue, " \t\r\n")
+ currentName = ""
+ currentValue = ""
+ } else if len(currentName) > 0 {
+ currentValue += line + "\n"
+ }
+ }
+
+ return result, nil
+}
+
+func (t TypeScriptify) backup(fileName string) error {
+ fileIn, err := os.Open(fileName)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return err
+ }
+ // No neet to backup, just return:
+ return nil
+ }
+ defer fileIn.Close()
+
+ bytes, err := io.ReadAll(fileIn)
+ if err != nil {
+ return err
+ }
+
+ _, backupFn := path.Split(fmt.Sprintf("%s-%s.backup", fileName, time.Now().Format("2006-01-02T15_04_05.99")))
+ if t.BackupDir != "" {
+ backupFn = path.Join(t.BackupDir, backupFn)
+ }
+
+ return os.WriteFile(backupFn, bytes, os.FileMode(0o700))
+}
+
+func (t TypeScriptify) ConvertToFile(fileName string, packageName string) error {
+ if len(t.BackupDir) > 0 {
+ err := t.backup(fileName)
+ if err != nil {
+ return err
+ }
+ }
+
+ customCode, err := loadCustomCode(fileName)
+ if err != nil {
+ return err
+ }
+
+ f, err := os.Create(fileName)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ converted, err := t.Convert(customCode)
+ if err != nil {
+ return err
+ }
+
+ var lines []string
+ sc := bufio.NewScanner(strings.NewReader(converted))
+ for sc.Scan() {
+ lines = append(lines, "\t"+sc.Text())
+ }
+
+ converted = "export namespace " + packageName + " {\n"
+ converted += strings.Join(lines, "\n")
+ converted += "\n}\n"
+
+ if _, err := f.WriteString("/* Do not change, this code is generated from Golang structs */\n\n"); err != nil {
+ return err
+ }
+ if _, err := f.WriteString(converted); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+type TSNamer interface {
+ TSName() string
+}
+
+func (t *TypeScriptify) convertEnum(depth int, typeOf reflect.Type, elements []enumElement) (string, error) {
+ t.logf(depth, "Converting enum %s", typeOf.String())
+ if _, found := t.alreadyConverted[typeOf.String()]; found { // Already converted
+ return "", nil
+ }
+ t.alreadyConverted[typeOf.String()] = true
+
+ entityName := t.Prefix + nameTypeOf(typeOf) + t.Suffix
+ result := "enum " + entityName + " {\n"
+
+ for _, val := range elements {
+ result += fmt.Sprintf("%s%s = %#v,\n", t.Indent, val.name, val.value)
+ }
+
+ result += "}"
+
+ if !t.DontExport {
+ result = "export " + result
+ }
+
+ return result, nil
+}
+
+func (t *TypeScriptify) getFieldOptions(structType reflect.Type, field reflect.StructField) TypeOptions {
+ // By default use options defined by tags:
+ opts := TypeOptions{TSTransform: field.Tag.Get(tsTransformTag), TSType: field.Tag.Get(tsType)}
+
+ overrides := []TypeOptions{}
+
+ // But there is maybe an struct-specific override:
+ for _, strct := range t.structTypes {
+ if strct.FieldOptions == nil {
+ continue
+ }
+ if strct.Type == structType {
+ if fldOpts, found := strct.FieldOptions[field.Type]; found {
+ overrides = append(overrides, fldOpts)
+ }
+ }
+ }
+
+ if fldOpts, found := t.fieldTypeOptions[field.Type]; found {
+ overrides = append(overrides, fldOpts)
+ }
+
+ for _, o := range overrides {
+ if o.TSTransform != "" {
+ opts.TSTransform = o.TSTransform
+ }
+ if o.TSType != "" {
+ opts.TSType = o.TSType
+ }
+ }
+
+ return opts
+}
+
+func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool) string {
+ jsonFieldName := ""
+ // function, complex, and channel types cannot be json-encoded
+ if field.Type.Kind() == reflect.Chan ||
+ field.Type.Kind() == reflect.Func ||
+ field.Type.Kind() == reflect.UnsafePointer ||
+ field.Type.Kind() == reflect.Complex128 ||
+ field.Type.Kind() == reflect.Complex64 {
+ return ""
+ }
+ jsonTag, hasTag := field.Tag.Lookup("json")
+ if !hasTag && field.IsExported() {
+ jsonFieldName = field.Name
+ if isPtr {
+ jsonFieldName += "?"
+ }
+ }
+ if len(jsonTag) > 0 {
+ jsonTagParts := strings.Split(jsonTag, ",")
+ if len(jsonTagParts) > 0 {
+ jsonFieldName = strings.Trim(jsonTagParts[0], t.Indent)
+ }
+ hasOmitEmpty := false
+ ignored := false
+ for _, t := range jsonTagParts {
+ if t == "" {
+ break
+ }
+ if t == "omitempty" {
+ hasOmitEmpty = true
+ break
+ }
+ if t == "-" {
+ ignored = true
+ break
+ }
+ }
+ if !ignored && isPtr || hasOmitEmpty {
+ jsonFieldName = fmt.Sprintf("%s?", jsonFieldName)
+ }
+ }
+ return jsonFieldName
+}
+
+func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode map[string]string) (string, error) {
+ if _, found := t.alreadyConverted[typeOf.String()]; found { // Already converted
+ return "", nil
+ }
+ fields := t.deepFields(typeOf)
+ t.logf(depth, "Converting type %s", typeOf.String())
+ if differentNamespaces(t.Namespace, typeOf) {
+ return "", nil
+ }
+
+ t.alreadyConverted[typeOf.String()] = true
+
+ entityName := t.Prefix + nameTypeOf(typeOf) + t.Suffix
+
+ if typeClashWithReservedKeyword(entityName) {
+ warnAboutTypesClash(entityName)
+ }
+
+ result := ""
+ if t.CreateInterface {
+ result += fmt.Sprintf("interface %s {\n", entityName)
+ } else {
+ result += fmt.Sprintf("class %s {\n", entityName)
+ }
+ if !t.DontExport {
+ result = "export " + result
+ }
+ builder := typeScriptClassBuilder{
+ types: t.kinds,
+ indent: t.Indent,
+ prefix: t.Prefix,
+ suffix: t.Suffix,
+ namespace: t.Namespace,
+ }
+
+ for _, field := range fields {
+ isPtr := field.Type.Kind() == reflect.Ptr
+ if isPtr {
+ field.Type = field.Type.Elem()
+ }
+ jsonFieldName := t.getJSONFieldName(field, isPtr)
+ if len(jsonFieldName) == 0 || jsonFieldName == "-" {
+ continue
+ }
+
+ var err error
+ fldOpts := t.getFieldOptions(typeOf, field)
+ if fldOpts.TSTransform != "" {
+ t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
+ err = builder.AddSimpleField(jsonFieldName, field, fldOpts)
+ } else if _, isEnum := t.enums[field.Type]; isEnum {
+ t.logf(depth, "- enum field %s.%s", typeOf.Name(), field.Name)
+ builder.AddEnumField(jsonFieldName, field)
+ } else if fldOpts.TSType != "" { // Struct:
+ t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
+ err = builder.AddSimpleField(jsonFieldName, field, fldOpts)
+ } else if field.Type.Kind() == reflect.Struct { // Struct:
+ t.logf(depth, "- struct %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String())
+
+ // Anonymous structures is ignored
+ // It is possible to generate them but hard to generate correct name
+ if field.Type.Name() != "" {
+ typeScriptChunk, err := t.convertType(depth+1, field.Type, customCode)
+ if err != nil {
+ return "", err
+ }
+ if typeScriptChunk != "" {
+ result = typeScriptChunk + "\n" + result
+ }
+ }
+
+ isKnownType := t.KnownStructs.Contains(getStructFQN(field.Type.String()))
+ if !isKnownType {
+ println("KnownStructs:", t.KnownStructs.Join("\t"))
+ println("Not found:", getStructFQN(field.Type.String()))
+ }
+ builder.AddStructField(jsonFieldName, field, !isKnownType)
+ } else if field.Type.Kind() == reflect.Map {
+ t.logf(depth, "- map field %s.%s", typeOf.Name(), field.Name)
+ // Also convert map key types if needed
+ var keyTypeToConvert reflect.Type
+ switch field.Type.Key().Kind() {
+ case reflect.Struct:
+ keyTypeToConvert = field.Type.Key()
+ case reflect.Ptr:
+ keyTypeToConvert = field.Type.Key().Elem()
+ }
+ if keyTypeToConvert != nil {
+ typeScriptChunk, err := t.convertType(depth+1, keyTypeToConvert, customCode)
+ if err != nil {
+ return "", err
+ }
+ if typeScriptChunk != "" {
+ result = typeScriptChunk + "\n" + result
+ }
+ }
+ // Also convert map value types if needed
+ var valueTypeToConvert reflect.Type
+ switch field.Type.Elem().Kind() {
+ case reflect.Struct:
+ valueTypeToConvert = field.Type.Elem()
+ case reflect.Ptr:
+ valueTypeToConvert = field.Type.Elem().Elem()
+ }
+ if valueTypeToConvert != nil {
+ typeScriptChunk, err := t.convertType(depth+1, valueTypeToConvert, customCode)
+ if err != nil {
+ return "", err
+ }
+ if typeScriptChunk != "" {
+ result = typeScriptChunk + "\n" + result
+ }
+ }
+
+ builder.AddMapField(jsonFieldName, field)
+ } else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array { // Slice:
+ if field.Type.Elem().Kind() == reflect.Ptr { // extract ptr type
+ field.Type = field.Type.Elem()
+ }
+
+ arrayDepth := 1
+ for field.Type.Elem().Kind() == reflect.Slice || field.Type.Elem().Kind() == reflect.Array { // Slice of slices:
+ field.Type = field.Type.Elem()
+ arrayDepth++
+ }
+
+ if field.Type.Elem().Kind() == reflect.Ptr { // extract ptr type
+ field.Type = field.Type.Elem()
+ }
+
+ if field.Type.Elem().Kind() == reflect.Struct { // Slice of structs:
+ t.logf(depth, "- struct slice %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String())
+ typeScriptChunk, err := t.convertType(depth+1, field.Type.Elem(), customCode)
+ if err != nil {
+ return "", err
+ }
+ if typeScriptChunk != "" {
+ result = typeScriptChunk + "\n" + result
+ }
+ builder.AddArrayOfStructsField(jsonFieldName, field, arrayDepth)
+ } else { // Slice of simple fields:
+ t.logf(depth, "- slice field %s.%s", typeOf.Name(), field.Name)
+ err = builder.AddSimpleArrayField(jsonFieldName, field, arrayDepth, fldOpts)
+ }
+ } else { // Simple field:
+ t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
+ // check if type is in known enum. If so, then replace TStype with enum name to avoid missing types
+ isKnownEnum := t.KnownEnums.Contains(getStructFQN(field.Type.String()))
+ if isKnownEnum {
+ err = builder.AddSimpleField(jsonFieldName, field, TypeOptions{
+ TSType: getStructFQN(field.Type.String()),
+ TSTransform: fldOpts.TSTransform,
+ })
+ } else {
+ err = builder.AddSimpleField(jsonFieldName, field, fldOpts)
+ }
+ }
+ if err != nil {
+ return "", err
+ }
+ }
+
+ if t.CreateFromMethod {
+ t.CreateConstructor = true
+ }
+
+ result += strings.Join(builder.fields, "\n") + "\n"
+ if !t.CreateInterface {
+ constructorBody := strings.Join(builder.constructorBody, "\n")
+ needsConvertValue := strings.Contains(constructorBody, "this.convertValues")
+ if t.CreateFromMethod {
+ result += fmt.Sprintf("\n%sstatic createFrom(source: any = {}) {\n", t.Indent)
+ result += fmt.Sprintf("%s%sreturn new %s(source);\n", t.Indent, t.Indent, entityName)
+ result += fmt.Sprintf("%s}\n", t.Indent)
+ }
+ if t.CreateConstructor {
+ result += fmt.Sprintf("\n%sconstructor(source: any = {}) {\n", t.Indent)
+ result += t.Indent + t.Indent + "if ('string' === typeof source) source = JSON.parse(source);\n"
+ result += constructorBody + "\n"
+ result += fmt.Sprintf("%s}\n", t.Indent)
+ }
+ if needsConvertValue && (t.CreateConstructor || t.CreateFromMethod) {
+ result += "\n" + indentLines(strings.ReplaceAll(tsConvertValuesFunc, "\t", t.Indent), 1) + "\n"
+ }
+ }
+
+ if customCode != nil {
+ code := customCode[entityName]
+ if len(code) != 0 {
+ result += t.Indent + "//[" + entityName + ":]\n" + code + "\n\n" + t.Indent + "//[end]\n"
+ }
+ }
+
+ result += "}"
+
+ return result, nil
+}
+
+func (t *TypeScriptify) AddImport(i string) {
+ for _, cimport := range t.customImports {
+ if cimport == i {
+ return
+ }
+ }
+
+ t.customImports = append(t.customImports, i)
+}
+
+type typeScriptClassBuilder struct {
+ types map[reflect.Kind]string
+ indent string
+ fields []string
+ createFromMethodBody []string
+ constructorBody []string
+ prefix, suffix string
+ namespace string
+}
+
+func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error {
+ fieldType := nameTypeOf(field.Type.Elem())
+ kind := field.Type.Elem().Kind()
+ typeScriptType, ok := t.types[kind]
+ if !ok {
+ typeScriptType = "any"
+ }
+
+ if len(fieldName) > 0 {
+ strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
+ if len(opts.TSType) > 0 {
+ t.addField(fieldName, opts.TSType, false)
+ t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
+ return nil
+ } else if len(typeScriptType) > 0 {
+ t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth)), false)
+ t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
+ return nil
+ }
+ }
+
+ return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType)
+}
+
+func (t *typeScriptClassBuilder) AddSimpleField(fieldName string, field reflect.StructField, opts TypeOptions) error {
+ fieldType := nameTypeOf(field.Type)
+ kind := field.Type.Kind()
+
+ typeScriptType, ok := t.types[kind]
+ if !ok {
+ typeScriptType = "any"
+ }
+
+ if len(opts.TSType) > 0 {
+ typeScriptType = opts.TSType
+ }
+
+ if len(typeScriptType) > 0 && len(fieldName) > 0 {
+ strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
+ t.addField(fieldName, typeScriptType, false)
+ if opts.TSTransform == "" {
+ t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
+ } else {
+ val := fmt.Sprintf(`source["%s"]`, strippedFieldName)
+ expression := strings.Replace(opts.TSTransform, "__VALUE__", val, -1)
+ t.addInitializerFieldLine(strippedFieldName, expression)
+ }
+ return nil
+ }
+
+ return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType)
+}
+
+func (t *typeScriptClassBuilder) AddEnumField(fieldName string, field reflect.StructField) {
+ fieldType := nameTypeOf(field.Type)
+ t.addField(fieldName, t.prefix+fieldType+t.suffix, false)
+ strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
+ t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
+}
+
+func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect.StructField, isAnyType bool) {
+ strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
+ classname := "null"
+ namespace := strings.Split(field.Type.String(), ".")[0]
+ fqname := t.prefix + nameTypeOf(field.Type) + t.suffix
+ if namespace != t.namespace {
+ fqname = namespace + "." + fqname
+ }
+
+ if !isAnyType {
+ classname = fqname
+ }
+
+ // Anonymous struct
+ if field.Type.Name() == "" {
+ classname = "Object"
+ }
+
+ t.addField(fieldName, fqname, isAnyType)
+ t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, classname))
+}
+
+func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int) {
+ fieldType := nameTypeOf(field.Type.Elem())
+ if differentNamespaces(t.namespace, field.Type.Elem()) {
+ fieldType = field.Type.Elem().String()
+ }
+ strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
+ t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth)), false)
+ t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix))
+}
+
+func (t *typeScriptClassBuilder) addInitializerFieldLine(fld, initializer string) {
+ var dotField string
+ if regexp.MustCompile(jsVariableNameRegex).Match([]byte(fld)) {
+ dotField = fmt.Sprintf(".%s", fld)
+ } else {
+ dotField = fmt.Sprintf(`["%s"]`, fld)
+ }
+ t.createFromMethodBody = append(t.createFromMethodBody, fmt.Sprint(t.indent, t.indent, "result", dotField, " = ", initializer, ";"))
+ t.constructorBody = append(t.constructorBody, fmt.Sprint(t.indent, t.indent, "this", dotField, " = ", initializer, ";"))
+}
+
+func (t *typeScriptClassBuilder) addField(fld, fldType string, isAnyType bool) {
+ isOptional := strings.HasSuffix(fld, "?")
+ strippedFieldName := strings.ReplaceAll(fld, "?", "")
+ if !regexp.MustCompile(jsVariableNameRegex).Match([]byte(strippedFieldName)) {
+ fld = fmt.Sprintf(`"%s"`, strippedFieldName)
+ if isOptional {
+ fld += "?"
+ }
+ }
+ if isAnyType {
+ fldType = strings.Split(fldType, ".")[0]
+ t.fields = append(t.fields, fmt.Sprint(t.indent, "// Go type: ", fldType, "\n", t.indent, fld, ": any;"))
+ } else {
+ t.fields = append(t.fields, fmt.Sprint(t.indent, fld, ": ", fldType, ";"))
+ }
+}
+
+func indentLines(str string, i int) string {
+ lines := strings.Split(str, "\n")
+ for n := range lines {
+ lines[n] = strings.Repeat("\t", i) + lines[n]
+ }
+ return strings.Join(lines, "\n")
+}
+
+func getStructFQN(in string) string {
+ result := strings.ReplaceAll(in, "[]", "")
+ result = strings.ReplaceAll(result, "*", "")
+ return result
+}
+
+func differentNamespaces(namespace string, typeOf reflect.Type) bool {
+ if strings.ContainsRune(typeOf.String(), '.') {
+ typeNamespace := strings.Split(typeOf.String(), ".")[0]
+ if namespace != typeNamespace {
+ return true
+ }
+ }
+ return false
+}
+
+func typeClashWithReservedKeyword(input string) bool {
+ in := strings.ToLower(strings.TrimSpace(input))
+ for _, v := range jsReservedKeywords {
+ if in == v {
+ return true
+ }
+ }
+
+ return false
+}
+
+func warnAboutTypesClash(entity string) {
+ // TODO: Refactor logging
+ l := log.New(os.Stderr, "", 0)
+ l.Printf("Usage of reserved keyword found and not supported: %s", entity)
+ log.Println("Please rename returned type or consider adding bindings config to your wails.json")
+}
diff --git a/v2/internal/webview2runtime/MicrosoftEdgeWebview2Setup.exe b/v2/internal/webview2runtime/MicrosoftEdgeWebview2Setup.exe
new file mode 100644
index 000000000..89a56ec16
Binary files /dev/null and b/v2/internal/webview2runtime/MicrosoftEdgeWebview2Setup.exe differ
diff --git a/v2/internal/webview2runtime/webview2installer.go b/v2/internal/webview2runtime/webview2installer.go
new file mode 100644
index 000000000..3645dae02
--- /dev/null
+++ b/v2/internal/webview2runtime/webview2installer.go
@@ -0,0 +1,21 @@
+package webview2runtime
+
+import (
+ _ "embed"
+ "os"
+ "path/filepath"
+)
+
+//go:embed MicrosoftEdgeWebview2Setup.exe
+var setupexe []byte
+
+// WriteInstallerToFile writes the installer file to the given file.
+func WriteInstallerToFile(targetFile string) error {
+ return os.WriteFile(targetFile, setupexe, 0o755)
+}
+
+// WriteInstaller writes the installer exe file to the given directory and returns the path to it.
+func WriteInstaller(targetPath string) (string, error) {
+ installer := filepath.Join(targetPath, `MicrosoftEdgeWebview2Setup.exe`)
+ return installer, WriteInstallerToFile(installer)
+}
diff --git a/v2/internal/webview2runtime/webview2runtime.go b/v2/internal/webview2runtime/webview2runtime.go
new file mode 100644
index 000000000..c5f6c0d53
--- /dev/null
+++ b/v2/internal/webview2runtime/webview2runtime.go
@@ -0,0 +1,169 @@
+//go:build windows
+// +build windows
+
+package webview2runtime
+
+import (
+ _ "embed"
+ "io"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "syscall"
+ "unsafe"
+)
+
+// Info contains all the information about an installation of the webview2 runtime.
+type Info struct {
+ Location string
+ Name string
+ Version string
+ SilentUninstall string
+}
+
+// IsOlderThan returns true if the installed version is older than the given required version.
+// Returns error if something goes wrong.
+func (i *Info) IsOlderThan(requiredVersion string) (bool, error) {
+ var mod = syscall.NewLazyDLL("WebView2Loader.dll")
+ var CompareBrowserVersions = mod.NewProc("CompareBrowserVersions")
+ v1, err := syscall.UTF16PtrFromString(i.Version)
+ if err != nil {
+ return false, err
+ }
+ v2, err := syscall.UTF16PtrFromString(requiredVersion)
+ if err != nil {
+ return false, err
+ }
+ var result int = 9
+ _, _, err = CompareBrowserVersions.Call(uintptr(unsafe.Pointer(v1)), uintptr(unsafe.Pointer(v2)), uintptr(unsafe.Pointer(&result)))
+ if result < -1 || result > 1 {
+ return false, err
+ }
+ return result == -1, nil
+}
+
+func downloadBootstrapper() (string, error) {
+ bootstrapperURL := `https://go.microsoft.com/fwlink/p/?LinkId=2124703`
+ installer := filepath.Join(os.TempDir(), `MicrosoftEdgeWebview2Setup.exe`)
+
+ // Download installer
+ out, err := os.Create(installer)
+ defer out.Close()
+ if err != nil {
+ return "", err
+ }
+ resp, err := http.Get(bootstrapperURL)
+ defer resp.Body.Close()
+ if err != nil {
+ err = out.Close()
+ return "", err
+ }
+ _, err = io.Copy(out, resp.Body)
+ if err != nil {
+ return "", err
+ }
+
+ return installer, nil
+}
+
+// InstallUsingEmbeddedBootstrapper will download the bootstrapper from Microsoft and run it to install
+// the latest version of the runtime.
+// Returns true if the installer ran successfully.
+// Returns an error if something goes wrong
+func InstallUsingEmbeddedBootstrapper() (bool, error) {
+ installer, err := WriteInstaller(os.TempDir())
+ if err != nil {
+ return false, err
+ }
+ result, err := runInstaller(installer)
+ if err != nil {
+ return false, err
+ }
+
+ return result, os.Remove(installer)
+
+}
+
+// InstallUsingBootstrapper will extract the embedded bootstrapper from Microsoft and run it to install
+// the latest version of the runtime.
+// Returns true if the installer ran successfully.
+// Returns an error if something goes wrong
+func InstallUsingBootstrapper() (bool, error) {
+
+ installer, err := downloadBootstrapper()
+ if err != nil {
+ return false, err
+ }
+
+ result, err := runInstaller(installer)
+ if err != nil {
+ return false, err
+ }
+
+ return result, os.Remove(installer)
+
+}
+
+func runInstaller(installer string) (bool, error) {
+ // Credit: https://stackoverflow.com/a/10385867
+ cmd := exec.Command(installer)
+ if err := cmd.Start(); err != nil {
+ return false, err
+ }
+ if err := cmd.Wait(); err != nil {
+ if exiterr, ok := err.(*exec.ExitError); ok {
+ if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
+ return status.ExitStatus() == 0, nil
+ }
+ }
+ }
+ return true, nil
+}
+
+// Confirm will prompt the user with a message and OK / CANCEL buttons.
+// Returns true if OK is selected by the user.
+// Returns an error if something went wrong.
+func Confirm(caption string, title string) (bool, error) {
+ var flags uint = 0x00000001 // MB_OKCANCEL
+ result, err := MessageBox(caption, title, flags)
+ if err != nil {
+ return false, err
+ }
+ return result == 1, nil
+}
+
+// Error will an error message to the user.
+// Returns an error if something went wrong.
+func Error(caption string, title string) error {
+ var flags uint = 0x00000010 // MB_ICONERROR
+ _, err := MessageBox(caption, title, flags)
+ return err
+}
+
+// MessageBox prompts the user with the given caption and title.
+// Flags may be provided to customise the dialog.
+// Returns an error if something went wrong.
+func MessageBox(caption string, title string, flags uint) (int, error) {
+ captionUTF16, err := syscall.UTF16PtrFromString(caption)
+ if err != nil {
+ return -1, err
+ }
+ titleUTF16, err := syscall.UTF16PtrFromString(title)
+ if err != nil {
+ return -1, err
+ }
+ ret, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call(
+ uintptr(0),
+ uintptr(unsafe.Pointer(captionUTF16)),
+ uintptr(unsafe.Pointer(titleUTF16)),
+ uintptr(flags))
+
+ return int(ret), nil
+}
+
+// OpenInstallerDownloadWebpage will open the browser on the WebView2 download page
+func OpenInstallerDownloadWebpage() error {
+ cmd := exec.Command("rundll32", "url.dll,FileProtocolHandler", "https://developer.microsoft.com/en-us/microsoft-edge/webview2/")
+ return cmd.Run()
+}
diff --git a/v2/internal/wv2installer/browser.go b/v2/internal/wv2installer/browser.go
new file mode 100644
index 000000000..2597bde6b
--- /dev/null
+++ b/v2/internal/wv2installer/browser.go
@@ -0,0 +1,25 @@
+//go:build windows && wv2runtime.browser
+// +build windows,wv2runtime.browser
+
+package wv2installer
+
+import (
+ "fmt"
+ "github.com/wailsapp/wails/v2/internal/webview2runtime"
+ "github.com/wailsapp/wails/v2/pkg/options/windows"
+)
+
+func doInstallationStrategy(installStatus installationStatus, messages *windows.Messages) error {
+ confirmed, err := webview2runtime.Confirm(messages.DownloadPage+MinimumRuntimeVersion, messages.MissingRequirements)
+ if err != nil {
+ return err
+ }
+ if confirmed {
+ err = webview2runtime.OpenInstallerDownloadWebpage()
+ if err != nil {
+ return err
+ }
+ }
+
+ return fmt.Errorf(messages.FailedToInstall)
+}
diff --git a/v2/internal/wv2installer/download.go b/v2/internal/wv2installer/download.go
new file mode 100644
index 000000000..0a054d661
--- /dev/null
+++ b/v2/internal/wv2installer/download.go
@@ -0,0 +1,35 @@
+//go:build windows && !wv2runtime.error && !wv2runtime.browser && !wv2runtime.embed
+// +build windows,!wv2runtime.error,!wv2runtime.browser,!wv2runtime.embed
+
+package wv2installer
+
+import (
+ "fmt"
+
+ "github.com/wailsapp/wails/v2/internal/webview2runtime"
+ "github.com/wailsapp/wails/v2/pkg/options/windows"
+)
+
+func doInstallationStrategy(installStatus installationStatus, messages *windows.Messages) error {
+ message := messages.InstallationRequired
+ if installStatus == needsUpdating {
+ message = messages.UpdateRequired
+ }
+ confirmed, err := webview2runtime.Confirm(message, messages.MissingRequirements)
+ if err != nil {
+ return err
+ }
+ if !confirmed {
+ return fmt.Errorf(messages.Webview2NotInstalled)
+ }
+ installedCorrectly, err := webview2runtime.InstallUsingBootstrapper()
+ if err != nil {
+ _ = webview2runtime.Error(err.Error(), messages.Error)
+ return err
+ }
+ if !installedCorrectly {
+ err = webview2runtime.Error(messages.FailedToInstall, messages.Error)
+ return err
+ }
+ return nil
+}
diff --git a/v2/internal/wv2installer/embed.go b/v2/internal/wv2installer/embed.go
new file mode 100644
index 000000000..942d6b51a
--- /dev/null
+++ b/v2/internal/wv2installer/embed.go
@@ -0,0 +1,35 @@
+//go:build windows && wv2runtime.embed
+// +build windows,wv2runtime.embed
+
+package wv2installer
+
+import (
+ "fmt"
+ "github.com/wailsapp/wails/v2/internal/webview2runtime"
+ "github.com/wailsapp/wails/v2/pkg/options/windows"
+)
+
+func doInstallationStrategy(installStatus installationStatus, messages *windows.Messages) error {
+ message := messages.InstallationRequired
+ if installStatus == needsUpdating {
+ message = messages.UpdateRequired
+ }
+ message += messages.PressOKToInstall
+ confirmed, err := webview2runtime.Confirm(message, messages.MissingRequirements)
+ if err != nil {
+ return err
+ }
+ if !confirmed {
+ return fmt.Errorf(messages.Webview2NotInstalled)
+ }
+ installedCorrectly, err := webview2runtime.InstallUsingEmbeddedBootstrapper()
+ if err != nil {
+ _ = webview2runtime.Error(err.Error(), messages.Error)
+ return err
+ }
+ if !installedCorrectly {
+ err = webview2runtime.Error(messages.FailedToInstall, messages.Error)
+ return err
+ }
+ return nil
+}
diff --git a/v2/internal/wv2installer/error.go b/v2/internal/wv2installer/error.go
new file mode 100644
index 000000000..ec48ef990
--- /dev/null
+++ b/v2/internal/wv2installer/error.go
@@ -0,0 +1,15 @@
+//go:build windows && wv2runtime.error
+// +build windows,wv2runtime.error
+
+package wv2installer
+
+import (
+ "fmt"
+ "github.com/wailsapp/wails/v2/internal/webview2runtime"
+ "github.com/wailsapp/wails/v2/pkg/options/windows"
+)
+
+func doInstallationStrategy(installStatus installationStatus, messages *windows.Messages) error {
+ _ = webview2runtime.Error(messages.ContactAdmin, messages.Error)
+ return fmt.Errorf(messages.Webview2NotInstalled)
+}
diff --git a/v2/internal/wv2installer/wv2installer.go b/v2/internal/wv2installer/wv2installer.go
new file mode 100644
index 000000000..c89ad196f
--- /dev/null
+++ b/v2/internal/wv2installer/wv2installer.go
@@ -0,0 +1,60 @@
+//go:build windows
+
+package wv2installer
+
+import (
+ "fmt"
+
+ "github.com/wailsapp/go-webview2/webviewloader"
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/windows"
+)
+
+const MinimumRuntimeVersion string = "94.0.992.31" // WebView2 SDK 1.0.992.28
+
+type installationStatus int
+
+const (
+ needsInstalling installationStatus = iota
+ needsUpdating
+)
+
+func Process(appoptions *options.App) (string, error) {
+ messages := windows.DefaultMessages()
+ if appoptions.Windows != nil && appoptions.Windows.Messages != nil {
+ messages = appoptions.Windows.Messages
+ }
+
+ installStatus := needsInstalling
+
+ // Override version check for manually specified webview path if present
+ var webviewPath = ""
+ if opts := appoptions.Windows; opts != nil && opts.WebviewBrowserPath != "" {
+ webviewPath = opts.WebviewBrowserPath
+ }
+
+ installedVersion, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(webviewPath)
+ if err != nil {
+ return "", err
+ }
+
+ if installedVersion != "" {
+ installStatus = needsUpdating
+ compareResult, err := webviewloader.CompareBrowserVersions(installedVersion, MinimumRuntimeVersion)
+ if err != nil {
+ return "", err
+ }
+ updateRequired := compareResult < 0
+ // Installed and does not require updating
+ if !updateRequired {
+ return installedVersion, nil
+ }
+ }
+
+ // Force error strategy if webview is manually specified
+ if webviewPath != "" {
+ return installedVersion, fmt.Errorf(messages.InvalidFixedWebview2)
+ }
+
+ return installedVersion, doInstallationStrategy(installStatus, messages)
+}
diff --git a/v2/pkg/application/application.go b/v2/pkg/application/application.go
new file mode 100644
index 000000000..8ba586969
--- /dev/null
+++ b/v2/pkg/application/application.go
@@ -0,0 +1,102 @@
+package application
+
+import (
+ "context"
+ "sync"
+
+ "github.com/wailsapp/wails/v2/internal/app"
+ "github.com/wailsapp/wails/v2/internal/signal"
+ "github.com/wailsapp/wails/v2/pkg/menu"
+ "github.com/wailsapp/wails/v2/pkg/options"
+)
+
+// Application is the main Wails application
+type Application struct {
+ application *app.App
+ options *options.App
+
+ // running flag
+ running bool
+
+ shutdown sync.Once
+}
+
+// NewWithOptions creates a new Application with the given options
+func NewWithOptions(options *options.App) *Application {
+ if options == nil {
+ return New()
+ }
+ return &Application{
+ options: options,
+ }
+}
+
+// New creates a new Application with the default options
+func New() *Application {
+ return &Application{
+ options: &options.App{},
+ }
+}
+
+// SetApplicationMenu sets the application menu
+func (a *Application) SetApplicationMenu(appMenu *menu.Menu) {
+ if a.running {
+ a.application.SetApplicationMenu(appMenu)
+ return
+ }
+
+ a.options.Menu = appMenu
+}
+
+// Run starts the application
+func (a *Application) Run() error {
+ err := applicationInit()
+ if err != nil {
+ return err
+ }
+
+ application, err := app.CreateApp(a.options)
+ if err != nil {
+ return err
+ }
+
+ a.application = application
+
+ // Control-C handlers
+ signal.OnShutdown(func() {
+ a.application.Shutdown()
+ })
+ signal.Start()
+
+ a.running = true
+
+ err = a.application.Run()
+ return err
+}
+
+// Quit will shut down the application
+func (a *Application) Quit() {
+ a.shutdown.Do(func() {
+ a.application.Shutdown()
+ })
+}
+
+// Bind the given struct to the application
+func (a *Application) Bind(boundStruct any) {
+ a.options.Bind = append(a.options.Bind, boundStruct)
+}
+
+func (a *Application) On(eventType EventType, callback func()) {
+ c := func(ctx context.Context) {
+ callback()
+ }
+
+ switch eventType {
+ case StartUp:
+ a.options.OnStartup = c
+ case ShutDown:
+ a.options.OnShutdown = c
+ case DomReady:
+ a.options.OnDomReady = c
+ }
+}
diff --git a/v2/pkg/application/events.go b/v2/pkg/application/events.go
new file mode 100644
index 000000000..3896e9e75
--- /dev/null
+++ b/v2/pkg/application/events.go
@@ -0,0 +1,9 @@
+package application
+
+type EventType int
+
+const (
+ StartUp EventType = iota
+ ShutDown
+ DomReady
+)
diff --git a/v2/pkg/application/init.go b/v2/pkg/application/init.go
new file mode 100644
index 000000000..0fc48cb05
--- /dev/null
+++ b/v2/pkg/application/init.go
@@ -0,0 +1,8 @@
+//go:build !windows
+// +build !windows
+
+package application
+
+func applicationInit() error {
+ return nil
+}
diff --git a/v2/pkg/application/init_windows.go b/v2/pkg/application/init_windows.go
new file mode 100644
index 000000000..7d2900d3d
--- /dev/null
+++ b/v2/pkg/application/init_windows.go
@@ -0,0 +1,16 @@
+//go:build windows
+
+package application
+
+import (
+ "fmt"
+ "syscall"
+)
+
+func applicationInit() error {
+ status, r, err := syscall.NewLazyDLL("user32.dll").NewProc("SetProcessDPIAware").Call()
+ if status == 0 {
+ return fmt.Errorf("exit status %d: %v %v", status, r, err)
+ }
+ return nil
+}
diff --git a/v2/pkg/assetserver/assethandler.go b/v2/pkg/assetserver/assethandler.go
new file mode 100644
index 000000000..b8e2df076
--- /dev/null
+++ b/v2/pkg/assetserver/assethandler.go
@@ -0,0 +1,205 @@
+package assetserver
+
+import (
+ "bytes"
+ "embed"
+ "errors"
+ "fmt"
+ "io"
+ iofs "io/fs"
+ "net/http"
+ "os"
+ "path"
+ "strconv"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+type Logger interface {
+ Debug(message string, args ...interface{})
+ Error(message string, args ...interface{})
+}
+
+//go:embed defaultindex.html
+var defaultHTML []byte
+
+const (
+ indexHTML = "index.html"
+)
+
+type assetHandler struct {
+ fs iofs.FS
+ handler http.Handler
+
+ logger Logger
+
+ retryMissingFiles bool
+}
+
+func NewAssetHandler(options assetserver.Options, log Logger) (http.Handler, error) {
+ vfs := options.Assets
+ if vfs != nil {
+ if _, err := vfs.Open("."); err != nil {
+ return nil, err
+ }
+
+ subDir, err := FindPathToFile(vfs, indexHTML)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ msg := "no `index.html` could be found in your Assets fs.FS"
+ if embedFs, isEmbedFs := vfs.(embed.FS); isEmbedFs {
+ rootFolder, _ := FindEmbedRootPath(embedFs)
+ msg += fmt.Sprintf(", please make sure the embedded directory '%s' is correct and contains your assets", rootFolder)
+ }
+
+ return nil, fmt.Errorf(msg)
+ }
+
+ return nil, err
+ }
+
+ vfs, err = iofs.Sub(vfs, path.Clean(subDir))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ var result http.Handler = &assetHandler{
+ fs: vfs,
+ handler: options.Handler,
+ logger: log,
+ }
+
+ if middleware := options.Middleware; middleware != nil {
+ result = middleware(result)
+ }
+
+ return result, nil
+}
+
+func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ url := req.URL.Path
+ handler := d.handler
+ if strings.EqualFold(req.Method, http.MethodGet) {
+ filename := path.Clean(strings.TrimPrefix(url, "/"))
+
+ d.logDebug("Handling request '%s' (file='%s')", url, filename)
+ if err := d.serveFSFile(rw, req, filename); err != nil {
+ if os.IsNotExist(err) {
+ if handler != nil {
+ d.logDebug("File '%s' not found, serving '%s' by AssetHandler", filename, url)
+ handler.ServeHTTP(rw, req)
+ err = nil
+ } else {
+ rw.WriteHeader(http.StatusNotFound)
+ err = nil
+ }
+ }
+
+ if err != nil {
+ d.logError("Unable to handle request '%s': %s", url, err)
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ }
+ }
+ } else if handler != nil {
+ d.logDebug("No GET request, serving '%s' by AssetHandler", url)
+ handler.ServeHTTP(rw, req)
+ } else {
+ rw.WriteHeader(http.StatusMethodNotAllowed)
+ }
+}
+
+// serveFSFile will try to load the file from the fs.FS and write it to the response
+func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, filename string) error {
+ if d.fs == nil {
+ return os.ErrNotExist
+ }
+
+ file, err := d.fs.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ statInfo, err := file.Stat()
+ if err != nil {
+ return err
+ }
+
+ url := req.URL.Path
+ isDirectoryPath := url == "" || url[len(url)-1] == '/'
+ if statInfo.IsDir() {
+ if !isDirectoryPath {
+ // If the URL doesn't end in a slash normally a http.redirect should be done, but that currently doesn't work on
+ // WebKit WebViews (macOS/Linux).
+ // So we handle this as a specific error
+ return fmt.Errorf("a directory has been requested without a trailing slash, please add a trailing slash to your request")
+ }
+
+ filename = path.Join(filename, indexHTML)
+
+ file, err = d.fs.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ statInfo, err = file.Stat()
+ if err != nil {
+ return err
+ }
+ } else if isDirectoryPath {
+ return fmt.Errorf("a file has been requested with a trailing slash, please remove the trailing slash from your request")
+ }
+
+ var buf [512]byte
+ var n int
+ if _, haveType := rw.Header()[HeaderContentType]; !haveType {
+ // Detect MimeType by sniffing the first 512 bytes
+ n, err = file.Read(buf[:])
+ if err != nil && err != io.EOF {
+ return err
+ }
+
+ // Do the custom MimeType sniffing even though http.ServeContent would do it in case
+ // of an io.ReadSeeker. We would like to have a consistent behaviour in both cases.
+ if contentType := GetMimetype(filename, buf[:n]); contentType != "" {
+ rw.Header().Set(HeaderContentType, contentType)
+ }
+ }
+
+ if fileSeeker, _ := file.(io.ReadSeeker); fileSeeker != nil {
+ if _, err := fileSeeker.Seek(0, io.SeekStart); err != nil {
+ return fmt.Errorf("seeker can't seek")
+ }
+
+ http.ServeContent(rw, req, statInfo.Name(), statInfo.ModTime(), fileSeeker)
+ return nil
+ }
+
+ size := strconv.FormatInt(statInfo.Size(), 10)
+ rw.Header().Set(HeaderContentLength, size)
+
+ // Write the first 512 bytes used for MimeType sniffing
+ _, err = io.Copy(rw, bytes.NewReader(buf[:n]))
+ if err != nil {
+ return err
+ }
+
+ // Copy the remaining content of the file
+ _, err = io.Copy(rw, file)
+ return err
+}
+
+func (d *assetHandler) logDebug(message string, args ...interface{}) {
+ if d.logger != nil {
+ d.logger.Debug("[AssetHandler] "+message, args...)
+ }
+}
+
+func (d *assetHandler) logError(message string, args ...interface{}) {
+ if d.logger != nil {
+ d.logger.Error("[AssetHandler] "+message, args...)
+ }
+}
diff --git a/v2/pkg/assetserver/assethandler_external.go b/v2/pkg/assetserver/assethandler_external.go
new file mode 100644
index 000000000..98b3404e9
--- /dev/null
+++ b/v2/pkg/assetserver/assethandler_external.go
@@ -0,0 +1,84 @@
+package assetserver
+
+import (
+ "errors"
+ "fmt"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+)
+
+func NewProxyServer(proxyURL string) http.Handler {
+ parsedURL, err := url.Parse(proxyURL)
+ if err != nil {
+ panic(err)
+ }
+ return httputil.NewSingleHostReverseProxy(parsedURL)
+}
+
+func NewExternalAssetsHandler(logger Logger, options assetserver.Options, url *url.URL) http.Handler {
+ baseHandler := options.Handler
+
+ errSkipProxy := fmt.Errorf("skip proxying")
+
+ proxy := httputil.NewSingleHostReverseProxy(url)
+ baseDirector := proxy.Director
+ proxy.Director = func(r *http.Request) {
+ baseDirector(r)
+ if logger != nil {
+ logger.Debug("[ExternalAssetHandler] Loading '%s'", r.URL)
+ }
+ }
+
+ proxy.ModifyResponse = func(res *http.Response) error {
+ if baseHandler == nil {
+ return nil
+ }
+
+ if res.StatusCode == http.StatusSwitchingProtocols {
+ return nil
+ }
+
+ if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusMethodNotAllowed {
+ return errSkipProxy
+ }
+
+ return nil
+ }
+
+ proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) {
+ if baseHandler != nil && errors.Is(err, errSkipProxy) {
+ if logger != nil {
+ logger.Debug("[ExternalAssetHandler] '%s' returned not found, using AssetHandler", r.URL)
+ }
+ baseHandler.ServeHTTP(rw, r)
+ } else {
+ if logger != nil {
+ logger.Error("[ExternalAssetHandler] Proxy error: %v", err)
+ }
+ rw.WriteHeader(http.StatusBadGateway)
+ }
+ }
+
+ var result http.Handler = http.HandlerFunc(
+ func(rw http.ResponseWriter, req *http.Request) {
+ if req.Method == http.MethodGet {
+ proxy.ServeHTTP(rw, req)
+ return
+ }
+
+ if baseHandler != nil {
+ baseHandler.ServeHTTP(rw, req)
+ return
+ }
+
+ rw.WriteHeader(http.StatusMethodNotAllowed)
+ })
+
+ if middleware := options.Middleware; middleware != nil {
+ result = middleware(result)
+ }
+
+ return result
+}
diff --git a/v2/pkg/assetserver/assetserver.go b/v2/pkg/assetserver/assetserver.go
new file mode 100644
index 000000000..59665c091
--- /dev/null
+++ b/v2/pkg/assetserver/assetserver.go
@@ -0,0 +1,255 @@
+package assetserver
+
+import (
+ "bytes"
+ "fmt"
+ "math/rand"
+ "net/http"
+ "strings"
+
+ "golang.org/x/net/html"
+ "html/template"
+
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+const (
+ runtimeJSPath = "/wails/runtime.js"
+ ipcJSPath = "/wails/ipc.js"
+ runtimePath = "/wails/runtime"
+)
+
+type RuntimeAssets interface {
+ DesktopIPC() []byte
+ WebsocketIPC() []byte
+ RuntimeDesktopJS() []byte
+}
+
+type RuntimeHandler interface {
+ HandleRuntimeCall(w http.ResponseWriter, r *http.Request)
+}
+
+type AssetServer struct {
+ handler http.Handler
+ runtimeJS []byte
+ ipcJS func(*http.Request) []byte
+
+ logger Logger
+ runtime RuntimeAssets
+
+ servingFromDisk bool
+ appendSpinnerToBody bool
+
+ // Use http based runtime
+ runtimeHandler RuntimeHandler
+
+ // plugin scripts
+ pluginScripts map[string]string
+
+ assetServerWebView
+}
+
+func NewAssetServerMainPage(bindingsJSON string, options *options.App, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
+ assetOptions, err := BuildAssetServerConfig(options)
+ if err != nil {
+ return nil, err
+ }
+ return NewAssetServer(bindingsJSON, assetOptions, servingFromDisk, logger, runtime)
+}
+
+func NewAssetServer(bindingsJSON string, options assetserver.Options, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
+ handler, err := NewAssetHandler(options, logger)
+ if err != nil {
+ return nil, err
+ }
+
+ return NewAssetServerWithHandler(handler, bindingsJSON, servingFromDisk, logger, runtime)
+}
+
+func NewAssetServerWithHandler(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
+
+ var buffer bytes.Buffer
+ if bindingsJSON != "" {
+ escapedBindingsJSON := template.JSEscapeString(bindingsJSON)
+ buffer.WriteString(`window.wailsbindings='` + escapedBindingsJSON + `';` + "\n")
+ }
+ buffer.Write(runtime.RuntimeDesktopJS())
+
+ result := &AssetServer{
+ handler: handler,
+ runtimeJS: buffer.Bytes(),
+
+ // 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 in dev mode.
+ servingFromDisk: servingFromDisk,
+ logger: logger,
+ runtime: runtime,
+ }
+
+ return result, nil
+}
+
+func (d *AssetServer) UseRuntimeHandler(handler RuntimeHandler) {
+ d.runtimeHandler = handler
+}
+
+func (d *AssetServer) AddPluginScript(pluginName string, script string) {
+ if d.pluginScripts == nil {
+ d.pluginScripts = make(map[string]string)
+ }
+ pluginName = strings.ReplaceAll(pluginName, "/", "_")
+ pluginName = html.EscapeString(pluginName)
+ pluginScriptName := fmt.Sprintf("/plugin_%s_%d.js", pluginName, rand.Intn(100000))
+ d.pluginScripts[pluginScriptName] = script
+}
+
+func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ if isWebSocket(req) {
+ // WebSockets are not supported by the AssetServer
+ rw.WriteHeader(http.StatusNotImplemented)
+ return
+ }
+
+ if d.servingFromDisk {
+ rw.Header().Add(HeaderCacheControl, "no-cache")
+ }
+
+ handler := d.handler
+ if req.Method != http.MethodGet {
+ handler.ServeHTTP(rw, req)
+ return
+ }
+
+ path := req.URL.Path
+ if path == runtimeJSPath {
+ d.writeBlob(rw, path, d.runtimeJS)
+ } else if path == runtimePath && d.runtimeHandler != nil {
+ d.runtimeHandler.HandleRuntimeCall(rw, req)
+ } else if path == ipcJSPath {
+ content := d.runtime.DesktopIPC()
+ if d.ipcJS != nil {
+ content = d.ipcJS(req)
+ }
+ d.writeBlob(rw, path, content)
+
+ } else if script, ok := d.pluginScripts[path]; ok {
+ d.writeBlob(rw, path, []byte(script))
+ } else if d.isRuntimeInjectionMatch(path) {
+ recorder := &bodyRecorder{
+ ResponseWriter: rw,
+ doRecord: func(code int, h http.Header) bool {
+ if code == http.StatusNotFound {
+ return true
+ }
+
+ if code != http.StatusOK {
+ return false
+ }
+
+ return strings.Contains(h.Get(HeaderContentType), "text/html")
+ },
+ }
+
+ handler.ServeHTTP(recorder, req)
+
+ body := recorder.Body()
+ if body == nil {
+ // The body has been streamed and not recorded, we are finished
+ return
+ }
+
+ code := recorder.Code()
+ switch code {
+ case http.StatusOK:
+ content, err := d.processIndexHTML(body.Bytes())
+ if err != nil {
+ d.serveError(rw, err, "Unable to processIndexHTML")
+ return
+ }
+ d.writeBlob(rw, indexHTML, content)
+
+ case http.StatusNotFound:
+ d.writeBlob(rw, indexHTML, defaultHTML)
+
+ default:
+ rw.WriteHeader(code)
+
+ }
+
+ } else {
+ handler.ServeHTTP(rw, req)
+ }
+}
+
+func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) {
+ htmlNode, err := getHTMLNode(indexHTML)
+ if err != nil {
+ return nil, err
+ }
+
+ if d.appendSpinnerToBody {
+ err = appendSpinnerToBody(htmlNode)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if err := insertScriptInHead(htmlNode, runtimeJSPath); err != nil {
+ return nil, err
+ }
+
+ if err := insertScriptInHead(htmlNode, ipcJSPath); err != nil {
+ return nil, err
+ }
+
+ // Inject plugins
+ for scriptName := range d.pluginScripts {
+ if err := insertScriptInHead(htmlNode, scriptName); err != nil {
+ return nil, err
+ }
+ }
+
+ var buffer bytes.Buffer
+ err = html.Render(&buffer, htmlNode)
+ if err != nil {
+ return nil, err
+ }
+ return buffer.Bytes(), nil
+}
+
+func (d *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {
+ err := serveFile(rw, filename, blob)
+ if err != nil {
+ d.serveError(rw, err, "Unable to write content %s", filename)
+ }
+}
+
+func (d *AssetServer) serveError(rw http.ResponseWriter, err error, msg string, args ...interface{}) {
+ args = append(args, err)
+ d.logError(msg+": %s", args...)
+ rw.WriteHeader(http.StatusInternalServerError)
+}
+
+func (d *AssetServer) logDebug(message string, args ...interface{}) {
+ if d.logger != nil {
+ d.logger.Debug("[AssetServer] "+message, args...)
+ }
+}
+
+func (d *AssetServer) logError(message string, args ...interface{}) {
+ if d.logger != nil {
+ d.logger.Error("[AssetServer] "+message, args...)
+ }
+}
+
+func (AssetServer) isRuntimeInjectionMatch(path string) bool {
+ if path == "" {
+ path = "/"
+ }
+
+ return strings.HasSuffix(path, "/") ||
+ strings.HasSuffix(path, "/"+indexHTML)
+}
diff --git a/v2/pkg/assetserver/assetserver_dev.go b/v2/pkg/assetserver/assetserver_dev.go
new file mode 100644
index 000000000..f6a2a0d2f
--- /dev/null
+++ b/v2/pkg/assetserver/assetserver_dev.go
@@ -0,0 +1,31 @@
+//go:build dev
+// +build dev
+
+package assetserver
+
+import (
+ "net/http"
+ "strings"
+)
+
+/*
+The assetserver for the dev mode.
+Depending on the UserAgent it injects a websocket based IPC script into `index.html` or the default desktop IPC. The
+default desktop IPC is injected when the webview accesses the devserver.
+*/
+func NewDevAssetServer(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
+ result, err := NewAssetServerWithHandler(handler, bindingsJSON, servingFromDisk, logger, runtime)
+ if err != nil {
+ return nil, err
+ }
+
+ result.appendSpinnerToBody = true
+ result.ipcJS = func(req *http.Request) []byte {
+ if strings.Contains(req.UserAgent(), WailsUserAgentValue) {
+ return runtime.DesktopIPC()
+ }
+ return runtime.WebsocketIPC()
+ }
+
+ return result, nil
+}
diff --git a/v2/pkg/assetserver/assetserver_webview.go b/v2/pkg/assetserver/assetserver_webview.go
new file mode 100644
index 000000000..63f80f0ae
--- /dev/null
+++ b/v2/pkg/assetserver/assetserver_webview.go
@@ -0,0 +1,185 @@
+package assetserver
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/wailsapp/wails/v2/pkg/assetserver/webview"
+)
+
+type assetServerWebView struct {
+ // ExpectedWebViewHost is checked against the Request Host of every WebViewRequest, other hosts won't be processed.
+ ExpectedWebViewHost string
+
+ dispatchInit sync.Once
+ dispatchReqC chan<- webview.Request
+ dispatchWorkers int
+}
+
+// ServeWebViewRequest processes the HTTP Request asynchronously by faking a golang HTTP Server.
+// The request will be finished with a StatusNotImplemented code if no handler has written to the response.
+// The AssetServer takes ownership of the request and the caller mustn't close it or access it in any other way.
+func (d *AssetServer) ServeWebViewRequest(req webview.Request) {
+ d.dispatchInit.Do(func() {
+ workers := d.dispatchWorkers
+ if workers <= 0 {
+ return
+ }
+
+ workerC := make(chan webview.Request, workers*2)
+ for i := 0; i < workers; i++ {
+ go func() {
+ for req := range workerC {
+ d.processWebViewRequest(req)
+ }
+ }()
+ }
+
+ dispatchC := make(chan webview.Request)
+ go queueingDispatcher(50, dispatchC, workerC)
+
+ d.dispatchReqC = dispatchC
+ })
+
+ if d.dispatchReqC == nil {
+ go d.processWebViewRequest(req)
+ } else {
+ d.dispatchReqC <- req
+ }
+}
+
+func (d *AssetServer) processWebViewRequest(r webview.Request) {
+ uri, _ := r.URL()
+ d.processWebViewRequestInternal(r)
+ if err := r.Close(); err != nil {
+ d.logError("Unable to call close for request for uri '%s'", uri)
+ }
+}
+
+// processWebViewRequestInternal processes the HTTP Request by faking a golang HTTP Server.
+// The request will be finished with a StatusNotImplemented code if no handler has written to the response.
+func (d *AssetServer) processWebViewRequestInternal(r webview.Request) {
+ uri := "unknown"
+ var err error
+
+ wrw := r.Response()
+ defer func() {
+ if err := wrw.Finish(); err != nil {
+ d.logError("Error finishing request '%s': %s", uri, err)
+ }
+ }()
+
+ var rw http.ResponseWriter = &contentTypeSniffer{rw: wrw} // Make sure we have a Content-Type sniffer
+ defer rw.WriteHeader(http.StatusNotImplemented) // This is a NOP when a handler has already written and set the status
+
+ uri, err = r.URL()
+ if err != nil {
+ d.logError("Error processing request, unable to get URL: %s (HttpResponse=500)", err)
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ method, err := r.Method()
+ if err != nil {
+ d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Method: %w", err))
+ return
+ }
+
+ header, err := r.Header()
+ if err != nil {
+ d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Header: %w", err))
+ return
+ }
+
+ body, err := r.Body()
+ if err != nil {
+ d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Body: %w", err))
+ return
+ }
+
+ if body == nil {
+ body = http.NoBody
+ }
+ defer body.Close()
+
+ req, err := http.NewRequest(method, uri, body)
+ if err != nil {
+ d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err))
+ return
+ }
+
+ // For server requests, the URL is parsed from the URI supplied on the Request-Line as stored in RequestURI. For
+ // most requests, fields other than Path and RawQuery will be empty. (See RFC 7230, Section 5.3)
+ req.URL.Scheme = ""
+ req.URL.Host = ""
+ req.URL.Fragment = ""
+ req.URL.RawFragment = ""
+
+ if url := req.URL; req.RequestURI == "" && url != nil {
+ req.RequestURI = url.String()
+ }
+
+ req.Header = header
+
+ if req.RemoteAddr == "" {
+ // 192.0.2.0/24 is "TEST-NET" in RFC 5737
+ req.RemoteAddr = "192.0.2.1:1234"
+ }
+
+ if req.ContentLength == 0 {
+ req.ContentLength = -1
+ } else {
+ size := strconv.FormatInt(req.ContentLength, 10)
+ req.Header.Set(HeaderContentLength, size)
+ }
+
+ if host := req.Header.Get(HeaderHost); host != "" {
+ req.Host = host
+ }
+
+ if expectedHost := d.ExpectedWebViewHost; expectedHost != "" && expectedHost != req.Host {
+ d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("expected host '%s' in request, but was '%s'", expectedHost, req.Host))
+ return
+ }
+
+ d.ServeHTTP(rw, req)
+}
+
+func (d *AssetServer) webviewRequestErrorHandler(uri string, rw http.ResponseWriter, err error) {
+ logInfo := uri
+ if uri, err := url.ParseRequestURI(uri); err == nil {
+ logInfo = strings.Replace(logInfo, fmt.Sprintf("%s://%s", uri.Scheme, uri.Host), "", 1)
+ }
+
+ d.logError("Error processing request '%s': %s (HttpResponse=500)", logInfo, err)
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+}
+
+func queueingDispatcher[T any](minQueueSize uint, inC <-chan T, outC chan<- T) {
+ q := newRingqueue[T](minQueueSize)
+ for {
+ in, ok := <-inC
+ if !ok {
+ return
+ }
+
+ q.Add(in)
+ for q.Len() != 0 {
+ out, _ := q.Peek()
+ select {
+ case outC <- out:
+ q.Remove()
+ case in, ok := <-inC:
+ if !ok {
+ return
+ }
+
+ q.Add(in)
+ }
+ }
+ }
+}
diff --git a/v2/pkg/assetserver/body_recorder.go b/v2/pkg/assetserver/body_recorder.go
new file mode 100644
index 000000000..fa3bc1e7c
--- /dev/null
+++ b/v2/pkg/assetserver/body_recorder.go
@@ -0,0 +1,61 @@
+package assetserver
+
+import (
+ "bytes"
+ "net/http"
+)
+
+type bodyRecorder struct {
+ http.ResponseWriter
+ doRecord func(code int, header http.Header) bool
+
+ body *bytes.Buffer
+ code int
+ wroteHeader bool
+}
+
+func (rw *bodyRecorder) Write(buf []byte) (int, error) {
+ rw.writeHeader(buf, http.StatusOK)
+ if rw.body != nil {
+ return rw.body.Write(buf)
+ }
+ return rw.ResponseWriter.Write(buf)
+}
+
+func (rw *bodyRecorder) WriteHeader(code int) {
+ rw.writeHeader(nil, code)
+}
+
+func (rw *bodyRecorder) Code() int {
+ return rw.code
+}
+
+func (rw *bodyRecorder) Body() *bytes.Buffer {
+ return rw.body
+}
+
+func (rw *bodyRecorder) writeHeader(buf []byte, code int) {
+ if rw.wroteHeader {
+ return
+ }
+
+ if rw.doRecord != nil {
+ header := rw.Header()
+ if len(buf) != 0 {
+ if _, hasType := header[HeaderContentType]; !hasType {
+ header.Set(HeaderContentType, http.DetectContentType(buf))
+ }
+ }
+
+ if rw.doRecord(code, header) {
+ rw.body = bytes.NewBuffer(nil)
+ }
+ }
+
+ if rw.body == nil {
+ rw.ResponseWriter.WriteHeader(code)
+ }
+
+ rw.code = code
+ rw.wroteHeader = true
+}
diff --git a/v2/pkg/assetserver/common.go b/v2/pkg/assetserver/common.go
new file mode 100644
index 000000000..57934e08e
--- /dev/null
+++ b/v2/pkg/assetserver/common.go
@@ -0,0 +1,135 @@
+package assetserver
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+ "golang.org/x/net/html"
+)
+
+func BuildAssetServerConfig(appOptions *options.App) (assetserver.Options, error) {
+ var options assetserver.Options
+ if opt := appOptions.AssetServer; opt != nil {
+ if appOptions.Assets != nil || appOptions.AssetsHandler != nil {
+ panic("It's not possible to use the deprecated Assets and AssetsHandler options and the new AssetServer option at the same time. Please migrate all your Assets options to the AssetServer option.")
+ }
+
+ options = *opt
+ } else {
+ options = assetserver.Options{
+ Assets: appOptions.Assets,
+ Handler: appOptions.AssetsHandler,
+ }
+ }
+
+ return options, options.Validate()
+}
+
+const (
+ HeaderHost = "Host"
+ HeaderContentType = "Content-Type"
+ HeaderContentLength = "Content-Length"
+ HeaderUserAgent = "User-Agent"
+ HeaderCacheControl = "Cache-Control"
+ HeaderUpgrade = "Upgrade"
+
+ WailsUserAgentValue = "wails.io"
+)
+
+func serveFile(rw http.ResponseWriter, filename string, blob []byte) error {
+ header := rw.Header()
+ header.Set(HeaderContentLength, strconv.Itoa(len(blob)))
+ if mimeType := header.Get(HeaderContentType); mimeType == "" {
+ mimeType = GetMimetype(filename, blob)
+ header.Set(HeaderContentType, mimeType)
+ }
+
+ rw.WriteHeader(http.StatusOK)
+ _, err := io.Copy(rw, bytes.NewReader(blob))
+ return err
+}
+
+func createScriptNode(scriptName string) *html.Node {
+ return &html.Node{
+ Type: html.ElementNode,
+ Data: "script",
+ Attr: []html.Attribute{
+ {
+ Key: "src",
+ Val: scriptName,
+ },
+ },
+ }
+}
+
+func createDivNode(id string) *html.Node {
+ return &html.Node{
+ Type: html.ElementNode,
+ Data: "div",
+ Attr: []html.Attribute{
+ {
+ Namespace: "",
+ Key: "id",
+ Val: id,
+ },
+ },
+ }
+}
+
+func insertScriptInHead(htmlNode *html.Node, scriptName string) error {
+ headNode := findFirstTag(htmlNode, "head")
+ if headNode == nil {
+ return errors.New("cannot find head in HTML")
+ }
+ scriptNode := createScriptNode(scriptName)
+ if headNode.FirstChild != nil {
+ headNode.InsertBefore(scriptNode, headNode.FirstChild)
+ } else {
+ headNode.AppendChild(scriptNode)
+ }
+ return nil
+}
+
+func appendSpinnerToBody(htmlNode *html.Node) error {
+ bodyNode := findFirstTag(htmlNode, "body")
+ if bodyNode == nil {
+ return errors.New("cannot find body in HTML")
+ }
+ scriptNode := createDivNode("wails-spinner")
+ bodyNode.AppendChild(scriptNode)
+ return nil
+}
+
+func getHTMLNode(htmldata []byte) (*html.Node, error) {
+ return html.Parse(bytes.NewReader(htmldata))
+}
+
+func findFirstTag(htmlnode *html.Node, tagName string) *html.Node {
+ var extractor func(*html.Node) *html.Node
+ var result *html.Node
+ extractor = func(node *html.Node) *html.Node {
+ if node.Type == html.ElementNode && node.Data == tagName {
+ return node
+ }
+ for child := node.FirstChild; child != nil; child = child.NextSibling {
+ result := extractor(child)
+ if result != nil {
+ return result
+ }
+ }
+ return nil
+ }
+ result = extractor(htmlnode)
+ return result
+}
+
+func isWebSocket(req *http.Request) bool {
+ upgrade := req.Header.Get(HeaderUpgrade)
+ return strings.EqualFold(upgrade, "websocket")
+}
diff --git a/v2/pkg/assetserver/content_type_sniffer.go b/v2/pkg/assetserver/content_type_sniffer.go
new file mode 100644
index 000000000..475428ae5
--- /dev/null
+++ b/v2/pkg/assetserver/content_type_sniffer.go
@@ -0,0 +1,42 @@
+package assetserver
+
+import (
+ "net/http"
+)
+
+type contentTypeSniffer struct {
+ rw http.ResponseWriter
+
+ wroteHeader bool
+}
+
+func (rw *contentTypeSniffer) Header() http.Header {
+ return rw.rw.Header()
+}
+
+func (rw *contentTypeSniffer) Write(buf []byte) (int, error) {
+ rw.writeHeader(buf)
+ return rw.rw.Write(buf)
+}
+
+func (rw *contentTypeSniffer) WriteHeader(code int) {
+ if rw.wroteHeader {
+ return
+ }
+
+ rw.rw.WriteHeader(code)
+ rw.wroteHeader = true
+}
+
+func (rw *contentTypeSniffer) writeHeader(b []byte) {
+ if rw.wroteHeader {
+ return
+ }
+
+ m := rw.rw.Header()
+ if _, hasType := m[HeaderContentType]; !hasType {
+ m.Set(HeaderContentType, http.DetectContentType(b))
+ }
+
+ rw.WriteHeader(http.StatusOK)
+}
diff --git a/v2/pkg/assetserver/defaultindex.html b/v2/pkg/assetserver/defaultindex.html
new file mode 100644
index 000000000..1ea97c405
--- /dev/null
+++ b/v2/pkg/assetserver/defaultindex.html
@@ -0,0 +1,39 @@
+
+
+
+
+ index.html not found
+
+
+
+
+
index.html not found
+
Please try reloading the page
+
+
\ No newline at end of file
diff --git a/v2/pkg/assetserver/fs.go b/v2/pkg/assetserver/fs.go
new file mode 100644
index 000000000..7ecc9cec8
--- /dev/null
+++ b/v2/pkg/assetserver/fs.go
@@ -0,0 +1,75 @@
+package assetserver
+
+import (
+ "embed"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// FindEmbedRootPath finds the root path in the embed FS. It's the directory which contains all the files.
+func FindEmbedRootPath(fsys embed.FS) (string, error) {
+ stopErr := fmt.Errorf("files or multiple dirs found")
+
+ fPath := ""
+ err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if d.IsDir() {
+ fPath = path
+ if entries, dErr := fs.ReadDir(fsys, path); dErr != nil {
+ return dErr
+ } else if len(entries) <= 1 {
+ return nil
+ }
+ }
+
+ return stopErr
+ })
+
+ if err != nil && err != stopErr {
+ return "", err
+ }
+
+ return fPath, nil
+}
+
+func FindPathToFile(fsys fs.FS, file string) (string, error) {
+ stat, _ := fs.Stat(fsys, file)
+ if stat != nil {
+ return ".", nil
+ }
+ var indexFiles []string
+ err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if strings.HasSuffix(path, file) {
+ indexFiles = append(indexFiles, path)
+ }
+ return nil
+ })
+ if err != nil {
+ return "", err
+ }
+
+ if len(indexFiles) > 1 {
+ selected := indexFiles[0]
+ for _, f := range indexFiles {
+ if len(f) < len(selected) {
+ selected = f
+ }
+ }
+ path, _ := filepath.Split(selected)
+ return path, nil
+ }
+ if len(indexFiles) > 0 {
+ path, _ := filepath.Split(indexFiles[0])
+ return path, nil
+ }
+ return "", fmt.Errorf("%s: %w", file, os.ErrNotExist)
+}
diff --git a/v2/pkg/assetserver/mimecache.go b/v2/pkg/assetserver/mimecache.go
new file mode 100644
index 000000000..9d97e8f5a
--- /dev/null
+++ b/v2/pkg/assetserver/mimecache.go
@@ -0,0 +1,67 @@
+package assetserver
+
+import (
+ "net/http"
+ "path/filepath"
+ "sync"
+
+ "github.com/wailsapp/mimetype"
+)
+
+var (
+ mimeCache = map[string]string{}
+ mimeMutex sync.Mutex
+
+ // The list of builtin mime-types by extension as defined by
+ // the golang standard lib package "mime"
+ // The standard lib also takes into account mime type definitions from
+ // etc files like '/etc/apache2/mime.types' but we want to have the
+ // same behavivour on all platforms and not depend on some external file.
+ mimeTypesByExt = map[string]string{
+ ".avif": "image/avif",
+ ".css": "text/css; charset=utf-8",
+ ".gif": "image/gif",
+ ".htm": "text/html; charset=utf-8",
+ ".html": "text/html; charset=utf-8",
+ ".jpeg": "image/jpeg",
+ ".jpg": "image/jpeg",
+ ".js": "text/javascript; charset=utf-8",
+ ".json": "application/json",
+ ".mjs": "text/javascript; charset=utf-8",
+ ".pdf": "application/pdf",
+ ".png": "image/png",
+ ".svg": "image/svg+xml",
+ ".wasm": "application/wasm",
+ ".webp": "image/webp",
+ ".xml": "text/xml; charset=utf-8",
+ }
+)
+
+func GetMimetype(filename string, data []byte) string {
+ mimeMutex.Lock()
+ defer mimeMutex.Unlock()
+
+ result := mimeTypesByExt[filepath.Ext(filename)]
+ if result != "" {
+ return result
+ }
+
+ result = mimeCache[filename]
+ if result != "" {
+ return result
+ }
+
+ detect := mimetype.Detect(data)
+ if detect == nil {
+ result = http.DetectContentType(data)
+ } else {
+ result = detect.String()
+ }
+
+ if result == "" {
+ result = "application/octet-stream"
+ }
+
+ mimeCache[filename] = result
+ return result
+}
diff --git a/v2/pkg/assetserver/mimecache_test.go b/v2/pkg/assetserver/mimecache_test.go
new file mode 100644
index 000000000..1496dbf52
--- /dev/null
+++ b/v2/pkg/assetserver/mimecache_test.go
@@ -0,0 +1,47 @@
+package assetserver
+
+import (
+ "testing"
+)
+
+func TestGetMimetype(t *testing.T) {
+ type args struct {
+ filename string
+ data []byte
+ }
+ bomUTF8 := []byte{0xef, 0xbb, 0xbf}
+ var emptyMsg []byte
+ css := []byte("body{margin:0;padding:0;background-color:#d579b2}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50;background-color:#ededed}#nav{padding:30px}#nav a{font-weight:700;color:#2c\n3e50}#nav a.router-link-exact-active{color:#42b983}.hello[data-v-4e26ad49]{margin:10px 0}")
+ html := []byte("title")
+ bomHtml := append(bomUTF8, html...)
+ svg := []byte("")
+ svgWithComment := append([]byte(""), svg...)
+ svgWithCommentAndControlChars := append([]byte(" \r\n "), svgWithComment...)
+ svgWithBomCommentAndControlChars := append(bomUTF8, append([]byte(" \r\n "), svgWithComment...)...)
+
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ // TODO: Add test cases.
+ {"nil data", args{"nil.svg", nil}, "image/svg+xml"},
+ {"empty data", args{"empty.html", emptyMsg}, "text/html; charset=utf-8"},
+ {"css", args{"test.css", css}, "text/css; charset=utf-8"},
+ {"js", args{"test.js", []byte("let foo = 'bar'; console.log(foo);")}, "text/javascript; charset=utf-8"},
+ {"mjs", args{"test.mjs", []byte("let foo = 'bar'; console.log(foo);")}, "text/javascript; charset=utf-8"},
+ {"html-utf8", args{"test_utf8.html", html}, "text/html; charset=utf-8"},
+ {"html-bom-utf8", args{"test_bom_utf8.html", bomHtml}, "text/html; charset=utf-8"},
+ {"svg", args{"test.svg", svg}, "image/svg+xml"},
+ {"svg-w-comment", args{"test_comment.svg", svgWithComment}, "image/svg+xml"},
+ {"svg-w-control-comment", args{"test_control_comment.svg", svgWithCommentAndControlChars}, "image/svg+xml"},
+ {"svg-w-bom-control-comment", args{"test_bom_control_comment.svg", svgWithBomCommentAndControlChars}, "image/svg+xml"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := GetMimetype(tt.args.filename, tt.args.data); got != tt.want {
+ t.Errorf("GetMimetype() = '%v', want '%v'", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/v2/pkg/assetserver/ringqueue.go b/v2/pkg/assetserver/ringqueue.go
new file mode 100644
index 000000000..b94e7cd5c
--- /dev/null
+++ b/v2/pkg/assetserver/ringqueue.go
@@ -0,0 +1,101 @@
+// Code from https://github.com/erikdubbelboer/ringqueue
+/*
+The MIT License (MIT)
+
+Copyright (c) 2015 Erik Dubbelboer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+package assetserver
+
+type ringqueue[T any] struct {
+ nodes []T
+ head int
+ tail int
+ cnt int
+
+ minSize int
+}
+
+func newRingqueue[T any](minSize uint) *ringqueue[T] {
+ if minSize < 2 {
+ minSize = 2
+ }
+ return &ringqueue[T]{
+ nodes: make([]T, minSize),
+ minSize: int(minSize),
+ }
+}
+
+func (q *ringqueue[T]) resize(n int) {
+ nodes := make([]T, n)
+ if q.head < q.tail {
+ copy(nodes, q.nodes[q.head:q.tail])
+ } else {
+ copy(nodes, q.nodes[q.head:])
+ copy(nodes[len(q.nodes)-q.head:], q.nodes[:q.tail])
+ }
+
+ q.tail = q.cnt % n
+ q.head = 0
+ q.nodes = nodes
+}
+
+func (q *ringqueue[T]) Add(i T) {
+ if q.cnt == len(q.nodes) {
+ // Also tested a grow rate of 1.5, see: http://stackoverflow.com/questions/2269063/buffer-growth-strategy
+ // In Go this resulted in a higher memory usage.
+ q.resize(q.cnt * 2)
+ }
+ q.nodes[q.tail] = i
+ q.tail = (q.tail + 1) % len(q.nodes)
+ q.cnt++
+}
+
+func (q *ringqueue[T]) Peek() (T, bool) {
+ if q.cnt == 0 {
+ var none T
+ return none, false
+ }
+ return q.nodes[q.head], true
+}
+
+func (q *ringqueue[T]) Remove() (T, bool) {
+ if q.cnt == 0 {
+ var none T
+ return none, false
+ }
+ i := q.nodes[q.head]
+ q.head = (q.head + 1) % len(q.nodes)
+ q.cnt--
+
+ if n := len(q.nodes) / 2; n > q.minSize && q.cnt <= n {
+ q.resize(n)
+ }
+
+ return i, true
+}
+
+func (q *ringqueue[T]) Cap() int {
+ return cap(q.nodes)
+}
+
+func (q *ringqueue[T]) Len() int {
+ return q.cnt
+}
diff --git a/v2/pkg/assetserver/testdata/index.html b/v2/pkg/assetserver/testdata/index.html
new file mode 100644
index 000000000..76da518f4
--- /dev/null
+++ b/v2/pkg/assetserver/testdata/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2/pkg/assetserver/testdata/main.css b/v2/pkg/assetserver/testdata/main.css
new file mode 100644
index 000000000..57b00e6c6
--- /dev/null
+++ b/v2/pkg/assetserver/testdata/main.css
@@ -0,0 +1,39 @@
+
+html {
+ text-align: center;
+ color: white;
+ background-color: rgba(1, 1, 1, 0.1);
+}
+
+body {
+ color: white;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ margin: 0;
+}
+
+#result {
+ margin-top: 1rem;
+}
+
+button {
+ -webkit-appearance: default-button;
+ padding: 6px;
+}
+
+#name {
+ border-radius: 3px;
+ outline: none;
+ height: 20px;
+ -webkit-font-smoothing: antialiased;
+}
+
+#logo {
+ width: 40%;
+ height: 40%;
+ padding-top: 20%;
+ margin: auto;
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTUxIDQzNiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMiIgeG1sbnM6dj0iaHR0cHM6Ly92ZWN0YS5pby9uYW5vIj48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iPjxwYXRoIGQ9Ik0xMDQuMDEgMzQ0LjM4OGgxOC40MDFsLTEuNzY4IDM5LjE2MSAxMi4xNDctMzkuMTYxaDE0Ljg2N2wtLjE4MSAzOS4xNjEgMTAuNTYxLTM5LjE2MWgxOC40MDFsLTIzLjI5NyA2Ni4xNzVoLTE2Ljk5N2wuMTgxLTQxLjM4My0xMi45MTcgNDEuMzgzaC0xNi45NTFsLTIuNDQ3LTY2LjE3NXptMTIwLjk3NSA0My4wNTloNy4zODhsLjIyNy0yNC41MjEtNy42MTUgMjQuNTIxem0tMjUuNzQ0IDIzLjExNmwyNC44ODMtNjYuMTc1aDIxLjgwMWw0LjY2NyA2Ni4xNzVoLTE4LjY3NGwuMDkyLTkuNzQ2aC0xMC45MjRsLTIuOTAxIDkuNzQ2aC0xOC45NDR6bTg4LjE4MyAwbDEwLjQ3LTY2LjE3NmgxOC40OTNsLTEwLjUxNiA2Ni4xNzUtMTguNDQ2LjAwMXptNjUuNzkzIDBsMTAuNTE2LTY2LjE3NWgxOC41MzZsLTcuODg2IDQ5Ljc2NmgxMy41NTJsLTIuNTgyIDE2LjQwOWgtMzIuMTM2em03NC43MjItMjAuMzUyYzIuMDU0IDEuNzIzIDQuMjE1IDMuMDUzIDYuNDgyIDMuOTlzNC40NCAxLjQwNCA2LjUyNiAxLjQwNGMxLjg0MyAwIDMuMzA4LS41MDYgNC4zOTYtMS41MThzMS42MzItMi4zOTUgMS42MzItNC4xNDhjMC0xLjUwOS0uNDU0LTMuMDEzLTEuMzU5LTQuNTA5cy0yLjY2LTMuNDgxLTUuMjU4LTUuOTU5Yy0zLjE0NC0zLjA1Mi01LjMwMy01Ljc0MS02LjQ4Mi04LjA2OXMtMS43NjYtNC44OTQtMS43NjYtNy43MDRjMC02LjMxNSAyLjAwMS0xMS4zMzIgNi4wMDUtMTUuMDQ4czkuNDM0LTUuNTc1IDE2LjI5NC01LjU3NWMyLjc4IDAgNS40MjIuMzEgNy45MzEuOTNzNS4wNiAxLjU3OSA3LjY2MSAyLjg3OGwtMi42MyAxNi4xMzZjLTEuOTk1LTEuMzktMy45MzUtMi40NDctNS44MjMtMy4xNzNzLTMuNjk0LTEuMDg5LTUuNDE3LTEuMDg5Yy0xLjU0MSAwLTIuNzU4LjQtMy42NDkgMS4ycy0xLjMzOCAxLjg5OC0xLjMzOCAzLjI4OGMwIDEuODc1IDEuNzA4IDQuNTAzIDUuMTIzIDcuODg2bC45OTcuOTk2YzMuNDQ1IDMuMzg2IDUuNzExIDYuMjg2IDYuNzk4IDguNzA1czEuNjMxIDUuMjA5IDEuNjMxIDguMzg0YzAgNy4wNzEtMi4xODMgMTIuNjQ2LTYuNTUgMTYuNzI0cy0xMC4zNDEgNi4xMi0xNy45MjUgNi4xMmMtMy4yMzQgMC02LjI5Mi0uMzg1LTkuMTc4LTEuMTU1cy01LjMxLTEuODM4LTcuMjc0LTMuMTk3bDMuMTczLTE3LjQ5NXoiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNLjg4My0uMDgxTC4xMjEuMDgxLjI1Ni0uMDYzLjg4My0uMDgxeiIgZmlsbD0idXJsKCNBKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTE2Ni41OTkgNC41NzEzMiA0LjU3MTMyIDE2Ni41OTkgMTQ3LjQwMyAxNjcuNjQ4KSIvPjxwYXRoIGQ9Ik0uODc4LS4yODVMLS4wNzMuNzEtMS4xODYuNTQyLjAxNS4yMDctLjg0Ni4wNzcuMzU1LS4yNThsLS44Ni0uMTNMLjY0OS0uNzFsLjIyOS40MjV6IiBmaWxsPSJ1cmwoI0IpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMTA2LjQ0MyAtMTYuMDY2OSAtMTYuMDY2OSAxMDYuNDQzIDQyOC4xOSAxODguMDMzKSIvPjxwYXRoIGQ9Ik0uNDQtLjA0aDAgMEwuMjY1LS4wNTYuMTc3LjQzNy0uMzExLS4yNTUuMjYyLS40MzdoLjMwNkwuNDQtLjA0eiIgZmlsbD0idXJsKCNDKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTExNC40ODQgLTE2Mi40MDggLTE2Mi40MDggMTE0LjQ4NCAzMzMuMjkxIDI4NS44MDQpIi8+PHBhdGggZD0iTS41IDBoMCAwIDB6IiBmaWxsPSJ1cmwoI0QpIiB0cmFuc2Zvcm09Im1hdHJpeCg2MS42OTE5IDU4LjgwOTEgNTguODA5MSAtNjEuNjkxOSAyNTguNjMxIDE4MC40MTMpIi8+PHBhdGggZD0iTS42MjItLjExNWguMTM5bC4wNDUuMTAyLjAyLjE5NS0uMjA0LS4yOTd6IiBmaWxsPSJ1cmwoI0UpIiB0cmFuc2Zvcm09Im1hdHJpeCgyMzguMTI2IDI5OC44OTMgMjk4Ljg5MyAtMjM4LjEyNiAxMTMuNTE2IC0xNTAuNTM2KSIvPjxwYXRoIGQ9Ik0uNDY3LjAwNUwuNDkuMDYyLjI3MS0uMDYyLjQ2Ny4wMDV6IiBmaWxsPSJ1cmwoI0YpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMzY5LjUyOSAtOTcuNDExOCAtOTcuNDExOCAzNjkuNTI5IDU4Mi4zOCA5NC4wMjcpIi8+PGcgZmlsbD0idXJsKCNCKSI+PHBhdGggZD0iTS4yLjAwMWwuMDE5LS4wMTkuMzk1LjAzLS4wOTUuMDc3TC4yODIuMDY4LjIuMTM1LjQ2My4xOTQuMzc0LjI2Ni4xMzguMTg2aDAgMEwuMDQ3LjAzMy0uMTMxLS4yNjYuMi4wMDF6IiB0cmFuc2Zvcm09Im1hdHJpeCgtNDk2LjE1NiAtNTMuOTc1MSAtNTMuOTc1MSA0OTYuMTU2IDM2Ny44ODggMTI1LjA4NSkiLz48cGF0aCBkPSJNLjczNSAwaDAgMCAweiIgdHJhbnNmb3JtPSJtYXRyaXgoMTg1LjA3NiAxNzYuNDI3IDE3Ni40MjcgLTE4NS4wNzYgMTUzLjQ0NiA4MC4xNDg4KSIvPjwvZz48L2c+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJBIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0zLjQ2OTQ1ZS0xOCwtMy40Njk0NWUtMTgsLTEsMCwtMy4wNTc2MWUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkIiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkMiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsLTEuMTEwMjJlLTE2LC0xLjExMDIyZS0xNiwtMSwwLC0yLjYxODYxZS0wNikiIHhsaW5rOmhyZWY9IiNHIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlMzMyMzIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM2YjAwMGQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iRCIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjAiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSwtNS41NTExMmUtMTcsLTUuNTUxMTJlLTE3LC0xLDAsLTEuNTc1NjJlLTA2KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJFIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC44MDE4OTksLTAuNTk3NDYsLTAuNTk3NDYsMC44MDE4OTksMS4zNDk1LDAuNDQ3NDU3KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJGIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0yLjc3NTU2ZS0xNywtMi43NzU1NmUtMTcsLTEsMCwtMS45MjgyNmUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIi8+PC9kZWZzPjwvc3ZnPg==");
+}
diff --git a/v2/pkg/assetserver/testdata/main.js b/v2/pkg/assetserver/testdata/main.js
new file mode 100644
index 000000000..274b4667c
--- /dev/null
+++ b/v2/pkg/assetserver/testdata/main.js
@@ -0,0 +1,20 @@
+import {ready} from '@wails/runtime';
+
+ready(() => {
+ // Get input + focus
+ let nameElement = document.getElementById("name");
+ nameElement.focus();
+
+ // Setup the greet function
+ window.greet = function () {
+
+ // Get name
+ let name = nameElement.value;
+
+ // Call App.Greet(name)
+ window.backend.main.App.Greet(name).then((result) => {
+ // Update result with data back from App.Greet()
+ document.getElementById("result").innerText = result;
+ });
+ };
+});
\ No newline at end of file
diff --git a/v2/pkg/assetserver/testdata/subdir/index.html b/v2/pkg/assetserver/testdata/subdir/index.html
new file mode 100644
index 000000000..76da518f4
--- /dev/null
+++ b/v2/pkg/assetserver/testdata/subdir/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2/pkg/assetserver/testdata/subdir/main.css b/v2/pkg/assetserver/testdata/subdir/main.css
new file mode 100644
index 000000000..57b00e6c6
--- /dev/null
+++ b/v2/pkg/assetserver/testdata/subdir/main.css
@@ -0,0 +1,39 @@
+
+html {
+ text-align: center;
+ color: white;
+ background-color: rgba(1, 1, 1, 0.1);
+}
+
+body {
+ color: white;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ margin: 0;
+}
+
+#result {
+ margin-top: 1rem;
+}
+
+button {
+ -webkit-appearance: default-button;
+ padding: 6px;
+}
+
+#name {
+ border-radius: 3px;
+ outline: none;
+ height: 20px;
+ -webkit-font-smoothing: antialiased;
+}
+
+#logo {
+ width: 40%;
+ height: 40%;
+ padding-top: 20%;
+ margin: auto;
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTUxIDQzNiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMiIgeG1sbnM6dj0iaHR0cHM6Ly92ZWN0YS5pby9uYW5vIj48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iPjxwYXRoIGQ9Ik0xMDQuMDEgMzQ0LjM4OGgxOC40MDFsLTEuNzY4IDM5LjE2MSAxMi4xNDctMzkuMTYxaDE0Ljg2N2wtLjE4MSAzOS4xNjEgMTAuNTYxLTM5LjE2MWgxOC40MDFsLTIzLjI5NyA2Ni4xNzVoLTE2Ljk5N2wuMTgxLTQxLjM4My0xMi45MTcgNDEuMzgzaC0xNi45NTFsLTIuNDQ3LTY2LjE3NXptMTIwLjk3NSA0My4wNTloNy4zODhsLjIyNy0yNC41MjEtNy42MTUgMjQuNTIxem0tMjUuNzQ0IDIzLjExNmwyNC44ODMtNjYuMTc1aDIxLjgwMWw0LjY2NyA2Ni4xNzVoLTE4LjY3NGwuMDkyLTkuNzQ2aC0xMC45MjRsLTIuOTAxIDkuNzQ2aC0xOC45NDR6bTg4LjE4MyAwbDEwLjQ3LTY2LjE3NmgxOC40OTNsLTEwLjUxNiA2Ni4xNzUtMTguNDQ2LjAwMXptNjUuNzkzIDBsMTAuNTE2LTY2LjE3NWgxOC41MzZsLTcuODg2IDQ5Ljc2NmgxMy41NTJsLTIuNTgyIDE2LjQwOWgtMzIuMTM2em03NC43MjItMjAuMzUyYzIuMDU0IDEuNzIzIDQuMjE1IDMuMDUzIDYuNDgyIDMuOTlzNC40NCAxLjQwNCA2LjUyNiAxLjQwNGMxLjg0MyAwIDMuMzA4LS41MDYgNC4zOTYtMS41MThzMS42MzItMi4zOTUgMS42MzItNC4xNDhjMC0xLjUwOS0uNDU0LTMuMDEzLTEuMzU5LTQuNTA5cy0yLjY2LTMuNDgxLTUuMjU4LTUuOTU5Yy0zLjE0NC0zLjA1Mi01LjMwMy01Ljc0MS02LjQ4Mi04LjA2OXMtMS43NjYtNC44OTQtMS43NjYtNy43MDRjMC02LjMxNSAyLjAwMS0xMS4zMzIgNi4wMDUtMTUuMDQ4czkuNDM0LTUuNTc1IDE2LjI5NC01LjU3NWMyLjc4IDAgNS40MjIuMzEgNy45MzEuOTNzNS4wNiAxLjU3OSA3LjY2MSAyLjg3OGwtMi42MyAxNi4xMzZjLTEuOTk1LTEuMzktMy45MzUtMi40NDctNS44MjMtMy4xNzNzLTMuNjk0LTEuMDg5LTUuNDE3LTEuMDg5Yy0xLjU0MSAwLTIuNzU4LjQtMy42NDkgMS4ycy0xLjMzOCAxLjg5OC0xLjMzOCAzLjI4OGMwIDEuODc1IDEuNzA4IDQuNTAzIDUuMTIzIDcuODg2bC45OTcuOTk2YzMuNDQ1IDMuMzg2IDUuNzExIDYuMjg2IDYuNzk4IDguNzA1czEuNjMxIDUuMjA5IDEuNjMxIDguMzg0YzAgNy4wNzEtMi4xODMgMTIuNjQ2LTYuNTUgMTYuNzI0cy0xMC4zNDEgNi4xMi0xNy45MjUgNi4xMmMtMy4yMzQgMC02LjI5Mi0uMzg1LTkuMTc4LTEuMTU1cy01LjMxLTEuODM4LTcuMjc0LTMuMTk3bDMuMTczLTE3LjQ5NXoiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNLjg4My0uMDgxTC4xMjEuMDgxLjI1Ni0uMDYzLjg4My0uMDgxeiIgZmlsbD0idXJsKCNBKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTE2Ni41OTkgNC41NzEzMiA0LjU3MTMyIDE2Ni41OTkgMTQ3LjQwMyAxNjcuNjQ4KSIvPjxwYXRoIGQ9Ik0uODc4LS4yODVMLS4wNzMuNzEtMS4xODYuNTQyLjAxNS4yMDctLjg0Ni4wNzcuMzU1LS4yNThsLS44Ni0uMTNMLjY0OS0uNzFsLjIyOS40MjV6IiBmaWxsPSJ1cmwoI0IpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMTA2LjQ0MyAtMTYuMDY2OSAtMTYuMDY2OSAxMDYuNDQzIDQyOC4xOSAxODguMDMzKSIvPjxwYXRoIGQ9Ik0uNDQtLjA0aDAgMEwuMjY1LS4wNTYuMTc3LjQzNy0uMzExLS4yNTUuMjYyLS40MzdoLjMwNkwuNDQtLjA0eiIgZmlsbD0idXJsKCNDKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTExNC40ODQgLTE2Mi40MDggLTE2Mi40MDggMTE0LjQ4NCAzMzMuMjkxIDI4NS44MDQpIi8+PHBhdGggZD0iTS41IDBoMCAwIDB6IiBmaWxsPSJ1cmwoI0QpIiB0cmFuc2Zvcm09Im1hdHJpeCg2MS42OTE5IDU4LjgwOTEgNTguODA5MSAtNjEuNjkxOSAyNTguNjMxIDE4MC40MTMpIi8+PHBhdGggZD0iTS42MjItLjExNWguMTM5bC4wNDUuMTAyLjAyLjE5NS0uMjA0LS4yOTd6IiBmaWxsPSJ1cmwoI0UpIiB0cmFuc2Zvcm09Im1hdHJpeCgyMzguMTI2IDI5OC44OTMgMjk4Ljg5MyAtMjM4LjEyNiAxMTMuNTE2IC0xNTAuNTM2KSIvPjxwYXRoIGQ9Ik0uNDY3LjAwNUwuNDkuMDYyLjI3MS0uMDYyLjQ2Ny4wMDV6IiBmaWxsPSJ1cmwoI0YpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMzY5LjUyOSAtOTcuNDExOCAtOTcuNDExOCAzNjkuNTI5IDU4Mi4zOCA5NC4wMjcpIi8+PGcgZmlsbD0idXJsKCNCKSI+PHBhdGggZD0iTS4yLjAwMWwuMDE5LS4wMTkuMzk1LjAzLS4wOTUuMDc3TC4yODIuMDY4LjIuMTM1LjQ2My4xOTQuMzc0LjI2Ni4xMzguMTg2aDAgMEwuMDQ3LjAzMy0uMTMxLS4yNjYuMi4wMDF6IiB0cmFuc2Zvcm09Im1hdHJpeCgtNDk2LjE1NiAtNTMuOTc1MSAtNTMuOTc1MSA0OTYuMTU2IDM2Ny44ODggMTI1LjA4NSkiLz48cGF0aCBkPSJNLjczNSAwaDAgMCAweiIgdHJhbnNmb3JtPSJtYXRyaXgoMTg1LjA3NiAxNzYuNDI3IDE3Ni40MjcgLTE4NS4wNzYgMTUzLjQ0NiA4MC4xNDg4KSIvPjwvZz48L2c+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJBIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0zLjQ2OTQ1ZS0xOCwtMy40Njk0NWUtMTgsLTEsMCwtMy4wNTc2MWUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkIiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkMiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsLTEuMTEwMjJlLTE2LC0xLjExMDIyZS0xNiwtMSwwLC0yLjYxODYxZS0wNikiIHhsaW5rOmhyZWY9IiNHIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlMzMyMzIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM2YjAwMGQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iRCIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjAiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSwtNS41NTExMmUtMTcsLTUuNTUxMTJlLTE3LC0xLDAsLTEuNTc1NjJlLTA2KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJFIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC44MDE4OTksLTAuNTk3NDYsLTAuNTk3NDYsMC44MDE4OTksMS4zNDk1LDAuNDQ3NDU3KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJGIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0yLjc3NTU2ZS0xNywtMi43NzU1NmUtMTcsLTEsMCwtMS45MjgyNmUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIi8+PC9kZWZzPjwvc3ZnPg==");
+}
diff --git a/v2/pkg/assetserver/testdata/subdir/main.js b/v2/pkg/assetserver/testdata/subdir/main.js
new file mode 100644
index 000000000..274b4667c
--- /dev/null
+++ b/v2/pkg/assetserver/testdata/subdir/main.js
@@ -0,0 +1,20 @@
+import {ready} from '@wails/runtime';
+
+ready(() => {
+ // Get input + focus
+ let nameElement = document.getElementById("name");
+ nameElement.focus();
+
+ // Setup the greet function
+ window.greet = function () {
+
+ // Get name
+ let name = nameElement.value;
+
+ // Call App.Greet(name)
+ window.backend.main.App.Greet(name).then((result) => {
+ // Update result with data back from App.Greet()
+ document.getElementById("result").innerText = result;
+ });
+ };
+});
\ No newline at end of file
diff --git a/v2/pkg/assetserver/testdata/testdata.go b/v2/pkg/assetserver/testdata/testdata.go
new file mode 100644
index 000000000..5387070ec
--- /dev/null
+++ b/v2/pkg/assetserver/testdata/testdata.go
@@ -0,0 +1,6 @@
+package testdata
+
+import "embed"
+
+//go:embed index.html main.css main.js
+var TopLevelFS embed.FS
diff --git a/v2/pkg/assetserver/webview/request.go b/v2/pkg/assetserver/webview/request.go
new file mode 100644
index 000000000..18ff29890
--- /dev/null
+++ b/v2/pkg/assetserver/webview/request.go
@@ -0,0 +1,17 @@
+package webview
+
+import (
+ "io"
+ "net/http"
+)
+
+type Request interface {
+ URL() (string, error)
+ Method() (string, error)
+ Header() (http.Header, error)
+ Body() (io.ReadCloser, error)
+
+ Response() ResponseWriter
+
+ Close() error
+}
diff --git a/v2/pkg/assetserver/webview/request_darwin.go b/v2/pkg/assetserver/webview/request_darwin.go
new file mode 100644
index 000000000..c44e5f196
--- /dev/null
+++ b/v2/pkg/assetserver/webview/request_darwin.go
@@ -0,0 +1,251 @@
+//go:build darwin
+
+package webview
+
+/*
+#cgo CFLAGS: -x objective-c
+#cgo LDFLAGS: -framework Foundation -framework WebKit
+
+#import
+#import
+#include
+
+static void URLSchemeTaskRetain(void *wkUrlSchemeTask) {
+ id urlSchemeTask = (id) wkUrlSchemeTask;
+ [urlSchemeTask retain];
+}
+
+static void URLSchemeTaskRelease(void *wkUrlSchemeTask) {
+ id urlSchemeTask = (id) wkUrlSchemeTask;
+ [urlSchemeTask release];
+}
+
+static const char * URLSchemeTaskRequestURL(void *wkUrlSchemeTask) {
+ id urlSchemeTask = (id) wkUrlSchemeTask;
+ @autoreleasepool {
+ return [urlSchemeTask.request.URL.absoluteString UTF8String];
+ }
+}
+
+static const char * URLSchemeTaskRequestMethod(void *wkUrlSchemeTask) {
+ id urlSchemeTask = (id) wkUrlSchemeTask;
+ @autoreleasepool {
+ return [urlSchemeTask.request.HTTPMethod UTF8String];
+ }
+}
+
+static const char * URLSchemeTaskRequestHeadersJSON(void *wkUrlSchemeTask) {
+ id urlSchemeTask = (id) wkUrlSchemeTask;
+ @autoreleasepool {
+ NSData *headerData = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil];
+ if (!headerData) {
+ return nil;
+ }
+
+ NSString* headerString = [[[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding] autorelease];
+ const char * headerJSON = [headerString UTF8String];
+
+ return strdup(headerJSON);
+ }
+}
+
+static bool URLSchemeTaskRequestBodyBytes(void *wkUrlSchemeTask, const void **body, int *bodyLen) {
+ id urlSchemeTask = (id) wkUrlSchemeTask;
+ @autoreleasepool {
+ if (!urlSchemeTask.request.HTTPBody) {
+ return false;
+ }
+
+ *body = urlSchemeTask.request.HTTPBody.bytes;
+ *bodyLen = urlSchemeTask.request.HTTPBody.length;
+ return true;
+ }
+}
+
+static bool URLSchemeTaskRequestBodyStreamOpen(void *wkUrlSchemeTask) {
+ id urlSchemeTask = (id) wkUrlSchemeTask;
+ @autoreleasepool {
+ if (!urlSchemeTask.request.HTTPBodyStream) {
+ return false;
+ }
+
+ [urlSchemeTask.request.HTTPBodyStream open];
+ return true;
+ }
+}
+
+static void URLSchemeTaskRequestBodyStreamClose(void *wkUrlSchemeTask) {
+ id urlSchemeTask = (id) wkUrlSchemeTask;
+ @autoreleasepool {
+ if (!urlSchemeTask.request.HTTPBodyStream) {
+ return;
+ }
+
+ [urlSchemeTask.request.HTTPBodyStream close];
+ }
+}
+
+static int URLSchemeTaskRequestBodyStreamRead(void *wkUrlSchemeTask, void *buf, int bufLen) {
+ id urlSchemeTask = (id) wkUrlSchemeTask;
+
+ @autoreleasepool {
+ NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream;
+ if (!stream) {
+ return -2;
+ }
+
+ NSStreamStatus status = stream.streamStatus;
+ if (status == NSStreamStatusAtEnd || !stream.hasBytesAvailable) {
+ return 0;
+ } else if (status != NSStreamStatusOpen) {
+ return -3;
+ }
+
+ return [stream read:buf maxLength:bufLen];
+ }
+}
+*/
+import "C"
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "unsafe"
+)
+
+// NewRequest creates as new WebViewRequest based on a pointer to an `id`
+func NewRequest(wkURLSchemeTask unsafe.Pointer) Request {
+ C.URLSchemeTaskRetain(wkURLSchemeTask)
+ return newRequestFinalizer(&request{task: wkURLSchemeTask})
+}
+
+var _ Request = &request{}
+
+type request struct {
+ task unsafe.Pointer
+
+ header http.Header
+ body io.ReadCloser
+ rw *responseWriter
+}
+
+func (r *request) URL() (string, error) {
+ return C.GoString(C.URLSchemeTaskRequestURL(r.task)), nil
+}
+
+func (r *request) Method() (string, error) {
+ return C.GoString(C.URLSchemeTaskRequestMethod(r.task)), nil
+}
+
+func (r *request) Header() (http.Header, error) {
+ if r.header != nil {
+ return r.header, nil
+ }
+
+ header := http.Header{}
+ if cHeaders := C.URLSchemeTaskRequestHeadersJSON(r.task); cHeaders != nil {
+ if headers := C.GoString(cHeaders); headers != "" {
+ var h map[string]string
+ if err := json.Unmarshal([]byte(headers), &h); err != nil {
+ return nil, fmt.Errorf("unable to unmarshal request headers: %s", err)
+ }
+
+ for k, v := range h {
+ header.Add(k, v)
+ }
+ }
+ C.free(unsafe.Pointer(cHeaders))
+ }
+ r.header = header
+ return header, nil
+}
+
+func (r *request) Body() (io.ReadCloser, error) {
+ if r.body != nil {
+ return r.body, nil
+ }
+
+ var body unsafe.Pointer
+ var bodyLen C.int
+ if C.URLSchemeTaskRequestBodyBytes(r.task, &body, &bodyLen) {
+ if body != nil && bodyLen > 0 {
+ r.body = io.NopCloser(bytes.NewReader(C.GoBytes(body, bodyLen)))
+ } else {
+ r.body = http.NoBody
+ }
+ } else if C.URLSchemeTaskRequestBodyStreamOpen(r.task) {
+ r.body = &requestBodyStreamReader{task: r.task}
+ }
+
+ return r.body, nil
+}
+
+func (r *request) Response() ResponseWriter {
+ if r.rw != nil {
+ return r.rw
+ }
+
+ r.rw = &responseWriter{r: r}
+ return r.rw
+}
+
+func (r *request) Close() error {
+ var err error
+ if r.body != nil {
+ err = r.body.Close()
+ }
+ err = r.Response().Finish()
+ if err != nil {
+ return err
+ }
+ C.URLSchemeTaskRelease(r.task)
+ return err
+}
+
+var _ io.ReadCloser = &requestBodyStreamReader{}
+
+type requestBodyStreamReader struct {
+ task unsafe.Pointer
+ closed bool
+}
+
+// Read implements io.Reader
+func (r *requestBodyStreamReader) Read(p []byte) (n int, err error) {
+ var content unsafe.Pointer
+ var contentLen int
+ if p != nil {
+ content = unsafe.Pointer(&p[0])
+ contentLen = len(p)
+ }
+
+ res := C.URLSchemeTaskRequestBodyStreamRead(r.task, content, C.int(contentLen))
+ if res > 0 {
+ return int(res), nil
+ }
+
+ switch res {
+ case 0:
+ return 0, io.EOF
+ case -1:
+ return 0, fmt.Errorf("body: stream error")
+ case -2:
+ return 0, fmt.Errorf("body: no stream defined")
+ case -3:
+ return 0, io.ErrClosedPipe
+ default:
+ return 0, fmt.Errorf("body: unknown error %d", res)
+ }
+}
+
+func (r *requestBodyStreamReader) Close() error {
+ if r.closed {
+ return nil
+ }
+ r.closed = true
+
+ C.URLSchemeTaskRequestBodyStreamClose(r.task)
+ return nil
+}
diff --git a/v2/pkg/assetserver/webview/request_finalizer.go b/v2/pkg/assetserver/webview/request_finalizer.go
new file mode 100644
index 000000000..6a8c6a928
--- /dev/null
+++ b/v2/pkg/assetserver/webview/request_finalizer.go
@@ -0,0 +1,40 @@
+package webview
+
+import (
+ "runtime"
+ "sync/atomic"
+)
+
+var _ Request = &requestFinalizer{}
+
+type requestFinalizer struct {
+ Request
+ closed int32
+}
+
+// newRequestFinalizer returns a request with a runtime finalizer to make sure it will be closed from the finalizer
+// if it has not been already closed.
+// It also makes sure Close() of the wrapping request is only called once.
+func newRequestFinalizer(r Request) Request {
+ rf := &requestFinalizer{Request: r}
+ // Make sure to async release since it might block the finalizer goroutine for a longer period
+ runtime.SetFinalizer(rf, func(obj *requestFinalizer) { rf.close(true) })
+ return rf
+}
+
+func (r *requestFinalizer) Close() error {
+ return r.close(false)
+}
+
+func (r *requestFinalizer) close(asyncRelease bool) error {
+ if atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
+ runtime.SetFinalizer(r, nil)
+ if asyncRelease {
+ go r.Request.Close()
+ return nil
+ } else {
+ return r.Request.Close()
+ }
+ }
+ return nil
+}
diff --git a/v2/pkg/assetserver/webview/request_linux.go b/v2/pkg/assetserver/webview/request_linux.go
new file mode 100644
index 000000000..c6785fb1c
--- /dev/null
+++ b/v2/pkg/assetserver/webview/request_linux.go
@@ -0,0 +1,85 @@
+//go:build linux
+// +build linux
+
+package webview
+
+/*
+#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0
+#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
+#cgo webkit2_41 pkg-config: webkit2gtk-4.1
+
+#include "gtk/gtk.h"
+#include "webkit2/webkit2.h"
+*/
+import "C"
+
+import (
+ "io"
+ "net/http"
+ "unsafe"
+)
+
+// NewRequest creates as new WebViewRequest based on a pointer to an `WebKitURISchemeRequest`
+func NewRequest(webKitURISchemeRequest unsafe.Pointer) Request {
+ webkitReq := (*C.WebKitURISchemeRequest)(webKitURISchemeRequest)
+ C.g_object_ref(C.gpointer(webkitReq))
+
+ req := &request{req: webkitReq}
+ return newRequestFinalizer(req)
+}
+
+var _ Request = &request{}
+
+type request struct {
+ req *C.WebKitURISchemeRequest
+
+ header http.Header
+ body io.ReadCloser
+ rw *responseWriter
+}
+
+func (r *request) URL() (string, error) {
+ return C.GoString(C.webkit_uri_scheme_request_get_uri(r.req)), nil
+}
+
+func (r *request) Method() (string, error) {
+ return webkit_uri_scheme_request_get_http_method(r.req), nil
+}
+
+func (r *request) Header() (http.Header, error) {
+ if r.header != nil {
+ return r.header, nil
+ }
+
+ r.header = webkit_uri_scheme_request_get_http_headers(r.req)
+ return r.header, nil
+}
+
+func (r *request) Body() (io.ReadCloser, error) {
+ if r.body != nil {
+ return r.body, nil
+ }
+
+ r.body = webkit_uri_scheme_request_get_http_body(r.req)
+
+ return r.body, nil
+}
+
+func (r *request) Response() ResponseWriter {
+ if r.rw != nil {
+ return r.rw
+ }
+
+ r.rw = &responseWriter{req: r.req}
+ return r.rw
+}
+
+func (r *request) Close() error {
+ var err error
+ if r.body != nil {
+ err = r.body.Close()
+ }
+ r.Response().Finish()
+ C.g_object_unref(C.gpointer(r.req))
+ return err
+}
diff --git a/v2/pkg/assetserver/webview/request_windows.go b/v2/pkg/assetserver/webview/request_windows.go
new file mode 100644
index 000000000..fa83cd8d7
--- /dev/null
+++ b/v2/pkg/assetserver/webview/request_windows.go
@@ -0,0 +1,217 @@
+//go:build windows
+// +build windows
+
+package webview
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/wailsapp/go-webview2/pkg/edge"
+)
+
+// NewRequest creates as new WebViewRequest for chromium. This Method must be called from the Main-Thread!
+func NewRequest(env *edge.ICoreWebView2Environment, args *edge.ICoreWebView2WebResourceRequestedEventArgs, invokeSync func(fn func())) (Request, error) {
+ req, err := args.GetRequest()
+ if err != nil {
+ return nil, fmt.Errorf("GetRequest failed: %s", err)
+ }
+ defer req.Release()
+
+ r := &request{
+ invokeSync: invokeSync,
+ }
+
+ code := http.StatusInternalServerError
+ r.response, err = env.CreateWebResourceResponse(nil, code, http.StatusText(code), "")
+ if err != nil {
+ return nil, fmt.Errorf("CreateWebResourceResponse failed: %s", err)
+ }
+
+ if err := args.PutResponse(r.response); err != nil {
+ r.finishResponse()
+ return nil, fmt.Errorf("PutResponse failed: %s", err)
+ }
+
+ r.deferral, err = args.GetDeferral()
+ if err != nil {
+ r.finishResponse()
+ return nil, fmt.Errorf("GetDeferral failed: %s", err)
+ }
+
+ r.url, r.urlErr = req.GetUri()
+ r.method, r.methodErr = req.GetMethod()
+ r.header, r.headerErr = getHeaders(req)
+
+ if content, err := req.GetContent(); err != nil {
+ r.bodyErr = err
+ } else if content != nil {
+ // It is safe to access Content from another Thread: https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/threading-model#thread-safety
+ r.body = &iStreamReleaseCloser{stream: content}
+ }
+
+ return r, nil
+}
+
+var _ Request = &request{}
+
+type request struct {
+ response *edge.ICoreWebView2WebResourceResponse
+ deferral *edge.ICoreWebView2Deferral
+
+ url string
+ urlErr error
+
+ method string
+ methodErr error
+
+ header http.Header
+ headerErr error
+
+ body io.ReadCloser
+ bodyErr error
+ rw *responseWriter
+
+ invokeSync func(fn func())
+}
+
+func (r *request) URL() (string, error) {
+ return r.url, r.urlErr
+}
+
+func (r *request) Method() (string, error) {
+ return r.method, r.methodErr
+}
+
+func (r *request) Header() (http.Header, error) {
+ return r.header, r.headerErr
+}
+
+func (r *request) Body() (io.ReadCloser, error) {
+ return r.body, r.bodyErr
+}
+
+func (r *request) Response() ResponseWriter {
+ if r.rw != nil {
+ return r.rw
+ }
+
+ r.rw = &responseWriter{req: r}
+ return r.rw
+}
+
+func (r *request) Close() error {
+ var errs []error
+ if r.body != nil {
+ if err := r.body.Close(); err != nil {
+ errs = append(errs, err)
+ }
+ r.body = nil
+ }
+
+ if err := r.Response().Finish(); err != nil {
+ errs = append(errs, err)
+ }
+
+ return combineErrs(errs)
+}
+
+// finishResponse must be called on the main-thread
+func (r *request) finishResponse() error {
+ var errs []error
+ if r.response != nil {
+ if err := r.response.Release(); err != nil {
+ errs = append(errs, err)
+ }
+ r.response = nil
+ }
+ if r.deferral != nil {
+ if err := r.deferral.Complete(); err != nil {
+ errs = append(errs, err)
+ }
+
+ if err := r.deferral.Release(); err != nil {
+ errs = append(errs, err)
+ }
+ r.deferral = nil
+ }
+ return combineErrs(errs)
+}
+
+type iStreamReleaseCloser struct {
+ stream *edge.IStream
+ closed bool
+}
+
+func (i *iStreamReleaseCloser) Read(p []byte) (int, error) {
+ if i.closed {
+ return 0, io.ErrClosedPipe
+ }
+ return i.stream.Read(p)
+}
+
+func (i *iStreamReleaseCloser) Close() error {
+ if i.closed {
+ return nil
+ }
+ i.closed = true
+ return i.stream.Release()
+}
+
+func getHeaders(req *edge.ICoreWebView2WebResourceRequest) (http.Header, error) {
+ header := http.Header{}
+ headers, err := req.GetHeaders()
+ if err != nil {
+ return nil, fmt.Errorf("GetHeaders Error: %s", err)
+ }
+ defer headers.Release()
+
+ headersIt, err := headers.GetIterator()
+ if err != nil {
+ return nil, fmt.Errorf("GetIterator Error: %s", err)
+ }
+ defer headersIt.Release()
+
+ for {
+ has, err := headersIt.HasCurrentHeader()
+ if err != nil {
+ return nil, fmt.Errorf("HasCurrentHeader Error: %s", err)
+ }
+ if !has {
+ break
+ }
+
+ name, value, err := headersIt.GetCurrentHeader()
+ if err != nil {
+ return nil, fmt.Errorf("GetCurrentHeader Error: %s", err)
+ }
+
+ header.Set(name, value)
+ if _, err := headersIt.MoveNext(); err != nil {
+ return nil, fmt.Errorf("MoveNext Error: %s", err)
+ }
+ }
+
+ // WebView2 has problems when a request returns a 304 status code and the WebView2 is going to hang for other
+ // requests including IPC calls.
+ // So prevent 304 status codes by removing the headers that are used in combinationwith caching.
+ header.Del("If-Modified-Since")
+ header.Del("If-None-Match")
+ return header, nil
+}
+
+func combineErrs(errs []error) error {
+ // TODO use Go1.20 errors.Join
+ if len(errs) == 0 {
+ return nil
+ }
+
+ errStrings := make([]string, len(errs))
+ for i, err := range errs {
+ errStrings[i] = err.Error()
+ }
+
+ return fmt.Errorf(strings.Join(errStrings, "\n"))
+}
diff --git a/v2/pkg/assetserver/webview/responsewriter.go b/v2/pkg/assetserver/webview/responsewriter.go
new file mode 100644
index 000000000..dacbb567d
--- /dev/null
+++ b/v2/pkg/assetserver/webview/responsewriter.go
@@ -0,0 +1,25 @@
+package webview
+
+import (
+ "errors"
+ "net/http"
+)
+
+const (
+ HeaderContentLength = "Content-Length"
+ HeaderContentType = "Content-Type"
+)
+
+var (
+ errRequestStopped = errors.New("request has been stopped")
+ errResponseFinished = errors.New("response has been finished")
+)
+
+// A ResponseWriter interface is used by an HTTP handler to
+// construct an HTTP response for the WebView.
+type ResponseWriter interface {
+ http.ResponseWriter
+
+ // Finish the response and flush all data. A Finish after the request has already been finished has no effect.
+ Finish() error
+}
diff --git a/v2/pkg/assetserver/webview/responsewriter_darwin.go b/v2/pkg/assetserver/webview/responsewriter_darwin.go
new file mode 100644
index 000000000..a3c73b6f1
--- /dev/null
+++ b/v2/pkg/assetserver/webview/responsewriter_darwin.go
@@ -0,0 +1,164 @@
+//go:build darwin
+
+package webview
+
+/*
+#cgo CFLAGS: -x objective-c
+#cgo LDFLAGS: -framework Foundation -framework WebKit
+
+#import
+#import
+
+typedef void (^schemeTaskCaller)(id);
+
+static bool urlSchemeTaskCall(void *wkUrlSchemeTask, schemeTaskCaller fn) {
+ id urlSchemeTask = (id) wkUrlSchemeTask;
+ if (urlSchemeTask == nil) {
+ return false;
+ }
+
+ @autoreleasepool {
+ @try {
+ fn(urlSchemeTask);
+ } @catch (NSException *exception) {
+ // This is very bad to detect a stopped schemeTask this should be implemented in a better way
+ // But it seems to be very tricky to not deadlock when keeping a lock curing executing fn()
+ // It seems like those call switch the thread back to the main thread and then deadlocks when they reentrant want
+ // to get the lock again to start another request or stop it.
+ if ([exception.reason isEqualToString: @"This task has already been stopped"]) {
+ return false;
+ }
+
+ @throw exception;
+ }
+
+ return true;
+ }
+}
+
+static bool URLSchemeTaskDidReceiveData(void *wkUrlSchemeTask, void* data, int datalength) {
+ return urlSchemeTaskCall(
+ wkUrlSchemeTask,
+ ^(id urlSchemeTask) {
+ NSData *nsdata = [NSData dataWithBytes:data length:datalength];
+ [urlSchemeTask didReceiveData:nsdata];
+ });
+}
+
+static bool URLSchemeTaskDidFinish(void *wkUrlSchemeTask) {
+ return urlSchemeTaskCall(
+ wkUrlSchemeTask,
+ ^(id urlSchemeTask) {
+ [urlSchemeTask didFinish];
+ });
+}
+
+static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCode, void *headersString, int headersStringLength) {
+ return urlSchemeTaskCall(
+ wkUrlSchemeTask,
+ ^(id urlSchemeTask) {
+ NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength];
+ NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData:nsHeadersJSON options: NSJSONReadingMutableContainers error: nil];
+ NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease];
+
+ [urlSchemeTask didReceiveResponse:response];
+ });
+}
+*/
+import "C"
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "unsafe"
+)
+
+var _ ResponseWriter = &responseWriter{}
+
+type responseWriter struct {
+ r *request
+
+ header http.Header
+ wroteHeader bool
+
+ finished bool
+}
+
+func (rw *responseWriter) Header() http.Header {
+ if rw.header == nil {
+ rw.header = http.Header{}
+ }
+ return rw.header
+}
+
+func (rw *responseWriter) Write(buf []byte) (int, error) {
+ if rw.finished {
+ return 0, errResponseFinished
+ }
+
+ rw.WriteHeader(http.StatusOK)
+
+ var contentLen int
+ if buf != nil {
+ contentLen = len(buf)
+ }
+
+ if contentLen > 0 {
+ // Create a C array to hold the data
+ cBuf := C.malloc(C.size_t(contentLen))
+ if cBuf == nil {
+ return 0, fmt.Errorf("memory allocation failed for %d bytes", contentLen)
+ }
+ defer C.free(cBuf)
+
+ // Copy the Go slice to the C array
+ C.memcpy(cBuf, unsafe.Pointer(&buf[0]), C.size_t(contentLen))
+
+ if !C.URLSchemeTaskDidReceiveData(rw.r.task, cBuf, C.int(contentLen)) {
+ return 0, errRequestStopped
+ }
+ } else {
+ if !C.URLSchemeTaskDidReceiveData(rw.r.task, nil, 0) {
+ return 0, errRequestStopped
+ }
+ }
+
+ return contentLen, nil
+}
+
+func (rw *responseWriter) WriteHeader(code int) {
+ if rw.wroteHeader || rw.finished {
+ return
+ }
+ rw.wroteHeader = true
+
+ header := map[string]string{}
+ for k := range rw.Header() {
+ header[k] = rw.Header().Get(k)
+ }
+ headerData, _ := json.Marshal(header)
+
+ var headers unsafe.Pointer
+ var headersLen int
+ if len(headerData) != 0 {
+ headers = unsafe.Pointer(&headerData[0])
+ headersLen = len(headerData)
+ }
+
+ C.URLSchemeTaskDidReceiveResponse(rw.r.task, C.int(code), headers, C.int(headersLen))
+}
+
+func (rw *responseWriter) Finish() error {
+ if !rw.wroteHeader {
+ rw.WriteHeader(http.StatusNotImplemented)
+ }
+
+ if rw.finished {
+ return nil
+ }
+ rw.finished = true
+
+ C.URLSchemeTaskDidFinish(rw.r.task)
+ return nil
+}
diff --git a/v2/pkg/assetserver/webview/responsewriter_linux.go b/v2/pkg/assetserver/webview/responsewriter_linux.go
new file mode 100644
index 000000000..59646ce29
--- /dev/null
+++ b/v2/pkg/assetserver/webview/responsewriter_linux.go
@@ -0,0 +1,132 @@
+//go:build linux
+// +build linux
+
+package webview
+
+/*
+#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0
+#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
+#cgo webkit2_41 pkg-config: webkit2gtk-4.1
+
+#include "gtk/gtk.h"
+#include "webkit2/webkit2.h"
+#include "gio/gunixinputstream.h"
+
+*/
+import "C"
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "strconv"
+ "syscall"
+ "unsafe"
+)
+
+type responseWriter struct {
+ req *C.WebKitURISchemeRequest
+
+ header http.Header
+ wroteHeader bool
+ finished bool
+
+ w io.WriteCloser
+ wErr error
+}
+
+func (rw *responseWriter) Header() http.Header {
+ if rw.header == nil {
+ rw.header = http.Header{}
+ }
+ return rw.header
+}
+
+func (rw *responseWriter) Write(buf []byte) (int, error) {
+ if rw.finished {
+ return 0, errResponseFinished
+ }
+
+ rw.WriteHeader(http.StatusOK)
+ if rw.wErr != nil {
+ return 0, rw.wErr
+ }
+ return rw.w.Write(buf)
+}
+
+func (rw *responseWriter) WriteHeader(code int) {
+ if rw.wroteHeader || rw.finished {
+ return
+ }
+ rw.wroteHeader = true
+
+ contentLength := int64(-1)
+ if sLen := rw.Header().Get(HeaderContentLength); sLen != "" {
+ if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 {
+ contentLength = pLen
+ }
+ }
+
+ // We can't use os.Pipe here, because that returns files with a finalizer for closing the FD. But the control over the
+ // read FD is given to the InputStream and will be closed there.
+ // Furthermore we especially don't want to have the FD_CLOEXEC
+ rFD, w, err := pipe()
+ if err != nil {
+ rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err))
+ return
+ }
+ rw.w = w
+
+ stream := C.g_unix_input_stream_new(C.int(rFD), C.gboolean(1))
+ defer C.g_object_unref(C.gpointer(stream))
+
+ if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), stream, contentLength); err != nil {
+ rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err))
+ return
+ }
+}
+
+func (rw *responseWriter) Finish() error {
+ if !rw.wroteHeader {
+ rw.WriteHeader(http.StatusNotImplemented)
+ }
+
+ if rw.finished {
+ return nil
+ }
+ rw.finished = true
+ if rw.w != nil {
+ rw.w.Close()
+ }
+ return nil
+}
+
+func (rw *responseWriter) finishWithError(code int, err error) {
+ if rw.w != nil {
+ rw.w.Close()
+ rw.w = &nopCloser{io.Discard}
+ }
+ rw.wErr = err
+
+ msg := C.CString(err.Error())
+ gerr := C.g_error_new_literal(C.g_quark_from_string(msg), C.int(code), msg)
+ C.webkit_uri_scheme_request_finish_error(rw.req, gerr)
+ C.g_error_free(gerr)
+ C.free(unsafe.Pointer(msg))
+}
+
+type nopCloser struct {
+ io.Writer
+}
+
+func (nopCloser) Close() error { return nil }
+
+func pipe() (r int, w *os.File, err error) {
+ var p [2]int
+ e := syscall.Pipe2(p[0:], 0)
+ if e != nil {
+ return 0, nil, fmt.Errorf("pipe2: %s", e)
+ }
+
+ return p[0], os.NewFile(uintptr(p[1]), "|1"), nil
+}
diff --git a/v2/pkg/assetserver/webview/responsewriter_windows.go b/v2/pkg/assetserver/webview/responsewriter_windows.go
new file mode 100644
index 000000000..748d9511b
--- /dev/null
+++ b/v2/pkg/assetserver/webview/responsewriter_windows.go
@@ -0,0 +1,105 @@
+//go:build windows
+// +build windows
+
+package webview
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "strings"
+)
+
+var _ http.ResponseWriter = &responseWriter{}
+
+type responseWriter struct {
+ req *request
+
+ header http.Header
+ wroteHeader bool
+ code int
+ body *bytes.Buffer
+
+ finished bool
+}
+
+func (rw *responseWriter) Header() http.Header {
+ if rw.header == nil {
+ rw.header = http.Header{}
+ }
+ return rw.header
+}
+
+func (rw *responseWriter) Write(buf []byte) (int, error) {
+ if rw.finished {
+ return 0, errResponseFinished
+ }
+
+ rw.WriteHeader(http.StatusOK)
+
+ return rw.body.Write(buf)
+}
+
+func (rw *responseWriter) WriteHeader(code int) {
+ if rw.wroteHeader || rw.finished {
+ return
+ }
+ rw.wroteHeader = true
+
+ if rw.body == nil {
+ rw.body = &bytes.Buffer{}
+ }
+
+ rw.code = code
+}
+
+func (rw *responseWriter) Finish() error {
+ if !rw.wroteHeader {
+ rw.WriteHeader(http.StatusNotImplemented)
+ }
+
+ if rw.finished {
+ return nil
+ }
+ rw.finished = true
+
+ var errs []error
+
+ code := rw.code
+ if code == http.StatusNotModified {
+ // WebView2 has problems when a request returns a 304 status code and the WebView2 is going to hang for other
+ // requests including IPC calls.
+ errs = append(errs, fmt.Errorf("AssetServer returned 304 - StatusNotModified which are going to hang WebView2, changed code to 505 - StatusInternalServerError"))
+ code = http.StatusInternalServerError
+ }
+
+ rw.req.invokeSync(func() {
+ resp := rw.req.response
+
+ hdrs, err := resp.GetHeaders()
+ if err != nil {
+ errs = append(errs, fmt.Errorf("Resp.GetHeaders failed: %s", err))
+ } else {
+ for k, v := range rw.header {
+ if err := hdrs.AppendHeader(k, strings.Join(v, ",")); err != nil {
+ errs = append(errs, fmt.Errorf("Resp.AppendHeader failed: %s", err))
+ }
+ }
+ hdrs.Release()
+ }
+
+ if err := resp.PutStatusCode(code); err != nil {
+ errs = append(errs, fmt.Errorf("Resp.PutStatusCode failed: %s", err))
+ }
+
+ if err := resp.PutByteContent(rw.body.Bytes()); err != nil {
+ errs = append(errs, fmt.Errorf("Resp.PutByteContent failed: %s", err))
+ }
+
+ if err := rw.req.finishResponse(); err != nil {
+ errs = append(errs, fmt.Errorf("Resp.finishResponse failed: %s", err))
+ }
+ })
+
+ return combineErrs(errs)
+}
diff --git a/v2/pkg/assetserver/webview/webkit2_36+.go b/v2/pkg/assetserver/webview/webkit2_36+.go
new file mode 100644
index 000000000..1f0db3c89
--- /dev/null
+++ b/v2/pkg/assetserver/webview/webkit2_36+.go
@@ -0,0 +1,71 @@
+//go:build linux && (webkit2_36 || webkit2_40 || webkit2_41 )
+
+package webview
+
+/*
+#cgo linux pkg-config: gtk+-3.0
+#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 libsoup-2.4
+#cgo webkit2_41 pkg-config: webkit2gtk-4.1 libsoup-3.0
+
+#include "gtk/gtk.h"
+#include "webkit2/webkit2.h"
+#include "libsoup/soup.h"
+*/
+import "C"
+
+import (
+ "net/http"
+ "strings"
+ "unsafe"
+)
+
+func webkit_uri_scheme_request_get_http_method(req *C.WebKitURISchemeRequest) string {
+ method := C.GoString(C.webkit_uri_scheme_request_get_http_method(req))
+ return strings.ToUpper(method)
+}
+
+func webkit_uri_scheme_request_get_http_headers(req *C.WebKitURISchemeRequest) http.Header {
+ hdrs := C.webkit_uri_scheme_request_get_http_headers(req)
+
+ var iter C.SoupMessageHeadersIter
+ C.soup_message_headers_iter_init(&iter, hdrs)
+
+ var name *C.char
+ var value *C.char
+
+ h := http.Header{}
+ for C.soup_message_headers_iter_next(&iter, &name, &value) != 0 {
+ h.Add(C.GoString(name), C.GoString(value))
+ }
+
+ return h
+}
+
+func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, stream *C.GInputStream, streamLength int64) error {
+ resp := C.webkit_uri_scheme_response_new(stream, C.gint64(streamLength))
+ defer C.g_object_unref(C.gpointer(resp))
+
+ cReason := C.CString(http.StatusText(code))
+ C.webkit_uri_scheme_response_set_status(resp, C.guint(code), cReason)
+ C.free(unsafe.Pointer(cReason))
+
+ cMimeType := C.CString(header.Get(HeaderContentType))
+ C.webkit_uri_scheme_response_set_content_type(resp, cMimeType)
+ C.free(unsafe.Pointer(cMimeType))
+
+ hdrs := C.soup_message_headers_new(C.SOUP_MESSAGE_HEADERS_RESPONSE)
+ for name, values := range header {
+ cName := C.CString(name)
+ for _, value := range values {
+ cValue := C.CString(value)
+ C.soup_message_headers_append(hdrs, cName, cValue)
+ C.free(unsafe.Pointer(cValue))
+ }
+ C.free(unsafe.Pointer(cName))
+ }
+
+ C.webkit_uri_scheme_response_set_http_headers(resp, hdrs)
+
+ C.webkit_uri_scheme_request_finish_with_response(req, resp)
+ return nil
+}
diff --git a/v2/pkg/assetserver/webview/webkit2_36.go b/v2/pkg/assetserver/webview/webkit2_36.go
new file mode 100644
index 000000000..cd200af8e
--- /dev/null
+++ b/v2/pkg/assetserver/webview/webkit2_36.go
@@ -0,0 +1,21 @@
+//go:build linux && webkit2_36
+
+package webview
+
+/*
+#cgo linux pkg-config: webkit2gtk-4.0
+
+#include "webkit2/webkit2.h"
+*/
+import "C"
+
+import (
+ "io"
+ "net/http"
+)
+
+const Webkit2MinMinorVersion = 36
+
+func webkit_uri_scheme_request_get_http_body(_ *C.WebKitURISchemeRequest) io.ReadCloser {
+ return http.NoBody
+}
diff --git a/v2/pkg/assetserver/webview/webkit2_40+.go b/v2/pkg/assetserver/webview/webkit2_40+.go
new file mode 100644
index 000000000..eb3e439f2
--- /dev/null
+++ b/v2/pkg/assetserver/webview/webkit2_40+.go
@@ -0,0 +1,85 @@
+//go:build linux && (webkit2_40 || webkit2_41)
+
+package webview
+
+/*
+#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0
+#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
+#cgo webkit2_41 pkg-config: webkit2gtk-4.1
+
+#include "gtk/gtk.h"
+#include "webkit2/webkit2.h"
+#include "gio/gunixinputstream.h"
+*/
+import "C"
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "unsafe"
+)
+
+func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser {
+ stream := C.webkit_uri_scheme_request_get_http_body(req)
+ if stream == nil {
+ return http.NoBody
+ }
+ return &webkitRequestBody{stream: stream}
+}
+
+type webkitRequestBody struct {
+ stream *C.GInputStream
+ closed bool
+}
+
+// Read implements io.Reader
+func (r *webkitRequestBody) Read(p []byte) (int, error) {
+ if r.closed {
+ return 0, io.ErrClosedPipe
+ }
+
+ var content unsafe.Pointer
+ var contentLen int
+ if p != nil {
+ content = unsafe.Pointer(&p[0])
+ contentLen = len(p)
+ }
+
+ var n C.gsize
+ var gErr *C.GError
+ res := C.g_input_stream_read_all(r.stream, content, C.gsize(contentLen), &n, nil, &gErr)
+ if res == 0 {
+ return 0, formatGError("stream read failed", gErr)
+ } else if n == 0 {
+ return 0, io.EOF
+ }
+ return int(n), nil
+}
+
+func (r *webkitRequestBody) Close() error {
+ if r.closed {
+ return nil
+ }
+ r.closed = true
+
+ // https://docs.gtk.org/gio/method.InputStream.close.html
+ // Streams will be automatically closed when the last reference is dropped, but you might want to call this function
+ // to make sure resources are released as early as possible.
+ var err error
+ var gErr *C.GError
+ if C.g_input_stream_close(r.stream, nil, &gErr) == 0 {
+ err = formatGError("stream close failed", gErr)
+ }
+ C.g_object_unref(C.gpointer(r.stream))
+ r.stream = nil
+ return err
+}
+
+func formatGError(msg string, gErr *C.GError, args ...any) error {
+ if gErr != nil && gErr.message != nil {
+ msg += ": " + C.GoString(gErr.message)
+ C.g_error_free(gErr)
+ }
+ return fmt.Errorf(msg, args...)
+}
diff --git a/v2/pkg/assetserver/webview/webkit2_40.go b/v2/pkg/assetserver/webview/webkit2_40.go
new file mode 100644
index 000000000..47b504383
--- /dev/null
+++ b/v2/pkg/assetserver/webview/webkit2_40.go
@@ -0,0 +1,5 @@
+//go:build linux && webkit2_40
+
+package webview
+
+const Webkit2MinMinorVersion = 40
diff --git a/v2/pkg/assetserver/webview/webkit2_41.go b/v2/pkg/assetserver/webview/webkit2_41.go
new file mode 100644
index 000000000..82f948d06
--- /dev/null
+++ b/v2/pkg/assetserver/webview/webkit2_41.go
@@ -0,0 +1,5 @@
+//go:build linux && webkit2_41
+
+package webview
+
+const Webkit2MinMinorVersion = 41
diff --git a/v2/pkg/assetserver/webview/webkit2_legacy.go b/v2/pkg/assetserver/webview/webkit2_legacy.go
new file mode 100644
index 000000000..1d1cf7c2b
--- /dev/null
+++ b/v2/pkg/assetserver/webview/webkit2_legacy.go
@@ -0,0 +1,48 @@
+//go:build linux && !(webkit2_36 || webkit2_40 || webkit2_41)
+
+package webview
+
+/*
+#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
+
+#include "gtk/gtk.h"
+#include "webkit2/webkit2.h"
+*/
+import "C"
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "unsafe"
+)
+
+const Webkit2MinMinorVersion = 0
+
+func webkit_uri_scheme_request_get_http_method(_ *C.WebKitURISchemeRequest) string {
+ return http.MethodGet
+}
+
+func webkit_uri_scheme_request_get_http_headers(_ *C.WebKitURISchemeRequest) http.Header {
+ // Fake some basic default headers that are needed if e.g. request are being proxied to the an external sever, like
+ // we do in the devserver.
+ h := http.Header{}
+ h.Add("Accept", "*/*")
+ h.Add("User-Agent", "wails.io/605.1.15")
+ return h
+}
+
+func webkit_uri_scheme_request_get_http_body(_ *C.WebKitURISchemeRequest) io.ReadCloser {
+ return http.NoBody
+}
+
+func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, stream *C.GInputStream, streamLength int64) error {
+ if code != http.StatusOK {
+ return fmt.Errorf("StatusCodes not supported: %d - %s", code, http.StatusText(code))
+ }
+
+ cMimeType := C.CString(header.Get(HeaderContentType))
+ C.webkit_uri_scheme_request_finish(req, stream, C.gint64(streamLength), cMimeType)
+ C.free(unsafe.Pointer(cMimeType))
+ return nil
+}
diff --git a/v2/pkg/buildassets/build/README.md b/v2/pkg/buildassets/build/README.md
new file mode 100644
index 000000000..1ae2f677f
--- /dev/null
+++ b/v2/pkg/buildassets/build/README.md
@@ -0,0 +1,35 @@
+# Build Directory
+
+The build directory is used to house all the build files and assets for your application.
+
+The structure is:
+
+* bin - Output directory
+* darwin - macOS specific files
+* windows - Windows specific files
+
+## Mac
+
+The `darwin` directory holds files specific to Mac builds.
+These may be customised and used as part of the build. To return these files to the default state, simply delete them
+and
+build with `wails build`.
+
+The directory contains the following files:
+
+- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
+- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
+
+## Windows
+
+The `windows` directory contains the manifest and rc files used when building with `wails build`.
+These may be customised for your application. To return these files to the default state, simply delete them and
+build with `wails build`.
+
+- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
+ use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
+ will be created using the `appicon.png` file in the build directory.
+- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
+- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
+ as well as the application itself (right click the exe -> properties -> details)
+- `wails.exe.manifest` - The main application manifest file.
\ No newline at end of file
diff --git a/v2/pkg/buildassets/build/appicon.png b/v2/pkg/buildassets/build/appicon.png
new file mode 100644
index 000000000..63617fe4f
Binary files /dev/null and b/v2/pkg/buildassets/build/appicon.png differ
diff --git a/v2/pkg/buildassets/build/darwin/Info.dev.plist b/v2/pkg/buildassets/build/darwin/Info.dev.plist
new file mode 100644
index 000000000..14121ef7c
--- /dev/null
+++ b/v2/pkg/buildassets/build/darwin/Info.dev.plist
@@ -0,0 +1,68 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.OutputFilename}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.wails.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
+
+
diff --git a/v2/pkg/buildassets/build/darwin/Info.plist b/v2/pkg/buildassets/build/darwin/Info.plist
new file mode 100644
index 000000000..d17a7475c
--- /dev/null
+++ b/v2/pkg/buildassets/build/darwin/Info.plist
@@ -0,0 +1,63 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.OutputFilename}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.wails.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+
+
diff --git a/v2/pkg/buildassets/build/windows/icon.ico b/v2/pkg/buildassets/build/windows/icon.ico
new file mode 100644
index 000000000..f33479841
Binary files /dev/null and b/v2/pkg/buildassets/build/windows/icon.ico differ
diff --git a/v2/pkg/buildassets/build/windows/info.json b/v2/pkg/buildassets/build/windows/info.json
new file mode 100644
index 000000000..9727946b7
--- /dev/null
+++ b/v2/pkg/buildassets/build/windows/info.json
@@ -0,0 +1,15 @@
+{
+ "fixed": {
+ "file_version": "{{.Info.ProductVersion}}"
+ },
+ "info": {
+ "0000": {
+ "ProductVersion": "{{.Info.ProductVersion}}",
+ "CompanyName": "{{.Info.CompanyName}}",
+ "FileDescription": "{{.Info.ProductName}}",
+ "LegalCopyright": "{{.Info.Copyright}}",
+ "ProductName": "{{.Info.ProductName}}",
+ "Comments": "{{.Info.Comments}}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/v2/pkg/buildassets/build/windows/installer/project.nsi b/v2/pkg/buildassets/build/windows/installer/project.nsi
new file mode 100644
index 000000000..654ae2e49
--- /dev/null
+++ b/v2/pkg/buildassets/build/windows/installer/project.nsi
@@ -0,0 +1,114 @@
+Unicode true
+
+####
+## Please note: Template replacements don't work in this file. They are provided with default defines like
+## mentioned underneath.
+## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
+## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
+## from outside of Wails for debugging and development of the installer.
+##
+## For development first make a wails nsis build to populate the "wails_tools.nsh":
+## > wails build --target windows/amd64 --nsis
+## Then you can call makensis on this file with specifying the path to your binary:
+## For a AMD64 only installer:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
+## For a ARM64 only installer:
+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
+## For a installer with both architectures:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
+####
+## The following information is taken from the ProjectInfo file, but they can be overwritten here.
+####
+## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
+## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
+## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
+## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
+## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
+###
+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+####
+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
+####
+## Include the wails tools
+####
+!include "wails_tools.nsh"
+
+# The version information for this two must consist of 4 parts
+VIProductVersion "${INFO_PRODUCTVERSION}.0"
+VIFileVersion "${INFO_PRODUCTVERSION}.0"
+
+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
+
+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
+ManifestDPIAware true
+
+!include "MUI.nsh"
+
+!define MUI_ICON "..\icon.ico"
+!define MUI_UNICON "..\icon.ico"
+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
+
+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
+!insertmacro MUI_PAGE_INSTFILES # Installing page.
+!insertmacro MUI_PAGE_FINISH # Finished installation page.
+
+!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
+
+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
+
+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
+#!uninstfinalize 'signtool --file "%1"'
+#!finalize 'signtool --file "%1"'
+
+Name "${INFO_PRODUCTNAME}"
+OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
+InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
+ShowInstDetails show # This will always show the installation details.
+
+Function .onInit
+ !insertmacro wails.checkArchitecture
+FunctionEnd
+
+Section
+ !insertmacro wails.setShellContext
+
+ !insertmacro wails.webview2runtime
+
+ SetOutPath $INSTDIR
+
+ !insertmacro wails.files
+
+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+
+ !insertmacro wails.associateFiles
+ !insertmacro wails.associateCustomProtocols
+
+ !insertmacro wails.writeUninstaller
+SectionEnd
+
+Section "uninstall"
+ !insertmacro wails.setShellContext
+
+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
+
+ RMDir /r $INSTDIR
+
+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
+
+ !insertmacro wails.unassociateFiles
+ !insertmacro wails.unassociateCustomProtocols
+
+ !insertmacro wails.deleteUninstaller
+SectionEnd
diff --git a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh
new file mode 100644
index 000000000..2f6d32195
--- /dev/null
+++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh
@@ -0,0 +1,249 @@
+# DO NOT EDIT - Generated automatically by `wails build`
+
+!include "x64.nsh"
+!include "WinVer.nsh"
+!include "FileFunc.nsh"
+
+!ifndef INFO_PROJECTNAME
+ !define INFO_PROJECTNAME "{{.Name}}"
+!endif
+!ifndef INFO_COMPANYNAME
+ !define INFO_COMPANYNAME "{{.Info.CompanyName}}"
+!endif
+!ifndef INFO_PRODUCTNAME
+ !define INFO_PRODUCTNAME "{{.Info.ProductName}}"
+!endif
+!ifndef INFO_PRODUCTVERSION
+ !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
+!endif
+!ifndef INFO_COPYRIGHT
+ !define INFO_COPYRIGHT "{{.Info.Copyright}}"
+!endif
+!ifndef PRODUCT_EXECUTABLE
+ !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
+!endif
+!ifndef UNINST_KEY_NAME
+ !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+!endif
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
+
+!ifndef REQUEST_EXECUTION_LEVEL
+ !define REQUEST_EXECUTION_LEVEL "admin"
+!endif
+
+RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
+
+!ifdef ARG_WAILS_AMD64_BINARY
+ !define SUPPORTS_AMD64
+!endif
+
+!ifdef ARG_WAILS_ARM64_BINARY
+ !define SUPPORTS_ARM64
+!endif
+
+!ifdef SUPPORTS_AMD64
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "amd64_arm64"
+ !else
+ !define ARCH "amd64"
+ !endif
+!else
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "arm64"
+ !else
+ !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
+ !endif
+!endif
+
+!macro wails.checkArchitecture
+ !ifndef WAILS_WIN10_REQUIRED
+ !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
+ !endif
+
+ !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
+ !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
+ !endif
+
+ ${If} ${AtLeastWin10}
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ IfSilent silentArch notSilentArch
+ silentArch:
+ SetErrorLevel 65
+ Abort
+ notSilentArch:
+ MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
+ Quit
+ ${else}
+ IfSilent silentWin notSilentWin
+ silentWin:
+ SetErrorLevel 64
+ Abort
+ notSilentWin:
+ MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
+ Quit
+ ${EndIf}
+
+ ok:
+!macroend
+
+!macro wails.files
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
+ ${EndIf}
+ !endif
+!macroend
+
+!macro wails.writeUninstaller
+ WriteUninstaller "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+ WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
+
+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
+ IntFmt $0 "0x%08X" $0
+ WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
+!macroend
+
+!macro wails.deleteUninstaller
+ Delete "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ DeleteRegKey HKLM "${UNINST_KEY}"
+!macroend
+
+!macro wails.setShellContext
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
+ SetShellVarContext all
+ ${else}
+ SetShellVarContext current
+ ${EndIf}
+!macroend
+
+# Install webview2 by launching the bootstrapper
+# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
+!macro wails.webview2runtime
+ !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
+ !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
+ !endif
+
+ SetRegView 64
+ # If the admin key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
+ # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+ ${EndIf}
+
+ SetDetailsPrint both
+ DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
+ SetDetailsPrint listonly
+
+ InitPluginsDir
+ CreateDirectory "$pluginsdir\webview2bootstrapper"
+ SetOutPath "$pluginsdir\webview2bootstrapper"
+ File "tmp\MicrosoftEdgeWebview2Setup.exe"
+ ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
+
+ SetDetailsPrint both
+ ok:
+!macroend
+
+# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
+!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
+!macroend
+
+!macro APP_UNASSOCIATE EXT FILECLASS
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
+
+ DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
+!macroend
+
+!macro wails.associateFiles
+ ; Create file associations
+ {{range .Info.FileAssociations}}
+ !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+ File "..\{{.IconName}}.ico"
+ {{end}}
+!macroend
+
+!macro wails.unassociateFiles
+ ; Delete app associations
+ {{range .Info.FileAssociations}}
+ !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
+
+ Delete "$INSTDIR\{{.IconName}}.ico"
+ {{end}}
+!macroend
+
+!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
+!macroend
+
+!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+!macroend
+
+!macro wails.associateCustomProtocols
+ ; Create custom protocols associations
+ {{range .Info.Protocols}}
+ !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+ {{end}}
+!macroend
+
+!macro wails.unassociateCustomProtocols
+ ; Delete app custom protocol associations
+ {{range .Info.Protocols}}
+ !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
+ {{end}}
+!macroend
diff --git a/v2/pkg/buildassets/build/windows/wails.exe.manifest b/v2/pkg/buildassets/build/windows/wails.exe.manifest
new file mode 100644
index 000000000..17e1a2387
--- /dev/null
+++ b/v2/pkg/buildassets/build/windows/wails.exe.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
\ No newline at end of file
diff --git a/v2/pkg/buildassets/buildassets.go b/v2/pkg/buildassets/buildassets.go
new file mode 100644
index 000000000..6934b98bd
--- /dev/null
+++ b/v2/pkg/buildassets/buildassets.go
@@ -0,0 +1,142 @@
+package buildassets
+
+import (
+ "bytes"
+ "embed"
+ "errors"
+ "fmt"
+ iofs "io/fs"
+ "os"
+ "path/filepath"
+ "text/template"
+
+ "github.com/leaanthony/gosod"
+ "github.com/samber/lo"
+ "github.com/wailsapp/wails/v2/internal/fs"
+ "github.com/wailsapp/wails/v2/internal/project"
+)
+
+//go:embed build
+var assets embed.FS
+
+// Same as assets but chrooted into /build/
+var buildAssets iofs.FS
+
+func init() {
+ buildAssets = lo.Must(iofs.Sub(assets, "build"))
+}
+
+// Install will install all default project assets
+func Install(targetDir string) error {
+ templateDir := gosod.New(assets)
+ err := templateDir.Extract(targetDir, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetLocalPath returns the local path of the requested build asset file
+func GetLocalPath(projectData *project.Project, file string) string {
+ return filepath.Clean(filepath.Join(projectData.GetBuildDir(), filepath.FromSlash(file)))
+}
+
+// ReadFile reads the file from the project build folder.
+// If the file does not exist it falls back to the embedded file and the file will be written
+// to the disk for customisation.
+func ReadFile(projectData *project.Project, file string) ([]byte, error) {
+ localFilePath := GetLocalPath(projectData, file)
+
+ content, err := os.ReadFile(localFilePath)
+ if errors.Is(err, iofs.ErrNotExist) {
+ // The file does not exist, let's read it from the assets FS and write it to disk
+ content, err := iofs.ReadFile(buildAssets, file)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := writeFileSystemFile(projectData, file, content); err != nil {
+ return nil, fmt.Errorf("Unable to create file in build folder: %s", err)
+ }
+ return content, nil
+ }
+
+ return content, err
+}
+
+// ReadFileWithProjectData reads the file from the project build folder and replaces ProjectInfo if necessary.
+// If the file does not exist it falls back to the embedded file and the file will be written
+// to the disk for customisation. The file written is the original unresolved one.
+func ReadFileWithProjectData(projectData *project.Project, file string) ([]byte, error) {
+ content, err := ReadFile(projectData, file)
+ if err != nil {
+ return nil, err
+ }
+
+ content, err = resolveProjectData(content, projectData)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to resolve data in %s: %w", file, err)
+ }
+ return content, nil
+}
+
+// ReadOriginalFileWithProjectDataAndSave reads the file from the embedded assets and replaces
+// ProjectInfo if necessary.
+// It will also write the resolved final file back to the project build folder.
+func ReadOriginalFileWithProjectDataAndSave(projectData *project.Project, file string) ([]byte, error) {
+ content, err := iofs.ReadFile(buildAssets, file)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to read file %s: %w", file, err)
+ }
+
+ content, err = resolveProjectData(content, projectData)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to resolve data in %s: %w", file, err)
+ }
+
+ if err := writeFileSystemFile(projectData, file, content); err != nil {
+ return nil, fmt.Errorf("Unable to create file in build folder: %w", err)
+ }
+ return content, nil
+}
+
+type assetData struct {
+ Name string
+ Info project.Info
+ OutputFilename string
+}
+
+func resolveProjectData(content []byte, projectData *project.Project) ([]byte, error) {
+ tmpl, err := template.New("").Parse(string(content))
+ if err != nil {
+ return nil, err
+ }
+
+ data := &assetData{
+ Name: projectData.Name,
+ Info: projectData.Info,
+ OutputFilename: projectData.OutputFilename,
+ }
+
+ var out bytes.Buffer
+ if err := tmpl.Execute(&out, data); err != nil {
+ return nil, err
+ }
+ return out.Bytes(), nil
+}
+
+func writeFileSystemFile(projectData *project.Project, file string, content []byte) error {
+ targetPath := GetLocalPath(projectData, file)
+
+ if dir := filepath.Dir(targetPath); !fs.DirExists(dir) {
+ if err := fs.MkDirs(dir, 0o755); err != nil {
+ return fmt.Errorf("Unable to create directory: %w", err)
+ }
+ }
+
+ if err := os.WriteFile(targetPath, content, 0o644); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/v2/pkg/buildassets/onhold/dialog/README.md b/v2/pkg/buildassets/onhold/dialog/README.md
new file mode 100644
index 000000000..3b9189a8f
--- /dev/null
+++ b/v2/pkg/buildassets/onhold/dialog/README.md
@@ -0,0 +1,29 @@
+## Dialog
+
+NOTE: Currently, this is a Mac only feature.
+
+Place any PNG file in this directory to be able to use them in message dialogs.
+The files should have names in the following format: `name[-(light|dark)][2x].png`
+
+Examples:
+
+* `mypic.png` - Standard definition icon with ID `mypic`
+* `mypic-light.png` - Standard definition icon with ID `mypic`, used when system theme is light
+* `mypic-dark.png` - Standard definition icon with ID `mypic`, used when system theme is dark
+* `mypic2x.png` - High definition icon with ID `mypic`
+* `mypic-light2x.png` - High definition icon with ID `mypic`, used when system theme is light
+* `mypic-dark2x.png` - High definition icon with ID `mypic`, used when system theme is dark
+
+### Order of preference
+
+Icons are selected with the following order of preference:
+
+For High Definition displays:
+* name-(theme)2x.png
+* name2x.png
+* name-(theme).png
+* name.png
+
+For Standard Definition displays:
+* name-(theme).png
+* name.png
\ No newline at end of file
diff --git a/v2/pkg/buildassets/onhold/dialog/info-dark.png b/v2/pkg/buildassets/onhold/dialog/info-dark.png
new file mode 100644
index 000000000..9ff6655ee
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/info-dark.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/info-dark2x.png b/v2/pkg/buildassets/onhold/dialog/info-dark2x.png
new file mode 100644
index 000000000..fcdf8006a
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/info-dark2x.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/info-light.png b/v2/pkg/buildassets/onhold/dialog/info-light.png
new file mode 100644
index 000000000..1fb32e8a9
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/info-light.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/info-light2x.png b/v2/pkg/buildassets/onhold/dialog/info-light2x.png
new file mode 100644
index 000000000..874b2d301
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/info-light2x.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/question-dark.png b/v2/pkg/buildassets/onhold/dialog/question-dark.png
new file mode 100644
index 000000000..c2387420e
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/question-dark.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/question-dark2x.png b/v2/pkg/buildassets/onhold/dialog/question-dark2x.png
new file mode 100644
index 000000000..86ea1b037
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/question-dark2x.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/question-light.png b/v2/pkg/buildassets/onhold/dialog/question-light.png
new file mode 100644
index 000000000..0d3b6ba02
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/question-light.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/question-light2x.png b/v2/pkg/buildassets/onhold/dialog/question-light2x.png
new file mode 100644
index 000000000..fcd21569f
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/question-light2x.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/warning-dark.png b/v2/pkg/buildassets/onhold/dialog/warning-dark.png
new file mode 100644
index 000000000..db432321b
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/warning-dark.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/warning-dark2x.png b/v2/pkg/buildassets/onhold/dialog/warning-dark2x.png
new file mode 100644
index 000000000..3325d22d2
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/warning-dark2x.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/warning-light.png b/v2/pkg/buildassets/onhold/dialog/warning-light.png
new file mode 100644
index 000000000..cf421a171
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/warning-light.png differ
diff --git a/v2/pkg/buildassets/onhold/dialog/warning-light2x.png b/v2/pkg/buildassets/onhold/dialog/warning-light2x.png
new file mode 100644
index 000000000..ff092f6cd
Binary files /dev/null and b/v2/pkg/buildassets/onhold/dialog/warning-light2x.png differ
diff --git a/v2/pkg/buildassets/onhold/tray/README.md b/v2/pkg/buildassets/onhold/tray/README.md
new file mode 100644
index 000000000..5f4e7b4e6
--- /dev/null
+++ b/v2/pkg/buildassets/onhold/tray/README.md
@@ -0,0 +1,8 @@
+## Tray
+
+Place any PNG file in this directory to be able to use them as tray icons.
+The name of the filename will be the ID to reference the image.
+
+Example:
+
+* `mypic.png` - May be referenced using `runtime.Tray.SetIcon("mypic")`
diff --git a/v2/pkg/clilogger/clilogger.go b/v2/pkg/clilogger/clilogger.go
new file mode 100644
index 000000000..efc202bbd
--- /dev/null
+++ b/v2/pkg/clilogger/clilogger.go
@@ -0,0 +1,61 @@
+package clilogger
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/wailsapp/wails/v2/internal/colour"
+)
+
+// CLILogger is used by the cli
+type CLILogger struct {
+ Writer io.Writer
+ mute bool
+}
+
+// New cli logger
+func New(writer io.Writer) *CLILogger {
+ return &CLILogger{
+ Writer: writer,
+ }
+}
+
+// Mute sets whether the logger should be muted
+func (c *CLILogger) Mute(value bool) {
+ c.mute = value
+}
+
+// Print works like Printf
+func (c *CLILogger) Print(message string, args ...interface{}) {
+ if c.mute {
+ return
+ }
+
+ _, err := fmt.Fprintf(c.Writer, message, args...)
+ if err != nil {
+ c.Fatal("FATAL: " + err.Error())
+ }
+}
+
+// Println works like Printf but with a line ending
+func (c *CLILogger) Println(message string, args ...interface{}) {
+ if c.mute {
+ return
+ }
+ temp := fmt.Sprintf(message, args...)
+ _, err := fmt.Fprintln(c.Writer, temp)
+ if err != nil {
+ c.Fatal("FATAL: " + err.Error())
+ }
+}
+
+// Fatal prints the given message then aborts
+func (c *CLILogger) Fatal(message string, args ...interface{}) {
+ temp := fmt.Sprintf(message, args...)
+ _, err := fmt.Fprintln(c.Writer, colour.Red("FATAL: "+temp))
+ if err != nil {
+ println(colour.Red("FATAL: " + err.Error()))
+ }
+ os.Exit(1)
+}
diff --git a/v2/pkg/commands/bindings/bindings.go b/v2/pkg/commands/bindings/bindings.go
new file mode 100644
index 000000000..82ce0d58f
--- /dev/null
+++ b/v2/pkg/commands/bindings/bindings.go
@@ -0,0 +1,97 @@
+package bindings
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "runtime"
+
+ "github.com/samber/lo"
+ "github.com/wailsapp/wails/v2/internal/colour"
+ "github.com/wailsapp/wails/v2/internal/shell"
+ "github.com/wailsapp/wails/v2/pkg/commands/buildtags"
+)
+
+// Options for generating bindings
+type Options struct {
+ Filename string
+ Tags []string
+ ProjectDirectory string
+ Compiler string
+ GoModTidy bool
+ TsPrefix string
+ TsSuffix string
+ TsOutputType string
+}
+
+// GenerateBindings generates bindings for the Wails project in the given ProjectDirectory.
+// If no project directory is given then the current working directory is used.
+func GenerateBindings(options Options) (string, error) {
+ filename, _ := lo.Coalesce(options.Filename, "wailsbindings")
+ if runtime.GOOS == "windows" {
+ filename += ".exe"
+ }
+
+ // go build -tags bindings -o bindings.exe
+ tempDir := os.TempDir()
+ filename = filepath.Join(tempDir, filename)
+
+ workingDirectory, _ := lo.Coalesce(options.ProjectDirectory, lo.Must(os.Getwd()))
+
+ var stdout, stderr string
+ var err error
+
+ tags := append(options.Tags, "bindings")
+ genModuleTags := lo.Without(tags, "desktop", "production", "debug", "dev")
+ tagString := buildtags.Stringify(genModuleTags)
+
+ if options.GoModTidy {
+ stdout, stderr, err = shell.RunCommand(workingDirectory, options.Compiler, "mod", "tidy")
+ if err != nil {
+ return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err)
+ }
+ }
+
+ envBuild := os.Environ()
+ envBuild = shell.SetEnv(envBuild, "GOOS", runtime.GOOS)
+ envBuild = shell.SetEnv(envBuild, "GOARCH", runtime.GOARCH)
+ // wailsbindings is executed on the build machine.
+ // So, use the default C compiler, not the one set for cross compiling.
+ envBuild = shell.RemoveEnv(envBuild, "CC")
+
+ stdout, stderr, err = shell.RunCommandWithEnv(envBuild, workingDirectory, options.Compiler, "build", "-buildvcs=false", "-tags", tagString, "-o", filename)
+ if err != nil {
+ return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err)
+ }
+
+ if runtime.GOOS == "darwin" {
+ // Remove quarantine attribute
+ stdout, stderr, err = shell.RunCommand(workingDirectory, "/usr/bin/xattr", "-rc", filename)
+ if err != nil {
+ return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err)
+ }
+ }
+
+ defer func() {
+ // Best effort removal of temp file
+ _ = os.Remove(filename)
+ }()
+
+ // Set environment variables accordingly
+ env := os.Environ()
+ env = shell.SetEnv(env, "tsprefix", options.TsPrefix)
+ env = shell.SetEnv(env, "tssuffix", options.TsSuffix)
+ env = shell.SetEnv(env, "tsoutputtype", options.TsOutputType)
+
+ stdout, stderr, err = shell.RunCommandWithEnv(env, workingDirectory, filename)
+ if err != nil {
+ return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err)
+ }
+
+ if stderr != "" {
+ log.Println(colour.DarkYellow(stderr))
+ }
+
+ return stdout, nil
+}
diff --git a/v2/pkg/commands/bindings/bindings_test.go b/v2/pkg/commands/bindings/bindings_test.go
new file mode 100644
index 000000000..53f42f2c7
--- /dev/null
+++ b/v2/pkg/commands/bindings/bindings_test.go
@@ -0,0 +1,128 @@
+package bindings
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/matryer/is"
+ "github.com/wailsapp/wails/v2/pkg/templates"
+)
+
+const standardBindings = `// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Greet(arg1) {
+ return window['go']['main']['App']['Greet'](arg1);
+}
+`
+
+const obfuscatedBindings = `// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Greet(arg1) {
+ return ObfuscatedCall(0, [arg1]);
+}
+`
+
+func TestGenerateBindings(t *testing.T) {
+
+ i := is.New(t)
+
+ // Get the directory of this file
+ _, filename, _, _ := runtime.Caller(0)
+ workingDirectory := filepath.Dir(filename)
+
+ projectDir := filepath.Join(workingDirectory, "test")
+
+ _ = os.RemoveAll(projectDir)
+
+ _, _, err := templates.Install(&templates.Options{
+ ProjectName: "test",
+ TemplateName: "plain",
+ WailsVersion: "latest",
+ })
+ if err != nil {
+ println(err.Error())
+ t.Fail()
+ }
+
+ defer func() {
+ _ = os.RemoveAll(projectDir)
+ }()
+
+ // Make the go.mod point to local
+ goModPath := filepath.Join(projectDir, "go.mod")
+ goMod, err := os.ReadFile(goModPath)
+ i.NoErr(err)
+ pathToRepository := filepath.Join(workingDirectory, "..", "..", "..")
+ absPathToRepo, _ := filepath.Abs(pathToRepository)
+ goModString := string(goMod)
+ goModSplit := strings.Split(goModString, "=>")
+ goModSplit[1] = absPathToRepo
+ goModString = strings.Join(goModSplit, "=> ")
+ goMod = []byte(strings.ReplaceAll(goModString, "// replace", "replace"))
+ // Write file back
+ err = os.WriteFile(goModPath, goMod, 0755)
+ i.NoErr(err)
+
+ tests := []struct {
+ name string
+ options Options
+ stdout string
+ expectedBindings string
+ wantErr bool
+ }{
+ {
+ name: "should generate standard bindings with no user tags",
+ options: Options{
+ ProjectDirectory: projectDir,
+ Compiler: "go",
+ GoModTidy: true,
+ },
+ expectedBindings: standardBindings,
+ stdout: "",
+ wantErr: false,
+ },
+ {
+ name: "should generate bindings when given tags",
+ options: Options{
+ ProjectDirectory: projectDir,
+ Compiler: "go",
+ Tags: []string{"test"},
+ GoModTidy: true,
+ },
+ expectedBindings: standardBindings,
+ stdout: "",
+ wantErr: false,
+ },
+ {
+ name: "should generate obfuscated bindings",
+ options: Options{
+ ProjectDirectory: projectDir,
+ Compiler: "go",
+ Tags: []string{"obfuscated"},
+ GoModTidy: true,
+ },
+ expectedBindings: obfuscatedBindings,
+ stdout: "",
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ stdout, err := GenerateBindings(tt.options)
+ i.True((err != nil) == tt.wantErr)
+ i.Equal(stdout, tt.stdout)
+ // Read bindings
+ bindingsFile := filepath.Join(projectDir, "frontend", "wailsjs", "go", "main", "App.js")
+ bindings, err := os.ReadFile(bindingsFile)
+ i.NoErr(err)
+ i.Equal(string(bindings), tt.expectedBindings)
+ })
+ }
+}
diff --git a/v2/pkg/commands/build/base.go b/v2/pkg/commands/build/base.go
new file mode 100644
index 000000000..239932ce8
--- /dev/null
+++ b/v2/pkg/commands/build/base.go
@@ -0,0 +1,612 @@
+package build
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+
+ "github.com/pterm/pterm"
+
+ "github.com/wailsapp/wails/v2/internal/system"
+
+ "github.com/leaanthony/gosod"
+ "github.com/wailsapp/wails/v2/internal/frontend/runtime/wrapper"
+
+ "github.com/pkg/errors"
+
+ "github.com/leaanthony/slicer"
+ "github.com/wailsapp/wails/v2/internal/fs"
+ "github.com/wailsapp/wails/v2/internal/project"
+ "github.com/wailsapp/wails/v2/internal/shell"
+ "github.com/wailsapp/wails/v2/pkg/clilogger"
+)
+
+const (
+ VERBOSE int = 2
+)
+
+// BaseBuilder is the common builder struct
+type BaseBuilder struct {
+ filesToDelete slicer.StringSlicer
+ projectData *project.Project
+ options *Options
+}
+
+// NewBaseBuilder creates a new BaseBuilder
+func NewBaseBuilder(options *Options) *BaseBuilder {
+ result := &BaseBuilder{
+ options: options,
+ }
+ return result
+}
+
+// SetProjectData sets the project data for this builder
+func (b *BaseBuilder) SetProjectData(projectData *project.Project) {
+ b.projectData = projectData
+}
+
+func (b *BaseBuilder) addFileToDelete(filename string) {
+ if !b.options.KeepAssets {
+ b.filesToDelete.Add(filename)
+ }
+}
+
+func (b *BaseBuilder) fileExists(path string) bool {
+ // if file doesn't exist, ignore
+ _, err := os.Stat(path)
+ if err != nil {
+ return !os.IsNotExist(err)
+ }
+ return true
+}
+
+func (b *BaseBuilder) convertFileToIntegerString(filename string) (string, error) {
+ rawData, err := os.ReadFile(filename)
+ if err != nil {
+ return "", err
+ }
+ return b.convertByteSliceToIntegerString(rawData), nil
+}
+
+func (b *BaseBuilder) convertByteSliceToIntegerString(data []byte) string {
+ // Create string builder
+ var result strings.Builder
+
+ if len(data) > 0 {
+
+ // Loop over all but 1 bytes
+ for i := 0; i < len(data)-1; i++ {
+ result.WriteString(fmt.Sprintf("%v,", data[i]))
+ }
+
+ result.WriteString(strconv.FormatUint(uint64(data[len(data)-1]), 10))
+ }
+
+ return result.String()
+}
+
+// CleanUp does post-build housekeeping
+func (b *BaseBuilder) CleanUp() {
+ // Delete all the files
+ b.filesToDelete.Each(func(filename string) {
+ // if file doesn't exist, ignore
+ if !b.fileExists(filename) {
+ return
+ }
+
+ // Delete file. We ignore errors because these files will be overwritten
+ // by the next build anyway.
+ _ = os.Remove(filename)
+ })
+}
+
+func commandPrettifier(args []string) string {
+ // If we have a single argument, just return it
+ if len(args) == 1 {
+ return args[0]
+ }
+ // If an argument contains a space, quote it
+ for i, arg := range args {
+ if strings.Contains(arg, " ") {
+ args[i] = fmt.Sprintf("\"%s\"", arg)
+ }
+ }
+ return strings.Join(args, " ")
+}
+
+func (b *BaseBuilder) OutputFilename(options *Options) string {
+ outputFile := options.OutputFile
+ if outputFile == "" {
+ target := strings.TrimSuffix(b.projectData.OutputFilename, ".exe")
+ if b.projectData.OutputType != "desktop" {
+ target += "-" + b.projectData.OutputType
+ }
+ // If we aren't using the standard compiler, add it to the filename
+ if options.Compiler != "go" {
+ // Parse the `go version` output. EG: `go version go1.16 windows/amd64`
+ stdout, _, err := shell.RunCommand(".", options.Compiler, "version")
+ if err != nil {
+ return ""
+ }
+ versionSplit := strings.Split(stdout, " ")
+ if len(versionSplit) == 4 {
+ target += "-" + versionSplit[2]
+ }
+ }
+ switch b.options.Platform {
+ case "windows":
+ outputFile = target + ".exe"
+ case "darwin", "linux":
+ if b.options.Arch == "" {
+ b.options.Arch = runtime.GOARCH
+ }
+ outputFile = fmt.Sprintf("%s-%s-%s", target, b.options.Platform, b.options.Arch)
+ }
+
+ }
+ return outputFile
+}
+
+// CompileProject compiles the project
+func (b *BaseBuilder) CompileProject(options *Options) error {
+ // Check if the runtime wrapper exists
+ err := generateRuntimeWrapper(options)
+ if err != nil {
+ return err
+ }
+
+ verbose := options.Verbosity == VERBOSE
+ // Run go mod tidy first
+ if !options.SkipModTidy {
+ cmd := exec.Command(options.Compiler, "mod", "tidy")
+ cmd.Stderr = os.Stderr
+ if verbose {
+ println("")
+ cmd.Stdout = os.Stdout
+ }
+ err = cmd.Run()
+ if err != nil {
+ return err
+ }
+ }
+
+ commands := slicer.String()
+
+ compiler := options.Compiler
+ if options.Obfuscated {
+ if !shell.CommandExists("garble") {
+ return fmt.Errorf("the 'garble' command was not found. Please install it with `go install mvdan.cc/garble@latest`")
+ } else {
+ compiler = "garble"
+ if options.GarbleArgs != "" {
+ commands.AddSlice(strings.Split(options.GarbleArgs, " "))
+ }
+ options.UserTags = append(options.UserTags, "obfuscated")
+ }
+ }
+
+ // Default go build command
+ commands.Add("build")
+
+ commands.Add("-buildvcs=false")
+
+ // Add better debugging flags
+ if options.Mode == Dev || options.Mode == Debug {
+ commands.Add("-gcflags")
+ commands.Add("all=-N -l")
+ }
+
+ if options.ForceBuild {
+ commands.Add("-a")
+ }
+
+ if options.TrimPath {
+ commands.Add("-trimpath")
+ }
+
+ if options.RaceDetector {
+ commands.Add("-race")
+ }
+
+ var tags slicer.StringSlicer
+ tags.Add(options.OutputType)
+ tags.AddSlice(options.UserTags)
+
+ // Add webview2 strategy if we have it
+ if options.WebView2Strategy != "" {
+ tags.Add(options.WebView2Strategy)
+ }
+
+ if options.Mode == Production || options.Mode == Debug {
+ tags.Add("production")
+ }
+ // This mode allows you to debug a production build (not dev build)
+ if options.Mode == Debug {
+ tags.Add("debug")
+ }
+
+ // This options allows you to enable devtools in production build (not dev build as it's always enabled there)
+ if options.Devtools {
+ tags.Add("devtools")
+ }
+
+ if options.Obfuscated {
+ tags.Add("obfuscated")
+ }
+
+ tags.Deduplicate()
+
+ // Add the output type build tag
+ commands.Add("-tags")
+ commands.Add(tags.Join(","))
+
+ // LDFlags
+ ldflags := slicer.String()
+ if options.LDFlags != "" {
+ ldflags.Add(options.LDFlags)
+ }
+
+ if options.Mode == Production {
+ ldflags.Add("-w", "-s")
+ if options.Platform == "windows" && !options.WindowsConsole {
+ ldflags.Add("-H windowsgui")
+ }
+ }
+
+ ldflags.Deduplicate()
+
+ if ldflags.Length() > 0 {
+ commands.Add("-ldflags")
+ commands.Add(ldflags.Join(" "))
+ }
+
+ // Get application build directory
+ appDir := options.BinDirectory
+ if options.CleanBinDirectory {
+ err = cleanBinDirectory(options)
+ if err != nil {
+ return err
+ }
+ }
+
+ // Set up output filename
+ outputFile := b.OutputFilename(options)
+ compiledBinary := filepath.Join(appDir, outputFile)
+ commands.Add("-o")
+ commands.Add(compiledBinary)
+
+ options.CompiledBinary = compiledBinary
+
+ // Build the application
+ cmd := exec.Command(compiler, commands.AsSlice()...)
+ cmd.Stderr = os.Stderr
+ if verbose {
+ pterm.Info.Println("Build command:", compiler, commandPrettifier(commands.AsSlice()))
+ cmd.Stdout = os.Stdout
+ }
+ // Set the directory
+ cmd.Dir = b.projectData.Path
+
+ // Add CGO flags
+ // TODO: Remove this as we don't generate headers any more
+ // We use the project/build dir as a temporary place for our generated c headers
+ buildBaseDir, err := fs.RelativeToCwd("build")
+ if err != nil {
+ return err
+ }
+
+ cmd.Env = os.Environ() // inherit env
+
+ if options.Platform != "windows" {
+ // Use shell.UpsertEnv so we don't overwrite user's CGO_CFLAGS
+ cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_CFLAGS", func(v string) string {
+ if options.Platform == "darwin" {
+ if v != "" {
+ v += " "
+ }
+ if !strings.Contains(v, "-mmacosx-version-min") {
+ v += "-mmacosx-version-min=10.13"
+ }
+ }
+ return v
+ })
+ // Use shell.UpsertEnv so we don't overwrite user's CGO_CXXFLAGS
+ cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_CXXFLAGS", func(v string) string {
+ if v != "" {
+ v += " "
+ }
+ v += "-I" + buildBaseDir
+ return v
+ })
+
+ cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_ENABLED", func(v string) string {
+ return "1"
+ })
+ if options.Platform == "darwin" {
+ // Determine version so we can link to newer frameworks
+ // Why doesn't CGO have this option?!?!
+ info, err := system.GetInfo()
+ if err != nil {
+ return err
+ }
+ versionSplit := strings.Split(info.OS.Version, ".")
+ majorVersion, err := strconv.Atoi(versionSplit[0])
+ if err != nil {
+ return err
+ }
+ addUTIFramework := majorVersion >= 11
+ // Set the minimum Mac SDK to 10.13
+ cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_LDFLAGS", func(v string) string {
+ if v != "" {
+ v += " "
+ }
+ if addUTIFramework {
+ v += "-framework UniformTypeIdentifiers "
+ }
+ if !strings.Contains(v, "-mmacosx-version-min") {
+ v += "-mmacosx-version-min=10.13"
+ }
+
+ return v
+ })
+ }
+ }
+
+ cmd.Env = shell.UpsertEnv(cmd.Env, "GOOS", func(v string) string {
+ return options.Platform
+ })
+
+ cmd.Env = shell.UpsertEnv(cmd.Env, "GOARCH", func(v string) string {
+ return options.Arch
+ })
+
+ if verbose {
+ printBulletPoint("Environment:", strings.Join(cmd.Env, " "))
+ }
+
+ // Run command
+ err = cmd.Run()
+ cmd.Stderr = os.Stderr
+
+ // Format error if we have one
+ if err != nil {
+ if options.Platform == "darwin" {
+ output, _ := cmd.CombinedOutput()
+ stdErr := string(output)
+ if strings.Contains(err.Error(), "ld: framework not found UniformTypeIdentifiers") ||
+ strings.Contains(stdErr, "ld: framework not found UniformTypeIdentifiers") {
+ pterm.Warning.Println(`
+NOTE: It would appear that you do not have the latest Xcode cli tools installed.
+Please reinstall by doing the following:
+ 1. Remove the current installation located at "xcode-select -p", EG: sudo rm -rf /Library/Developer/CommandLineTools
+ 2. Install latest Xcode tools: xcode-select --install`)
+ }
+ }
+ return err
+ }
+
+ if !options.Compress {
+ return nil
+ }
+
+ printBulletPoint("Compressing application: ")
+
+ // Do we have upx installed?
+ if !shell.CommandExists("upx") {
+ pterm.Warning.Println("Warning: Cannot compress binary: upx not found")
+ return nil
+ }
+
+ args := []string{"--best", "--no-color", "--no-progress", options.CompiledBinary}
+
+ if options.CompressFlags != "" {
+ args = strings.Split(options.CompressFlags, " ")
+ args = append(args, options.CompiledBinary)
+ }
+
+ if verbose {
+ pterm.Info.Println("upx", strings.Join(args, " "))
+ }
+
+ output, err := exec.Command("upx", args...).Output()
+ if err != nil {
+ return errors.Wrap(err, "Error during compression:")
+ }
+ pterm.Println("Done.")
+ if verbose {
+ pterm.Info.Println(string(output))
+ }
+
+ return nil
+}
+
+func generateRuntimeWrapper(options *Options) error {
+ if options.WailsJSDir == "" {
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ options.WailsJSDir = filepath.Join(cwd, "frontend")
+ }
+ wrapperDir := filepath.Join(options.WailsJSDir, "wailsjs", "runtime")
+ _ = os.RemoveAll(wrapperDir)
+ extractor := gosod.New(wrapper.RuntimeWrapper)
+ err := extractor.Extract(wrapperDir, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// NpmInstall runs "npm install" in the given directory
+func (b *BaseBuilder) NpmInstall(sourceDir string, verbose bool) error {
+ return b.NpmInstallUsingCommand(sourceDir, "npm install", verbose)
+}
+
+// NpmInstallUsingCommand runs the given install command in the specified npm project directory
+func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand string, verbose bool) error {
+ packageJSON := filepath.Join(sourceDir, "package.json")
+
+ // Check package.json exists
+ if !fs.FileExists(packageJSON) {
+ // No package.json, no install
+ return nil
+ }
+
+ install := false
+
+ // Get the MD5 sum of package.json
+ packageJSONMD5 := fs.MustMD5File(packageJSON)
+
+ // Check whether we need to npm install
+ packageChecksumFile := filepath.Join(sourceDir, "package.json.md5")
+ if fs.FileExists(packageChecksumFile) {
+ // Compare checksums
+ storedChecksum := fs.MustLoadString(packageChecksumFile)
+ if storedChecksum != packageJSONMD5 {
+ fs.MustWriteString(packageChecksumFile, packageJSONMD5)
+ install = true
+ }
+ } else {
+ install = true
+ fs.MustWriteString(packageChecksumFile, packageJSONMD5)
+ }
+
+ // Install if node_modules doesn't exist
+ nodeModulesDir := filepath.Join(sourceDir, "node_modules")
+ if !fs.DirExists(nodeModulesDir) {
+ install = true
+ }
+
+ // check if forced install
+ if b.options.ForceBuild {
+ install = true
+ }
+
+ // Shortcut installation
+ if !install {
+ if verbose {
+ pterm.Println("Skipping npm install")
+ }
+ return nil
+ }
+
+ // Split up the InstallCommand and execute it
+ cmd := strings.Split(installCommand, " ")
+ stdout, stderr, err := shell.RunCommand(sourceDir, cmd[0], cmd[1:]...)
+ if verbose || err != nil {
+ for _, l := range strings.Split(stdout, "\n") {
+ pterm.Printf(" %s\n", l)
+ }
+ for _, l := range strings.Split(stderr, "\n") {
+ pterm.Printf(" %s\n", l)
+ }
+ }
+
+ return err
+}
+
+// NpmRun executes the npm target in the provided directory
+func (b *BaseBuilder) NpmRun(projectDir, buildTarget string, verbose bool) error {
+ stdout, stderr, err := shell.RunCommand(projectDir, "npm", "run", buildTarget)
+ if verbose || err != nil {
+ for _, l := range strings.Split(stdout, "\n") {
+ pterm.Printf(" %s\n", l)
+ }
+ for _, l := range strings.Split(stderr, "\n") {
+ pterm.Printf(" %s\n", l)
+ }
+ }
+ return err
+}
+
+// NpmRunWithEnvironment executes the npm target in the provided directory, with the given environment variables
+func (b *BaseBuilder) NpmRunWithEnvironment(projectDir, buildTarget string, verbose bool, envvars []string) error {
+ cmd := shell.CreateCommand(projectDir, "npm", "run", buildTarget)
+ cmd.Env = append(os.Environ(), envvars...)
+ var stdo, stde bytes.Buffer
+ cmd.Stdout = &stdo
+ cmd.Stderr = &stde
+ err := cmd.Run()
+ if verbose || err != nil {
+ for _, l := range strings.Split(stdo.String(), "\n") {
+ pterm.Printf(" %s\n", l)
+ }
+ for _, l := range strings.Split(stde.String(), "\n") {
+ pterm.Printf(" %s\n", l)
+ }
+ }
+ return err
+}
+
+// BuildFrontend executes the `npm build` command for the frontend directory
+func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error {
+ verbose := b.options.Verbosity == VERBOSE
+
+ frontendDir := b.projectData.GetFrontendDir()
+ if !fs.DirExists(frontendDir) {
+ return fmt.Errorf("frontend directory '%s' does not exist", frontendDir)
+ }
+
+ // Check there is an 'InstallCommand' provided in wails.json
+ installCommand := b.projectData.InstallCommand
+ if b.projectData.OutputType == "dev" {
+ installCommand = b.projectData.GetDevInstallerCommand()
+ }
+ if installCommand == "" {
+ // No - don't install
+ printBulletPoint("No Install command. Skipping.")
+ pterm.Println("")
+ } else {
+ // Do install if needed
+ printBulletPoint("Installing frontend dependencies: ")
+ if verbose {
+ pterm.Println("")
+ pterm.Info.Println("Install command: '" + installCommand + "'")
+ }
+ if err := b.NpmInstallUsingCommand(frontendDir, installCommand, verbose); err != nil {
+ return err
+ }
+ outputLogger.Println("Done.")
+ }
+
+ // Check if there is a build command
+ buildCommand := b.projectData.BuildCommand
+ if b.projectData.OutputType == "dev" {
+ buildCommand = b.projectData.GetDevBuildCommand()
+ }
+ if buildCommand == "" {
+ printBulletPoint("No Build command. Skipping.")
+ pterm.Println("")
+ // No - ignore
+ return nil
+ }
+
+ printBulletPoint("Compiling frontend: ")
+ cmd := strings.Split(buildCommand, " ")
+ if verbose {
+ pterm.Println("")
+ pterm.Info.Println("Build command: '" + buildCommand + "'")
+ }
+ stdout, stderr, err := shell.RunCommand(frontendDir, cmd[0], cmd[1:]...)
+ if verbose || err != nil {
+ for _, l := range strings.Split(stdout, "\n") {
+ pterm.Printf(" %s\n", l)
+ }
+ for _, l := range strings.Split(stderr, "\n") {
+ pterm.Printf(" %s\n", l)
+ }
+ }
+ if err != nil {
+ return err
+ }
+
+ pterm.Println("Done.")
+ return nil
+}
diff --git a/v2/pkg/commands/build/base_test.go b/v2/pkg/commands/build/base_test.go
new file mode 100644
index 000000000..3b48b24b6
--- /dev/null
+++ b/v2/pkg/commands/build/base_test.go
@@ -0,0 +1,34 @@
+package build
+
+import "testing"
+
+func Test_commandPrettifier(t *testing.T) {
+ tests := []struct {
+ name string
+ input []string
+ want string
+ }{
+ {
+ name: "empty",
+ input: []string{},
+ want: "",
+ },
+ {
+ name: "one arg",
+ input: []string{"one"},
+ want: "one",
+ },
+ {
+ name: "args where one has spaces",
+ input: []string{"one", "two three"},
+ want: `one "two three"`,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := commandPrettifier(tt.input); got != tt.want {
+ t.Errorf("commandPrettifier() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/v2/pkg/commands/build/build.go b/v2/pkg/commands/build/build.go
new file mode 100644
index 000000000..7263f63ae
--- /dev/null
+++ b/v2/pkg/commands/build/build.go
@@ -0,0 +1,450 @@
+package build
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/google/shlex"
+ "github.com/pterm/pterm"
+ "github.com/samber/lo"
+
+ "github.com/wailsapp/wails/v2/internal/staticanalysis"
+ "github.com/wailsapp/wails/v2/pkg/commands/bindings"
+
+ "github.com/wailsapp/wails/v2/internal/fs"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+
+ "github.com/wailsapp/wails/v2/internal/project"
+ "github.com/wailsapp/wails/v2/pkg/clilogger"
+)
+
+// Mode is the type used to indicate the build modes
+type Mode int
+
+const (
+ // Dev mode
+ Dev Mode = iota
+ // Production mode
+ Production
+ // Debug build
+ Debug
+)
+
+// Options contains all the build options as well as the project data
+type Options struct {
+ LDFlags string // Optional flags to pass to linker
+ UserTags []string // Tags to pass to the Go compiler
+ Logger *clilogger.CLILogger // All output to the logger
+ OutputType string // EG: desktop, server....
+ Mode Mode // release or dev
+ Devtools bool // Enable devtools in production
+ ProjectData *project.Project // The project data
+ Pack bool // Create a package for the app after building
+ Platform string // The platform to build for
+ Arch string // The architecture to build for
+ Compiler string // The compiler command to use
+ SkipModTidy bool // Skip mod tidy before compile
+ IgnoreFrontend bool // Indicates if the frontend does not need building
+ IgnoreApplication bool // Indicates if the application does not need building
+ OutputFile string // Override the output filename
+ BinDirectory string // Directory to use to write the built applications
+ CleanBinDirectory bool // Indicates if the bin output directory should be cleaned before building
+ CompiledBinary string // Fully qualified path to the compiled binary
+ KeepAssets bool // Keep the generated assets/files
+ Verbosity int // Verbosity level (0 - silent, 1 - default, 2 - verbose)
+ Compress bool // Compress the final binary
+ CompressFlags string // Flags to pass to UPX
+ WebView2Strategy string // WebView2 installer strategy
+ RunDelve bool // Indicates if we should run delve after the build
+ WailsJSDir string // Directory to generate the wailsjs module
+ ForceBuild bool // Force
+ BundleName string // Bundlename for Mac
+ TrimPath bool // Use Go's trimpath compiler flag
+ RaceDetector bool // Build with Go's race detector
+ WindowsConsole bool // Indicates that the windows console should be kept
+ Obfuscated bool // Indicates that bound methods should be obfuscated
+ GarbleArgs string // The arguments for Garble
+ SkipBindings bool // Skip binding generation
+ SkipEmbedCreate bool // Skip creation of embed files
+}
+
+// Build the project!
+func Build(options *Options) (string, error) {
+ // Extract logger
+ outputLogger := options.Logger
+
+ // Get working directory
+ cwd, err := os.Getwd()
+ if err != nil {
+ return "", err
+ }
+
+ // wails js dir
+ options.WailsJSDir = options.ProjectData.GetWailsJSDir()
+
+ // Set build directory
+ options.BinDirectory = filepath.Join(options.ProjectData.GetBuildDir(), "bin")
+
+ // Save the project type
+ options.ProjectData.OutputType = options.OutputType
+
+ // Create builder
+ var builder Builder
+
+ switch options.OutputType {
+ case "desktop":
+ builder = newDesktopBuilder(options)
+ case "dev":
+ builder = newDesktopBuilder(options)
+ default:
+ return "", fmt.Errorf("cannot build assets for output type %s", options.ProjectData.OutputType)
+ }
+
+ // Set up our clean up method
+ defer builder.CleanUp()
+
+ // Initialise Builder
+ builder.SetProjectData(options.ProjectData)
+
+ hookArgs := map[string]string{
+ "${platform}": options.Platform + "/" + options.Arch,
+ }
+
+ for _, hook := range []string{options.Platform + "/" + options.Arch, options.Platform + "/*", "*/*"} {
+ if err := execPreBuildHook(outputLogger, options, hook, hookArgs); err != nil {
+ return "", err
+ }
+ }
+
+ // Create embed directories if they don't exist
+ if !options.SkipEmbedCreate {
+ if err := CreateEmbedDirectories(cwd, options); err != nil {
+ return "", err
+ }
+ }
+
+ // Generate bindings
+ if !options.SkipBindings {
+ err = GenerateBindings(options)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ if !options.IgnoreFrontend {
+ err = builder.BuildFrontend(outputLogger)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ compileBinary := ""
+ if !options.IgnoreApplication {
+ compileBinary, err = execBuildApplication(builder, options)
+ if err != nil {
+ return "", err
+ }
+
+ hookArgs["${bin}"] = compileBinary
+ for _, hook := range []string{options.Platform + "/" + options.Arch, options.Platform + "/*", "*/*"} {
+ if err := execPostBuildHook(outputLogger, options, hook, hookArgs); err != nil {
+ return "", err
+ }
+ }
+
+ }
+ return compileBinary, nil
+}
+
+func CreateEmbedDirectories(cwd string, buildOptions *Options) error {
+ path := cwd
+ if buildOptions.ProjectData != nil {
+ path = buildOptions.ProjectData.Path
+ }
+ embedDetails, err := staticanalysis.GetEmbedDetails(path)
+ if err != nil {
+ return err
+ }
+
+ for _, embedDetail := range embedDetails {
+ fullPath := embedDetail.GetFullPath()
+ // assumes path is directory only if it has no extension
+ if filepath.Ext(fullPath) == "" {
+ if _, err := os.Stat(fullPath); os.IsNotExist(err) {
+ err := os.MkdirAll(fullPath, 0o755)
+ if err != nil {
+ return err
+ }
+ f, err := os.Create(filepath.Join(fullPath, "gitkeep"))
+ if err != nil {
+ return err
+ }
+ _ = f.Close()
+ }
+ }
+ }
+
+ return nil
+}
+
+func fatal(message string) {
+ printer := pterm.PrefixPrinter{
+ MessageStyle: &pterm.ThemeDefault.FatalMessageStyle,
+ Prefix: pterm.Prefix{
+ Style: &pterm.ThemeDefault.FatalPrefixStyle,
+ Text: " FATAL ",
+ },
+ }
+ printer.Println(message)
+ os.Exit(1)
+}
+
+func printBulletPoint(text string, args ...any) {
+ item := pterm.BulletListItem{
+ Level: 2,
+ Text: text,
+ }
+ t, err := pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{item}).Srender()
+ if err != nil {
+ fatal(err.Error())
+ }
+ t = strings.Trim(t, "\n\r")
+ pterm.Printf(t, args...)
+}
+
+func GenerateBindings(buildOptions *Options) error {
+ obfuscated := buildOptions.Obfuscated
+ if obfuscated {
+ printBulletPoint("Generating obfuscated bindings: ")
+ buildOptions.UserTags = append(buildOptions.UserTags, "obfuscated")
+ } else {
+ printBulletPoint("Generating bindings: ")
+ }
+
+ if buildOptions.ProjectData.Bindings.TsGeneration.OutputType == "" {
+ buildOptions.ProjectData.Bindings.TsGeneration.OutputType = "classes"
+ }
+
+ // Generate Bindings
+ output, err := bindings.GenerateBindings(bindings.Options{
+ Compiler: buildOptions.Compiler,
+ Tags: buildOptions.UserTags,
+ GoModTidy: !buildOptions.SkipModTidy,
+ TsPrefix: buildOptions.ProjectData.Bindings.TsGeneration.Prefix,
+ TsSuffix: buildOptions.ProjectData.Bindings.TsGeneration.Suffix,
+ TsOutputType: buildOptions.ProjectData.Bindings.TsGeneration.OutputType,
+ })
+ if err != nil {
+ return err
+ }
+
+ if buildOptions.Verbosity == VERBOSE {
+ pterm.Info.Println(output)
+ }
+
+ pterm.Println("Done.")
+
+ return nil
+}
+
+func execBuildApplication(builder Builder, options *Options) (string, error) {
+ // If we are building for windows, we will need to generate the asset bundle before
+ // compilation. This will be a .syso file in the project root
+ if options.Pack && options.Platform == "windows" {
+ printBulletPoint("Generating application assets: ")
+ err := packageApplicationForWindows(options)
+ if err != nil {
+ return "", err
+ }
+ pterm.Println("Done.")
+
+ // When we finish, we will want to remove the syso file
+ defer func() {
+ err := os.Remove(filepath.Join(options.ProjectData.Path, strings.ReplaceAll(options.ProjectData.Name, " ", "_")+"-res.syso"))
+ if err != nil {
+ fatal(err.Error())
+ }
+ }()
+ }
+
+ // Compile the application
+ printBulletPoint("Compiling application: ")
+
+ if options.Platform == "darwin" && options.Arch == "universal" {
+ outputFile := builder.OutputFilename(options)
+ amd64Filename := outputFile + "-amd64"
+ arm64Filename := outputFile + "-arm64"
+
+ // Build amd64 first
+ options.Arch = "amd64"
+ options.OutputFile = amd64Filename
+ options.CleanBinDirectory = false
+ if options.Verbosity == VERBOSE {
+ pterm.Println("Building AMD64 Target: " + filepath.Join(options.BinDirectory, options.OutputFile))
+ }
+ err := builder.CompileProject(options)
+ if err != nil {
+ return "", err
+ }
+ // Build arm64
+ options.Arch = "arm64"
+ options.OutputFile = arm64Filename
+ options.CleanBinDirectory = false
+ if options.Verbosity == VERBOSE {
+ pterm.Println("Building ARM64 Target: " + filepath.Join(options.BinDirectory, options.OutputFile))
+ }
+ err = builder.CompileProject(options)
+
+ if err != nil {
+ return "", err
+ }
+ // Run lipo
+ if options.Verbosity == VERBOSE {
+ pterm.Println(fmt.Sprintf("Running lipo: lipo -create -output %s %s %s", outputFile, amd64Filename, arm64Filename))
+ }
+ _, stderr, err := shell.RunCommand(options.BinDirectory, "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename)
+ if err != nil {
+ return "", fmt.Errorf("%s - %s", err.Error(), stderr)
+ }
+ // Remove temp binaries
+ err = fs.DeleteFile(filepath.Join(options.BinDirectory, amd64Filename))
+ if err != nil {
+ return "", err
+ }
+ err = fs.DeleteFile(filepath.Join(options.BinDirectory, arm64Filename))
+ if err != nil {
+ return "", err
+ }
+ options.ProjectData.OutputFilename = outputFile
+ options.CompiledBinary = filepath.Join(options.BinDirectory, outputFile)
+ } else {
+ err := builder.CompileProject(options)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ if runtime.GOOS == "darwin" {
+ // Remove quarantine attribute
+ if _, err := os.Stat(options.CompiledBinary); os.IsNotExist(err) {
+ return "", fmt.Errorf("compiled binary does not exist at path: %s", options.CompiledBinary)
+ }
+ stdout, stderr, err := shell.RunCommand(options.BinDirectory, "/usr/bin/xattr", "-rc", options.CompiledBinary)
+ if err != nil {
+ return "", fmt.Errorf("%s - %s", err.Error(), stderr)
+ }
+ if options.Verbosity == VERBOSE && stdout != "" {
+ pterm.Info.Println(stdout)
+ }
+ }
+
+ pterm.Println("Done.")
+
+ // Do we need to pack the app for non-windows?
+ if options.Pack && options.Platform != "windows" {
+
+ printBulletPoint("Packaging application: ")
+
+ // TODO: Allow cross platform build
+ err := packageProject(options, runtime.GOOS)
+ if err != nil {
+ return "", err
+ }
+ pterm.Println("Done.")
+ }
+
+ if options.Platform == "windows" {
+ const nativeWebView2Loader = "native_webview2loader"
+
+ tags := options.UserTags
+ if lo.Contains(tags, nativeWebView2Loader) {
+ message := "You are using the legacy native WebView2Loader. This loader will be deprecated in the near future. Please report any bugs related to the new loader: https://github.com/wailsapp/wails/issues/2004"
+ pterm.Warning.Println(message)
+ } else {
+ tags = append(tags, nativeWebView2Loader)
+ message := fmt.Sprintf("Wails is now using the new Go WebView2Loader. If you encounter any issues with it, please report them to https://github.com/wailsapp/wails/issues/2004. You could also use the old legacy loader with `-tags %s`, but keep in mind this will be deprecated in the near future.", strings.Join(tags, ","))
+ pterm.Info.Println(message)
+ }
+ }
+
+ if options.Platform == "darwin" && (options.Mode == Debug || options.Devtools) {
+ pterm.Warning.Println("This darwin build contains the use of private APIs. This will not pass Apple's AppStore approval process. Please use it only as a test build for testing and debug purposes.")
+ }
+
+ return options.CompiledBinary, nil
+}
+
+func execPreBuildHook(outputLogger *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string) error {
+ preBuildHook := options.ProjectData.PreBuildHooks[hookIdentifier]
+ if preBuildHook == "" {
+ return nil
+ }
+
+ return executeBuildHook(outputLogger, options, hookIdentifier, argReplacements, preBuildHook, "pre")
+}
+
+func execPostBuildHook(outputLogger *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string) error {
+ postBuildHook := options.ProjectData.PostBuildHooks[hookIdentifier]
+ if postBuildHook == "" {
+ return nil
+ }
+
+ return executeBuildHook(outputLogger, options, hookIdentifier, argReplacements, postBuildHook, "post")
+}
+
+func executeBuildHook(_ *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string, buildHook string, hookName string) error {
+ if !options.ProjectData.RunNonNativeBuildHooks {
+ if hookIdentifier == "" {
+ // That's the global hook
+ } else {
+ platformOfHook := strings.Split(hookIdentifier, "/")[0]
+ if platformOfHook == "*" {
+ // That's OK, we don't have a specific platform of the hook
+ } else if platformOfHook == runtime.GOOS {
+ // The hook is for host platform
+ } else {
+ // Skip a hook which is not native
+ printBulletPoint(fmt.Sprintf("Non native build hook '%s': Skipping.", hookIdentifier))
+ return nil
+ }
+ }
+ }
+
+ printBulletPoint("Executing %s build hook '%s': ", hookName, hookIdentifier)
+ args, err := shlex.Split(buildHook)
+ if err != nil {
+ return fmt.Errorf("could not parse %s build hook command: %w", hookName, err)
+ }
+ for i, arg := range args {
+ newArg := argReplacements[arg]
+ if newArg == "" {
+ continue
+ }
+ args[i] = newArg
+ }
+
+ if options.Verbosity == VERBOSE {
+ pterm.Info.Println(strings.Join(args, " "))
+ }
+
+ if !fs.DirExists(options.BinDirectory) {
+ if err := fs.MkDirs(options.BinDirectory); err != nil {
+ return fmt.Errorf("could not create target directory: %s", err.Error())
+ }
+ }
+
+ stdout, stderr, err := shell.RunCommand(options.BinDirectory, args[0], args[1:]...)
+ if options.Verbosity == VERBOSE {
+ pterm.Info.Println(stdout)
+ }
+ if err != nil {
+ return fmt.Errorf("%s - %s", err.Error(), stderr)
+ }
+ pterm.Println("Done.")
+
+ return nil
+}
diff --git a/v2/pkg/commands/build/builder.go b/v2/pkg/commands/build/builder.go
new file mode 100644
index 000000000..6a220c530
--- /dev/null
+++ b/v2/pkg/commands/build/builder.go
@@ -0,0 +1,15 @@
+package build
+
+import (
+ "github.com/wailsapp/wails/v2/internal/project"
+ "github.com/wailsapp/wails/v2/pkg/clilogger"
+)
+
+// Builder defines a builder that can build Wails applications
+type Builder interface {
+ SetProjectData(projectData *project.Project)
+ BuildFrontend(logger *clilogger.CLILogger) error
+ CompileProject(options *Options) error
+ OutputFilename(options *Options) string
+ CleanUp()
+}
diff --git a/v2/pkg/commands/build/desktop.go b/v2/pkg/commands/build/desktop.go
new file mode 100644
index 000000000..c54eb3035
--- /dev/null
+++ b/v2/pkg/commands/build/desktop.go
@@ -0,0 +1,12 @@
+package build
+
+// DesktopBuilder builds applications for the desktop
+type DesktopBuilder struct {
+ *BaseBuilder
+}
+
+func newDesktopBuilder(options *Options) *DesktopBuilder {
+ return &DesktopBuilder{
+ BaseBuilder: NewBaseBuilder(options),
+ }
+}
diff --git a/v2/pkg/commands/build/internal/packager/darwin/Info.plist b/v2/pkg/commands/build/internal/packager/darwin/Info.plist
new file mode 100644
index 000000000..9736913ae
--- /dev/null
+++ b/v2/pkg/commands/build/internal/packager/darwin/Info.plist
@@ -0,0 +1,14 @@
+
+
+ CFBundlePackageTypeAPPL
+ CFBundleName{{.Title}}
+ CFBundleExecutable{{.Title}}
+ CFBundleIdentifiercom.wails.{{.Title}}
+ CFBundleVersion1.0.0
+ CFBundleGetInfoStringBuilt using Wails (https://wails.io)
+ CFBundleShortVersionString1.0.0
+ CFBundleIconFileiconfile
+ LSMinimumSystemVersion10.13.0
+ NSHighResolutionCapabletrue
+ NSHumanReadableCopyrightCopyright.........
+
\ No newline at end of file
diff --git a/v2/pkg/commands/build/internal/packager/icon1024.png b/v2/pkg/commands/build/internal/packager/icon1024.png
new file mode 100644
index 000000000..a3ad26ce7
Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon1024.png differ
diff --git a/v2/pkg/commands/build/internal/packager/icon128.png b/v2/pkg/commands/build/internal/packager/icon128.png
new file mode 100644
index 000000000..ea74d31b5
Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon128.png differ
diff --git a/v2/pkg/commands/build/internal/packager/icon256.png b/v2/pkg/commands/build/internal/packager/icon256.png
new file mode 100644
index 000000000..d7031f3a6
Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon256.png differ
diff --git a/v2/pkg/commands/build/internal/packager/icon32.png b/v2/pkg/commands/build/internal/packager/icon32.png
new file mode 100644
index 000000000..231136baa
Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon32.png differ
diff --git a/v2/pkg/commands/build/internal/packager/icon512.png b/v2/pkg/commands/build/internal/packager/icon512.png
new file mode 100644
index 000000000..53c612c7b
Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon512.png differ
diff --git a/v2/pkg/commands/build/internal/packager/icon64.png b/v2/pkg/commands/build/internal/packager/icon64.png
new file mode 100644
index 000000000..a2b304154
Binary files /dev/null and b/v2/pkg/commands/build/internal/packager/icon64.png differ
diff --git a/v2/pkg/commands/build/internal/packager/linux/app.desktop b/v2/pkg/commands/build/internal/packager/linux/app.desktop
new file mode 100644
index 000000000..59f0456d2
--- /dev/null
+++ b/v2/pkg/commands/build/internal/packager/linux/app.desktop
@@ -0,0 +1,6 @@
+[Desktop Entry]
+Type=Application
+Exec={{.Name}}
+Name={{.Name}}
+Icon={{.Name}}
+Categories=Utility;
\ No newline at end of file
diff --git a/v2/pkg/commands/build/nsis_installer.go b/v2/pkg/commands/build/nsis_installer.go
new file mode 100644
index 000000000..820df2d1d
--- /dev/null
+++ b/v2/pkg/commands/build/nsis_installer.go
@@ -0,0 +1,119 @@
+package build
+
+import (
+ "fmt"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/fs"
+ "github.com/wailsapp/wails/v2/internal/shell"
+ "github.com/wailsapp/wails/v2/internal/webview2runtime"
+ "github.com/wailsapp/wails/v2/pkg/buildassets"
+)
+
+const (
+ nsisTypeSingle = "single"
+ nsisTypeMultiple = "multiple"
+
+ nsisFolder = "windows/installer"
+ nsisProjectFile = "project.nsi"
+ nsisToolsFile = "wails_tools.nsh"
+ nsisWebView2SetupFile = "tmp/MicrosoftEdgeWebview2Setup.exe"
+)
+
+func GenerateNSISInstaller(options *Options, amd64Binary string, arm64Binary string) error {
+ outputLogger := options.Logger
+ outputLogger.Println("Creating NSIS installer\n------------------------------")
+
+ // Ensure the file exists, if not the template will be written.
+ projectFile := path.Join(nsisFolder, nsisProjectFile)
+ if _, err := buildassets.ReadFile(options.ProjectData, projectFile); err != nil {
+ return fmt.Errorf("Unable to generate NSIS installer project template: %w", err)
+ }
+
+ // Write the resolved nsis tools
+ toolsFile := path.Join(nsisFolder, nsisToolsFile)
+ if _, err := buildassets.ReadOriginalFileWithProjectDataAndSave(options.ProjectData, toolsFile); err != nil {
+ return fmt.Errorf("Unable to generate NSIS tools file: %w", err)
+ }
+
+ // Write the WebView2 SetupFile
+ webviewSetup := buildassets.GetLocalPath(options.ProjectData, path.Join(nsisFolder, nsisWebView2SetupFile))
+ if dir := filepath.Dir(webviewSetup); !fs.DirExists(dir) {
+ if err := fs.MkDirs(dir, 0o755); err != nil {
+ return err
+ }
+ }
+
+ if err := webview2runtime.WriteInstallerToFile(webviewSetup); err != nil {
+ return fmt.Errorf("Unable to write WebView2 Bootstrapper Setup: %w", err)
+ }
+
+ if !shell.CommandExists("makensis") {
+ outputLogger.Println("Warning: Cannot create installer: makensis not found")
+ return nil
+ }
+
+ nsisType := options.ProjectData.NSISType
+ if nsisType == nsisTypeSingle && (amd64Binary == "" || arm64Binary == "") {
+ nsisType = ""
+ }
+
+ switch nsisType {
+ case "":
+ fallthrough
+ case nsisTypeMultiple:
+ if amd64Binary != "" {
+ if err := makeNSIS(options, "amd64", amd64Binary, ""); err != nil {
+ return err
+ }
+ }
+
+ if arm64Binary != "" {
+ if err := makeNSIS(options, "arm64", "", arm64Binary); err != nil {
+ return err
+ }
+ }
+
+ case nsisTypeSingle:
+ if err := makeNSIS(options, "single", amd64Binary, arm64Binary); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("Unsupported nsisType: %s", nsisType)
+ }
+
+ return nil
+}
+
+func makeNSIS(options *Options, installerKind string, amd64Binary string, arm64Binary string) error {
+ verbose := options.Verbosity == VERBOSE
+ outputLogger := options.Logger
+
+ outputLogger.Print(" - Building '%s' installer: ", installerKind)
+ args := []string{}
+ if amd64Binary != "" {
+ args = append(args, "-DARG_WAILS_AMD64_BINARY="+amd64Binary)
+ }
+ if arm64Binary != "" {
+ args = append(args, "-DARG_WAILS_ARM64_BINARY="+arm64Binary)
+ }
+ args = append(args, nsisProjectFile)
+
+ if verbose {
+ outputLogger.Println("makensis %s", strings.Join(args, " "))
+ }
+
+ installerDir := buildassets.GetLocalPath(options.ProjectData, nsisFolder)
+ stdOut, stdErr, err := shell.RunCommand(installerDir, "makensis", args...)
+ if err != nil || verbose {
+ outputLogger.Println(stdOut)
+ outputLogger.Println(stdErr)
+ }
+ if err != nil {
+ return fmt.Errorf("Error during creation of the installer: %w", err)
+ }
+ outputLogger.Println("Done.")
+ return nil
+}
diff --git a/v2/pkg/commands/build/packager.go b/v2/pkg/commands/build/packager.go
new file mode 100644
index 000000000..d406256f9
--- /dev/null
+++ b/v2/pkg/commands/build/packager.go
@@ -0,0 +1,301 @@
+package build
+
+import (
+ "bytes"
+ "fmt"
+ "image"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/leaanthony/winicon"
+ "github.com/tc-hib/winres"
+ "github.com/tc-hib/winres/version"
+ "github.com/wailsapp/wails/v2/internal/project"
+
+ "github.com/jackmordaunt/icns"
+ "github.com/pkg/errors"
+ "github.com/wailsapp/wails/v2/pkg/buildassets"
+
+ "github.com/wailsapp/wails/v2/internal/fs"
+)
+
+// PackageProject packages the application
+func packageProject(options *Options, platform string) error {
+ var err error
+ switch platform {
+ case "darwin":
+ err = packageApplicationForDarwin(options)
+ case "windows":
+ err = packageApplicationForWindows(options)
+ case "linux":
+ err = packageApplicationForLinux(options)
+ default:
+ err = fmt.Errorf("packing not supported for %s yet", platform)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// cleanBinDirectory will remove an existing bin directory and recreate it
+func cleanBinDirectory(options *Options) error {
+ buildDirectory := options.BinDirectory
+
+ // Clear out old builds
+ if fs.DirExists(buildDirectory) {
+ err := os.RemoveAll(buildDirectory)
+ if err != nil {
+ return err
+ }
+ }
+
+ // Create clean directory
+ err := os.MkdirAll(buildDirectory, 0o700)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func packageApplicationForDarwin(options *Options) error {
+ var err error
+
+ // Create directory structure
+ bundlename := options.BundleName
+ if bundlename == "" {
+ bundlename = options.ProjectData.Name + ".app"
+ }
+
+ contentsDirectory := filepath.Join(options.BinDirectory, bundlename, "/Contents")
+ exeDir := filepath.Join(contentsDirectory, "/MacOS")
+ err = fs.MkDirs(exeDir, 0o755)
+ if err != nil {
+ return err
+ }
+ resourceDir := filepath.Join(contentsDirectory, "/Resources")
+ err = fs.MkDirs(resourceDir, 0o755)
+ if err != nil {
+ return err
+ }
+ // Copy binary
+ packedBinaryPath := filepath.Join(exeDir, options.ProjectData.OutputFilename)
+ err = fs.MoveFile(options.CompiledBinary, packedBinaryPath)
+ if err != nil {
+ return errors.Wrap(err, "Cannot move file: "+options.CompiledBinary)
+ }
+
+ // Generate Info.plist
+ err = processPList(options, contentsDirectory)
+ if err != nil {
+ return err
+ }
+
+ // Generate App Icon
+ err = processDarwinIcon(options.ProjectData, "appicon", resourceDir, "iconfile")
+ if err != nil {
+ return err
+ }
+
+ // Generate FileAssociation Icons
+ for _, fileAssociation := range options.ProjectData.Info.FileAssociations {
+ err = processDarwinIcon(options.ProjectData, fileAssociation.IconName, resourceDir, "")
+ if err != nil {
+ return err
+ }
+ }
+
+ options.CompiledBinary = packedBinaryPath
+
+ return nil
+}
+
+func processPList(options *Options, contentsDirectory string) error {
+ sourcePList := "Info.plist"
+ if options.Mode == Dev {
+ // Use Info.dev.plist if using build mode
+ sourcePList = "Info.dev.plist"
+ }
+
+ // Read the resolved BuildAssets file and copy it to the destination
+ content, err := buildassets.ReadFileWithProjectData(options.ProjectData, "darwin/"+sourcePList)
+ if err != nil {
+ return err
+ }
+
+ targetFile := filepath.Join(contentsDirectory, "Info.plist")
+ return os.WriteFile(targetFile, content, 0o644)
+}
+
+func processDarwinIcon(projectData *project.Project, iconName string, resourceDir string, destIconName string) (err error) {
+ appIcon, err := buildassets.ReadFile(projectData, iconName+".png")
+ if err != nil {
+ return err
+ }
+
+ srcImg, _, err := image.Decode(bytes.NewBuffer(appIcon))
+ if err != nil {
+ return err
+ }
+
+ if destIconName == "" {
+ destIconName = iconName
+ }
+
+ tgtBundle := filepath.Join(resourceDir, destIconName+".icns")
+ dest, err := os.Create(tgtBundle)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ err = dest.Close()
+ if err == nil {
+ return
+ }
+ }()
+ return icns.Encode(dest, srcImg)
+}
+
+func packageApplicationForWindows(options *Options) error {
+ // Generate app icon
+ var err error
+ err = generateIcoFile(options, "appicon", "icon")
+ if err != nil {
+ return err
+ }
+
+ // Generate FileAssociation Icons
+ for _, fileAssociation := range options.ProjectData.Info.FileAssociations {
+ err = generateIcoFile(options, fileAssociation.IconName, "")
+ if err != nil {
+ return err
+ }
+ }
+
+ // Create syso file
+ err = compileResources(options)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func packageApplicationForLinux(_ *Options) error {
+ return nil
+}
+
+func generateIcoFile(options *Options, iconName string, destIconName string) error {
+ content, err := buildassets.ReadFile(options.ProjectData, iconName+".png")
+ if err != nil {
+ return err
+ }
+
+ if destIconName == "" {
+ destIconName = iconName
+ }
+
+ // Check ico file exists already
+ icoFile := buildassets.GetLocalPath(options.ProjectData, "windows/"+destIconName+".ico")
+ if !fs.FileExists(icoFile) {
+ if dir := filepath.Dir(icoFile); !fs.DirExists(dir) {
+ if err := fs.MkDirs(dir, 0o755); err != nil {
+ return err
+ }
+ }
+
+ output, err := os.OpenFile(icoFile, os.O_CREATE|os.O_WRONLY, 0o644)
+ if err != nil {
+ return err
+ }
+ defer output.Close()
+
+ err = winicon.GenerateIcon(bytes.NewBuffer(content), output, []int{256, 128, 64, 48, 32, 16})
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func compileResources(options *Options) error {
+ currentDir, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ defer func() {
+ _ = os.Chdir(currentDir)
+ }()
+ windowsDir := filepath.Join(options.ProjectData.GetBuildDir(), "windows")
+ err = os.Chdir(windowsDir)
+ if err != nil {
+ return err
+ }
+ rs := winres.ResourceSet{}
+ icon := filepath.Join(windowsDir, "icon.ico")
+ iconFile, err := os.Open(icon)
+ if err != nil {
+ return err
+ }
+ defer iconFile.Close()
+ ico, err := winres.LoadICO(iconFile)
+ if err != nil {
+ return fmt.Errorf("couldn't load icon from icon.ico: %w", err)
+ }
+ err = rs.SetIcon(winres.RT_ICON, ico)
+ if err != nil {
+ return err
+ }
+
+ manifestData, err := buildassets.ReadFileWithProjectData(options.ProjectData, "windows/wails.exe.manifest")
+ if err != nil {
+ return err
+ }
+
+ xmlData, err := winres.AppManifestFromXML(manifestData)
+ if err != nil {
+ return err
+ }
+ rs.SetManifest(xmlData)
+
+ versionInfo, err := buildassets.ReadFileWithProjectData(options.ProjectData, "windows/info.json")
+ if err != nil {
+ return err
+ }
+
+ if len(versionInfo) != 0 {
+ var v version.Info
+ if err := v.UnmarshalJSON(versionInfo); err != nil {
+ return err
+ }
+ rs.SetVersionInfo(v)
+ }
+
+ // replace spaces with underscores as go build behaves weirdly with spaces in syso filename
+ targetFile := filepath.Join(options.ProjectData.Path, strings.ReplaceAll(options.ProjectData.Name, " ", "_")+"-res.syso")
+ fout, err := os.Create(targetFile)
+ if err != nil {
+ return err
+ }
+ defer fout.Close()
+
+ archs := map[string]winres.Arch{
+ "amd64": winres.ArchAMD64,
+ "arm64": winres.ArchARM64,
+ "386": winres.ArchI386,
+ }
+ targetArch, supported := archs[options.Arch]
+ if !supported {
+ return fmt.Errorf("arch '%s' not supported", options.Arch)
+ }
+
+ err = rs.WriteObject(fout, targetArch)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/v2/pkg/commands/build/packager_linux.go b/v2/pkg/commands/build/packager_linux.go
new file mode 100644
index 000000000..6b297d3ec
--- /dev/null
+++ b/v2/pkg/commands/build/packager_linux.go
@@ -0,0 +1,5 @@
+package build
+
+func packageApplication(_ *Options) error {
+ return nil
+}
diff --git a/v2/pkg/commands/buildtags/buildtags.go b/v2/pkg/commands/buildtags/buildtags.go
new file mode 100644
index 000000000..5cca16acf
--- /dev/null
+++ b/v2/pkg/commands/buildtags/buildtags.go
@@ -0,0 +1,52 @@
+package buildtags
+
+import (
+ "errors"
+ "strings"
+
+ "github.com/samber/lo"
+)
+
+// Parse parses the given tags string and returns
+// a cleaned slice of strings. Both comma and space delimited
+// tags are supported but not mixed. If mixed, an error is returned.
+func Parse(tags string) ([]string, error) {
+ if tags == "" {
+ return nil, nil
+ }
+
+ separator := ""
+ if strings.Contains(tags, ",") {
+ separator = ","
+ }
+ if strings.Contains(tags, " ") {
+ if separator != "" {
+ return nil, errors.New("cannot use both space and comma separated values with `-tags` flag")
+ }
+ separator = " "
+ }
+ if separator == "" {
+ // We couldn't find any separator, so the whole string is used as user tag
+ // Otherwise we would end up with a list of every single character of the tags string,
+ // e.g.: `t,e,s,t`
+ return []string{tags}, nil
+ }
+
+ var userTags []string
+ for _, tag := range strings.Split(tags, separator) {
+ thisTag := strings.TrimSpace(tag)
+ if thisTag != "" {
+ userTags = append(userTags, thisTag)
+ }
+ }
+ return userTags, nil
+}
+
+// Stringify converts the given tags slice to a string compatible
+// with the go build -tags flag
+func Stringify(tags []string) string {
+ tags = lo.Map(tags, func(tag string, _ int) string {
+ return strings.TrimSpace(tag)
+ })
+ return strings.Join(tags, ",")
+}
diff --git a/v2/pkg/commands/buildtags/buildtags_test.go b/v2/pkg/commands/buildtags/buildtags_test.go
new file mode 100644
index 000000000..c13a158c7
--- /dev/null
+++ b/v2/pkg/commands/buildtags/buildtags_test.go
@@ -0,0 +1,78 @@
+package buildtags
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestParse(t *testing.T) {
+ tests := []struct {
+ name string
+ tags string
+ want []string
+ wantErr bool
+ }{
+ {
+ name: "should support single tags",
+ tags: "test",
+ want: []string{"test"},
+ wantErr: false,
+ },
+ {
+ name: "should support space delimited tags",
+ tags: "test test2",
+ want: []string{"test", "test2"},
+ wantErr: false,
+ },
+ {
+ name: "should support comma delimited tags",
+ tags: "test,test2",
+ want: []string{"test", "test2"},
+ wantErr: false,
+ },
+ {
+ name: "should error if mixed tags",
+ tags: "test,test2 test3",
+ want: nil,
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := Parse(tt.tags)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Parse() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestStringify(t *testing.T) {
+ tests := []struct {
+ name string
+ tags []string
+ want string
+ }{
+ {
+ name: "should support single tags",
+ tags: []string{"test"},
+ want: "test",
+ },
+ {
+ name: "should support multiple tags",
+ tags: []string{"test", "test2"},
+ want: "test,test2",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := Stringify(tt.tags); got != tt.want {
+ t.Errorf("Stringify() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/v2/pkg/git/git.go b/v2/pkg/git/git.go
new file mode 100644
index 000000000..a0ac68ca9
--- /dev/null
+++ b/v2/pkg/git/git.go
@@ -0,0 +1,64 @@
+package git
+
+import (
+ "encoding/json"
+ "fmt"
+ "runtime"
+ "strings"
+
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+func gitcommand() string {
+ gitcommand := "git"
+ if runtime.GOOS == "windows" {
+ gitcommand = "git.exe"
+ }
+
+ return gitcommand
+}
+
+// IsInstalled returns true if git is installed for the given platform
+func IsInstalled() bool {
+ return shell.CommandExists(gitcommand())
+}
+
+// Email tries to retrieve the
+func Email() (string, error) {
+ stdout, _, err := shell.RunCommand(".", gitcommand(), "config", "user.email")
+ return stdout, err
+}
+
+// Name tries to retrieve the
+func Name() (string, error) {
+ errMsg := "failed to retrieve git user name: %w"
+ stdout, _, err := shell.RunCommand(".", gitcommand(), "config", "user.name")
+ if err != nil {
+ return "", fmt.Errorf(errMsg, err)
+ }
+ name := strings.TrimSpace(stdout)
+ return EscapeName(name)
+}
+
+func EscapeName(str string) (string, error) {
+ b, err := json.Marshal(str)
+ if err != nil {
+ return "", err
+ }
+ // Remove the surrounding quotes
+ escaped := string(b[1 : len(b)-1])
+
+ // Check if username is JSON compliant
+ var js json.RawMessage
+ jsonVal := fmt.Sprintf(`{"name": "%s"}`, escaped)
+ err = json.Unmarshal([]byte(jsonVal), &js)
+ if err != nil {
+ return "", fmt.Errorf("failed to retrieve git user name: %w", err)
+ }
+ return escaped, nil
+}
+
+func InitRepo(projectDir string) error {
+ _, _, err := shell.RunCommand(projectDir, gitcommand(), "init")
+ return err
+}
diff --git a/v2/pkg/git/git_test.go b/v2/pkg/git/git_test.go
new file mode 100644
index 000000000..238008ec3
--- /dev/null
+++ b/v2/pkg/git/git_test.go
@@ -0,0 +1,44 @@
+package git
+
+import (
+ "testing"
+)
+
+func TestEscapeName1(t *testing.T) {
+ type args struct {
+ str string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ wantErr bool
+ }{
+ {
+ name: "Escape Apostrophe",
+ args: args{
+ str: `John O'Keefe`,
+ },
+ want: `John O'Keefe`,
+ },
+ {
+ name: "Escape backslash",
+ args: args{
+ str: `MYDOMAIN\USER`,
+ },
+ want: `MYDOMAIN\\USER`,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := EscapeName(tt.args.str)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("EscapeName() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("EscapeName() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/v2/pkg/logger/default.go b/v2/pkg/logger/default.go
new file mode 100644
index 000000000..ac83d4f2f
--- /dev/null
+++ b/v2/pkg/logger/default.go
@@ -0,0 +1,49 @@
+package logger
+
+import (
+ "os"
+)
+
+// DefaultLogger is a utility to log messages to a number of destinations
+type DefaultLogger struct{}
+
+// NewDefaultLogger creates a new Logger.
+func NewDefaultLogger() Logger {
+ return &DefaultLogger{}
+}
+
+// Print works like Sprintf.
+func (l *DefaultLogger) Print(message string) {
+ println(message)
+}
+
+// Trace level logging. Works like Sprintf.
+func (l *DefaultLogger) Trace(message string) {
+ println("TRA | " + message)
+}
+
+// Debug level logging. Works like Sprintf.
+func (l *DefaultLogger) Debug(message string) {
+ println("DEB | " + message)
+}
+
+// Info level logging. Works like Sprintf.
+func (l *DefaultLogger) Info(message string) {
+ println("INF | " + message)
+}
+
+// Warning level logging. Works like Sprintf.
+func (l *DefaultLogger) Warning(message string) {
+ println("WAR | " + message)
+}
+
+// Error level logging. Works like Sprintf.
+func (l *DefaultLogger) Error(message string) {
+ println("ERR | " + message)
+}
+
+// Fatal level logging. Works like Sprintf.
+func (l *DefaultLogger) Fatal(message string) {
+ println("FAT | " + message)
+ os.Exit(1)
+}
diff --git a/v2/pkg/logger/filelogger.go b/v2/pkg/logger/filelogger.go
new file mode 100644
index 000000000..954c46f59
--- /dev/null
+++ b/v2/pkg/logger/filelogger.go
@@ -0,0 +1,66 @@
+package logger
+
+import (
+ "log"
+ "os"
+)
+
+// FileLogger is a utility to log messages to a number of destinations
+type FileLogger struct {
+ filename string
+}
+
+// NewFileLogger creates a new Logger.
+func NewFileLogger(filename string) Logger {
+ return &FileLogger{
+ filename: filename,
+ }
+}
+
+// Print works like Sprintf.
+func (l *FileLogger) Print(message string) {
+ f, err := os.OpenFile(l.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if _, err := f.WriteString(message); err != nil {
+ f.Close()
+ log.Fatal(err)
+ }
+ f.Close()
+}
+
+func (l *FileLogger) Println(message string) {
+ l.Print(message + "\n")
+}
+
+// Trace level logging. Works like Sprintf.
+func (l *FileLogger) Trace(message string) {
+ l.Println("TRACE | " + message)
+}
+
+// Debug level logging. Works like Sprintf.
+func (l *FileLogger) Debug(message string) {
+ l.Println("DEBUG | " + message)
+}
+
+// Info level logging. Works like Sprintf.
+func (l *FileLogger) Info(message string) {
+ l.Println("INFO | " + message)
+}
+
+// Warning level logging. Works like Sprintf.
+func (l *FileLogger) Warning(message string) {
+ l.Println("WARN | " + message)
+}
+
+// Error level logging. Works like Sprintf.
+func (l *FileLogger) Error(message string) {
+ l.Println("ERROR | " + message)
+}
+
+// Fatal level logging. Works like Sprintf.
+func (l *FileLogger) Fatal(message string) {
+ l.Println("FATAL | " + message)
+ os.Exit(1)
+}
diff --git a/v2/pkg/logger/logger.go b/v2/pkg/logger/logger.go
new file mode 100644
index 000000000..990dffe75
--- /dev/null
+++ b/v2/pkg/logger/logger.go
@@ -0,0 +1,72 @@
+package logger
+
+import (
+ "fmt"
+ "strings"
+)
+
+// LogLevel is an unsigned 8bit int
+type LogLevel uint8
+
+const (
+ // TRACE level
+ TRACE LogLevel = 1
+
+ // DEBUG level logging
+ DEBUG LogLevel = 2
+
+ // INFO level logging
+ INFO LogLevel = 3
+
+ // WARNING level logging
+ WARNING LogLevel = 4
+
+ // ERROR level logging
+ ERROR LogLevel = 5
+)
+
+var logLevelMap = map[string]LogLevel{
+ "trace": TRACE,
+ "debug": DEBUG,
+ "info": INFO,
+ "warning": WARNING,
+ "error": ERROR,
+}
+
+func StringToLogLevel(input string) (LogLevel, error) {
+ result, ok := logLevelMap[strings.ToLower(input)]
+ if !ok {
+ return ERROR, fmt.Errorf("invalid log level: %s", input)
+ }
+ return result, nil
+}
+
+// String returns the string representation of the LogLevel
+func (l LogLevel) String() string {
+ switch l {
+ case TRACE:
+ return "trace"
+ case DEBUG:
+ return "debug"
+ case INFO:
+ return "info"
+ case WARNING:
+ return "warning"
+ case ERROR:
+ return "error"
+ default:
+ return "debug"
+ }
+}
+
+// Logger specifies the methods required to attach
+// a logger to a Wails application
+type Logger interface {
+ Print(message string)
+ Trace(message string)
+ Debug(message string)
+ Info(message string)
+ Warning(message string)
+ Error(message string)
+ Fatal(message string)
+}
diff --git a/v2/pkg/mac/login_darwin.go b/v2/pkg/mac/login_darwin.go
new file mode 100644
index 000000000..b2390e305
--- /dev/null
+++ b/v2/pkg/mac/login_darwin.go
@@ -0,0 +1,60 @@
+// Package mac provides MacOS related utility functions for Wails applications
+package mac
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/leaanthony/slicer"
+ "github.com/pkg/errors"
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+// StartAtLogin will either add or remove this application to/from the login
+// items, depending on the given boolean flag. The limitation is that the
+// currently running app must be in an app bundle.
+func StartAtLogin(enabled bool) error {
+ exe, err := os.Executable()
+ if err != nil {
+ return errors.Wrap(err, "Error running os.Executable:")
+ }
+ binName := filepath.Base(exe)
+ if !strings.HasSuffix(exe, "/Contents/MacOS/"+binName) {
+ return fmt.Errorf("app needs to be running as package.app file to start at login")
+ }
+ appPath := strings.TrimSuffix(exe, "/Contents/MacOS/"+binName)
+ var command string
+ if enabled {
+ command = fmt.Sprintf("tell application \"System Events\" to make login item at end with properties {name: \"%s\",path:\"%s\", hidden:false}", binName, appPath)
+ } else {
+ command = fmt.Sprintf("tell application \"System Events\" to delete login item \"%s\"", binName)
+ }
+ _, stde, err := shell.RunCommand("/tmp", "osascript", "-e", command)
+ if err != nil {
+ return errors.Wrap(err, stde)
+ }
+ return nil
+}
+
+// StartsAtLogin will indicate if this application is in the login
+// items. The limitation is that the currently running app must be
+// in an app bundle.
+func StartsAtLogin() (bool, error) {
+ exe, err := os.Executable()
+ if err != nil {
+ return false, err
+ }
+ binName := filepath.Base(exe)
+ if !strings.HasSuffix(exe, "/Contents/MacOS/"+binName) {
+ return false, fmt.Errorf("app needs to be running as package.app file to start at login")
+ }
+ results, stde, err := shell.RunCommand("/tmp", "osascript", "-e", `tell application "System Events" to get the name of every login item`)
+ if err != nil {
+ return false, errors.Wrap(err, stde)
+ }
+ results = strings.TrimSpace(results)
+ startupApps := slicer.String(strings.Split(results, ", "))
+ return startupApps.Contains(binName), nil
+}
diff --git a/v2/pkg/mac/notification_darwin.go b/v2/pkg/mac/notification_darwin.go
new file mode 100644
index 000000000..243f07c78
--- /dev/null
+++ b/v2/pkg/mac/notification_darwin.go
@@ -0,0 +1,30 @@
+// Package mac provides MacOS related utility functions for Wails applications
+package mac
+
+import (
+ "fmt"
+
+ "github.com/pkg/errors"
+ "github.com/wailsapp/wails/v2/internal/shell"
+)
+
+// ShowNotification will either add or remove this application to/from the login
+// items, depending on the given boolean flag. The limitation is that the
+// currently running app must be in an app bundle.
+func ShowNotification(title string, subtitle string, message string, sound string) error {
+ command := fmt.Sprintf("display notification \"%s\"", message)
+ if len(title) > 0 {
+ command += fmt.Sprintf(" with title \"%s\"", title)
+ }
+ if len(subtitle) > 0 {
+ command += fmt.Sprintf(" subtitle \"%s\"", subtitle)
+ }
+ if len(sound) > 0 {
+ command += fmt.Sprintf(" sound name \"%s\"", sound)
+ }
+ _, stde, err := shell.RunCommand("/tmp", "osascript", "-e", command)
+ if err != nil {
+ return errors.Wrap(err, stde)
+ }
+ return nil
+}
diff --git a/v2/pkg/mac/notification_darwin_test.go b/v2/pkg/mac/notification_darwin_test.go
new file mode 100644
index 000000000..7d14c00e5
--- /dev/null
+++ b/v2/pkg/mac/notification_darwin_test.go
@@ -0,0 +1,34 @@
+package mac
+
+import "testing"
+
+func TestShowNotification(t *testing.T) {
+ type args struct {
+ title string
+ subtitle string
+ message string
+ sound string
+ }
+ tests := []struct {
+ name string
+ title string
+ subtitle string
+ message string
+ sound string
+ wantErr bool
+ }{
+ {"No message", "", "", "", "", false},
+ {"Title only", "I am a Title", "", "", "", false},
+ {"SubTitle only", "", "I am a subtitle", "", "", false},
+ {"Message only", "", "", "I am a message!", "", false},
+ {"Sound only", "", "", "", "submarine.aiff", false},
+ {"Full", "Title", "Subtitle", "This is a long message to show that text gets wrapped in a notification", "submarine.aiff", false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if err := ShowNotification(tt.title, tt.subtitle, tt.message, tt.sound); (err != nil) != tt.wantErr {
+ t.Errorf("ShowNotification() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/v2/pkg/menu/README.md b/v2/pkg/menu/README.md
new file mode 100644
index 000000000..7c66a1051
--- /dev/null
+++ b/v2/pkg/menu/README.md
@@ -0,0 +1,10 @@
+# Menus
+
+Menu support is heavily inspired by Electron's approach.
+
+## Features
+
+ * Supports Text, Checkbox, Radio, Submenu and Separator
+ * Radio groups are defined as any number of adjacent radio items
+ * UTF-8 menu labels
+ * UTF-8 menu IDs
\ No newline at end of file
diff --git a/v2/pkg/menu/callback.go b/v2/pkg/menu/callback.go
new file mode 100644
index 000000000..a02664ac0
--- /dev/null
+++ b/v2/pkg/menu/callback.go
@@ -0,0 +1,8 @@
+package menu
+
+type CallbackData struct {
+ MenuItem *MenuItem
+ // ContextData string
+}
+
+type Callback func(*CallbackData)
diff --git a/v2/pkg/menu/colours/colours.go b/v2/pkg/menu/colours/colours.go
new file mode 100644
index 000000000..5fb74eabd
--- /dev/null
+++ b/v2/pkg/menu/colours/colours.go
@@ -0,0 +1,68 @@
+package main
+
+import (
+ "bytes"
+ _ "embed"
+ "encoding/json"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "text/template"
+)
+
+type Rgb struct {
+ R uint8 `json:"r"`
+ G uint8 `json:"g"`
+ B uint8 `json:"b"`
+}
+
+type Hsl struct {
+ H float64 `json:"h"`
+ S float64 `json:"s"`
+ L float64 `json:"l"`
+}
+
+type InputCol struct {
+ Colorid uint8 `json:"colorId"`
+ Hexstring string `json:"hexString"`
+ Rgb Rgb `json:"rgb"`
+ Hsl Hsl `json:"hsl"`
+ Name string `json:"name"`
+}
+
+//go:embed gen.tmpl
+var Template string
+
+func main() {
+ var Cols []InputCol
+
+ resp, err := http.Get("https://jonasjacek.github.io/colors/data.json")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer resp.Body.Close()
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = json.Unmarshal(data, &Cols)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ t, err := template.New("cols").Parse(Template)
+ if err != nil {
+ log.Fatal(err)
+ }
+ var buffer bytes.Buffer
+ err = t.Execute(&buffer, Cols)
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = os.WriteFile(filepath.Join("..", "cols.go"), buffer.Bytes(), 0o755)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/v2/pkg/menu/colours/gen.tmpl b/v2/pkg/menu/colours/gen.tmpl
new file mode 100644
index 000000000..bd5d1ff3c
--- /dev/null
+++ b/v2/pkg/menu/colours/gen.tmpl
@@ -0,0 +1,29 @@
+package menu
+
+type Rgb struct {
+ R uint8 `json:"r"`
+ G uint8 `json:"g"`
+ B uint8 `json:"b"`
+}
+
+type Hsl struct {
+ H float64 `json:"h"`
+ S float64 `json:"s"`
+ L float64 `json:"l"`
+}
+
+type Col struct {
+ Hex string `json:"hex"`
+ Rgb Rgb `json:"rgb"`
+ Hsl Hsl `json:"hsl"`
+ Name string `json:"name"`
+}
+
+var Cols = []*Col{ {{range $col := .}}
+ {
+ Hex: "{{.Hexstring}}",
+ Rgb: Rgb{ {{.Rgb.R}}, {{.Rgb.G}}, {{.Rgb.B}} },
+ Hsl: Hsl{ {{.Hsl.H}}, {{.Hsl.S}}, {{.Hsl.L}} },
+ Name: "{{.Name}}",
+ },{{end}}
+}
diff --git a/v2/pkg/menu/cols.go b/v2/pkg/menu/cols.go
new file mode 100755
index 000000000..738e4baaf
--- /dev/null
+++ b/v2/pkg/menu/cols.go
@@ -0,0 +1,1559 @@
+package menu
+
+type Rgb struct {
+ R uint8 `json:"r"`
+ G uint8 `json:"g"`
+ B uint8 `json:"b"`
+}
+
+type Hsl struct {
+ H float64 `json:"h"`
+ S float64 `json:"s"`
+ L float64 `json:"l"`
+}
+
+type Col struct {
+ Hex string `json:"hex"`
+ Rgb Rgb `json:"rgb"`
+ Hsl Hsl `json:"hsl"`
+ Name string `json:"name"`
+}
+
+var Cols = []*Col{
+ {
+ Hex: "#000000",
+ Rgb: Rgb{0, 0, 0},
+ Hsl: Hsl{0, 0, 0},
+ Name: "Black",
+ },
+ {
+ Hex: "#800000",
+ Rgb: Rgb{128, 0, 0},
+ Hsl: Hsl{0, 100, 25},
+ Name: "Maroon",
+ },
+ {
+ Hex: "#008000",
+ Rgb: Rgb{0, 128, 0},
+ Hsl: Hsl{120, 100, 25},
+ Name: "Green",
+ },
+ {
+ Hex: "#808000",
+ Rgb: Rgb{128, 128, 0},
+ Hsl: Hsl{60, 100, 25},
+ Name: "Olive",
+ },
+ {
+ Hex: "#000080",
+ Rgb: Rgb{0, 0, 128},
+ Hsl: Hsl{240, 100, 25},
+ Name: "Navy",
+ },
+ {
+ Hex: "#800080",
+ Rgb: Rgb{128, 0, 128},
+ Hsl: Hsl{300, 100, 25},
+ Name: "Purple",
+ },
+ {
+ Hex: "#008080",
+ Rgb: Rgb{0, 128, 128},
+ Hsl: Hsl{180, 100, 25},
+ Name: "Teal",
+ },
+ {
+ Hex: "#c0c0c0",
+ Rgb: Rgb{192, 192, 192},
+ Hsl: Hsl{0, 0, 75},
+ Name: "Silver",
+ },
+ {
+ Hex: "#808080",
+ Rgb: Rgb{128, 128, 128},
+ Hsl: Hsl{0, 0, 50},
+ Name: "Grey",
+ },
+ {
+ Hex: "#ff0000",
+ Rgb: Rgb{255, 0, 0},
+ Hsl: Hsl{0, 100, 50},
+ Name: "Red",
+ },
+ {
+ Hex: "#00ff00",
+ Rgb: Rgb{0, 255, 0},
+ Hsl: Hsl{120, 100, 50},
+ Name: "Lime",
+ },
+ {
+ Hex: "#ffff00",
+ Rgb: Rgb{255, 255, 0},
+ Hsl: Hsl{60, 100, 50},
+ Name: "Yellow",
+ },
+ {
+ Hex: "#0000ff",
+ Rgb: Rgb{0, 0, 255},
+ Hsl: Hsl{240, 100, 50},
+ Name: "Blue",
+ },
+ {
+ Hex: "#ff00ff",
+ Rgb: Rgb{255, 0, 255},
+ Hsl: Hsl{300, 100, 50},
+ Name: "Fuchsia",
+ },
+ {
+ Hex: "#00ffff",
+ Rgb: Rgb{0, 255, 255},
+ Hsl: Hsl{180, 100, 50},
+ Name: "Aqua",
+ },
+ {
+ Hex: "#ffffff",
+ Rgb: Rgb{255, 255, 255},
+ Hsl: Hsl{0, 0, 100},
+ Name: "White",
+ },
+ {
+ Hex: "#000000",
+ Rgb: Rgb{0, 0, 0},
+ Hsl: Hsl{0, 0, 0},
+ Name: "Grey0",
+ },
+ {
+ Hex: "#00005f",
+ Rgb: Rgb{0, 0, 95},
+ Hsl: Hsl{240, 100, 18},
+ Name: "NavyBlue",
+ },
+ {
+ Hex: "#000087",
+ Rgb: Rgb{0, 0, 135},
+ Hsl: Hsl{240, 100, 26},
+ Name: "DarkBlue",
+ },
+ {
+ Hex: "#0000af",
+ Rgb: Rgb{0, 0, 175},
+ Hsl: Hsl{240, 100, 34},
+ Name: "Blue3",
+ },
+ {
+ Hex: "#0000d7",
+ Rgb: Rgb{0, 0, 215},
+ Hsl: Hsl{240, 100, 42},
+ Name: "Blue3",
+ },
+ {
+ Hex: "#0000ff",
+ Rgb: Rgb{0, 0, 255},
+ Hsl: Hsl{240, 100, 50},
+ Name: "Blue1",
+ },
+ {
+ Hex: "#005f00",
+ Rgb: Rgb{0, 95, 0},
+ Hsl: Hsl{120, 100, 18},
+ Name: "DarkGreen",
+ },
+ {
+ Hex: "#005f5f",
+ Rgb: Rgb{0, 95, 95},
+ Hsl: Hsl{180, 100, 18},
+ Name: "DeepSkyBlue4",
+ },
+ {
+ Hex: "#005f87",
+ Rgb: Rgb{0, 95, 135},
+ Hsl: Hsl{197.777777777778, 100, 26},
+ Name: "DeepSkyBlue4",
+ },
+ {
+ Hex: "#005faf",
+ Rgb: Rgb{0, 95, 175},
+ Hsl: Hsl{207.428571428571, 100, 34},
+ Name: "DeepSkyBlue4",
+ },
+ {
+ Hex: "#005fd7",
+ Rgb: Rgb{0, 95, 215},
+ Hsl: Hsl{213.488372093023, 100, 42},
+ Name: "DodgerBlue3",
+ },
+ {
+ Hex: "#005fff",
+ Rgb: Rgb{0, 95, 255},
+ Hsl: Hsl{217.647058823529, 100, 50},
+ Name: "DodgerBlue2",
+ },
+ {
+ Hex: "#008700",
+ Rgb: Rgb{0, 135, 0},
+ Hsl: Hsl{120, 100, 26},
+ Name: "Green4",
+ },
+ {
+ Hex: "#00875f",
+ Rgb: Rgb{0, 135, 95},
+ Hsl: Hsl{162.222222222222, 100, 26},
+ Name: "SpringGreen4",
+ },
+ {
+ Hex: "#008787",
+ Rgb: Rgb{0, 135, 135},
+ Hsl: Hsl{180, 100, 26},
+ Name: "Turquoise4",
+ },
+ {
+ Hex: "#0087af",
+ Rgb: Rgb{0, 135, 175},
+ Hsl: Hsl{193.714285714286, 100, 34},
+ Name: "DeepSkyBlue3",
+ },
+ {
+ Hex: "#0087d7",
+ Rgb: Rgb{0, 135, 215},
+ Hsl: Hsl{202.325581395349, 100, 42},
+ Name: "DeepSkyBlue3",
+ },
+ {
+ Hex: "#0087ff",
+ Rgb: Rgb{0, 135, 255},
+ Hsl: Hsl{208.235294117647, 100, 50},
+ Name: "DodgerBlue1",
+ },
+ {
+ Hex: "#00af00",
+ Rgb: Rgb{0, 175, 0},
+ Hsl: Hsl{120, 100, 34},
+ Name: "Green3",
+ },
+ {
+ Hex: "#00af5f",
+ Rgb: Rgb{0, 175, 95},
+ Hsl: Hsl{152.571428571429, 100, 34},
+ Name: "SpringGreen3",
+ },
+ {
+ Hex: "#00af87",
+ Rgb: Rgb{0, 175, 135},
+ Hsl: Hsl{166.285714285714, 100, 34},
+ Name: "DarkCyan",
+ },
+ {
+ Hex: "#00afaf",
+ Rgb: Rgb{0, 175, 175},
+ Hsl: Hsl{180, 100, 34},
+ Name: "LightSeaGreen",
+ },
+ {
+ Hex: "#00afd7",
+ Rgb: Rgb{0, 175, 215},
+ Hsl: Hsl{191.162790697674, 100, 42},
+ Name: "DeepSkyBlue2",
+ },
+ {
+ Hex: "#00afff",
+ Rgb: Rgb{0, 175, 255},
+ Hsl: Hsl{198.823529411765, 100, 50},
+ Name: "DeepSkyBlue1",
+ },
+ {
+ Hex: "#00d700",
+ Rgb: Rgb{0, 215, 0},
+ Hsl: Hsl{120, 100, 42},
+ Name: "Green3",
+ },
+ {
+ Hex: "#00d75f",
+ Rgb: Rgb{0, 215, 95},
+ Hsl: Hsl{146.511627906977, 100, 42},
+ Name: "SpringGreen3",
+ },
+ {
+ Hex: "#00d787",
+ Rgb: Rgb{0, 215, 135},
+ Hsl: Hsl{157.674418604651, 100, 42},
+ Name: "SpringGreen2",
+ },
+ {
+ Hex: "#00d7af",
+ Rgb: Rgb{0, 215, 175},
+ Hsl: Hsl{168.837209302326, 100, 42},
+ Name: "Cyan3",
+ },
+ {
+ Hex: "#00d7d7",
+ Rgb: Rgb{0, 215, 215},
+ Hsl: Hsl{180, 100, 42},
+ Name: "DarkTurquoise",
+ },
+ {
+ Hex: "#00d7ff",
+ Rgb: Rgb{0, 215, 255},
+ Hsl: Hsl{189.411764705882, 100, 50},
+ Name: "Turquoise2",
+ },
+ {
+ Hex: "#00ff00",
+ Rgb: Rgb{0, 255, 0},
+ Hsl: Hsl{120, 100, 50},
+ Name: "Green1",
+ },
+ {
+ Hex: "#00ff5f",
+ Rgb: Rgb{0, 255, 95},
+ Hsl: Hsl{142.352941176471, 100, 50},
+ Name: "SpringGreen2",
+ },
+ {
+ Hex: "#00ff87",
+ Rgb: Rgb{0, 255, 135},
+ Hsl: Hsl{151.764705882353, 100, 50},
+ Name: "SpringGreen1",
+ },
+ {
+ Hex: "#00ffaf",
+ Rgb: Rgb{0, 255, 175},
+ Hsl: Hsl{161.176470588235, 100, 50},
+ Name: "MediumSpringGreen",
+ },
+ {
+ Hex: "#00ffd7",
+ Rgb: Rgb{0, 255, 215},
+ Hsl: Hsl{170.588235294118, 100, 50},
+ Name: "Cyan2",
+ },
+ {
+ Hex: "#00ffff",
+ Rgb: Rgb{0, 255, 255},
+ Hsl: Hsl{180, 100, 50},
+ Name: "Cyan1",
+ },
+ {
+ Hex: "#5f0000",
+ Rgb: Rgb{95, 0, 0},
+ Hsl: Hsl{0, 100, 18},
+ Name: "DarkRed",
+ },
+ {
+ Hex: "#5f005f",
+ Rgb: Rgb{95, 0, 95},
+ Hsl: Hsl{300, 100, 18},
+ Name: "DeepPink4",
+ },
+ {
+ Hex: "#5f0087",
+ Rgb: Rgb{95, 0, 135},
+ Hsl: Hsl{282.222222222222, 100, 26},
+ Name: "Purple4",
+ },
+ {
+ Hex: "#5f00af",
+ Rgb: Rgb{95, 0, 175},
+ Hsl: Hsl{272.571428571429, 100, 34},
+ Name: "Purple4",
+ },
+ {
+ Hex: "#5f00d7",
+ Rgb: Rgb{95, 0, 215},
+ Hsl: Hsl{266.511627906977, 100, 42},
+ Name: "Purple3",
+ },
+ {
+ Hex: "#5f00ff",
+ Rgb: Rgb{95, 0, 255},
+ Hsl: Hsl{262.352941176471, 100, 50},
+ Name: "BlueViolet",
+ },
+ {
+ Hex: "#5f5f00",
+ Rgb: Rgb{95, 95, 0},
+ Hsl: Hsl{60, 100, 18},
+ Name: "Orange4",
+ },
+ {
+ Hex: "#5f5f5f",
+ Rgb: Rgb{95, 95, 95},
+ Hsl: Hsl{0, 0, 37},
+ Name: "Grey37",
+ },
+ {
+ Hex: "#5f5f87",
+ Rgb: Rgb{95, 95, 135},
+ Hsl: Hsl{240, 17, 45},
+ Name: "MediumPurple4",
+ },
+ {
+ Hex: "#5f5faf",
+ Rgb: Rgb{95, 95, 175},
+ Hsl: Hsl{240, 33, 52},
+ Name: "SlateBlue3",
+ },
+ {
+ Hex: "#5f5fd7",
+ Rgb: Rgb{95, 95, 215},
+ Hsl: Hsl{240, 60, 60},
+ Name: "SlateBlue3",
+ },
+ {
+ Hex: "#5f5fff",
+ Rgb: Rgb{95, 95, 255},
+ Hsl: Hsl{240, 100, 68},
+ Name: "RoyalBlue1",
+ },
+ {
+ Hex: "#5f8700",
+ Rgb: Rgb{95, 135, 0},
+ Hsl: Hsl{77.7777777777778, 100, 26},
+ Name: "Chartreuse4",
+ },
+ {
+ Hex: "#5f875f",
+ Rgb: Rgb{95, 135, 95},
+ Hsl: Hsl{120, 17, 45},
+ Name: "DarkSeaGreen4",
+ },
+ {
+ Hex: "#5f8787",
+ Rgb: Rgb{95, 135, 135},
+ Hsl: Hsl{180, 17, 45},
+ Name: "PaleTurquoise4",
+ },
+ {
+ Hex: "#5f87af",
+ Rgb: Rgb{95, 135, 175},
+ Hsl: Hsl{210, 33, 52},
+ Name: "SteelBlue",
+ },
+ {
+ Hex: "#5f87d7",
+ Rgb: Rgb{95, 135, 215},
+ Hsl: Hsl{220, 60, 60},
+ Name: "SteelBlue3",
+ },
+ {
+ Hex: "#5f87ff",
+ Rgb: Rgb{95, 135, 255},
+ Hsl: Hsl{225, 100, 68},
+ Name: "CornflowerBlue",
+ },
+ {
+ Hex: "#5faf00",
+ Rgb: Rgb{95, 175, 0},
+ Hsl: Hsl{87.4285714285714, 100, 34},
+ Name: "Chartreuse3",
+ },
+ {
+ Hex: "#5faf5f",
+ Rgb: Rgb{95, 175, 95},
+ Hsl: Hsl{120, 33, 52},
+ Name: "DarkSeaGreen4",
+ },
+ {
+ Hex: "#5faf87",
+ Rgb: Rgb{95, 175, 135},
+ Hsl: Hsl{150, 33, 52},
+ Name: "CadetBlue",
+ },
+ {
+ Hex: "#5fafaf",
+ Rgb: Rgb{95, 175, 175},
+ Hsl: Hsl{180, 33, 52},
+ Name: "CadetBlue",
+ },
+ {
+ Hex: "#5fafd7",
+ Rgb: Rgb{95, 175, 215},
+ Hsl: Hsl{200, 60, 60},
+ Name: "SkyBlue3",
+ },
+ {
+ Hex: "#5fafff",
+ Rgb: Rgb{95, 175, 255},
+ Hsl: Hsl{210, 100, 68},
+ Name: "SteelBlue1",
+ },
+ {
+ Hex: "#5fd700",
+ Rgb: Rgb{95, 215, 0},
+ Hsl: Hsl{93.4883720930233, 100, 42},
+ Name: "Chartreuse3",
+ },
+ {
+ Hex: "#5fd75f",
+ Rgb: Rgb{95, 215, 95},
+ Hsl: Hsl{120, 60, 60},
+ Name: "PaleGreen3",
+ },
+ {
+ Hex: "#5fd787",
+ Rgb: Rgb{95, 215, 135},
+ Hsl: Hsl{140, 60, 60},
+ Name: "SeaGreen3",
+ },
+ {
+ Hex: "#5fd7af",
+ Rgb: Rgb{95, 215, 175},
+ Hsl: Hsl{160, 60, 60},
+ Name: "Aquamarine3",
+ },
+ {
+ Hex: "#5fd7d7",
+ Rgb: Rgb{95, 215, 215},
+ Hsl: Hsl{180, 60, 60},
+ Name: "MediumTurquoise",
+ },
+ {
+ Hex: "#5fd7ff",
+ Rgb: Rgb{95, 215, 255},
+ Hsl: Hsl{195, 100, 68},
+ Name: "SteelBlue1",
+ },
+ {
+ Hex: "#5fff00",
+ Rgb: Rgb{95, 255, 0},
+ Hsl: Hsl{97.6470588235294, 100, 50},
+ Name: "Chartreuse2",
+ },
+ {
+ Hex: "#5fff5f",
+ Rgb: Rgb{95, 255, 95},
+ Hsl: Hsl{120, 100, 68},
+ Name: "SeaGreen2",
+ },
+ {
+ Hex: "#5fff87",
+ Rgb: Rgb{95, 255, 135},
+ Hsl: Hsl{135, 100, 68},
+ Name: "SeaGreen1",
+ },
+ {
+ Hex: "#5fffaf",
+ Rgb: Rgb{95, 255, 175},
+ Hsl: Hsl{150, 100, 68},
+ Name: "SeaGreen1",
+ },
+ {
+ Hex: "#5fffd7",
+ Rgb: Rgb{95, 255, 215},
+ Hsl: Hsl{165, 100, 68},
+ Name: "Aquamarine1",
+ },
+ {
+ Hex: "#5fffff",
+ Rgb: Rgb{95, 255, 255},
+ Hsl: Hsl{180, 100, 68},
+ Name: "DarkSlateGray2",
+ },
+ {
+ Hex: "#870000",
+ Rgb: Rgb{135, 0, 0},
+ Hsl: Hsl{0, 100, 26},
+ Name: "DarkRed",
+ },
+ {
+ Hex: "#87005f",
+ Rgb: Rgb{135, 0, 95},
+ Hsl: Hsl{317.777777777778, 100, 26},
+ Name: "DeepPink4",
+ },
+ {
+ Hex: "#870087",
+ Rgb: Rgb{135, 0, 135},
+ Hsl: Hsl{300, 100, 26},
+ Name: "DarkMagenta",
+ },
+ {
+ Hex: "#8700af",
+ Rgb: Rgb{135, 0, 175},
+ Hsl: Hsl{286.285714285714, 100, 34},
+ Name: "DarkMagenta",
+ },
+ {
+ Hex: "#8700d7",
+ Rgb: Rgb{135, 0, 215},
+ Hsl: Hsl{277.674418604651, 100, 42},
+ Name: "DarkViolet",
+ },
+ {
+ Hex: "#8700ff",
+ Rgb: Rgb{135, 0, 255},
+ Hsl: Hsl{271.764705882353, 100, 50},
+ Name: "Purple",
+ },
+ {
+ Hex: "#875f00",
+ Rgb: Rgb{135, 95, 0},
+ Hsl: Hsl{42.2222222222222, 100, 26},
+ Name: "Orange4",
+ },
+ {
+ Hex: "#875f5f",
+ Rgb: Rgb{135, 95, 95},
+ Hsl: Hsl{0, 17, 45},
+ Name: "LightPink4",
+ },
+ {
+ Hex: "#875f87",
+ Rgb: Rgb{135, 95, 135},
+ Hsl: Hsl{300, 17, 45},
+ Name: "Plum4",
+ },
+ {
+ Hex: "#875faf",
+ Rgb: Rgb{135, 95, 175},
+ Hsl: Hsl{270, 33, 52},
+ Name: "MediumPurple3",
+ },
+ {
+ Hex: "#875fd7",
+ Rgb: Rgb{135, 95, 215},
+ Hsl: Hsl{260, 60, 60},
+ Name: "MediumPurple3",
+ },
+ {
+ Hex: "#875fff",
+ Rgb: Rgb{135, 95, 255},
+ Hsl: Hsl{255, 100, 68},
+ Name: "SlateBlue1",
+ },
+ {
+ Hex: "#878700",
+ Rgb: Rgb{135, 135, 0},
+ Hsl: Hsl{60, 100, 26},
+ Name: "Yellow4",
+ },
+ {
+ Hex: "#87875f",
+ Rgb: Rgb{135, 135, 95},
+ Hsl: Hsl{60, 17, 45},
+ Name: "Wheat4",
+ },
+ {
+ Hex: "#878787",
+ Rgb: Rgb{135, 135, 135},
+ Hsl: Hsl{0, 0, 52},
+ Name: "Grey53",
+ },
+ {
+ Hex: "#8787af",
+ Rgb: Rgb{135, 135, 175},
+ Hsl: Hsl{240, 20, 60},
+ Name: "LightSlateGrey",
+ },
+ {
+ Hex: "#8787d7",
+ Rgb: Rgb{135, 135, 215},
+ Hsl: Hsl{240, 50, 68},
+ Name: "MediumPurple",
+ },
+ {
+ Hex: "#8787ff",
+ Rgb: Rgb{135, 135, 255},
+ Hsl: Hsl{240, 100, 76},
+ Name: "LightSlateBlue",
+ },
+ {
+ Hex: "#87af00",
+ Rgb: Rgb{135, 175, 0},
+ Hsl: Hsl{73.7142857142857, 100, 34},
+ Name: "Yellow4",
+ },
+ {
+ Hex: "#87af5f",
+ Rgb: Rgb{135, 175, 95},
+ Hsl: Hsl{90, 33, 52},
+ Name: "DarkOliveGreen3",
+ },
+ {
+ Hex: "#87af87",
+ Rgb: Rgb{135, 175, 135},
+ Hsl: Hsl{120, 20, 60},
+ Name: "DarkSeaGreen",
+ },
+ {
+ Hex: "#87afaf",
+ Rgb: Rgb{135, 175, 175},
+ Hsl: Hsl{180, 20, 60},
+ Name: "LightSkyBlue3",
+ },
+ {
+ Hex: "#87afd7",
+ Rgb: Rgb{135, 175, 215},
+ Hsl: Hsl{210, 50, 68},
+ Name: "LightSkyBlue3",
+ },
+ {
+ Hex: "#87afff",
+ Rgb: Rgb{135, 175, 255},
+ Hsl: Hsl{220, 100, 76},
+ Name: "SkyBlue2",
+ },
+ {
+ Hex: "#87d700",
+ Rgb: Rgb{135, 215, 0},
+ Hsl: Hsl{82.3255813953488, 100, 42},
+ Name: "Chartreuse2",
+ },
+ {
+ Hex: "#87d75f",
+ Rgb: Rgb{135, 215, 95},
+ Hsl: Hsl{100, 60, 60},
+ Name: "DarkOliveGreen3",
+ },
+ {
+ Hex: "#87d787",
+ Rgb: Rgb{135, 215, 135},
+ Hsl: Hsl{120, 50, 68},
+ Name: "PaleGreen3",
+ },
+ {
+ Hex: "#87d7af",
+ Rgb: Rgb{135, 215, 175},
+ Hsl: Hsl{150, 50, 68},
+ Name: "DarkSeaGreen3",
+ },
+ {
+ Hex: "#87d7d7",
+ Rgb: Rgb{135, 215, 215},
+ Hsl: Hsl{180, 50, 68},
+ Name: "DarkSlateGray3",
+ },
+ {
+ Hex: "#87d7ff",
+ Rgb: Rgb{135, 215, 255},
+ Hsl: Hsl{200, 100, 76},
+ Name: "SkyBlue1",
+ },
+ {
+ Hex: "#87ff00",
+ Rgb: Rgb{135, 255, 0},
+ Hsl: Hsl{88.2352941176471, 100, 50},
+ Name: "Chartreuse1",
+ },
+ {
+ Hex: "#87ff5f",
+ Rgb: Rgb{135, 255, 95},
+ Hsl: Hsl{105, 100, 68},
+ Name: "LightGreen",
+ },
+ {
+ Hex: "#87ff87",
+ Rgb: Rgb{135, 255, 135},
+ Hsl: Hsl{120, 100, 76},
+ Name: "LightGreen",
+ },
+ {
+ Hex: "#87ffaf",
+ Rgb: Rgb{135, 255, 175},
+ Hsl: Hsl{140, 100, 76},
+ Name: "PaleGreen1",
+ },
+ {
+ Hex: "#87ffd7",
+ Rgb: Rgb{135, 255, 215},
+ Hsl: Hsl{160, 100, 76},
+ Name: "Aquamarine1",
+ },
+ {
+ Hex: "#87ffff",
+ Rgb: Rgb{135, 255, 255},
+ Hsl: Hsl{180, 100, 76},
+ Name: "DarkSlateGray1",
+ },
+ {
+ Hex: "#af0000",
+ Rgb: Rgb{175, 0, 0},
+ Hsl: Hsl{0, 100, 34},
+ Name: "Red3",
+ },
+ {
+ Hex: "#af005f",
+ Rgb: Rgb{175, 0, 95},
+ Hsl: Hsl{327.428571428571, 100, 34},
+ Name: "DeepPink4",
+ },
+ {
+ Hex: "#af0087",
+ Rgb: Rgb{175, 0, 135},
+ Hsl: Hsl{313.714285714286, 100, 34},
+ Name: "MediumVioletRed",
+ },
+ {
+ Hex: "#af00af",
+ Rgb: Rgb{175, 0, 175},
+ Hsl: Hsl{300, 100, 34},
+ Name: "Magenta3",
+ },
+ {
+ Hex: "#af00d7",
+ Rgb: Rgb{175, 0, 215},
+ Hsl: Hsl{288.837209302326, 100, 42},
+ Name: "DarkViolet",
+ },
+ {
+ Hex: "#af00ff",
+ Rgb: Rgb{175, 0, 255},
+ Hsl: Hsl{281.176470588235, 100, 50},
+ Name: "Purple",
+ },
+ {
+ Hex: "#af5f00",
+ Rgb: Rgb{175, 95, 0},
+ Hsl: Hsl{32.5714285714286, 100, 34},
+ Name: "DarkOrange3",
+ },
+ {
+ Hex: "#af5f5f",
+ Rgb: Rgb{175, 95, 95},
+ Hsl: Hsl{0, 33, 52},
+ Name: "IndianRed",
+ },
+ {
+ Hex: "#af5f87",
+ Rgb: Rgb{175, 95, 135},
+ Hsl: Hsl{330, 33, 52},
+ Name: "HotPink3",
+ },
+ {
+ Hex: "#af5faf",
+ Rgb: Rgb{175, 95, 175},
+ Hsl: Hsl{300, 33, 52},
+ Name: "MediumOrchid3",
+ },
+ {
+ Hex: "#af5fd7",
+ Rgb: Rgb{175, 95, 215},
+ Hsl: Hsl{280, 60, 60},
+ Name: "MediumOrchid",
+ },
+ {
+ Hex: "#af5fff",
+ Rgb: Rgb{175, 95, 255},
+ Hsl: Hsl{270, 100, 68},
+ Name: "MediumPurple2",
+ },
+ {
+ Hex: "#af8700",
+ Rgb: Rgb{175, 135, 0},
+ Hsl: Hsl{46.2857142857143, 100, 34},
+ Name: "DarkGoldenrod",
+ },
+ {
+ Hex: "#af875f",
+ Rgb: Rgb{175, 135, 95},
+ Hsl: Hsl{30, 33, 52},
+ Name: "LightSalmon3",
+ },
+ {
+ Hex: "#af8787",
+ Rgb: Rgb{175, 135, 135},
+ Hsl: Hsl{0, 20, 60},
+ Name: "RosyBrown",
+ },
+ {
+ Hex: "#af87af",
+ Rgb: Rgb{175, 135, 175},
+ Hsl: Hsl{300, 20, 60},
+ Name: "Grey63",
+ },
+ {
+ Hex: "#af87d7",
+ Rgb: Rgb{175, 135, 215},
+ Hsl: Hsl{270, 50, 68},
+ Name: "MediumPurple2",
+ },
+ {
+ Hex: "#af87ff",
+ Rgb: Rgb{175, 135, 255},
+ Hsl: Hsl{260, 100, 76},
+ Name: "MediumPurple1",
+ },
+ {
+ Hex: "#afaf00",
+ Rgb: Rgb{175, 175, 0},
+ Hsl: Hsl{60, 100, 34},
+ Name: "Gold3",
+ },
+ {
+ Hex: "#afaf5f",
+ Rgb: Rgb{175, 175, 95},
+ Hsl: Hsl{60, 33, 52},
+ Name: "DarkKhaki",
+ },
+ {
+ Hex: "#afaf87",
+ Rgb: Rgb{175, 175, 135},
+ Hsl: Hsl{60, 20, 60},
+ Name: "NavajoWhite3",
+ },
+ {
+ Hex: "#afafaf",
+ Rgb: Rgb{175, 175, 175},
+ Hsl: Hsl{0, 0, 68},
+ Name: "Grey69",
+ },
+ {
+ Hex: "#afafd7",
+ Rgb: Rgb{175, 175, 215},
+ Hsl: Hsl{240, 33, 76},
+ Name: "LightSteelBlue3",
+ },
+ {
+ Hex: "#afafff",
+ Rgb: Rgb{175, 175, 255},
+ Hsl: Hsl{240, 100, 84},
+ Name: "LightSteelBlue",
+ },
+ {
+ Hex: "#afd700",
+ Rgb: Rgb{175, 215, 0},
+ Hsl: Hsl{71.1627906976744, 100, 42},
+ Name: "Yellow3",
+ },
+ {
+ Hex: "#afd75f",
+ Rgb: Rgb{175, 215, 95},
+ Hsl: Hsl{80, 60, 60},
+ Name: "DarkOliveGreen3",
+ },
+ {
+ Hex: "#afd787",
+ Rgb: Rgb{175, 215, 135},
+ Hsl: Hsl{90, 50, 68},
+ Name: "DarkSeaGreen3",
+ },
+ {
+ Hex: "#afd7af",
+ Rgb: Rgb{175, 215, 175},
+ Hsl: Hsl{120, 33, 76},
+ Name: "DarkSeaGreen2",
+ },
+ {
+ Hex: "#afd7d7",
+ Rgb: Rgb{175, 215, 215},
+ Hsl: Hsl{180, 33, 76},
+ Name: "LightCyan3",
+ },
+ {
+ Hex: "#afd7ff",
+ Rgb: Rgb{175, 215, 255},
+ Hsl: Hsl{210, 100, 84},
+ Name: "LightSkyBlue1",
+ },
+ {
+ Hex: "#afff00",
+ Rgb: Rgb{175, 255, 0},
+ Hsl: Hsl{78.8235294117647, 100, 50},
+ Name: "GreenYellow",
+ },
+ {
+ Hex: "#afff5f",
+ Rgb: Rgb{175, 255, 95},
+ Hsl: Hsl{90, 100, 68},
+ Name: "DarkOliveGreen2",
+ },
+ {
+ Hex: "#afff87",
+ Rgb: Rgb{175, 255, 135},
+ Hsl: Hsl{100, 100, 76},
+ Name: "PaleGreen1",
+ },
+ {
+ Hex: "#afffaf",
+ Rgb: Rgb{175, 255, 175},
+ Hsl: Hsl{120, 100, 84},
+ Name: "DarkSeaGreen2",
+ },
+ {
+ Hex: "#afffd7",
+ Rgb: Rgb{175, 255, 215},
+ Hsl: Hsl{150, 100, 84},
+ Name: "DarkSeaGreen1",
+ },
+ {
+ Hex: "#afffff",
+ Rgb: Rgb{175, 255, 255},
+ Hsl: Hsl{180, 100, 84},
+ Name: "PaleTurquoise1",
+ },
+ {
+ Hex: "#d70000",
+ Rgb: Rgb{215, 0, 0},
+ Hsl: Hsl{0, 100, 42},
+ Name: "Red3",
+ },
+ {
+ Hex: "#d7005f",
+ Rgb: Rgb{215, 0, 95},
+ Hsl: Hsl{333.488372093023, 100, 42},
+ Name: "DeepPink3",
+ },
+ {
+ Hex: "#d70087",
+ Rgb: Rgb{215, 0, 135},
+ Hsl: Hsl{322.325581395349, 100, 42},
+ Name: "DeepPink3",
+ },
+ {
+ Hex: "#d700af",
+ Rgb: Rgb{215, 0, 175},
+ Hsl: Hsl{311.162790697674, 100, 42},
+ Name: "Magenta3",
+ },
+ {
+ Hex: "#d700d7",
+ Rgb: Rgb{215, 0, 215},
+ Hsl: Hsl{300, 100, 42},
+ Name: "Magenta3",
+ },
+ {
+ Hex: "#d700ff",
+ Rgb: Rgb{215, 0, 255},
+ Hsl: Hsl{290.588235294118, 100, 50},
+ Name: "Magenta2",
+ },
+ {
+ Hex: "#d75f00",
+ Rgb: Rgb{215, 95, 0},
+ Hsl: Hsl{26.5116279069767, 100, 42},
+ Name: "DarkOrange3",
+ },
+ {
+ Hex: "#d75f5f",
+ Rgb: Rgb{215, 95, 95},
+ Hsl: Hsl{0, 60, 60},
+ Name: "IndianRed",
+ },
+ {
+ Hex: "#d75f87",
+ Rgb: Rgb{215, 95, 135},
+ Hsl: Hsl{340, 60, 60},
+ Name: "HotPink3",
+ },
+ {
+ Hex: "#d75faf",
+ Rgb: Rgb{215, 95, 175},
+ Hsl: Hsl{320, 60, 60},
+ Name: "HotPink2",
+ },
+ {
+ Hex: "#d75fd7",
+ Rgb: Rgb{215, 95, 215},
+ Hsl: Hsl{300, 60, 60},
+ Name: "Orchid",
+ },
+ {
+ Hex: "#d75fff",
+ Rgb: Rgb{215, 95, 255},
+ Hsl: Hsl{285, 100, 68},
+ Name: "MediumOrchid1",
+ },
+ {
+ Hex: "#d78700",
+ Rgb: Rgb{215, 135, 0},
+ Hsl: Hsl{37.6744186046512, 100, 42},
+ Name: "Orange3",
+ },
+ {
+ Hex: "#d7875f",
+ Rgb: Rgb{215, 135, 95},
+ Hsl: Hsl{20, 60, 60},
+ Name: "LightSalmon3",
+ },
+ {
+ Hex: "#d78787",
+ Rgb: Rgb{215, 135, 135},
+ Hsl: Hsl{0, 50, 68},
+ Name: "LightPink3",
+ },
+ {
+ Hex: "#d787af",
+ Rgb: Rgb{215, 135, 175},
+ Hsl: Hsl{330, 50, 68},
+ Name: "Pink3",
+ },
+ {
+ Hex: "#d787d7",
+ Rgb: Rgb{215, 135, 215},
+ Hsl: Hsl{300, 50, 68},
+ Name: "Plum3",
+ },
+ {
+ Hex: "#d787ff",
+ Rgb: Rgb{215, 135, 255},
+ Hsl: Hsl{280, 100, 76},
+ Name: "Violet",
+ },
+ {
+ Hex: "#d7af00",
+ Rgb: Rgb{215, 175, 0},
+ Hsl: Hsl{48.8372093023256, 100, 42},
+ Name: "Gold3",
+ },
+ {
+ Hex: "#d7af5f",
+ Rgb: Rgb{215, 175, 95},
+ Hsl: Hsl{40, 60, 60},
+ Name: "LightGoldenrod3",
+ },
+ {
+ Hex: "#d7af87",
+ Rgb: Rgb{215, 175, 135},
+ Hsl: Hsl{30, 50, 68},
+ Name: "Tan",
+ },
+ {
+ Hex: "#d7afaf",
+ Rgb: Rgb{215, 175, 175},
+ Hsl: Hsl{0, 33, 76},
+ Name: "MistyRose3",
+ },
+ {
+ Hex: "#d7afd7",
+ Rgb: Rgb{215, 175, 215},
+ Hsl: Hsl{300, 33, 76},
+ Name: "Thistle3",
+ },
+ {
+ Hex: "#d7afff",
+ Rgb: Rgb{215, 175, 255},
+ Hsl: Hsl{270, 100, 84},
+ Name: "Plum2",
+ },
+ {
+ Hex: "#d7d700",
+ Rgb: Rgb{215, 215, 0},
+ Hsl: Hsl{60, 100, 42},
+ Name: "Yellow3",
+ },
+ {
+ Hex: "#d7d75f",
+ Rgb: Rgb{215, 215, 95},
+ Hsl: Hsl{60, 60, 60},
+ Name: "Khaki3",
+ },
+ {
+ Hex: "#d7d787",
+ Rgb: Rgb{215, 215, 135},
+ Hsl: Hsl{60, 50, 68},
+ Name: "LightGoldenrod2",
+ },
+ {
+ Hex: "#d7d7af",
+ Rgb: Rgb{215, 215, 175},
+ Hsl: Hsl{60, 33, 76},
+ Name: "LightYellow3",
+ },
+ {
+ Hex: "#d7d7d7",
+ Rgb: Rgb{215, 215, 215},
+ Hsl: Hsl{0, 0, 84},
+ Name: "Grey84",
+ },
+ {
+ Hex: "#d7d7ff",
+ Rgb: Rgb{215, 215, 255},
+ Hsl: Hsl{240, 100, 92},
+ Name: "LightSteelBlue1",
+ },
+ {
+ Hex: "#d7ff00",
+ Rgb: Rgb{215, 255, 0},
+ Hsl: Hsl{69.4117647058823, 100, 50},
+ Name: "Yellow2",
+ },
+ {
+ Hex: "#d7ff5f",
+ Rgb: Rgb{215, 255, 95},
+ Hsl: Hsl{75, 100, 68},
+ Name: "DarkOliveGreen1",
+ },
+ {
+ Hex: "#d7ff87",
+ Rgb: Rgb{215, 255, 135},
+ Hsl: Hsl{80, 100, 76},
+ Name: "DarkOliveGreen1",
+ },
+ {
+ Hex: "#d7ffaf",
+ Rgb: Rgb{215, 255, 175},
+ Hsl: Hsl{90, 100, 84},
+ Name: "DarkSeaGreen1",
+ },
+ {
+ Hex: "#d7ffd7",
+ Rgb: Rgb{215, 255, 215},
+ Hsl: Hsl{120, 100, 92},
+ Name: "Honeydew2",
+ },
+ {
+ Hex: "#d7ffff",
+ Rgb: Rgb{215, 255, 255},
+ Hsl: Hsl{180, 100, 92},
+ Name: "LightCyan1",
+ },
+ {
+ Hex: "#ff0000",
+ Rgb: Rgb{255, 0, 0},
+ Hsl: Hsl{0, 100, 50},
+ Name: "Red1",
+ },
+ {
+ Hex: "#ff005f",
+ Rgb: Rgb{255, 0, 95},
+ Hsl: Hsl{337.647058823529, 100, 50},
+ Name: "DeepPink2",
+ },
+ {
+ Hex: "#ff0087",
+ Rgb: Rgb{255, 0, 135},
+ Hsl: Hsl{328.235294117647, 100, 50},
+ Name: "DeepPink1",
+ },
+ {
+ Hex: "#ff00af",
+ Rgb: Rgb{255, 0, 175},
+ Hsl: Hsl{318.823529411765, 100, 50},
+ Name: "DeepPink1",
+ },
+ {
+ Hex: "#ff00d7",
+ Rgb: Rgb{255, 0, 215},
+ Hsl: Hsl{309.411764705882, 100, 50},
+ Name: "Magenta2",
+ },
+ {
+ Hex: "#ff00ff",
+ Rgb: Rgb{255, 0, 255},
+ Hsl: Hsl{300, 100, 50},
+ Name: "Magenta1",
+ },
+ {
+ Hex: "#ff5f00",
+ Rgb: Rgb{255, 95, 0},
+ Hsl: Hsl{22.3529411764706, 100, 50},
+ Name: "OrangeRed1",
+ },
+ {
+ Hex: "#ff5f5f",
+ Rgb: Rgb{255, 95, 95},
+ Hsl: Hsl{0, 100, 68},
+ Name: "IndianRed1",
+ },
+ {
+ Hex: "#ff5f87",
+ Rgb: Rgb{255, 95, 135},
+ Hsl: Hsl{345, 100, 68},
+ Name: "IndianRed1",
+ },
+ {
+ Hex: "#ff5faf",
+ Rgb: Rgb{255, 95, 175},
+ Hsl: Hsl{330, 100, 68},
+ Name: "HotPink",
+ },
+ {
+ Hex: "#ff5fd7",
+ Rgb: Rgb{255, 95, 215},
+ Hsl: Hsl{315, 100, 68},
+ Name: "HotPink",
+ },
+ {
+ Hex: "#ff5fff",
+ Rgb: Rgb{255, 95, 255},
+ Hsl: Hsl{300, 100, 68},
+ Name: "MediumOrchid1",
+ },
+ {
+ Hex: "#ff8700",
+ Rgb: Rgb{255, 135, 0},
+ Hsl: Hsl{31.7647058823529, 100, 50},
+ Name: "DarkOrange",
+ },
+ {
+ Hex: "#ff875f",
+ Rgb: Rgb{255, 135, 95},
+ Hsl: Hsl{15, 100, 68},
+ Name: "Salmon1",
+ },
+ {
+ Hex: "#ff8787",
+ Rgb: Rgb{255, 135, 135},
+ Hsl: Hsl{0, 100, 76},
+ Name: "LightCoral",
+ },
+ {
+ Hex: "#ff87af",
+ Rgb: Rgb{255, 135, 175},
+ Hsl: Hsl{340, 100, 76},
+ Name: "PaleVioletRed1",
+ },
+ {
+ Hex: "#ff87d7",
+ Rgb: Rgb{255, 135, 215},
+ Hsl: Hsl{320, 100, 76},
+ Name: "Orchid2",
+ },
+ {
+ Hex: "#ff87ff",
+ Rgb: Rgb{255, 135, 255},
+ Hsl: Hsl{300, 100, 76},
+ Name: "Orchid1",
+ },
+ {
+ Hex: "#ffaf00",
+ Rgb: Rgb{255, 175, 0},
+ Hsl: Hsl{41.1764705882353, 100, 50},
+ Name: "Orange1",
+ },
+ {
+ Hex: "#ffaf5f",
+ Rgb: Rgb{255, 175, 95},
+ Hsl: Hsl{30, 100, 68},
+ Name: "SandyBrown",
+ },
+ {
+ Hex: "#ffaf87",
+ Rgb: Rgb{255, 175, 135},
+ Hsl: Hsl{20, 100, 76},
+ Name: "LightSalmon1",
+ },
+ {
+ Hex: "#ffafaf",
+ Rgb: Rgb{255, 175, 175},
+ Hsl: Hsl{0, 100, 84},
+ Name: "LightPink1",
+ },
+ {
+ Hex: "#ffafd7",
+ Rgb: Rgb{255, 175, 215},
+ Hsl: Hsl{330, 100, 84},
+ Name: "Pink1",
+ },
+ {
+ Hex: "#ffafff",
+ Rgb: Rgb{255, 175, 255},
+ Hsl: Hsl{300, 100, 84},
+ Name: "Plum1",
+ },
+ {
+ Hex: "#ffd700",
+ Rgb: Rgb{255, 215, 0},
+ Hsl: Hsl{50.5882352941176, 100, 50},
+ Name: "Gold1",
+ },
+ {
+ Hex: "#ffd75f",
+ Rgb: Rgb{255, 215, 95},
+ Hsl: Hsl{45, 100, 68},
+ Name: "LightGoldenrod2",
+ },
+ {
+ Hex: "#ffd787",
+ Rgb: Rgb{255, 215, 135},
+ Hsl: Hsl{40, 100, 76},
+ Name: "LightGoldenrod2",
+ },
+ {
+ Hex: "#ffd7af",
+ Rgb: Rgb{255, 215, 175},
+ Hsl: Hsl{30, 100, 84},
+ Name: "NavajoWhite1",
+ },
+ {
+ Hex: "#ffd7d7",
+ Rgb: Rgb{255, 215, 215},
+ Hsl: Hsl{0, 100, 92},
+ Name: "MistyRose1",
+ },
+ {
+ Hex: "#ffd7ff",
+ Rgb: Rgb{255, 215, 255},
+ Hsl: Hsl{300, 100, 92},
+ Name: "Thistle1",
+ },
+ {
+ Hex: "#ffff00",
+ Rgb: Rgb{255, 255, 0},
+ Hsl: Hsl{60, 100, 50},
+ Name: "Yellow1",
+ },
+ {
+ Hex: "#ffff5f",
+ Rgb: Rgb{255, 255, 95},
+ Hsl: Hsl{60, 100, 68},
+ Name: "LightGoldenrod1",
+ },
+ {
+ Hex: "#ffff87",
+ Rgb: Rgb{255, 255, 135},
+ Hsl: Hsl{60, 100, 76},
+ Name: "Khaki1",
+ },
+ {
+ Hex: "#ffffaf",
+ Rgb: Rgb{255, 255, 175},
+ Hsl: Hsl{60, 100, 84},
+ Name: "Wheat1",
+ },
+ {
+ Hex: "#ffffd7",
+ Rgb: Rgb{255, 255, 215},
+ Hsl: Hsl{60, 100, 92},
+ Name: "Cornsilk1",
+ },
+ {
+ Hex: "#ffffff",
+ Rgb: Rgb{255, 255, 255},
+ Hsl: Hsl{0, 0, 100},
+ Name: "Grey100",
+ },
+ {
+ Hex: "#080808",
+ Rgb: Rgb{8, 8, 8},
+ Hsl: Hsl{0, 0, 3},
+ Name: "Grey3",
+ },
+ {
+ Hex: "#121212",
+ Rgb: Rgb{18, 18, 18},
+ Hsl: Hsl{0, 0, 7},
+ Name: "Grey7",
+ },
+ {
+ Hex: "#1c1c1c",
+ Rgb: Rgb{28, 28, 28},
+ Hsl: Hsl{0, 0, 10},
+ Name: "Grey11",
+ },
+ {
+ Hex: "#262626",
+ Rgb: Rgb{38, 38, 38},
+ Hsl: Hsl{0, 0, 14},
+ Name: "Grey15",
+ },
+ {
+ Hex: "#303030",
+ Rgb: Rgb{48, 48, 48},
+ Hsl: Hsl{0, 0, 18},
+ Name: "Grey19",
+ },
+ {
+ Hex: "#3a3a3a",
+ Rgb: Rgb{58, 58, 58},
+ Hsl: Hsl{0, 0, 22},
+ Name: "Grey23",
+ },
+ {
+ Hex: "#444444",
+ Rgb: Rgb{68, 68, 68},
+ Hsl: Hsl{0, 0, 26},
+ Name: "Grey27",
+ },
+ {
+ Hex: "#4e4e4e",
+ Rgb: Rgb{78, 78, 78},
+ Hsl: Hsl{0, 0, 30},
+ Name: "Grey30",
+ },
+ {
+ Hex: "#585858",
+ Rgb: Rgb{88, 88, 88},
+ Hsl: Hsl{0, 0, 34},
+ Name: "Grey35",
+ },
+ {
+ Hex: "#626262",
+ Rgb: Rgb{98, 98, 98},
+ Hsl: Hsl{0, 0, 37},
+ Name: "Grey39",
+ },
+ {
+ Hex: "#6c6c6c",
+ Rgb: Rgb{108, 108, 108},
+ Hsl: Hsl{0, 0, 40},
+ Name: "Grey42",
+ },
+ {
+ Hex: "#767676",
+ Rgb: Rgb{118, 118, 118},
+ Hsl: Hsl{0, 0, 46},
+ Name: "Grey46",
+ },
+ {
+ Hex: "#808080",
+ Rgb: Rgb{128, 128, 128},
+ Hsl: Hsl{0, 0, 50},
+ Name: "Grey50",
+ },
+ {
+ Hex: "#8a8a8a",
+ Rgb: Rgb{138, 138, 138},
+ Hsl: Hsl{0, 0, 54},
+ Name: "Grey54",
+ },
+ {
+ Hex: "#949494",
+ Rgb: Rgb{148, 148, 148},
+ Hsl: Hsl{0, 0, 58},
+ Name: "Grey58",
+ },
+ {
+ Hex: "#9e9e9e",
+ Rgb: Rgb{158, 158, 158},
+ Hsl: Hsl{0, 0, 61},
+ Name: "Grey62",
+ },
+ {
+ Hex: "#a8a8a8",
+ Rgb: Rgb{168, 168, 168},
+ Hsl: Hsl{0, 0, 65},
+ Name: "Grey66",
+ },
+ {
+ Hex: "#b2b2b2",
+ Rgb: Rgb{178, 178, 178},
+ Hsl: Hsl{0, 0, 69},
+ Name: "Grey70",
+ },
+ {
+ Hex: "#bcbcbc",
+ Rgb: Rgb{188, 188, 188},
+ Hsl: Hsl{0, 0, 73},
+ Name: "Grey74",
+ },
+ {
+ Hex: "#c6c6c6",
+ Rgb: Rgb{198, 198, 198},
+ Hsl: Hsl{0, 0, 77},
+ Name: "Grey78",
+ },
+ {
+ Hex: "#d0d0d0",
+ Rgb: Rgb{208, 208, 208},
+ Hsl: Hsl{0, 0, 81},
+ Name: "Grey82",
+ },
+ {
+ Hex: "#dadada",
+ Rgb: Rgb{218, 218, 218},
+ Hsl: Hsl{0, 0, 85},
+ Name: "Grey85",
+ },
+ {
+ Hex: "#e4e4e4",
+ Rgb: Rgb{228, 228, 228},
+ Hsl: Hsl{0, 0, 89},
+ Name: "Grey89",
+ },
+ {
+ Hex: "#eeeeee",
+ Rgb: Rgb{238, 238, 238},
+ Hsl: Hsl{0, 0, 93},
+ Name: "Grey93",
+ },
+}
diff --git a/v2/pkg/menu/contextmenu.go b/v2/pkg/menu/contextmenu.go
new file mode 100644
index 000000000..e24b04067
--- /dev/null
+++ b/v2/pkg/menu/contextmenu.go
@@ -0,0 +1,13 @@
+package menu
+
+type ContextMenu struct {
+ ID string
+ Menu *Menu
+}
+
+func NewContextMenu(ID string, menu *Menu) *ContextMenu {
+ return &ContextMenu{
+ ID: ID,
+ Menu: menu,
+ }
+}
diff --git a/v2/pkg/menu/keys/keys.go b/v2/pkg/menu/keys/keys.go
new file mode 100644
index 000000000..961edab2d
--- /dev/null
+++ b/v2/pkg/menu/keys/keys.go
@@ -0,0 +1,104 @@
+package keys
+
+import (
+ "fmt"
+ "strings"
+)
+
+// Modifier is actually a string
+type Modifier string
+
+const (
+ // CmdOrCtrlKey represents Command on Mac and Control on other platforms
+ CmdOrCtrlKey Modifier = "cmdorctrl"
+ // OptionOrAltKey represents Option on Mac and Alt on other platforms
+ OptionOrAltKey Modifier = "optionoralt"
+ // ShiftKey represents the shift key on all systems
+ ShiftKey Modifier = "shift"
+ // SuperKey represents Command on Mac and the Windows key on the other platforms
+ // SuperKey Modifier = "super"
+ // ControlKey represents the control key on all systems
+ ControlKey Modifier = "ctrl"
+)
+
+var modifierMap = map[string]Modifier{
+ "cmdorctrl": CmdOrCtrlKey,
+ "optionoralt": OptionOrAltKey,
+ "shift": ShiftKey,
+ //"super": SuperKey,
+ "ctrl": ControlKey,
+}
+
+func parseModifier(text string) (*Modifier, error) {
+ lowertext := strings.ToLower(text)
+ result, valid := modifierMap[lowertext]
+ if !valid {
+ return nil, fmt.Errorf("'%s' is not a valid modifier", text)
+ }
+
+ return &result, nil
+}
+
+// Accelerator holds the keyboard shortcut for a menu item
+type Accelerator struct {
+ Key string
+ Modifiers []Modifier
+}
+
+// Key creates a standard key Accelerator
+func Key(key string) *Accelerator {
+ return &Accelerator{
+ Key: strings.ToLower(key),
+ }
+}
+
+// CmdOrCtrl creates a 'CmdOrCtrl' Accelerator
+func CmdOrCtrl(key string) *Accelerator {
+ return &Accelerator{
+ Key: strings.ToLower(key),
+ Modifiers: []Modifier{CmdOrCtrlKey},
+ }
+}
+
+// OptionOrAlt creates a 'OptionOrAlt' Accelerator
+func OptionOrAlt(key string) *Accelerator {
+ return &Accelerator{
+ Key: strings.ToLower(key),
+ Modifiers: []Modifier{OptionOrAltKey},
+ }
+}
+
+// Shift creates a 'Shift' Accelerator
+func Shift(key string) *Accelerator {
+ return &Accelerator{
+ Key: strings.ToLower(key),
+ Modifiers: []Modifier{ShiftKey},
+ }
+}
+
+// Control creates a 'Control' Accelerator
+func Control(key string) *Accelerator {
+ return &Accelerator{
+ Key: strings.ToLower(key),
+ Modifiers: []Modifier{ControlKey},
+ }
+}
+
+//
+//// Super creates a 'Super' Accelerator
+//func Super(key string) *Accelerator {
+// return &Accelerator{
+// Key: strings.ToLower(key),
+// Modifiers: []Modifier{SuperKey},
+// }
+//}
+
+// Combo creates an Accelerator with multiple Modifiers
+func Combo(key string, modifier1 Modifier, modifier2 Modifier, rest ...Modifier) *Accelerator {
+ result := &Accelerator{
+ Key: key,
+ Modifiers: []Modifier{modifier1, modifier2},
+ }
+ result.Modifiers = append(result.Modifiers, rest...)
+ return result
+}
diff --git a/v2/pkg/menu/keys/macmodifiers.go b/v2/pkg/menu/keys/macmodifiers.go
new file mode 100644
index 000000000..7da618b4e
--- /dev/null
+++ b/v2/pkg/menu/keys/macmodifiers.go
@@ -0,0 +1,26 @@
+package keys
+
+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.
+)
+
+var macModifierMap = map[Modifier]int{
+ CmdOrCtrlKey: NSEventModifierFlagCommand,
+ ControlKey: NSEventModifierFlagControl,
+ OptionOrAltKey: NSEventModifierFlagOption,
+ ShiftKey: NSEventModifierFlagShift,
+}
+
+func ToMacModifier(accelerator *Accelerator) int {
+ if accelerator == nil {
+ return 0
+ }
+ result := 0
+ for _, modifier := range accelerator.Modifiers {
+ result |= macModifierMap[modifier]
+ }
+ return result
+}
diff --git a/v2/pkg/menu/keys/macmodifiers_test.go b/v2/pkg/menu/keys/macmodifiers_test.go
new file mode 100644
index 000000000..8be1bd05a
--- /dev/null
+++ b/v2/pkg/menu/keys/macmodifiers_test.go
@@ -0,0 +1,31 @@
+package keys
+
+import "testing"
+
+func TestToMacModifier(t *testing.T) {
+
+ tests := []struct {
+ name string
+ accelerator *Accelerator
+ want int
+ }{
+ // TODO: Add test cases.
+ {"nil", nil, 0},
+ {"empty", &Accelerator{}, 0},
+ {"key", &Accelerator{Key: "p"}, 0},
+ {"cmd", CmdOrCtrl(""), NSEventModifierFlagCommand},
+ {"ctrl", Control(""), NSEventModifierFlagControl},
+ {"shift", Shift(""), NSEventModifierFlagShift},
+ {"option", OptionOrAlt(""), NSEventModifierFlagOption},
+ {"cmd+ctrl", Combo("", CmdOrCtrlKey, ControlKey), NSEventModifierFlagCommand | NSEventModifierFlagControl},
+ {"cmd+ctrl+shift", Combo("", CmdOrCtrlKey, ControlKey, ShiftKey), NSEventModifierFlagCommand | NSEventModifierFlagControl | NSEventModifierFlagShift},
+ {"cmd+ctrl+shift+option", Combo("", CmdOrCtrlKey, ControlKey, ShiftKey, OptionOrAltKey), NSEventModifierFlagCommand | NSEventModifierFlagControl | NSEventModifierFlagShift | NSEventModifierFlagOption},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := ToMacModifier(tt.accelerator); got != tt.want {
+ t.Errorf("ToMacModifier() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/v2/pkg/menu/keys/parser.go b/v2/pkg/menu/keys/parser.go
new file mode 100644
index 000000000..6e8e12376
--- /dev/null
+++ b/v2/pkg/menu/keys/parser.go
@@ -0,0 +1,87 @@
+package keys
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/leaanthony/slicer"
+)
+
+var namedKeys = slicer.String([]string{"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
+ if namedKeys.Contains(key) {
+ 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
+}
+
+func Parse(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")
+ }
+
+ // Keep track of modifiers we have processed
+ var modifiersProcessed slicer.StringSlicer
+
+ // 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 = 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)
+ }
+ // Needs to be unique
+ if modifiersProcessed.Contains(lowercaseComponent) {
+ return nil, fmt.Errorf("Modifier '%s' is defined twice for shortcut: %s", component, shortcut)
+ }
+
+ // Save this data
+ result.Modifiers = append(result.Modifiers, thisModifier)
+ modifiersProcessed.Add(lowercaseComponent)
+ }
+
+ return &result, nil
+}
diff --git a/v2/pkg/menu/keys/parser_test.go b/v2/pkg/menu/keys/parser_test.go
new file mode 100644
index 000000000..f63f22bed
--- /dev/null
+++ b/v2/pkg/menu/keys/parser_test.go
@@ -0,0 +1,37 @@
+package keys
+
+import (
+ "testing"
+
+ "github.com/matryer/is"
+)
+
+func TestParse(t *testing.T) {
+
+ i := is.New(t)
+
+ type args struct {
+ Input string
+ Expected *Accelerator
+ }
+
+ gooddata := []args{
+ {"CmdOrCtrl+A", CmdOrCtrl("A")},
+ {"SHIFT+.", Shift(".")},
+ {"CTRL+plus", Control("+")},
+ {"CTRL+SHIFT+escApe", Combo("escape", ControlKey, ShiftKey)},
+ {";", Key(";")},
+ {"OptionOrAlt+Page Down", OptionOrAlt("Page Down")},
+ }
+ for _, tt := range gooddata {
+ result, err := Parse(tt.Input)
+ i.NoErr(err)
+ i.Equal(result, tt.Expected)
+ }
+ baddata := []string{"CmdOrCrl+A", "SHIT+.", "CTL+plus", "CTRL+SHIF+esApe", "escap", "Sper+Tab", "OptionOrAlt"}
+ for _, d := range baddata {
+ result, err := Parse(d)
+ i.True(err != nil)
+ i.Equal(result, nil)
+ }
+}
diff --git a/v2/pkg/menu/keys/stringify.go b/v2/pkg/menu/keys/stringify.go
new file mode 100644
index 000000000..92498f5d4
--- /dev/null
+++ b/v2/pkg/menu/keys/stringify.go
@@ -0,0 +1,41 @@
+package keys
+
+import (
+ "strings"
+
+ "github.com/leaanthony/slicer"
+)
+
+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",
+ },
+}
+
+func Stringify(accelerator *Accelerator, platform string) string {
+ result := slicer.String()
+ for _, modifier := range accelerator.Modifiers {
+ result.Add(modifierStringMap[platform][modifier])
+ }
+ result.Deduplicate()
+ result.Add(strings.ToUpper(accelerator.Key))
+ return result.Join("+")
+}
diff --git a/v2/pkg/menu/keys/stringify_test.go b/v2/pkg/menu/keys/stringify_test.go
new file mode 100644
index 000000000..e6ba26221
--- /dev/null
+++ b/v2/pkg/menu/keys/stringify_test.go
@@ -0,0 +1,75 @@
+package keys
+
+import (
+ "strconv"
+ "testing"
+)
+
+func TestStringify(t *testing.T) {
+
+ const Windows = "windows"
+ const Mac = "darwin"
+ const Linux = "linux"
+ tests := []struct {
+ arg *Accelerator
+ want string
+ platform string
+ }{
+ // Single Keys
+ {Key("a"), "A", Windows},
+ {Key(""), "", Windows},
+ {Key("?"), "?", Windows},
+ {Key("a"), "A", Mac},
+ {Key(""), "", Mac},
+ {Key("?"), "?", Mac},
+ {Key("a"), "A", Linux},
+ {Key(""), "", Linux},
+ {Key("?"), "?", Linux},
+
+ // Single modifier
+ {Control("a"), "Ctrl+A", Windows},
+ {Control("a"), "Ctrl+A", Mac},
+ {Control("a"), "Ctrl+A", Linux},
+ {CmdOrCtrl("a"), "Ctrl+A", Windows},
+ {CmdOrCtrl("a"), "Cmd+A", Mac},
+ {CmdOrCtrl("a"), "Ctrl+A", Linux},
+ {Shift("a"), "Shift+A", Windows},
+ {Shift("a"), "Shift+A", Mac},
+ {Shift("a"), "Shift+A", Linux},
+ {OptionOrAlt("a"), "Alt+A", Windows},
+ {OptionOrAlt("a"), "Option+A", Mac},
+ {OptionOrAlt("a"), "Alt+A", Linux},
+ //{Super("a"), "Win+A", Windows},
+ //{Super("a"), "Cmd+A", Mac},
+ //{Super("a"), "Super+A", Linux},
+
+ // Dual Combo non duplicate
+ {Combo("a", ControlKey, OptionOrAltKey), "Ctrl+Alt+A", Windows},
+ {Combo("a", ControlKey, OptionOrAltKey), "Ctrl+Option+A", Mac},
+ {Combo("a", ControlKey, OptionOrAltKey), "Ctrl+Alt+A", Linux},
+ {Combo("a", CmdOrCtrlKey, OptionOrAltKey), "Ctrl+Alt+A", Windows},
+ {Combo("a", CmdOrCtrlKey, OptionOrAltKey), "Cmd+Option+A", Mac},
+ {Combo("a", CmdOrCtrlKey, OptionOrAltKey), "Ctrl+Alt+A", Linux},
+ {Combo("a", ShiftKey, OptionOrAltKey), "Shift+Alt+A", Windows},
+ {Combo("a", ShiftKey, OptionOrAltKey), "Shift+Option+A", Mac},
+ {Combo("a", ShiftKey, OptionOrAltKey), "Shift+Alt+A", Linux},
+ //{Combo("a", SuperKey, OptionOrAltKey), "Win+Alt+A", Windows},
+ //{Combo("a", SuperKey, OptionOrAltKey), "Cmd+Option+A", Mac},
+ //{Combo("a", SuperKey, OptionOrAltKey), "Super+Alt+A", Linux},
+
+ // Combo duplicate
+ {Combo("a", OptionOrAltKey, OptionOrAltKey), "Alt+A", Windows},
+ {Combo("a", OptionOrAltKey, OptionOrAltKey), "Option+A", Mac},
+ {Combo("a", OptionOrAltKey, OptionOrAltKey), "Alt+A", Linux},
+ //{Combo("a", OptionOrAltKey, SuperKey, OptionOrAltKey), "Alt+Win+A", Windows},
+ //{Combo("a", OptionOrAltKey, SuperKey, OptionOrAltKey), "Option+Cmd+A", Mac},
+ //{Combo("a", OptionOrAltKey, SuperKey, OptionOrAltKey), "Alt+Super+A", Linux},
+ }
+ for index, tt := range tests {
+ t.Run(strconv.Itoa(index), func(t *testing.T) {
+ if got := Stringify(tt.arg, tt.platform); got != tt.want {
+ t.Errorf("Stringify() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/v2/pkg/menu/mac.go b/v2/pkg/menu/mac.go
new file mode 100644
index 000000000..0889e5c42
--- /dev/null
+++ b/v2/pkg/menu/mac.go
@@ -0,0 +1,12 @@
+package menu
+
+/*
+// DefaultMacMenu returns a default menu including the default
+// Application and Edit menus. Use `.Append()` to add to it.
+func DefaultMacMenu() *Menu {
+ return NewMenuFromItems(
+ AppMenu(),
+ EditMenu(),
+ )
+}
+*/
diff --git a/v2/pkg/menu/menu.go b/v2/pkg/menu/menu.go
new file mode 100644
index 000000000..86acbd1d0
--- /dev/null
+++ b/v2/pkg/menu/menu.go
@@ -0,0 +1,75 @@
+package menu
+
+import "github.com/wailsapp/wails/v2/pkg/menu/keys"
+
+type Menu struct {
+ Items []*MenuItem
+}
+
+func NewMenu() *Menu {
+ return &Menu{}
+}
+
+func (m *Menu) Append(item *MenuItem) {
+ m.Items = append(m.Items, item)
+}
+
+// Merge will append the items in the given menu
+// into this menu
+func (m *Menu) Merge(menu *Menu) {
+ m.Items = append(m.Items, menu.Items...)
+}
+
+// AddText adds a TextMenu item to the menu
+func (m *Menu) AddText(label string, accelerator *keys.Accelerator, click Callback) *MenuItem {
+ item := Text(label, accelerator, click)
+ m.Append(item)
+ return item
+}
+
+// AddCheckbox adds a CheckboxMenu item to the menu
+func (m *Menu) AddCheckbox(label string, checked bool, accelerator *keys.Accelerator, click Callback) *MenuItem {
+ item := Checkbox(label, checked, accelerator, click)
+ m.Append(item)
+ return item
+}
+
+// AddRadio adds a radio item to the menu
+func (m *Menu) AddRadio(label string, checked bool, accelerator *keys.Accelerator, click Callback) *MenuItem {
+ item := Radio(label, checked, accelerator, click)
+ m.Append(item)
+ return item
+}
+
+// AddSeparator adds a separator to the menu
+func (m *Menu) AddSeparator() {
+ item := Separator()
+ m.Append(item)
+}
+
+func (m *Menu) AddSubmenu(label string) *Menu {
+ submenu := NewMenu()
+ item := SubMenu(label, submenu)
+ m.Append(item)
+ return submenu
+}
+
+func (m *Menu) Prepend(item *MenuItem) {
+ m.Items = append([]*MenuItem{item}, m.Items...)
+}
+
+func NewMenuFromItems(first *MenuItem, rest ...*MenuItem) *Menu {
+ result := NewMenu()
+ result.Append(first)
+ for _, item := range rest {
+ result.Append(item)
+ }
+
+ return result
+}
+
+func (m *Menu) setParent(menuItem *MenuItem) {
+ for _, item := range m.Items {
+ item.parent = menuItem
+ }
+}
diff --git a/v2/pkg/menu/menuitem.go b/v2/pkg/menu/menuitem.go
new file mode 100644
index 000000000..bffc522d8
--- /dev/null
+++ b/v2/pkg/menu/menuitem.go
@@ -0,0 +1,329 @@
+package menu
+
+import (
+ "sync"
+
+ "github.com/wailsapp/wails/v2/pkg/menu/keys"
+)
+
+// MenuItem represents a menuitem contained in a menu
+type MenuItem struct {
+ // Label is what appears as the menu text
+ Label string
+ // Role is a predefined menu type
+ Role Role
+ // Accelerator holds a representation of a key binding
+ Accelerator *keys.Accelerator
+ // Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu
+ Type Type
+ // Disabled makes the item unselectable
+ Disabled bool
+ // Hidden ensures that the item is not shown in the menu
+ Hidden bool
+ // Checked indicates if the item is selected (used by Checkbox and Radio types only)
+ Checked bool
+ // SubMenu contains a list of menu items that will be shown as a submenu
+ // SubMenu []*MenuItem `json:"SubMenu,omitempty"`
+ SubMenu *Menu
+
+ // Callback function when menu clicked
+ Click Callback
+ /*
+ // Text Colour
+ RGBA string
+
+ // Font
+ FontSize int
+ FontName string
+
+ // Image - base64 image data
+ Image string
+
+ // MacTemplateImage indicates that on a Mac, this image is a template image
+ MacTemplateImage bool
+
+ // MacAlternate indicates that this item is an alternative to the previous menu item
+ MacAlternate bool
+
+ // Tooltip
+ Tooltip string
+ */
+ // This holds the menu item's parent.
+ parent *MenuItem
+
+ // Used for locking when removing elements
+ removeLock sync.Mutex
+}
+
+// Parent returns the parent of the menu item.
+// If it is a top level menu then it returns nil.
+func (m *MenuItem) Parent() *MenuItem {
+ return m.parent
+}
+
+// Append will attempt to append the given menu item to
+// this item's submenu items. If this menu item is not a
+// submenu, then this method will not add the item and
+// simply return false.
+func (m *MenuItem) Append(item *MenuItem) bool {
+ if !m.isSubMenu() {
+ return false
+ }
+ item.parent = m
+ m.SubMenu.Append(item)
+ return true
+}
+
+// Prepend will attempt to prepend the given menu item to
+// this item's submenu items. If this menu item is not a
+// submenu, then this method will not add the item and
+// simply return false.
+func (m *MenuItem) Prepend(item *MenuItem) bool {
+ if !m.isSubMenu() {
+ return false
+ }
+ item.parent = m
+ m.SubMenu.Prepend(item)
+ return true
+}
+
+func (m *MenuItem) Remove() {
+ // Iterate my parent's children
+ m.Parent().removeChild(m)
+}
+
+func (m *MenuItem) removeChild(item *MenuItem) {
+ m.removeLock.Lock()
+ for index, child := range m.SubMenu.Items {
+ if item == child {
+ m.SubMenu.Items = append(m.SubMenu.Items[:index], m.SubMenu.Items[index+1:]...)
+ }
+ }
+ m.removeLock.Unlock()
+}
+
+// InsertAfter attempts to add the given item after this item in the parent
+// menu. If there is no parent menu (we are a top level menu) then false is
+// returned
+func (m *MenuItem) InsertAfter(item *MenuItem) bool {
+ // We need to find my parent
+ if m.parent == nil {
+ return false
+ }
+
+ // Get my parent to insert the item
+ return m.parent.insertNewItemAfterGivenItem(m, item)
+}
+
+// InsertBefore attempts to add the given item before this item in the parent
+// menu. If there is no parent menu (we are a top level menu) then false is
+// returned
+func (m *MenuItem) InsertBefore(item *MenuItem) bool {
+ // We need to find my parent
+ if m.parent == nil {
+ return false
+ }
+
+ // Get my parent to insert the item
+ return m.parent.insertNewItemBeforeGivenItem(m, item)
+}
+
+// insertNewItemAfterGivenItem will insert the given item after the given target
+// in this item's submenu. If we are not a submenu,
+// then something bad has happened :/
+func (m *MenuItem) insertNewItemAfterGivenItem(target *MenuItem,
+ newItem *MenuItem,
+) bool {
+ if !m.isSubMenu() {
+ return false
+ }
+
+ // Find the index of the target
+ targetIndex := m.getItemIndex(target)
+ if targetIndex == -1 {
+ return false
+ }
+
+ // Insert element into slice
+ return m.insertItemAtIndex(targetIndex+1, newItem)
+}
+
+// insertNewItemBeforeGivenItem will insert the given item before the given
+// target in this item's submenu. If we are not a submenu, then something bad
+// has happened :/
+func (m *MenuItem) insertNewItemBeforeGivenItem(target *MenuItem,
+ newItem *MenuItem,
+) bool {
+ if !m.isSubMenu() {
+ return false
+ }
+
+ // Find the index of the target
+ targetIndex := m.getItemIndex(target)
+ if targetIndex == -1 {
+ return false
+ }
+
+ // Insert element into slice
+ return m.insertItemAtIndex(targetIndex, newItem)
+}
+
+func (m *MenuItem) isSubMenu() bool {
+ return m.Type == SubmenuType
+}
+
+// getItemIndex returns the index of the given target relative to this menu
+func (m *MenuItem) getItemIndex(target *MenuItem) int {
+ // This should only be called on submenus
+ if !m.isSubMenu() {
+ return -1
+ }
+
+ // hunt down that bad boy
+ for index, item := range m.SubMenu.Items {
+ if item == target {
+ return index
+ }
+ }
+
+ return -1
+}
+
+// insertItemAtIndex attempts to insert the given item into the submenu at
+// the given index
+// Credit: https://stackoverflow.com/a/61822301
+func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool {
+ // If index is OOB, return false
+ if index > len(m.SubMenu.Items) {
+ return false
+ }
+
+ // Save parent reference
+ target.parent = m
+
+ // If index is last item, then just regular append
+ if index == len(m.SubMenu.Items) {
+ m.SubMenu.Items = append(m.SubMenu.Items, target)
+ return true
+ }
+
+ m.SubMenu.Items = append(m.SubMenu.Items[:index+1], m.SubMenu.Items[index:]...)
+ m.SubMenu.Items[index] = target
+ return true
+}
+
+func (m *MenuItem) SetLabel(name string) {
+ if m.Label == name {
+ return
+ }
+ m.Label = name
+}
+
+func (m *MenuItem) IsSeparator() bool {
+ return m.Type == SeparatorType
+}
+
+func (m *MenuItem) IsCheckbox() bool {
+ return m.Type == CheckboxType
+}
+
+func (m *MenuItem) Disable() *MenuItem {
+ m.Disabled = true
+ return m
+}
+
+func (m *MenuItem) Enable() *MenuItem {
+ m.Disabled = false
+ return m
+}
+
+func (m *MenuItem) OnClick(click Callback) *MenuItem {
+ m.Click = click
+ return m
+}
+
+func (m *MenuItem) SetAccelerator(acc *keys.Accelerator) *MenuItem {
+ m.Accelerator = acc
+ return m
+}
+
+func (m *MenuItem) SetChecked(value bool) *MenuItem {
+ m.Checked = value
+ if m.Type != RadioType {
+ m.Type = CheckboxType
+ }
+ return m
+}
+
+func (m *MenuItem) Hide() *MenuItem {
+ m.Hidden = true
+ return m
+}
+
+func (m *MenuItem) Show() *MenuItem {
+ m.Hidden = false
+ return m
+}
+
+func (m *MenuItem) IsRadio() bool {
+ return m.Type == RadioType
+}
+
+func Label(label string) *MenuItem {
+ return &MenuItem{
+ Type: TextType,
+ Label: label,
+ }
+}
+
+// Text is a helper to create basic Text menu items
+func Text(label string, accelerator *keys.Accelerator, click Callback) *MenuItem {
+ return &MenuItem{
+ Label: label,
+ Type: TextType,
+ Accelerator: accelerator,
+ Click: click,
+ }
+}
+
+// Separator provides a menu separator
+func Separator() *MenuItem {
+ return &MenuItem{
+ Type: SeparatorType,
+ }
+}
+
+// Radio is a helper to create basic Radio menu items with an accelerator
+func Radio(label string, selected bool, accelerator *keys.Accelerator, click Callback) *MenuItem {
+ return &MenuItem{
+ Label: label,
+ Type: RadioType,
+ Checked: selected,
+ Accelerator: accelerator,
+ Click: click,
+ }
+}
+
+// Checkbox is a helper to create basic Checkbox menu items
+func Checkbox(label string, checked bool, accelerator *keys.Accelerator, click Callback) *MenuItem {
+ return &MenuItem{
+ Label: label,
+ Type: CheckboxType,
+ Checked: checked,
+ Accelerator: accelerator,
+ Click: click,
+ }
+}
+
+// SubMenu is a helper to create Submenus
+func SubMenu(label string, menu *Menu) *MenuItem {
+ result := &MenuItem{
+ Label: label,
+ SubMenu: menu,
+ Type: SubmenuType,
+ }
+
+ menu.setParent(result)
+
+ return result
+}
diff --git a/v2/pkg/menu/menuroles.go b/v2/pkg/menu/menuroles.go
new file mode 100644
index 000000000..bcc0657fc
--- /dev/null
+++ b/v2/pkg/menu/menuroles.go
@@ -0,0 +1,214 @@
+// Package menu provides all the functions and structs related to menus in a Wails application.
+// Heavily inspired by Electron (c) 2013-2020 Github Inc.
+// Electron License: https://github.com/electron/electron/blob/master/LICENSE
+package menu
+
+// Role is a type to identify menu roles
+type Role int
+
+// These constants need to be kept in sync with `v2/internal/frontend/desktop/darwin/Role.h`
+const (
+ AppMenuRole Role = 1
+ EditMenuRole = 2
+ WindowMenuRole = 3
+ // AboutRole Role = "about"
+ // UndoRole Role = "undo"
+ // RedoRole Role = "redo"
+ // CutRole Role = "cut"
+ // CopyRole Role = "copy"
+ // PasteRole Role = "paste"
+ // PasteAndMatchStyleRole Role = "pasteAndMatchStyle"
+ // SelectAllRole Role = "selectAll"
+ // DeleteRole Role = "delete"
+ // MinimizeRole Role = "minimize"
+ // QuitRole Role = "quit"
+ // TogglefullscreenRole Role = "togglefullscreen"
+ // FileMenuRole Role = "fileMenu"
+ // ViewMenuRole Role = "viewMenu"
+ // WindowMenuRole Role = "windowMenu"
+ // HideRole Role = "hide"
+ // HideOthersRole Role = "hideOthers"
+ // UnhideRole Role = "unhide"
+ // FrontRole Role = "front"
+ // ZoomRole Role = "zoom"
+ // WindowSubMenuRole Role = "windowSubMenu"
+ // HelpSubMenuRole Role = "helpSubMenu"
+ // SeparatorItemRole Role = "separatorItem"
+)
+
+/*
+// About provides a MenuItem with the About role
+func About() *MenuItem {
+ return &MenuItem{
+ Role: AboutRole,
+ }
+}
+
+// Undo provides a MenuItem with the Undo role
+func Undo() *MenuItem {
+ return &MenuItem{
+ Role: UndoRole,
+ }
+}
+
+// Redo provides a MenuItem with the Redo role
+func Redo() *MenuItem {
+ return &MenuItem{
+ Role: RedoRole,
+ }
+}
+
+// Cut provides a MenuItem with the Cut role
+func Cut() *MenuItem {
+ return &MenuItem{
+ Role: CutRole,
+ }
+}
+
+// Copy provides a MenuItem with the Copy role
+func Copy() *MenuItem {
+ return &MenuItem{
+ Role: CopyRole,
+ }
+}
+
+// Paste provides a MenuItem with the Paste role
+func Paste() *MenuItem {
+ return &MenuItem{
+ Role: PasteRole,
+ }
+}
+
+// PasteAndMatchStyle provides a MenuItem with the PasteAndMatchStyle role
+func PasteAndMatchStyle() *MenuItem {
+ return &MenuItem{
+ Role: PasteAndMatchStyleRole,
+ }
+}
+
+// SelectAll provides a MenuItem with the SelectAll role
+func SelectAll() *MenuItem {
+ return &MenuItem{
+ Role: SelectAllRole,
+ }
+}
+
+// Delete provides a MenuItem with the Delete role
+func Delete() *MenuItem {
+ return &MenuItem{
+ Role: DeleteRole,
+ }
+}
+
+// Minimize provides a MenuItem with the Minimize role
+func Minimize() *MenuItem {
+ return &MenuItem{
+ Role: MinimizeRole,
+ }
+}
+
+// Quit provides a MenuItem with the Quit role
+func Quit() *MenuItem {
+ return &MenuItem{
+ Role: QuitRole,
+ }
+}
+
+// ToggleFullscreen provides a MenuItem with the ToggleFullscreen role
+func ToggleFullscreen() *MenuItem {
+ return &MenuItem{
+ Role: TogglefullscreenRole,
+ }
+}
+
+// FileMenu provides a MenuItem with the whole default "File" menu (Close / Quit)
+func FileMenu() *MenuItem {
+ return &MenuItem{
+ Role: FileMenuRole,
+ }
+}
+*/
+
+// EditMenu provides a MenuItem with the whole default "Edit" menu (Undo, Copy, etc.).
+func EditMenu() *MenuItem {
+ return &MenuItem{
+ Role: EditMenuRole,
+ }
+}
+
+/*
+// ViewMenu provides a MenuItem with the whole default "View" menu (Reload, Toggle Developer Tools, etc.)
+func ViewMenu() *MenuItem {
+ return &MenuItem{
+ Role: ViewMenuRole,
+ }
+}
+*/
+
+// WindowMenu provides a MenuItem with the whole default "Window" menu (Minimize, Zoom, etc.).
+// On MacOS currently all options in there won't work if the window is frameless.
+func WindowMenu() *MenuItem {
+ return &MenuItem{
+ Role: WindowMenuRole,
+ }
+}
+
+// These roles are Mac only
+
+// AppMenu provides a MenuItem with the whole default "App" menu (About, Services, etc.)
+func AppMenu() *MenuItem {
+ return &MenuItem{
+ Role: AppMenuRole,
+ }
+}
+
+/*
+// Hide provides a MenuItem that maps to the hide action.
+func Hide() *MenuItem {
+ return &MenuItem{
+ Role: HideRole,
+ }
+}
+
+// HideOthers provides a MenuItem that maps to the hideOtherApplications action.
+func HideOthers() *MenuItem {
+ return &MenuItem{
+ Role: HideOthersRole,
+ }
+}
+
+// UnHide provides a MenuItem that maps to the unHideAllApplications action.
+func UnHide() *MenuItem {
+ return &MenuItem{
+ Role: UnhideRole,
+ }
+}
+
+// Front provides a MenuItem that maps to the arrangeInFront action.
+func Front() *MenuItem {
+ return &MenuItem{
+ Role: FrontRole,
+ }
+}
+
+// Zoom provides a MenuItem that maps to the performZoom action.
+func Zoom() *MenuItem {
+ return &MenuItem{
+ Role: ZoomRole,
+ }
+}
+
+// WindowSubMenu provides a MenuItem with the "Window" submenu.
+func WindowSubMenu() *MenuItem {
+ return &MenuItem{
+ Role: WindowSubMenuRole,
+ }
+}
+
+// HelpSubMenu provides a MenuItem with the "Help" submenu.
+func HelpSubMenu() *MenuItem {
+ return &MenuItem{
+ Role: HelpSubMenuRole,
+ }
+}
+*/
diff --git a/v2/pkg/menu/styledlabel.go b/v2/pkg/menu/styledlabel.go
new file mode 100644
index 000000000..1e996b971
--- /dev/null
+++ b/v2/pkg/menu/styledlabel.go
@@ -0,0 +1,261 @@
+package menu
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+type TextStyle int
+
+const (
+ Bold TextStyle = 1 << 0
+ Faint TextStyle = 1 << 1
+ Italic TextStyle = 1 << 2
+ Blinking TextStyle = 1 << 3
+ Inversed TextStyle = 1 << 4
+ Invisible TextStyle = 1 << 5
+ Underlined TextStyle = 1 << 6
+ Strikethrough TextStyle = 1 << 7
+)
+
+type StyledText struct {
+ Label string
+ FgCol *Col
+ BgCol *Col
+ Style TextStyle
+}
+
+func (s *StyledText) Bold() bool {
+ return s.Style&Bold == Bold
+}
+
+func (s *StyledText) Faint() bool {
+ return s.Style&Faint == Faint
+}
+
+func (s *StyledText) Italic() bool {
+ return s.Style&Italic == Italic
+}
+
+func (s *StyledText) Blinking() bool {
+ return s.Style&Blinking == Blinking
+}
+
+func (s *StyledText) Inversed() bool {
+ return s.Style&Inversed == Inversed
+}
+
+func (s *StyledText) Invisible() bool {
+ return s.Style&Invisible == Invisible
+}
+
+func (s *StyledText) Underlined() bool {
+ return s.Style&Underlined == Underlined
+}
+
+func (s *StyledText) Strikethrough() bool {
+ return s.Style&Strikethrough == Strikethrough
+}
+
+var ansiColorMap = map[string]map[string]*Col{
+ "Normal": {
+ "30": Cols[0],
+ "31": Cols[1],
+ "32": Cols[2],
+ "33": Cols[3],
+ "34": Cols[4],
+ "35": Cols[5],
+ "36": Cols[6],
+ "37": Cols[7],
+ },
+ "Bold": {
+ "30": Cols[8],
+ "31": Cols[9],
+ "32": Cols[10],
+ "33": Cols[11],
+ "34": Cols[12],
+ "35": Cols[13],
+ "36": Cols[14],
+ "37": Cols[15],
+ },
+ "Faint": {
+ "30": Cols[0],
+ "31": Cols[1],
+ "32": Cols[2],
+ "33": Cols[3],
+ "34": Cols[4],
+ "35": Cols[5],
+ "36": Cols[6],
+ "37": Cols[7],
+ },
+}
+
+func ParseANSI(input string) ([]*StyledText, error) {
+ var result []*StyledText
+ invalid := fmt.Errorf("invalid ansi string")
+ missingTerminator := fmt.Errorf("missing escape terminator 'm'")
+ invalidTrueColorSequence := fmt.Errorf("invalid TrueColor sequence")
+ invalid256ColSequence := fmt.Errorf("invalid 256 colour sequence")
+ index := 0
+ var currentStyledText *StyledText = &StyledText{}
+
+ if len(input) == 0 {
+ return nil, invalid
+ }
+
+ for {
+ // Read all chars to next escape code
+ esc := strings.Index(input, "\033[")
+
+ // If no more esc chars, save what's left and return
+ if esc == -1 {
+ text := input[index:]
+ if len(text) > 0 {
+ currentStyledText.Label = text
+ result = append(result, currentStyledText)
+ }
+ return result, nil
+ }
+ label := input[:esc]
+ if len(label) > 0 {
+ currentStyledText.Label = label
+ result = append(result, currentStyledText)
+ currentStyledText = &StyledText{
+ Label: "",
+ FgCol: currentStyledText.FgCol,
+ BgCol: currentStyledText.BgCol,
+ Style: currentStyledText.Style,
+ }
+ }
+ input = input[esc:]
+ // skip
+ input = input[2:]
+
+ // Read in params
+ endesc := strings.Index(input, "m")
+ if endesc == -1 {
+ return nil, missingTerminator
+ }
+ paramText := input[:endesc]
+ input = input[endesc+1:]
+ params := strings.Split(paramText, ";")
+ colourMap := ansiColorMap["Normal"]
+ skip := 0
+ for index, param := range params {
+ if skip > 0 {
+ skip--
+ continue
+ }
+ switch param {
+ case "0":
+ // Reset styles
+ if len(params) == 1 {
+ if len(currentStyledText.Label) > 0 {
+ result = append(result, currentStyledText)
+ currentStyledText = &StyledText{
+ Label: "",
+ FgCol: currentStyledText.FgCol,
+ BgCol: currentStyledText.BgCol,
+ Style: currentStyledText.Style,
+ }
+ continue
+ }
+ }
+ currentStyledText.Style = 0
+ currentStyledText.FgCol = nil
+ currentStyledText.BgCol = nil
+ case "1":
+ // Bold
+ colourMap = ansiColorMap["Bold"]
+ currentStyledText.Style |= Bold
+ case "2":
+ // Dim/Feint
+ colourMap = ansiColorMap["Faint"]
+ currentStyledText.Style |= Faint
+ case "3":
+ // Italic
+ currentStyledText.Style |= Italic
+ case "4":
+ // Underlined
+ currentStyledText.Style |= Underlined
+ case "5":
+ // Blinking
+ currentStyledText.Style |= Blinking
+ case "7":
+ // Inverse
+ currentStyledText.Style |= Inversed
+ case "8":
+ // Invisible
+ currentStyledText.Style |= Invisible
+ case "9":
+ // Strikethrough
+ currentStyledText.Style |= Strikethrough
+ case "30", "31", "32", "33", "34", "35", "36", "37":
+ currentStyledText.FgCol = colourMap[param]
+ case "40", "41", "42", "43", "44", "45", "46", "47":
+ currentStyledText.BgCol = colourMap[param]
+ case "38", "48":
+ if len(params)-index < 2 {
+ return nil, invalid
+ }
+ // 256 colours
+ if params[index+1] == "5" {
+ skip = 2
+ colIndexText := params[index+2]
+ colIndex, err := strconv.Atoi(colIndexText)
+ if err != nil {
+ return nil, invalid256ColSequence
+ }
+ if colIndex < 0 || colIndex > 255 {
+ return nil, invalid256ColSequence
+ }
+ if param == "38" {
+ currentStyledText.FgCol = Cols[colIndex]
+ continue
+ }
+ currentStyledText.BgCol = Cols[colIndex]
+ continue
+ }
+ // we must have 4 params left
+ if len(params)-index < 4 {
+ return nil, invalidTrueColorSequence
+ }
+ if params[index+1] != "2" {
+ return nil, invalidTrueColorSequence
+ }
+ var r, g, b uint8
+ ri, err := strconv.Atoi(params[index+2])
+ if err != nil {
+ return nil, invalidTrueColorSequence
+ }
+ gi, err := strconv.Atoi(params[index+3])
+ if err != nil {
+ return nil, invalidTrueColorSequence
+ }
+ bi, err := strconv.Atoi(params[index+4])
+ if err != nil {
+ return nil, invalidTrueColorSequence
+ }
+ if bi > 255 || gi > 255 || ri > 255 {
+ return nil, invalidTrueColorSequence
+ }
+ if bi < 0 || gi < 0 || ri < 0 {
+ return nil, invalidTrueColorSequence
+ }
+ r = uint8(ri)
+ g = uint8(gi)
+ b = uint8(bi)
+ skip = 4
+ colvalue := fmt.Sprintf("#%02x%02x%02x", r, g, b)
+ if param == "38" {
+ currentStyledText.FgCol = &Col{Hex: colvalue, Rgb: Rgb{r, g, b}}
+ continue
+ }
+ currentStyledText.BgCol = &Col{Hex: colvalue}
+ default:
+ return nil, invalid
+ }
+ }
+ }
+}
diff --git a/v2/pkg/menu/styledlabel_test.go b/v2/pkg/menu/styledlabel_test.go
new file mode 100644
index 000000000..1c43480e8
--- /dev/null
+++ b/v2/pkg/menu/styledlabel_test.go
@@ -0,0 +1,197 @@
+package menu
+
+import (
+ "testing"
+
+ "github.com/matryer/is"
+)
+
+func TestParseAnsi16SingleColour(t *testing.T) {
+ is := is.New(t)
+ tests := []struct {
+ name string
+ input string
+ wantText string
+ wantColor string
+ wantErr bool
+ }{
+ {"No formatting", "Hello World", "Hello World", "", false},
+ {"Black", "\u001b[0;30mHello World\033[0m", "Hello World", "Black", false},
+ {"Red", "\u001b[0;31mHello World\033[0m", "Hello World", "Maroon", false},
+ {"Green", "\u001b[0;32mHello World\033[0m", "Hello World", "Green", false},
+ {"Yellow", "\u001b[0;33m😀\033[0m", "😀", "Olive", false},
+ {"Blue", "\u001b[0;34m123\033[0m", "123", "Navy", false},
+ {"Purple", "\u001b[0;35m👩🏽🔧\u001B[0m", "👩🏽🔧", "Purple", false},
+ {"Cyan", "\033[0;36m😀\033[0m", "😀", "Teal", false},
+ {"White", "\u001b[0;37m[0;37m\033[0m", "[0;37m", "Silver", false},
+ {"Black Bold", "\u001b[1;30mHello World\033[0m", "Hello World", "Grey", false},
+ {"Red Bold", "\u001b[1;31mHello World\033[0m", "Hello World", "Red", false},
+ {"Green Bold", "\u001b[1;32mTest\033[0m", "Test", "Lime", false},
+ {"Yellow Bold", "\u001b[1;33m😀\033[0m", "😀", "Yellow", false},
+ {"Blue Bold", "\u001b[1;34m123\033[0m", "123", "Blue", false},
+ {"Purple Bold", "\u001b[1;35m👩🏽🔧\u001B[0m", "👩🏽🔧", "Fuchsia", false},
+ {"Cyan Bold", "\033[1;36m😀\033[0m", "😀", "Aqua", false},
+ {"White Bold", "\u001b[1;37m[0;37m\033[0m", "[0;37m", "White", false},
+ {"Blank", "", "", "", true},
+ {"Emoji", "😀👩🏽🔧", "😀👩🏽🔧", "", false},
+ {"Spaces", " ", " ", "", false},
+ {"Bad code", "\u001b[1 ", "", "", true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParseANSI(tt.input)
+ is.Equal(err != nil, tt.wantErr)
+ expectedLength := 1
+ if tt.wantErr {
+ expectedLength = 0
+ }
+ is.Equal(len(got), expectedLength)
+ if expectedLength == 1 {
+ if len(tt.wantColor) > 0 {
+ is.True(got[0].FgCol != nil)
+ is.Equal(got[0].FgCol.Name, tt.wantColor)
+ }
+ }
+ })
+ }
+}
+
+func TestParseAnsi16MultiColour(t *testing.T) {
+ is := is.New(t)
+ tests := []struct {
+ name string
+ input string
+ want []*StyledText
+ wantErr bool
+ }{
+ {"Black & Red", "\u001B[0;30mHello World\u001B[0m\u001B[0;31mHello World\u001B[0m", []*StyledText{
+ {Label: "Hello World", FgCol: &Col{Name: "Black"}},
+ {Label: "Hello World", FgCol: &Col{Name: "Maroon"}},
+ }, false},
+ {"Text then Black & Red", "This is great!\u001B[0;30mHello World\u001B[0m\u001B[0;31mHello World\u001B[0m", []*StyledText{
+ {Label: "This is great!"},
+ {Label: "Hello World", FgCol: &Col{Name: "Black"}},
+ {Label: "Hello World", FgCol: &Col{Name: "Maroon"}},
+ }, false},
+ {"Text Reset then Black & Red", "This is great!\u001B[0m\u001B[0;30mHello World\u001B[0m\u001B[0;31mHello World\u001B[0m", []*StyledText{
+ {Label: "This is great!"},
+ {Label: "Hello World", FgCol: &Col{Name: "Black"}},
+ {Label: "Hello World", FgCol: &Col{Name: "Maroon"}},
+ }, false},
+ {"Black & Red no reset", "\u001B[0;30mHello World\u001B[0;31mHello World", []*StyledText{
+ {Label: "Hello World", FgCol: &Col{Name: "Black"}},
+ {Label: "Hello World", FgCol: &Col{Name: "Maroon"}},
+ }, false},
+ {"Black,space,Red", "\u001B[0;30mHello World\u001B[0m \u001B[0;31mHello World\u001B[0m", []*StyledText{
+ {Label: "Hello World", FgCol: &Col{Name: "Black"}},
+ {Label: " "},
+ {Label: "Hello World", FgCol: &Col{Name: "Maroon"}},
+ }, false},
+ {"Black,Red,Blue,Green underlined", "\033[4;30mBlack\u001B[0m\u001B[4;31mRed\u001B[0m\u001B[4;34mBlue\u001B[0m\u001B[4;32mGreen\u001B[0m", []*StyledText{
+ {Label: "Black", FgCol: &Col{Name: "Black"}, Style: Underlined},
+ {Label: "Red", FgCol: &Col{Name: "Maroon"}, Style: Underlined},
+ {Label: "Blue", FgCol: &Col{Name: "Navy"}, Style: Underlined},
+ {Label: "Green", FgCol: &Col{Name: "Green"}, Style: Underlined},
+ }, false},
+ {"Black,Red,Blue,Green bold", "\033[1;30mBlack\u001B[0m\u001B[1;31mRed\u001B[0m\u001B[1;34mBlue\u001B[0m\u001B[1;32mGreen\u001B[0m", []*StyledText{
+ {Label: "Black", FgCol: &Col{Name: "Grey"}, Style: Bold},
+ {Label: "Red", FgCol: &Col{Name: "Red"}, Style: Bold},
+ {Label: "Blue", FgCol: &Col{Name: "Blue"}, Style: Bold},
+ {Label: "Green", FgCol: &Col{Name: "Lime"}, Style: Bold},
+ }, false},
+ {"Green Feint & Yellow Italic", "\u001B[2;32m👩🏽🔧\u001B[0m\u001B[0;3;33m👩🏽🔧\u001B[0m", []*StyledText{
+ {Label: "👩🏽🔧", FgCol: &Col{Name: "Green"}, Style: Faint},
+ {Label: "👩🏽🔧", FgCol: &Col{Name: "Olive"}, Style: Italic},
+ }, false},
+ {"Green Blinking & Yellow Inversed", "\u001B[5;32m👩🏽🔧\u001B[0m\u001B[0;7;33m👩🏽🔧\u001B[0m", []*StyledText{
+ {Label: "👩🏽🔧", FgCol: &Col{Name: "Green"}, Style: Blinking},
+ {Label: "👩🏽🔧", FgCol: &Col{Name: "Olive"}, Style: Inversed},
+ }, false},
+ {"Green Invisible & Yellow Invisible & Strikethrough", "\u001B[8;32m👩🏽🔧\u001B[0m\u001B[9;33m👩🏽🔧\u001B[0m", []*StyledText{
+ {Label: "👩🏽🔧", FgCol: &Col{Name: "Green"}, Style: Invisible},
+ {Label: "👩🏽🔧", FgCol: &Col{Name: "Olive"}, Style: Strikethrough},
+ }, false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParseANSI(tt.input)
+ is.Equal(err != nil, tt.wantErr)
+ for index, w := range tt.want {
+ is.Equal(got[index].Label, w.Label)
+ if w.FgCol != nil {
+ is.Equal(got[index].FgCol.Name, w.FgCol.Name)
+ }
+ is.Equal(got[index].Style, w.Style)
+ }
+ })
+ }
+}
+
+func TestParseAnsi256(t *testing.T) {
+ is := is.New(t)
+ tests := []struct {
+ name string
+ input string
+ want []*StyledText
+ wantErr bool
+ }{
+ {"Grey93 & DarkViolet", "\u001B[38;5;255mGrey93\u001B[0m\u001B[38;5;128mDarkViolet\u001B[0m", []*StyledText{
+ {Label: "Grey93", FgCol: &Col{Name: "Grey93"}},
+ {Label: "DarkViolet", FgCol: &Col{Name: "DarkViolet"}},
+ }, false},
+ {"Grey93 Bold & DarkViolet Italic", "\u001B[0;1;38;5;255mGrey93\u001B[0m\u001B[0;3;38;5;128mDarkViolet\u001B[0m", []*StyledText{
+ {Label: "Grey93", FgCol: &Col{Name: "Grey93"}, Style: Bold},
+ {Label: "DarkViolet", FgCol: &Col{Name: "DarkViolet"}, Style: Italic},
+ }, false},
+ {"Grey93 Bold & DarkViolet Italic", "\u001B[0;1;38;5;256mGrey93\u001B[0m", nil, true},
+ {"Grey93 Bold & DarkViolet Italic", "\u001B[0;1;38;5;-1mGrey93\u001B[0m", nil, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParseANSI(tt.input)
+ is.Equal(err != nil, tt.wantErr)
+ for index, w := range tt.want {
+ is.Equal(got[index].Label, w.Label)
+ if w.FgCol != nil {
+ is.Equal(got[index].FgCol.Name, w.FgCol.Name)
+ }
+ is.Equal(got[index].Style, w.Style)
+ }
+ })
+ }
+}
+
+func TestParseAnsiTrueColor(t *testing.T) {
+ is := is.New(t)
+ tests := []struct {
+ name string
+ input string
+ want []*StyledText
+ wantErr bool
+ }{
+ {"Red", "\u001B[38;2;255;0;0mRed\u001B[0m", []*StyledText{
+ {Label: "Red", FgCol: &Col{Rgb: Rgb{255, 0, 0}, Hex: "#ff0000"}},
+ }, false},
+ {"Red, text, Green", "\u001B[38;2;255;0;0mRed\u001B[0mI am plain text\u001B[38;2;0;255;0mGreen\u001B[0m", []*StyledText{
+ {Label: "Red", FgCol: &Col{Rgb: Rgb{255, 0, 0}, Hex: "#ff0000"}},
+ {Label: "I am plain text"},
+ {Label: "Green", FgCol: &Col{Rgb: Rgb{0, 255, 0}, Hex: "#00ff00"}},
+ }, false},
+ {"Bad 1", "\u001B[38;2;256;0;0mRed\u001B[0m", nil, true},
+ {"Bad 2", "\u001B[38;2;-1;0;0mRed\u001B[0m", nil, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParseANSI(tt.input)
+ is.Equal(err != nil, tt.wantErr)
+ for index, w := range tt.want {
+ is.Equal(got[index].Label, w.Label)
+ if w.FgCol != nil {
+ is.Equal(got[index].FgCol.Hex, w.FgCol.Hex)
+ is.Equal(got[index].FgCol.Rgb, w.FgCol.Rgb)
+ }
+ is.Equal(got[index].Style, w.Style)
+ }
+ })
+ }
+}
diff --git a/v2/pkg/menu/tray.go b/v2/pkg/menu/tray.go
new file mode 100644
index 000000000..c8728f1f7
--- /dev/null
+++ b/v2/pkg/menu/tray.go
@@ -0,0 +1,42 @@
+package menu
+
+// TrayMenu are the options
+type TrayMenu struct {
+ // Label is the text we wish to display in the tray
+ Label string
+
+ // Image is the name of the tray icon we wish to display.
+ // These are read up during build from /trayicons and
+ // the filenames are used as IDs, minus the extension
+ // EG: /trayicons/main.png can be referenced here with "main"
+ // If the image is not a filename, it will be treated as base64 image data
+ Image string
+
+ // MacTemplateImage indicates that on a Mac, this image is a template image
+ MacTemplateImage bool
+
+ // Text Colour
+ RGBA string
+
+ // Font
+ FontSize int
+ FontName string
+
+ // Tooltip
+ Tooltip string
+
+ // Callback function when menu clicked
+ // Click Callback `json:"-"`
+
+ // Disabled makes the item unselectable
+ Disabled bool
+
+ // Menu is the initial menu we wish to use for the tray
+ Menu *Menu
+
+ // OnOpen is called when the Menu is opened
+ OnOpen func()
+
+ // OnClose is called when the Menu is closed
+ OnClose func()
+}
diff --git a/v2/pkg/menu/type.go b/v2/pkg/menu/type.go
new file mode 100644
index 000000000..829ea05ad
--- /dev/null
+++ b/v2/pkg/menu/type.go
@@ -0,0 +1,17 @@
+package menu
+
+// Type of the menu item
+type Type string
+
+const (
+ // TextType is the text menuitem type
+ TextType Type = "Text"
+ // SeparatorType is the Separator menuitem type
+ SeparatorType Type = "Separator"
+ // SubmenuType is the Submenu menuitem type
+ SubmenuType Type = "Submenu"
+ // CheckboxType is the Checkbox menuitem type
+ CheckboxType Type = "Checkbox"
+ // RadioType is the Radio menuitem type
+ RadioType Type = "Radio"
+)
diff --git a/v2/pkg/menu/windows.go b/v2/pkg/menu/windows.go
new file mode 100644
index 000000000..d251e287e
--- /dev/null
+++ b/v2/pkg/menu/windows.go
@@ -0,0 +1,13 @@
+package menu
+
+/*
+// DefaultWindowsMenu returns a default menu including the default
+// Application and Edit menus. Use `.Append()` to add to it.
+func DefaultWindowsMenu() *Menu {
+ return NewMenuFromItems(
+ FileMenu(),
+ EditMenu(),
+ WindowMenu(),
+ )
+}
+*/
diff --git a/v2/pkg/options/assetserver/middleware.go b/v2/pkg/options/assetserver/middleware.go
new file mode 100644
index 000000000..b3826ab7d
--- /dev/null
+++ b/v2/pkg/options/assetserver/middleware.go
@@ -0,0 +1,20 @@
+package assetserver
+
+import (
+ "net/http"
+)
+
+// Middleware defines a 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
+ }
+}
diff --git a/v2/pkg/options/assetserver/options.go b/v2/pkg/options/assetserver/options.go
new file mode 100644
index 000000000..674451a0c
--- /dev/null
+++ b/v2/pkg/options/assetserver/options.go
@@ -0,0 +1,45 @@
+package assetserver
+
+import (
+ "fmt"
+ "io/fs"
+ "net/http"
+)
+
+// Options defines the configuration of the AssetServer.
+type Options struct {
+ // Assets defines the static assets to be used. A GET request is first tried to be served from this Assets. If the Assets returns
+ // `os.ErrNotExist` for that file, the request handling will fallback to the Handler and tries to serve the GET
+ // request from it.
+ //
+ // If set to nil, all GET requests will be forwarded to Handler.
+ Assets fs.FS
+
+ // Handler will be called for every GET request that can't be served from Assets, due to `os.ErrNotExist`. Furthermore all
+ // non GET requests will always be served from this Handler.
+ //
+ // If not defined, the result is the following in cases where the Handler would have been called:
+ // GET request: `http.StatusNotFound`
+ // Other request: `http.StatusMethodNotAllowed`
+ 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.
+ //
+ // If not defined, the default AssetServer request chain is executed.
+ //
+ // Multiple Middlewares can be chained together with:
+ // ChainMiddleware(middleware ...Middleware) Middleware
+ Middleware Middleware
+}
+
+// Validate the options
+func (o Options) Validate() error {
+ if o.Assets == nil && o.Handler == nil && o.Middleware == nil {
+ return fmt.Errorf("AssetServer options invalid: either Assets, Handler or Middleware must be set")
+ }
+
+ return nil
+}
diff --git a/v2/pkg/options/debug.go b/v2/pkg/options/debug.go
new file mode 100644
index 000000000..b1e989b9a
--- /dev/null
+++ b/v2/pkg/options/debug.go
@@ -0,0 +1,7 @@
+package options
+
+// Debug options which are taken into account in debug builds.
+type Debug struct {
+ // OpenInspectorOnStartup opens the inspector on startup of the app.
+ OpenInspectorOnStartup bool
+}
diff --git a/v2/pkg/options/linux/linux.go b/v2/pkg/options/linux/linux.go
new file mode 100644
index 000000000..797450c27
--- /dev/null
+++ b/v2/pkg/options/linux/linux.go
@@ -0,0 +1,56 @@
+package linux
+
+// WebviewGpuPolicy values used for determining the webview's hardware acceleration policy.
+type WebviewGpuPolicy int
+
+const (
+ // WebviewGpuPolicyAlways Hardware acceleration is always enabled.
+ WebviewGpuPolicyAlways WebviewGpuPolicy = iota
+ // WebviewGpuPolicyOnDemand Hardware acceleration is enabled/disabled as request by web contents.
+ WebviewGpuPolicyOnDemand
+ // WebviewGpuPolicyNever Hardware acceleration is always disabled.
+ WebviewGpuPolicyNever
+)
+
+// Options specific to Linux builds
+type Options struct {
+ // Icon Sets up the icon representing the window. This icon is used when the window is minimized
+ // (also known as iconified).
+ Icon []byte
+
+ // WindowIsTranslucent sets the window's background to transparent when enabled.
+ WindowIsTranslucent bool
+
+ // Messages are messages that can be customised
+ Messages *Messages
+
+ // WebviewGpuPolicy used for determining the hardware acceleration policy for the webview.
+ // - WebviewGpuPolicyAlways
+ // - WebviewGpuPolicyOnDemand
+ // - WebviewGpuPolicyNever
+ //
+ // Due to https://github.com/wailsapp/wails/issues/2977, if options.Linux is nil
+ // in the call to wails.Run(), WebviewGpuPolicy is set by default to WebviewGpuPolicyNever.
+ // Client code may override this behavior by passing a non-nil Options and set
+ // WebviewGpuPolicy as needed.
+ WebviewGpuPolicy WebviewGpuPolicy
+
+ // 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
+}
+
+type Messages struct {
+ WebKit2GTKMinRequired string
+}
+
+func DefaultMessages() *Messages {
+ return &Messages{
+ WebKit2GTKMinRequired: "This application requires at least WebKit2GTK %s to be installed.",
+ }
+}
diff --git a/v2/pkg/options/mac/appearance.go b/v2/pkg/options/mac/appearance.go
new file mode 100644
index 000000000..934bb208a
--- /dev/null
+++ b/v2/pkg/options/mac/appearance.go
@@ -0,0 +1,23 @@
+package mac
+
+// AppearanceType is a type of Appearance for Cocoa windows
+type AppearanceType string
+
+const (
+ // DefaultAppearance uses the default system value
+ DefaultAppearance AppearanceType = ""
+ // NSAppearanceNameAqua - The standard light system appearance.
+ NSAppearanceNameAqua AppearanceType = "NSAppearanceNameAqua"
+ // NSAppearanceNameDarkAqua - The standard dark system appearance.
+ NSAppearanceNameDarkAqua AppearanceType = "NSAppearanceNameDarkAqua"
+ // NSAppearanceNameVibrantLight - The light vibrant appearance
+ NSAppearanceNameVibrantLight AppearanceType = "NSAppearanceNameVibrantLight"
+ // NSAppearanceNameAccessibilityHighContrastAqua - A high-contrast version of the standard light system appearance.
+ NSAppearanceNameAccessibilityHighContrastAqua AppearanceType = "NSAppearanceNameAccessibilityHighContrastAqua"
+ // NSAppearanceNameAccessibilityHighContrastDarkAqua - A high-contrast version of the standard dark system appearance.
+ NSAppearanceNameAccessibilityHighContrastDarkAqua AppearanceType = "NSAppearanceNameAccessibilityHighContrastDarkAqua"
+ // NSAppearanceNameAccessibilityHighContrastVibrantLight - A high-contrast version of the light vibrant appearance.
+ NSAppearanceNameAccessibilityHighContrastVibrantLight AppearanceType = "NSAppearanceNameAccessibilityHighContrastVibrantLight"
+ // NSAppearanceNameAccessibilityHighContrastVibrantDark - A high-contrast version of the dark vibrant appearance.
+ NSAppearanceNameAccessibilityHighContrastVibrantDark AppearanceType = "NSAppearanceNameAccessibilityHighContrastVibrantDark"
+)
diff --git a/v2/pkg/options/mac/mac.go b/v2/pkg/options/mac/mac.go
new file mode 100644
index 000000000..152145114
--- /dev/null
+++ b/v2/pkg/options/mac/mac.go
@@ -0,0 +1,31 @@
+package mac
+
+//type ActivationPolicy int
+//
+//const (
+// NSApplicationActivationPolicyRegular ActivationPolicy = 0
+// NSApplicationActivationPolicyAccessory ActivationPolicy = 1
+// NSApplicationActivationPolicyProhibited ActivationPolicy = 2
+//)
+
+type AboutInfo struct {
+ Title string
+ Message string
+ Icon []byte
+}
+
+// Options are options specific to Mac
+type Options struct {
+ TitleBar *TitleBar
+ Appearance AppearanceType
+ ContentProtection bool
+ WebviewIsTransparent bool
+ WindowIsTranslucent bool
+ Preferences *Preferences
+ DisableZoom bool
+ // ActivationPolicy ActivationPolicy
+ About *AboutInfo
+ OnFileOpen func(filePath string) `json:"-"`
+ OnUrlOpen func(filePath string) `json:"-"`
+ // URLHandlers map[string]func(string)
+}
diff --git a/v2/pkg/options/mac/preferences.go b/v2/pkg/options/mac/preferences.go
new file mode 100644
index 000000000..0749ccb18
--- /dev/null
+++ b/v2/pkg/options/mac/preferences.go
@@ -0,0 +1,21 @@
+package mac
+
+import "github.com/leaanthony/u"
+
+var (
+ Enabled = u.True
+ Disabled = u.False
+)
+
+// Preferences allows to set webkit preferences
+type Preferences struct {
+ // A Boolean value that indicates whether pressing the tab key changes the focus to links and form controls.
+ // Set to false by default.
+ TabFocusesLinks u.Bool
+ // A Boolean value that indicates whether to allow people to select or otherwise interact with text.
+ // Set to true by default.
+ TextInteractionEnabled u.Bool
+ // A Boolean value that indicates whether a web view can display content full screen.
+ // Set to false by default
+ FullscreenEnabled u.Bool
+}
diff --git a/v2/pkg/options/mac/titlebar.go b/v2/pkg/options/mac/titlebar.go
new file mode 100644
index 000000000..51e0832ca
--- /dev/null
+++ b/v2/pkg/options/mac/titlebar.go
@@ -0,0 +1,52 @@
+package mac
+
+// TitleBar contains options for the Mac titlebar
+type TitleBar struct {
+ TitlebarAppearsTransparent bool
+ HideTitle bool
+ HideTitleBar bool
+ FullSizeContent bool
+ UseToolbar bool
+ HideToolbarSeparator bool
+}
+
+// TitleBarDefault results in the default Mac Titlebar
+func TitleBarDefault() *TitleBar {
+ return &TitleBar{
+ TitlebarAppearsTransparent: false,
+ HideTitle: false,
+ HideTitleBar: false,
+ FullSizeContent: false,
+ UseToolbar: false,
+ HideToolbarSeparator: false,
+ }
+}
+
+// Credit: Comments from Electron site
+
+// TitleBarHidden results in a hidden title bar and a full size content window,
+// yet the title bar still has the standard window controls (“traffic lights”)
+// in the top left.
+func TitleBarHidden() *TitleBar {
+ return &TitleBar{
+ TitlebarAppearsTransparent: true,
+ HideTitle: true,
+ HideTitleBar: false,
+ FullSizeContent: true,
+ UseToolbar: false,
+ HideToolbarSeparator: false,
+ }
+}
+
+// TitleBarHiddenInset results in a hidden title bar with an alternative look where
+// the traffic light buttons are slightly more inset from the window edge.
+func TitleBarHiddenInset() *TitleBar {
+ return &TitleBar{
+ TitlebarAppearsTransparent: true,
+ HideTitle: true,
+ HideTitleBar: false,
+ FullSizeContent: true,
+ UseToolbar: true,
+ HideToolbarSeparator: true,
+ }
+}
diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go
new file mode 100644
index 000000000..0f62d5e4b
--- /dev/null
+++ b/v2/pkg/options/options.go
@@ -0,0 +1,276 @@
+package options
+
+import (
+ "context"
+ "html"
+ "io/fs"
+ "net/http"
+ "os"
+ "path/filepath"
+ "runtime"
+
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+ "github.com/wailsapp/wails/v2/pkg/options/linux"
+ "github.com/wailsapp/wails/v2/pkg/options/mac"
+ "github.com/wailsapp/wails/v2/pkg/options/windows"
+
+ "github.com/wailsapp/wails/v2/pkg/menu"
+
+ "github.com/wailsapp/wails/v2/pkg/logger"
+)
+
+type WindowStartState int
+
+const (
+ Normal WindowStartState = 0
+ Maximised WindowStartState = 1
+ Minimised WindowStartState = 2
+ Fullscreen WindowStartState = 3
+)
+
+type Experimental struct{}
+
+// App contains options for creating the App
+type App struct {
+ Title string
+ Width int
+ Height int
+ DisableResize bool
+ Fullscreen bool
+ Frameless bool
+ MinWidth int
+ MinHeight int
+ MaxWidth int
+ MaxHeight int
+ StartHidden bool
+ HideWindowOnClose bool
+ AlwaysOnTop bool
+ // BackgroundColour is the background colour of the window
+ // You can use the options.NewRGB and options.NewRGBA functions to create a new colour
+ BackgroundColour *RGBA
+ // Deprecated: Use AssetServer.Assets instead.
+ Assets fs.FS
+ // Deprecated: Use AssetServer.Handler instead.
+ AssetsHandler http.Handler
+ // AssetServer configures the Assets for the application
+ AssetServer *assetserver.Options
+ Menu *menu.Menu
+ Logger logger.Logger `json:"-"`
+ LogLevel logger.LogLevel
+ LogLevelProduction logger.LogLevel
+ OnStartup func(ctx context.Context) `json:"-"`
+ OnDomReady func(ctx context.Context) `json:"-"`
+ OnShutdown func(ctx context.Context) `json:"-"`
+ OnBeforeClose func(ctx context.Context) (prevent bool) `json:"-"`
+ Bind []interface{}
+ EnumBind []interface{}
+ WindowStartState WindowStartState
+
+ // ErrorFormatter overrides the formatting of errors returned by backend methods
+ ErrorFormatter ErrorFormatter
+
+ // CSS property to test for draggable elements. Default "--wails-draggable"
+ CSSDragProperty string
+
+ // The CSS Value that the CSSDragProperty must have to be draggable, EG: "drag"
+ CSSDragValue string
+
+ // EnableDefaultContextMenu enables the browser's default context-menu in production
+ // This menu is already enabled in development and debug builds
+ EnableDefaultContextMenu bool
+
+ // EnableFraudulentWebsiteDetection enables scan services for fraudulent content, such as malware or phishing attempts.
+ // These services might send information from your app like URLs navigated to and possibly other content to cloud
+ // services of Apple and Microsoft.
+ EnableFraudulentWebsiteDetection bool
+
+ SingleInstanceLock *SingleInstanceLock
+
+ Windows *windows.Options
+ Mac *mac.Options
+ Linux *linux.Options
+
+ // Experimental options
+ Experimental *Experimental
+
+ // Debug options for debug builds. These options will be ignored in a production build.
+ Debug Debug
+
+ // DragAndDrop options for drag and drop behavior
+ DragAndDrop *DragAndDrop
+
+ // DisablePanicRecovery disables the panic recovery system in messages processing
+ DisablePanicRecovery bool
+
+ // List of additional allowed origins for bindings in format "https://*.myapp.com,https://example.com"
+ BindingsAllowedOrigins string
+}
+
+type ErrorFormatter func(error) any
+
+type RGBA struct {
+ R uint8 `json:"r"`
+ G uint8 `json:"g"`
+ B uint8 `json:"b"`
+ A uint8 `json:"a"`
+}
+
+// NewRGBA creates a new RGBA struct with the given values
+func NewRGBA(r, g, b, a uint8) *RGBA {
+ return &RGBA{
+ R: r,
+ G: g,
+ B: b,
+ A: a,
+ }
+}
+
+// NewRGB creates a new RGBA struct with the given values and Alpha set to 255
+func NewRGB(r, g, b uint8) *RGBA {
+ return &RGBA{
+ R: r,
+ G: g,
+ B: b,
+ A: 255,
+ }
+}
+
+// MergeDefaults will set the minimum default values for an application
+func MergeDefaults(appoptions *App) {
+ // Do set defaults
+ if appoptions.Width <= 0 {
+ appoptions.Width = 1024
+ }
+ if appoptions.Height <= 0 {
+ appoptions.Height = 768
+ }
+ if appoptions.Logger == nil {
+ appoptions.Logger = logger.NewDefaultLogger()
+ }
+ if appoptions.LogLevel == 0 {
+ appoptions.LogLevel = logger.INFO
+ }
+ if appoptions.LogLevelProduction == 0 {
+ appoptions.LogLevelProduction = logger.ERROR
+ }
+ if appoptions.CSSDragProperty == "" {
+ appoptions.CSSDragProperty = "--wails-draggable"
+ }
+ if appoptions.CSSDragValue == "" {
+ appoptions.CSSDragValue = "drag"
+ }
+ if appoptions.DragAndDrop == nil {
+ appoptions.DragAndDrop = &DragAndDrop{}
+ }
+ if appoptions.DragAndDrop.CSSDropProperty == "" {
+ appoptions.DragAndDrop.CSSDropProperty = "--wails-drop-target"
+ }
+ if appoptions.DragAndDrop.CSSDropValue == "" {
+ appoptions.DragAndDrop.CSSDropValue = "drop"
+ }
+ if appoptions.BackgroundColour == nil {
+ appoptions.BackgroundColour = &RGBA{
+ R: 255,
+ G: 255,
+ B: 255,
+ A: 255,
+ }
+ }
+
+ // Ensure max and min are valid
+ processMinMaxConstraints(appoptions)
+
+ // Default menus
+ processMenus(appoptions)
+
+ // Process Drag Options
+ processDragOptions(appoptions)
+}
+
+type SingleInstanceLock struct {
+ // uniqueId that will be used for setting up messaging between instances
+ UniqueId string
+ OnSecondInstanceLaunch func(secondInstanceData SecondInstanceData)
+}
+
+type SecondInstanceData struct {
+ Args []string
+ WorkingDirectory string
+}
+
+type DragAndDrop struct {
+
+ // EnableFileDrop enables wails' drag and drop functionality that returns the dropped in files' absolute paths.
+ EnableFileDrop bool
+
+ // Disable webview's drag and drop functionality.
+ //
+ // It can be used to prevent accidental file opening of dragged in files in the webview, when there is no need for drag and drop.
+ DisableWebViewDrop bool
+
+ // CSS property to test for drag and drop target elements. Default "--wails-drop-target"
+ CSSDropProperty string
+
+ // The CSS Value that the CSSDropProperty must have to be a valid drop target. Default "drop"
+ CSSDropValue string
+}
+
+func NewSecondInstanceData() (*SecondInstanceData, error) {
+ ex, err := os.Executable()
+ if err != nil {
+ return nil, err
+ }
+ workingDirectory := filepath.Dir(ex)
+
+ return &SecondInstanceData{
+ Args: os.Args[1:],
+ WorkingDirectory: workingDirectory,
+ }, nil
+}
+
+func processMenus(appoptions *App) {
+ switch runtime.GOOS {
+ case "darwin":
+ if appoptions.Menu == nil {
+ items := []*menu.MenuItem{
+ menu.EditMenu(),
+ }
+ if !appoptions.Frameless {
+ items = append(items, menu.WindowMenu()) // Current options in Window Menu only work if not frameless
+ }
+
+ appoptions.Menu = menu.NewMenuFromItems(menu.AppMenu(), items...)
+ }
+ }
+}
+
+func processMinMaxConstraints(appoptions *App) {
+ if appoptions.MinWidth > 0 && appoptions.MaxWidth > 0 {
+ if appoptions.MinWidth > appoptions.MaxWidth {
+ appoptions.MinWidth = appoptions.MaxWidth
+ }
+ }
+ if appoptions.MinHeight > 0 && appoptions.MaxHeight > 0 {
+ if appoptions.MinHeight > appoptions.MaxHeight {
+ appoptions.MinHeight = appoptions.MaxHeight
+ }
+ }
+ // Ensure width and height are limited if max/min is set
+ if appoptions.Width < appoptions.MinWidth {
+ appoptions.Width = appoptions.MinWidth
+ }
+ if appoptions.MaxWidth > 0 && appoptions.Width > appoptions.MaxWidth {
+ appoptions.Width = appoptions.MaxWidth
+ }
+ if appoptions.Height < appoptions.MinHeight {
+ appoptions.Height = appoptions.MinHeight
+ }
+ if appoptions.MaxHeight > 0 && appoptions.Height > appoptions.MaxHeight {
+ appoptions.Height = appoptions.MaxHeight
+ }
+}
+
+func processDragOptions(appoptions *App) {
+ appoptions.CSSDragProperty = html.EscapeString(appoptions.CSSDragProperty)
+ appoptions.CSSDragValue = html.EscapeString(appoptions.CSSDragValue)
+}
diff --git a/v2/pkg/options/options_test.go b/v2/pkg/options/options_test.go
new file mode 100644
index 000000000..0cacd702d
--- /dev/null
+++ b/v2/pkg/options/options_test.go
@@ -0,0 +1,85 @@
+package options
+
+import (
+ "testing"
+)
+
+func TestMergeDefaultsWH(t *testing.T) {
+ tests := []struct {
+ name string
+ appoptions *App
+ wantWidth int
+ wantHeight int
+ }{
+ {
+ name: "No width and height",
+ appoptions: &App{},
+ wantWidth: 1024,
+ wantHeight: 768,
+ },
+ {
+ name: "Basic width and height",
+ appoptions: &App{
+ Width: 800,
+ Height: 600,
+ },
+ wantWidth: 800,
+ wantHeight: 600,
+ },
+ {
+ name: "With MinWidth and MinHeight",
+ appoptions: &App{
+ Width: 200,
+ MinWidth: 800,
+ Height: 100,
+ MinHeight: 600,
+ },
+ wantWidth: 800,
+ wantHeight: 600,
+ },
+ {
+ name: "With MaxWidth and MaxHeight",
+ appoptions: &App{
+ Width: 900,
+ MaxWidth: 800,
+ Height: 700,
+ MaxHeight: 600,
+ },
+ wantWidth: 800,
+ wantHeight: 600,
+ },
+ {
+ name: "With MinWidth more than MaxWidth",
+ appoptions: &App{
+ Width: 900,
+ MinWidth: 900,
+ MaxWidth: 800,
+ Height: 600,
+ },
+ wantWidth: 800,
+ wantHeight: 600,
+ },
+ {
+ name: "With MinHeight more than MaxHeight",
+ appoptions: &App{
+ Width: 800,
+ Height: 700,
+ MinHeight: 900,
+ MaxHeight: 600,
+ },
+ wantWidth: 800,
+ wantHeight: 600,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ MergeDefaults(tt.appoptions)
+ if tt.appoptions.Width != tt.wantWidth {
+ t.Errorf("MergeDefaults().Width =%v, want %v", tt.appoptions.Width, tt.wantWidth)
+ }
+ if tt.appoptions.Height != tt.wantHeight {
+ t.Errorf("MergeDefaults().Height =%v, want %v", tt.appoptions.Height, tt.wantHeight)
+ }
+ })
+ }
+}
diff --git a/v2/pkg/options/windows/windows.go b/v2/pkg/options/windows/windows.go
new file mode 100644
index 000000000..1fe351455
--- /dev/null
+++ b/v2/pkg/options/windows/windows.go
@@ -0,0 +1,167 @@
+package windows
+
+type Theme int
+
+type Messages struct {
+ InstallationRequired string
+ UpdateRequired string
+ MissingRequirements string
+ Webview2NotInstalled string
+ Error string
+ FailedToInstall string
+ DownloadPage string
+ PressOKToInstall string
+ ContactAdmin string
+ InvalidFixedWebview2 string
+ WebView2ProcessCrash string
+}
+
+const (
+ // SystemDefault will use whatever the system theme is. The application will follow system theme changes.
+ SystemDefault Theme = 0
+ // Dark Mode
+ Dark Theme = 1
+ // Light Mode
+ Light Theme = 2
+)
+
+type BackdropType int32
+
+const (
+ Auto BackdropType = 0
+ None BackdropType = 1
+ Mica BackdropType = 2
+ Acrylic BackdropType = 3
+ Tabbed BackdropType = 4
+)
+
+const (
+ // Default is 0, which means no changes to the default Windows DLL search behavior
+ DLLSearchDefault uint32 = 0
+ // LoadLibrary flags for determining from where to search for a DLL
+ DLLSearchDontResolveDllReferences uint32 = 0x1 // windows.DONT_RESOLVE_DLL_REFERENCES
+ DLLSearchAsDataFile uint32 = 0x2 // windows.LOAD_LIBRARY_AS_DATAFILE
+ DLLSearchWithAlteredPath uint32 = 0x8 // windows.LOAD_WITH_ALTERED_SEARCH_PATH
+ DLLSearchIgnoreCodeAuthzLevel uint32 = 0x10 // windows.LOAD_IGNORE_CODE_AUTHZ_LEVEL
+ DLLSearchAsImageResource uint32 = 0x20 // windows.LOAD_LIBRARY_AS_IMAGE_RESOURCE
+ DLLSearchAsDataFileExclusive uint32 = 0x40 // windows.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE
+ DLLSearchRequireSignedTarget uint32 = 0x80 // windows.LOAD_LIBRARY_REQUIRE_SIGNED_TARGET
+ DLLSearchDllLoadDir uint32 = 0x100 // windows.LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
+ DLLSearchApplicationDir uint32 = 0x200 // windows.LOAD_LIBRARY_SEARCH_APPLICATION_DIR
+ DLLSearchUserDirs uint32 = 0x400 // windows.LOAD_LIBRARY_SEARCH_USER_DIRS
+ DLLSearchSystem32 uint32 = 0x800 // windows.LOAD_LIBRARY_SEARCH_SYSTEM32
+ DLLSearchDefaultDirs uint32 = 0x1000 // windows.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
+ DLLSearchSafeCurrentDirs uint32 = 0x2000 // windows.LOAD_LIBRARY_SAFE_CURRENT_DIRS
+ DLLSearchSystem32NoForwarder uint32 = 0x4000 // windows.LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER
+ DLLSearchOsIntegrityContinuity uint32 = 0x8000 // windows.LOAD_LIBRARY_OS_INTEGRITY_CONTINUITY
+)
+
+func RGB(r, g, b uint8) int32 {
+ col := int32(b)
+ col = col<<8 | int32(g)
+ col = col<<8 | int32(r)
+ return col
+}
+
+// ThemeSettings contains optional colours to use.
+// They may be set using the hex values: 0x00BBGGRR
+type ThemeSettings struct {
+ DarkModeTitleBar int32
+ DarkModeTitleBarInactive int32
+ DarkModeTitleText int32
+ DarkModeTitleTextInactive int32
+ DarkModeBorder int32
+ DarkModeBorderInactive int32
+ LightModeTitleBar int32
+ LightModeTitleBarInactive int32
+ LightModeTitleText int32
+ LightModeTitleTextInactive int32
+ LightModeBorder int32
+ LightModeBorderInactive int32
+}
+
+// Options are options specific to Windows
+type Options struct {
+ ContentProtection bool
+ WebviewIsTransparent bool
+ WindowIsTranslucent bool
+ DisableWindowIcon bool
+
+ IsZoomControlEnabled bool
+ ZoomFactor float64
+
+ DisablePinchZoom bool
+
+ // Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown.
+ // "Rounded Corners" are only available on Windows 11.
+ DisableFramelessWindowDecorations 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
+
+ // Dark/Light or System Default Theme
+ Theme Theme
+
+ // Custom settings for dark/light mode
+ CustomTheme *ThemeSettings
+
+ // Select the type of translucent backdrop. Requires Windows 11 22621 or later.
+ BackdropType BackdropType
+
+ // User messages that can be customised
+ Messages *Messages
+
+ // ResizeDebounceMS is the amount of time to debounce redraws of webview2
+ // when resizing the window
+ ResizeDebounceMS uint16
+
+ // OnSuspend is called when Windows enters low power mode
+ OnSuspend func()
+
+ // OnResume is called when Windows resumes from low power mode
+ OnResume func()
+
+ // WebviewGpuIsDisabled is used to enable / disable GPU acceleration for the webview
+ WebviewGpuIsDisabled bool
+
+ // WebviewDisableRendererCodeIntegrity disables the `RendererCodeIntegrity` of WebView2. Some Security Endpoint
+ // Protection Software inject themself into the WebView2 with unsigned or wrongly signed dlls, which is not allowed
+ // and will stop the WebView2 processes. Those security software need an update to fix this issue or one can disable
+ // the integrity check with this flag.
+ //
+ // The event viewer log contains `Code Integrity Errors` like mentioned here: https://github.com/MicrosoftEdge/WebView2Feedback/issues/2051
+ //
+ // !! Please keep in mind when disabling this feature, this also allows malicious software to inject into the WebView2 !!
+ WebviewDisableRendererCodeIntegrity bool
+
+ // Configure whether swipe gestures should be enabled
+ EnableSwipeGestures bool
+
+ // Class name for the window. If empty, 'wailsWindow' will be used.
+ WindowClassName string
+
+ // DLLSearchPaths controls which directories are searched when loading DLLs
+ // Set to 0 for default behavior, or combine multiple flags with bitwise OR
+ // Example: DLLSearchApplicationDir | DLLSearchSystem32
+ DLLSearchPaths uint32
+}
+
+func DefaultMessages() *Messages {
+ return &Messages{
+ InstallationRequired: "The WebView2 runtime is required. Press Ok to download and install. Note: The installer will download silently so please wait.",
+ UpdateRequired: "The WebView2 runtime needs updating. Press Ok to download and install. Note: The installer will download silently so please wait.",
+ MissingRequirements: "Missing Requirements",
+ Webview2NotInstalled: "WebView2 runtime not installed",
+ Error: "Error",
+ FailedToInstall: "The runtime failed to install correctly. Please try again.",
+ DownloadPage: "This application requires the WebView2 runtime. Press OK to open the download page. Minimum version required: ",
+ PressOKToInstall: "Press Ok to install.",
+ ContactAdmin: "The WebView2 runtime is required to run this application. Please contact your system administrator.",
+ InvalidFixedWebview2: "The WebView2 runtime is manually specified, but It is not valid. Check minimum required version and webview2 path.",
+ WebView2ProcessCrash: "The WebView2 process crashed and the application needs to be restarted.",
+ }
+}
diff --git a/v2/pkg/runtime/browser.go b/v2/pkg/runtime/browser.go
new file mode 100644
index 000000000..28ffc5e05
--- /dev/null
+++ b/v2/pkg/runtime/browser.go
@@ -0,0 +1,11 @@
+package runtime
+
+import (
+ "context"
+)
+
+// BrowserOpenURL uses the system default browser to open the url
+func BrowserOpenURL(ctx context.Context, url string) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.BrowserOpenURL(url)
+}
diff --git a/v2/pkg/runtime/clipboard.go b/v2/pkg/runtime/clipboard.go
new file mode 100644
index 000000000..fab9e9e54
--- /dev/null
+++ b/v2/pkg/runtime/clipboard.go
@@ -0,0 +1,13 @@
+package runtime
+
+import "context"
+
+func ClipboardGetText(ctx context.Context) (string, error) {
+ appFrontend := getFrontend(ctx)
+ return appFrontend.ClipboardGetText()
+}
+
+func ClipboardSetText(ctx context.Context, text string) error {
+ appFrontend := getFrontend(ctx)
+ return appFrontend.ClipboardSetText(text)
+}
diff --git a/v2/pkg/runtime/dialog.go b/v2/pkg/runtime/dialog.go
new file mode 100644
index 000000000..16ae659e1
--- /dev/null
+++ b/v2/pkg/runtime/dialog.go
@@ -0,0 +1,80 @@
+package runtime
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/wailsapp/wails/v2/internal/frontend"
+ "github.com/wailsapp/wails/v2/internal/fs"
+)
+
+// FileFilter defines a filter for dialog boxes
+type FileFilter = frontend.FileFilter
+
+// OpenDialogOptions contains the options for the OpenDialogOptions runtime method
+type OpenDialogOptions = frontend.OpenDialogOptions
+
+// SaveDialogOptions contains the options for the SaveDialog runtime method
+type SaveDialogOptions = frontend.SaveDialogOptions
+
+type DialogType = frontend.DialogType
+
+const (
+ InfoDialog = frontend.InfoDialog
+ WarningDialog = frontend.WarningDialog
+ ErrorDialog = frontend.ErrorDialog
+ QuestionDialog = frontend.QuestionDialog
+)
+
+// MessageDialogOptions contains the options for the Message dialogs, EG Info, Warning, etc runtime methods
+type MessageDialogOptions = frontend.MessageDialogOptions
+
+// OpenDirectoryDialog prompts the user to select a directory
+func OpenDirectoryDialog(ctx context.Context, dialogOptions OpenDialogOptions) (string, error) {
+ appFrontend := getFrontend(ctx)
+ if dialogOptions.DefaultDirectory != "" {
+ if !fs.DirExists(dialogOptions.DefaultDirectory) {
+ return "", fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory)
+ }
+ }
+ return appFrontend.OpenDirectoryDialog(dialogOptions)
+}
+
+// OpenFileDialog prompts the user to select a file
+func OpenFileDialog(ctx context.Context, dialogOptions OpenDialogOptions) (string, error) {
+ appFrontend := getFrontend(ctx)
+ if dialogOptions.DefaultDirectory != "" {
+ if !fs.DirExists(dialogOptions.DefaultDirectory) {
+ return "", fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory)
+ }
+ }
+ return appFrontend.OpenFileDialog(dialogOptions)
+}
+
+// OpenMultipleFilesDialog prompts the user to select a file
+func OpenMultipleFilesDialog(ctx context.Context, dialogOptions OpenDialogOptions) ([]string, error) {
+ appFrontend := getFrontend(ctx)
+ if dialogOptions.DefaultDirectory != "" {
+ if !fs.DirExists(dialogOptions.DefaultDirectory) {
+ return nil, fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory)
+ }
+ }
+ return appFrontend.OpenMultipleFilesDialog(dialogOptions)
+}
+
+// SaveFileDialog prompts the user to select a file
+func SaveFileDialog(ctx context.Context, dialogOptions SaveDialogOptions) (string, error) {
+ appFrontend := getFrontend(ctx)
+ if dialogOptions.DefaultDirectory != "" {
+ if !fs.DirExists(dialogOptions.DefaultDirectory) {
+ return "", fmt.Errorf("default directory '%s' does not exist", dialogOptions.DefaultDirectory)
+ }
+ }
+ return appFrontend.SaveFileDialog(dialogOptions)
+}
+
+// MessageDialog show a message dialog to the user
+func MessageDialog(ctx context.Context, dialogOptions MessageDialogOptions) (string, error) {
+ appFrontend := getFrontend(ctx)
+ return appFrontend.MessageDialog(dialogOptions)
+}
diff --git a/v2/pkg/runtime/draganddrop.go b/v2/pkg/runtime/draganddrop.go
new file mode 100644
index 000000000..2db9c773c
--- /dev/null
+++ b/v2/pkg/runtime/draganddrop.go
@@ -0,0 +1,37 @@
+package runtime
+
+import (
+ "context"
+ "fmt"
+)
+
+// OnFileDrop returns a slice of file path strings when a drop is finished.
+func OnFileDrop(ctx context.Context, callback func(x, y int, paths []string)) {
+ if callback == nil {
+ LogError(ctx, "OnFileDrop called with a nil callback")
+ return
+ }
+ EventsOn(ctx, "wails:file-drop", func(optionalData ...interface{}) {
+ if len(optionalData) != 3 {
+ callback(0, 0, nil)
+ }
+ x, ok := optionalData[0].(int)
+ if !ok {
+ LogError(ctx, fmt.Sprintf("invalid x coordinate in drag and drop: %v", optionalData[0]))
+ }
+ y, ok := optionalData[1].(int)
+ if !ok {
+ LogError(ctx, fmt.Sprintf("invalid y coordinate in drag and drop: %v", optionalData[1]))
+ }
+ paths, ok := optionalData[2].([]string)
+ if !ok {
+ LogError(ctx, fmt.Sprintf("invalid path data in drag and drop: %v", optionalData[2]))
+ }
+ callback(x, y, paths)
+ })
+}
+
+// OnFileDropOff removes the drag and drop listeners and handlers.
+func OnFileDropOff(ctx context.Context) {
+ EventsOff(ctx, "wails:file-drop")
+}
diff --git a/v2/pkg/runtime/events.go b/v2/pkg/runtime/events.go
new file mode 100644
index 000000000..84aff7d74
--- /dev/null
+++ b/v2/pkg/runtime/events.go
@@ -0,0 +1,49 @@
+package runtime
+
+import (
+ "context"
+)
+
+// EventsOn registers a listener for the given event name. It returns a function to cancel the listener
+func EventsOn(ctx context.Context, eventName string, callback func(optionalData ...interface{})) func() {
+ events := getEvents(ctx)
+ return events.On(eventName, callback)
+}
+
+// EventsOff unregisters a listener for the given event name, optionally multiple listeners can be unregistered via `additionalEventNames`
+func EventsOff(ctx context.Context, eventName string, additionalEventNames ...string) {
+ events := getEvents(ctx)
+ events.Off(eventName)
+
+ if len(additionalEventNames) > 0 {
+ for _, eventName := range additionalEventNames {
+ events.Off(eventName)
+ }
+ }
+}
+
+// EventsOff unregisters a listener for the given event name, optionally multiple listeners can be unregistered via `additionalEventNames`
+func EventsOffAll(ctx context.Context) {
+ events := getEvents(ctx)
+ events.OffAll()
+}
+
+// EventsOnce registers a listener for the given event name. After the first callback, the
+// listener is deleted. It returns a function to cancel the listener
+func EventsOnce(ctx context.Context, eventName string, callback func(optionalData ...interface{})) func() {
+ events := getEvents(ctx)
+ return events.Once(eventName, callback)
+}
+
+// EventsOnMultiple registers a listener for the given event name, that may be called a maximum of 'counter' times. It returns a function
+// to cancel the listener
+func EventsOnMultiple(ctx context.Context, eventName string, callback func(optionalData ...interface{}), counter int) func() {
+ events := getEvents(ctx)
+ return events.OnMultiple(eventName, callback, counter)
+}
+
+// EventsEmit pass through
+func EventsEmit(ctx context.Context, eventName string, optionalData ...interface{}) {
+ events := getEvents(ctx)
+ events.Emit(eventName, optionalData...)
+}
diff --git a/v2/pkg/runtime/log.go b/v2/pkg/runtime/log.go
new file mode 100644
index 000000000..3c2756f06
--- /dev/null
+++ b/v2/pkg/runtime/log.go
@@ -0,0 +1,105 @@
+package runtime
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/wailsapp/wails/v2/pkg/logger"
+)
+
+// LogPrint prints a Print level message
+func LogPrint(ctx context.Context, message string) {
+ myLogger := getLogger(ctx)
+ myLogger.Print(message)
+}
+
+// LogTrace prints a Trace level message
+func LogTrace(ctx context.Context, message string) {
+ myLogger := getLogger(ctx)
+ myLogger.Trace(message)
+}
+
+// LogDebug prints a Debug level message
+func LogDebug(ctx context.Context, message string) {
+ myLogger := getLogger(ctx)
+ myLogger.Debug(message)
+}
+
+// LogInfo prints a Info level message
+func LogInfo(ctx context.Context, message string) {
+ myLogger := getLogger(ctx)
+ myLogger.Info(message)
+}
+
+// LogWarning prints a Warning level message
+func LogWarning(ctx context.Context, message string) {
+ myLogger := getLogger(ctx)
+ myLogger.Warning(message)
+}
+
+// LogError prints a Error level message
+func LogError(ctx context.Context, message string) {
+ myLogger := getLogger(ctx)
+ myLogger.Error(message)
+}
+
+// LogFatal prints a Fatal level message
+func LogFatal(ctx context.Context, message string) {
+ myLogger := getLogger(ctx)
+ myLogger.Fatal(message)
+}
+
+// LogPrintf prints a Print level message
+func LogPrintf(ctx context.Context, format string, args ...interface{}) {
+ msg := fmt.Sprintf(format, args...)
+ myLogger := getLogger(ctx)
+ myLogger.Print(msg)
+}
+
+// LogTracef prints a Trace level message
+func LogTracef(ctx context.Context, format string, args ...interface{}) {
+ msg := fmt.Sprintf(format, args...)
+ myLogger := getLogger(ctx)
+ myLogger.Trace(msg)
+}
+
+// LogDebugf prints a Debug level message
+func LogDebugf(ctx context.Context, format string, args ...interface{}) {
+ msg := fmt.Sprintf(format, args...)
+ myLogger := getLogger(ctx)
+ myLogger.Debug(msg)
+}
+
+// LogInfof prints a Info level message
+func LogInfof(ctx context.Context, format string, args ...interface{}) {
+ msg := fmt.Sprintf(format, args...)
+ myLogger := getLogger(ctx)
+ myLogger.Info(msg)
+}
+
+// LogWarningf prints a Warning level message
+func LogWarningf(ctx context.Context, format string, args ...interface{}) {
+ msg := fmt.Sprintf(format, args...)
+ myLogger := getLogger(ctx)
+ myLogger.Warning(msg)
+}
+
+// LogErrorf prints a Error level message
+func LogErrorf(ctx context.Context, format string, args ...interface{}) {
+ msg := fmt.Sprintf(format, args...)
+ myLogger := getLogger(ctx)
+ myLogger.Error(msg)
+}
+
+// LogFatalf prints a Fatal level message
+func LogFatalf(ctx context.Context, format string, args ...interface{}) {
+ msg := fmt.Sprintf(format, args...)
+ myLogger := getLogger(ctx)
+ myLogger.Fatal(msg)
+}
+
+// LogSetLogLevel sets the log level
+func LogSetLogLevel(ctx context.Context, level logger.LogLevel) {
+ myLogger := getLogger(ctx)
+ myLogger.SetLogLevel(level)
+}
diff --git a/v2/pkg/runtime/menu.go b/v2/pkg/runtime/menu.go
new file mode 100644
index 000000000..09bd640c5
--- /dev/null
+++ b/v2/pkg/runtime/menu.go
@@ -0,0 +1,17 @@
+package runtime
+
+import (
+ "context"
+
+ "github.com/wailsapp/wails/v2/pkg/menu"
+)
+
+func MenuSetApplicationMenu(ctx context.Context, menu *menu.Menu) {
+ frontend := getFrontend(ctx)
+ frontend.MenuSetApplicationMenu(menu)
+}
+
+func MenuUpdateApplicationMenu(ctx context.Context) {
+ frontend := getFrontend(ctx)
+ frontend.MenuUpdateApplicationMenu()
+}
diff --git a/v2/pkg/runtime/runtime.go b/v2/pkg/runtime/runtime.go
new file mode 100644
index 000000000..6de5ea798
--- /dev/null
+++ b/v2/pkg/runtime/runtime.go
@@ -0,0 +1,107 @@
+package runtime
+
+import (
+ "context"
+ "log"
+ goruntime "runtime"
+
+ "github.com/wailsapp/wails/v2/internal/frontend"
+ "github.com/wailsapp/wails/v2/internal/logger"
+)
+
+const contextError = `An invalid context was passed. This method requires the specific context given in the lifecycle hooks:
+https://wails.io/docs/reference/runtime/intro`
+
+func getFrontend(ctx context.Context) frontend.Frontend {
+ if ctx == nil {
+ pc, _, _, _ := goruntime.Caller(1)
+ funcName := goruntime.FuncForPC(pc).Name()
+ log.Fatalf("cannot call '%s': %s", funcName, contextError)
+ }
+ result := ctx.Value("frontend")
+ if result != nil {
+ return result.(frontend.Frontend)
+ }
+ pc, _, _, _ := goruntime.Caller(1)
+ funcName := goruntime.FuncForPC(pc).Name()
+ log.Fatalf("cannot call '%s': %s", funcName, contextError)
+ return nil
+}
+
+func getLogger(ctx context.Context) *logger.Logger {
+ if ctx == nil {
+ pc, _, _, _ := goruntime.Caller(1)
+ funcName := goruntime.FuncForPC(pc).Name()
+ log.Fatalf("cannot call '%s': %s", funcName, contextError)
+ }
+ result := ctx.Value("logger")
+ if result != nil {
+ return result.(*logger.Logger)
+ }
+ pc, _, _, _ := goruntime.Caller(1)
+ funcName := goruntime.FuncForPC(pc).Name()
+ log.Fatalf("cannot call '%s': %s", funcName, contextError)
+ return nil
+}
+
+func getEvents(ctx context.Context) frontend.Events {
+ if ctx == nil {
+ pc, _, _, _ := goruntime.Caller(1)
+ funcName := goruntime.FuncForPC(pc).Name()
+ log.Fatalf("cannot call '%s': %s", funcName, contextError)
+ }
+ result := ctx.Value("events")
+ if result != nil {
+ return result.(frontend.Events)
+ }
+ pc, _, _, _ := goruntime.Caller(1)
+ funcName := goruntime.FuncForPC(pc).Name()
+ log.Fatalf("cannot call '%s': %s", funcName, contextError)
+ return nil
+}
+
+// Quit the application
+func Quit(ctx context.Context) {
+ if ctx == nil {
+ log.Fatalf("Error calling 'runtime.Quit': %s", contextError)
+ }
+ appFrontend := getFrontend(ctx)
+ appFrontend.Quit()
+}
+
+// Hide the application
+func Hide(ctx context.Context) {
+ if ctx == nil {
+ log.Fatalf("Error calling 'runtime.Hide': %s", contextError)
+ }
+ appFrontend := getFrontend(ctx)
+ appFrontend.Hide()
+}
+
+// Show the application if it is hidden
+func Show(ctx context.Context) {
+ if ctx == nil {
+ log.Fatalf("Error calling 'runtime.Show': %s", contextError)
+ }
+ appFrontend := getFrontend(ctx)
+ appFrontend.Show()
+}
+
+// EnvironmentInfo contains information about the environment
+type EnvironmentInfo struct {
+ BuildType string `json:"buildType"`
+ Platform string `json:"platform"`
+ Arch string `json:"arch"`
+}
+
+// Environment returns information about the environment
+func Environment(ctx context.Context) EnvironmentInfo {
+ var result EnvironmentInfo
+ buildType := ctx.Value("buildtype")
+ if buildType != nil {
+ result.BuildType = buildType.(string)
+ }
+ result.Platform = goruntime.GOOS
+ result.Arch = goruntime.GOARCH
+ return result
+}
diff --git a/v2/pkg/runtime/screen.go b/v2/pkg/runtime/screen.go
new file mode 100644
index 000000000..c4d526692
--- /dev/null
+++ b/v2/pkg/runtime/screen.go
@@ -0,0 +1,15 @@
+package runtime
+
+import (
+ "context"
+
+ "github.com/wailsapp/wails/v2/internal/frontend"
+)
+
+type Screen = frontend.Screen
+
+// ScreenGetAll returns all screens
+func ScreenGetAll(ctx context.Context) ([]Screen, error) {
+ appFrontend := getFrontend(ctx)
+ return appFrontend.ScreenGetAll()
+}
diff --git a/v2/pkg/runtime/signal_linux.go b/v2/pkg/runtime/signal_linux.go
new file mode 100644
index 000000000..6a7ed5db3
--- /dev/null
+++ b/v2/pkg/runtime/signal_linux.go
@@ -0,0 +1,65 @@
+//go:build linux
+
+package runtime
+
+/*
+#include
+#include
+#include
+#include
+
+static void fix_signal(int signum)
+{
+ struct sigaction st;
+
+ if (sigaction(signum, NULL, &st) < 0) {
+ return;
+ }
+ st.sa_flags |= SA_ONSTACK;
+ sigaction(signum, &st, NULL);
+}
+
+static void fix_all_signals()
+{
+#if defined(SIGSEGV)
+ fix_signal(SIGSEGV);
+#endif
+#if defined(SIGBUS)
+ fix_signal(SIGBUS);
+#endif
+#if defined(SIGFPE)
+ fix_signal(SIGFPE);
+#endif
+#if defined(SIGABRT)
+ fix_signal(SIGABRT);
+#endif
+}
+*/
+import "C"
+
+// ResetSignalHandlers resets signal handlers to allow panic recovery.
+//
+// On Linux, WebKit (used for the webview) may install signal handlers without
+// the SA_ONSTACK flag, which prevents Go from properly recovering from panics
+// caused by nil pointer dereferences or other memory access violations.
+//
+// Call this function immediately before code that might panic to ensure
+// the signal handlers are properly configured for Go's panic recovery mechanism.
+//
+// Example usage:
+//
+// go func() {
+// defer func() {
+// if err := recover(); err != nil {
+// log.Printf("Recovered from panic: %v", err)
+// }
+// }()
+// runtime.ResetSignalHandlers()
+// // Code that might panic...
+// }()
+//
+// Note: This function only has an effect on Linux. On other platforms,
+// it is a no-op.
+func ResetSignalHandlers() {
+ C.fix_all_signals()
+}
diff --git a/v2/pkg/runtime/signal_other.go b/v2/pkg/runtime/signal_other.go
new file mode 100644
index 000000000..3171a700c
--- /dev/null
+++ b/v2/pkg/runtime/signal_other.go
@@ -0,0 +1,18 @@
+//go:build !linux
+
+package runtime
+
+// ResetSignalHandlers resets signal handlers to allow panic recovery.
+//
+// On Linux, WebKit (used for the webview) may install signal handlers without
+// the SA_ONSTACK flag, which prevents Go from properly recovering from panics
+// caused by nil pointer dereferences or other memory access violations.
+//
+// Call this function immediately before code that might panic to ensure
+// the signal handlers are properly configured for Go's panic recovery mechanism.
+//
+// Note: This function only has an effect on Linux. On other platforms,
+// it is a no-op.
+func ResetSignalHandlers() {
+ // No-op on non-Linux platforms
+}
diff --git a/v2/pkg/runtime/window.go b/v2/pkg/runtime/window.go
new file mode 100644
index 000000000..62345e2e4
--- /dev/null
+++ b/v2/pkg/runtime/window.go
@@ -0,0 +1,186 @@
+package runtime
+
+import (
+ "context"
+
+ "github.com/wailsapp/wails/v2/pkg/options"
+)
+
+// WindowSetTitle sets the title of the window
+func WindowSetTitle(ctx context.Context, title string) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowSetTitle(title)
+}
+
+// WindowFullscreen makes the window fullscreen
+func WindowFullscreen(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowFullscreen()
+}
+
+// WindowUnfullscreen makes the window UnFullscreen
+func WindowUnfullscreen(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowUnfullscreen()
+}
+
+// WindowCenter the window on the current screen
+func WindowCenter(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowCenter()
+}
+
+// WindowReload will reload the window contents
+func WindowReload(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowReload()
+}
+
+// WindowReloadApp will reload the application
+func WindowReloadApp(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowReloadApp()
+}
+
+func WindowSetSystemDefaultTheme(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowSetSystemDefaultTheme()
+}
+
+func WindowSetLightTheme(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowSetLightTheme()
+}
+
+func WindowSetDarkTheme(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowSetDarkTheme()
+}
+
+// WindowShow shows the window if hidden
+func WindowShow(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowShow()
+}
+
+// WindowHide the window
+func WindowHide(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowHide()
+}
+
+// WindowSetSize sets the size of the window
+func WindowSetSize(ctx context.Context, width int, height int) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowSetSize(width, height)
+}
+
+func WindowGetSize(ctx context.Context) (int, int) {
+ appFrontend := getFrontend(ctx)
+ return appFrontend.WindowGetSize()
+}
+
+// WindowSetMinSize sets the minimum size of the window
+func WindowSetMinSize(ctx context.Context, width int, height int) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowSetMinSize(width, height)
+}
+
+// WindowSetMaxSize sets the maximum size of the window
+func WindowSetMaxSize(ctx context.Context, width int, height int) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowSetMaxSize(width, height)
+}
+
+// WindowSetAlwaysOnTop sets the window AlwaysOnTop or not on top
+func WindowSetAlwaysOnTop(ctx context.Context, b bool) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowSetAlwaysOnTop(b)
+}
+
+// WindowSetPosition sets the position of the window
+func WindowSetPosition(ctx context.Context, x int, y int) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowSetPosition(x, y)
+}
+
+func WindowGetPosition(ctx context.Context) (int, int) {
+ appFrontend := getFrontend(ctx)
+ return appFrontend.WindowGetPosition()
+}
+
+// WindowMaximise the window
+func WindowMaximise(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowMaximise()
+}
+
+// WindowToggleMaximise the window
+func WindowToggleMaximise(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowToggleMaximise()
+}
+
+// WindowUnmaximise the window
+func WindowUnmaximise(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowUnmaximise()
+}
+
+// WindowMinimise the window
+func WindowMinimise(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowMinimise()
+}
+
+// WindowUnminimise the window
+func WindowUnminimise(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowUnminimise()
+}
+
+// WindowIsFullscreen get the window state is window Fullscreen
+func WindowIsFullscreen(ctx context.Context) bool {
+ appFrontend := getFrontend(ctx)
+ return appFrontend.WindowIsFullscreen()
+}
+
+// WindowIsMaximised get the window state is window Maximised
+func WindowIsMaximised(ctx context.Context) bool {
+ appFrontend := getFrontend(ctx)
+ return appFrontend.WindowIsMaximised()
+}
+
+// WindowIsMinimised get the window state is window Minimised
+func WindowIsMinimised(ctx context.Context) bool {
+ appFrontend := getFrontend(ctx)
+ return appFrontend.WindowIsMinimised()
+}
+
+// WindowIsNormal get the window state is window Normal
+func WindowIsNormal(ctx context.Context) bool {
+ appFrontend := getFrontend(ctx)
+ return appFrontend.WindowIsNormal()
+}
+
+// WindowExecJS executes the given Js in the window
+func WindowExecJS(ctx context.Context, js string) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.ExecJS(js)
+}
+
+func WindowSetBackgroundColour(ctx context.Context, R, G, B, A uint8) {
+ appFrontend := getFrontend(ctx)
+ col := &options.RGBA{
+ R: R,
+ G: G,
+ B: B,
+ A: A,
+ }
+ appFrontend.WindowSetBackgroundColour(col)
+}
+
+func WindowPrint(ctx context.Context) {
+ appFrontend := getFrontend(ctx)
+ appFrontend.WindowPrint()
+}
diff --git a/v2/pkg/templates/base/.gitignore.tmpl b/v2/pkg/templates/base/.gitignore.tmpl
new file mode 100644
index 000000000..129d52294
--- /dev/null
+++ b/v2/pkg/templates/base/.gitignore.tmpl
@@ -0,0 +1,3 @@
+build/bin
+node_modules
+frontend/dist
diff --git a/v2/pkg/templates/base/README.md b/v2/pkg/templates/base/README.md
new file mode 100644
index 000000000..abd8b9cd2
--- /dev/null
+++ b/v2/pkg/templates/base/README.md
@@ -0,0 +1,19 @@
+# README
+
+## About
+
+This is the official Wails $NAME template.
+
+You can configure the project by editing `wails.json`. More information about the project settings can be found
+here: https://wails.io/docs/reference/project-config
+
+## Live Development
+
+To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
+server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
+and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
+to this in your browser, and you can call your Go code from devtools.
+
+## Building
+
+To build a redistributable, production mode package, use `wails build`.
diff --git a/v2/pkg/templates/base/app.tmpl.go b/v2/pkg/templates/base/app.tmpl.go
new file mode 100644
index 000000000..af53038a1
--- /dev/null
+++ b/v2/pkg/templates/base/app.tmpl.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "context"
+ "fmt"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called when the app starts. The context is saved
+// so we can call the runtime methods
+func (a *App) startup(ctx context.Context) {
+ a.ctx = ctx
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+ return fmt.Sprintf("Hello %s, It's show time!", name)
+}
diff --git a/v2/pkg/templates/base/go.mod.tmpl b/v2/pkg/templates/base/go.mod.tmpl
new file mode 100644
index 000000000..4b34d1668
--- /dev/null
+++ b/v2/pkg/templates/base/go.mod.tmpl
@@ -0,0 +1,7 @@
+module changeme
+
+go 1.23.0
+
+require github.com/wailsapp/wails/v2 {{.WailsVersion}}
+
+// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}}
\ No newline at end of file
diff --git a/v2/pkg/templates/base/main.go.tmpl b/v2/pkg/templates/base/main.go.tmpl
new file mode 100644
index 000000000..571cf6b10
--- /dev/null
+++ b/v2/pkg/templates/base/main.go.tmpl
@@ -0,0 +1,36 @@
+package main
+
+import (
+"embed"
+
+"github.com/wailsapp/wails/v2"
+"github.com/wailsapp/wails/v2/pkg/options"
+"github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+//go:embed frontend/dist
+var assets embed.FS
+
+func main() {
+// Create an instance of the app structure
+app := NewApp()
+
+// Create application with options
+err := wails.Run(&options.App{
+Title: "{{.ProjectName}}",
+Width: 1024,
+Height: 768,
+AssetServer: &assetserver.Options{
+ Assets: assets,
+},
+BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+OnStartup: app.startup,
+Bind: []interface{}{
+app,
+},
+})
+
+if err != nil {
+println("Error:", err.Error())
+}
+}
diff --git a/v2/pkg/templates/base/template.json b/v2/pkg/templates/base/template.json
new file mode 100644
index 000000000..8ba8f2193
--- /dev/null
+++ b/v2/pkg/templates/base/template.json
@@ -0,0 +1,7 @@
+{
+ "name": "$NAME",
+ "shortname": "$SHORTNAME",
+ "author": "Lea Anthony",
+ "description": "$DESCRIPTION",
+ "helpurl": "https://wails.io"
+}
\ No newline at end of file
diff --git a/v2/pkg/templates/base/wails.tmpl.json b/v2/pkg/templates/base/wails.tmpl.json
new file mode 100644
index 000000000..ce4ffe365
--- /dev/null
+++ b/v2/pkg/templates/base/wails.tmpl.json
@@ -0,0 +1,13 @@
+{
+ "$scheme": "https://wails.io/schemas/config.v2.json",
+ "name": "{{.ProjectName}}",
+ "outputfilename": "{{.BinaryName}}",
+ "frontend:install": "npm install",
+ "frontend:build": "npm run build",
+ "frontend:dev:watcher": "npm run dev",
+ "frontend:dev:serverUrl": "auto",
+ "author": {
+ "name": "{{.AuthorName}}",
+ "email": "{{.AuthorEmail}}"
+ }
+}
diff --git a/v2/pkg/templates/generate/assets/common/.gitignore.tmpl b/v2/pkg/templates/generate/assets/common/.gitignore.tmpl
new file mode 100644
index 000000000..d44c22f8c
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/common/.gitignore.tmpl
@@ -0,0 +1,3 @@
+build/bin
+node_modules
+frontend/dist
\ No newline at end of file
diff --git a/v2/pkg/templates/generate/assets/common/frontend/dist/gitkeep b/v2/pkg/templates/generate/assets/common/frontend/dist/gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/pkg/templates/generate/assets/common/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/pkg/templates/generate/assets/common/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/assets/common/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..01c74ee9b
Binary files /dev/null and b/v2/pkg/templates/generate/assets/common/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/pkg/templates/generate/assets/common/frontend/src/style.css b/v2/pkg/templates/generate/assets/common/frontend/src/style.css
new file mode 100644
index 000000000..3940d6c63
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/common/frontend/src/style.css
@@ -0,0 +1,26 @@
+html {
+ background-color: rgba(27, 38, 54, 1);
+ text-align: center;
+ color: white;
+}
+
+body {
+ margin: 0;
+ color: white;
+ font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+}
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
+}
+
+#app {
+ height: 100vh;
+ text-align: center;
+}
diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.d.ts
new file mode 100644
index 000000000..43173cfce
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.d.ts
@@ -0,0 +1,4 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Greet(arg1: string): Promise;
diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.js
new file mode 100644
index 000000000..0ee085c95
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/go/main/App.js
@@ -0,0 +1,7 @@
+// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Greet(arg1) {
+ return window['go']['main']['App']['Greet'](arg1);
+}
diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/package.json
new file mode 100644
index 000000000..1e7c8a5d7
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@wailsapp/runtime",
+ "version": "2.0.0",
+ "description": "Wails Javascript runtime library",
+ "main": "runtime.js",
+ "types": "runtime.d.ts",
+ "scripts": {
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/wailsapp/wails.git"
+ },
+ "keywords": [
+ "Wails",
+ "Javascript",
+ "Go"
+ ],
+ "author": "Lea Anthony ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/wailsapp/wails/issues"
+ },
+ "homepage": "https://github.com/wailsapp/wails#readme"
+}
diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.d.ts
new file mode 100644
index 000000000..336fb07aa
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.d.ts
@@ -0,0 +1,211 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export interface Size {
+ w: number;
+ h: number;
+}
+
+export interface Screen {
+ isCurrent: boolean;
+ isPrimary: boolean;
+ width: number
+ height: number
+}
+
+// Environment information such as platform, buildtype, ...
+export interface EnvironmentInfo {
+ buildType: string;
+ platform: string;
+ arch: string;
+}
+
+// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
+// emits the given event. Optional data may be passed with the event.
+// This will trigger any event listeners.
+export function EventsEmit(eventName: string, ...data: any): void;
+
+// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
+export function EventsOn(eventName: string, callback: (...data: any) => void): void;
+
+// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
+// sets up a listener for the given event name, but will only trigger a given number times.
+export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void;
+
+// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
+// sets up a listener for the given event name, but will only trigger once.
+export function EventsOnce(eventName: string, callback: (...data: any) => void): void;
+
+// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff)
+// unregisters the listener for the given event name.
+export function EventsOff(eventName: string): void;
+
+// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
+// unregisters all event listeners.
+export function EventsOffAll(): void;
+
+// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
+// logs the given message as a raw message
+export function LogPrint(message: string): void;
+
+// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
+// logs the given message at the `trace` log level.
+export function LogTrace(message: string): void;
+
+// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
+// logs the given message at the `debug` log level.
+export function LogDebug(message: string): void;
+
+// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
+// logs the given message at the `error` log level.
+export function LogError(message: string): void;
+
+// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
+// logs the given message at the `fatal` log level.
+// The application will quit after calling this method.
+export function LogFatal(message: string): void;
+
+// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
+// logs the given message at the `info` log level.
+export function LogInfo(message: string): void;
+
+// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
+// logs the given message at the `warning` log level.
+export function LogWarning(message: string): void;
+
+// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
+// Forces a reload by the main application as well as connected browsers.
+export function WindowReload(): void;
+
+// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
+// Reloads the application frontend.
+export function WindowReloadApp(): void;
+
+// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
+// Sets the window AlwaysOnTop or not on top.
+export function WindowSetAlwaysOnTop(b: boolean): void;
+
+// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
+// *Windows only*
+// Sets window theme to system default (dark/light).
+export function WindowSetSystemDefaultTheme(): void;
+
+// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
+// *Windows only*
+// Sets window to light theme.
+export function WindowSetLightTheme(): void;
+
+// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
+// *Windows only*
+// Sets window to dark theme.
+export function WindowSetDarkTheme(): void;
+
+// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
+// Centers the window on the monitor the window is currently on.
+export function WindowCenter(): void;
+
+// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
+// Sets the text in the window title bar.
+export function WindowSetTitle(title: string): void;
+
+// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
+// Makes the window full screen.
+export function WindowFullscreen(): void;
+
+// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
+// Restores the previous window dimensions and position prior to full screen.
+export function WindowUnfullscreen(): void;
+
+// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
+// Sets the width and height of the window.
+export function WindowSetSize(width: number, height: number): void;
+
+// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
+// Gets the width and height of the window.
+export function WindowGetSize(): Promise;
+
+// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
+// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMaxSize(width: number, height: number): void;
+
+// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
+// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMinSize(width: number, height: number): void;
+
+// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
+// Sets the window position relative to the monitor the window is currently on.
+export function WindowSetPosition(x: number, y: number): void;
+
+// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
+// Gets the window position relative to the monitor the window is currently on.
+export function WindowGetPosition(): Promise;
+
+// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
+// Hides the window.
+export function WindowHide(): void;
+
+// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
+// Shows the window, if it is currently hidden.
+export function WindowShow(): void;
+
+// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
+// Maximises the window to fill the screen.
+export function WindowMaximise(): void;
+
+// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
+// Toggles between Maximised and UnMaximised.
+export function WindowToggleMaximise(): void;
+
+// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
+// Restores the window to the dimensions and position prior to maximising.
+export function WindowUnmaximise(): void;
+
+// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
+// Minimises the window.
+export function WindowMinimise(): void;
+
+// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
+// Restores the window to the dimensions and position prior to minimising.
+export function WindowUnminimise(): void;
+
+// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
+// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
+export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
+
+// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
+// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
+export function ScreenGetAll(): Promise;
+
+// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
+// Opens the given URL in the system browser.
+export function BrowserOpenURL(url: string): void;
+
+// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
+// Returns information about the environment
+export function Environment(): Promise;
+
+// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
+// Quits the application.
+export function Quit(): void;
+
+// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
+// Hides the application.
+export function Hide(): void;
+
+// [Show](https://wails.io/docs/reference/runtime/intro#show)
+// Shows the application.
+export function Show(): void;
diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.js
new file mode 100644
index 000000000..b5ae16d56
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.js
@@ -0,0 +1,182 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export function LogPrint(message) {
+ window.runtime.LogPrint(message);
+}
+
+export function LogTrace(message) {
+ window.runtime.LogTrace(message);
+}
+
+export function LogDebug(message) {
+ window.runtime.LogDebug(message);
+}
+
+export function LogInfo(message) {
+ window.runtime.LogInfo(message);
+}
+
+export function LogWarning(message) {
+ window.runtime.LogWarning(message);
+}
+
+export function LogError(message) {
+ window.runtime.LogError(message);
+}
+
+export function LogFatal(message) {
+ window.runtime.LogFatal(message);
+}
+
+export function EventsOnMultiple(eventName, callback, maxCallbacks) {
+ window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
+}
+
+export function EventsOn(eventName, callback) {
+ EventsOnMultiple(eventName, callback, -1);
+}
+
+export function EventsOff(eventName) {
+ return window.runtime.EventsOff(eventName);
+}
+
+export function EventsOffAll() {
+ return window.runtime.EventsOffAll();
+}
+
+export function EventsOnce(eventName, callback) {
+ EventsOnMultiple(eventName, callback, 1);
+}
+
+export function EventsEmit(eventName) {
+ let args = [eventName].slice.call(arguments);
+ return window.runtime.EventsEmit.apply(null, args);
+}
+
+export function WindowReload() {
+ window.runtime.WindowReload();
+}
+
+export function WindowReloadApp() {
+ window.runtime.WindowReloadApp();
+}
+
+export function WindowSetAlwaysOnTop(b) {
+ window.runtime.WindowSetAlwaysOnTop(b);
+}
+
+export function WindowSetSystemDefaultTheme() {
+ window.runtime.WindowSetSystemDefaultTheme();
+}
+
+export function WindowSetLightTheme() {
+ window.runtime.WindowSetLightTheme();
+}
+
+export function WindowSetDarkTheme() {
+ window.runtime.WindowSetDarkTheme();
+}
+
+export function WindowCenter() {
+ window.runtime.WindowCenter();
+}
+
+export function WindowSetTitle(title) {
+ window.runtime.WindowSetTitle(title);
+}
+
+export function WindowFullscreen() {
+ window.runtime.WindowFullscreen();
+}
+
+export function WindowUnfullscreen() {
+ window.runtime.WindowUnfullscreen();
+}
+
+export function WindowGetSize() {
+ return window.runtime.WindowGetSize();
+}
+
+export function WindowSetSize(width, height) {
+ window.runtime.WindowSetSize(width, height);
+}
+
+export function WindowSetMaxSize(width, height) {
+ window.runtime.WindowSetMaxSize(width, height);
+}
+
+export function WindowSetMinSize(width, height) {
+ window.runtime.WindowSetMinSize(width, height);
+}
+
+export function WindowSetPosition(x, y) {
+ window.runtime.WindowSetPosition(x, y);
+}
+
+export function WindowGetPosition() {
+ return window.runtime.WindowGetPosition();
+}
+
+export function WindowHide() {
+ window.runtime.WindowHide();
+}
+
+export function WindowShow() {
+ window.runtime.WindowShow();
+}
+
+export function WindowMaximise() {
+ window.runtime.WindowMaximise();
+}
+
+export function WindowToggleMaximise() {
+ window.runtime.WindowToggleMaximise();
+}
+
+export function WindowUnmaximise() {
+ window.runtime.WindowUnmaximise();
+}
+
+export function WindowMinimise() {
+ window.runtime.WindowMinimise();
+}
+
+export function WindowUnminimise() {
+ window.runtime.WindowUnminimise();
+}
+
+export function WindowSetBackgroundColour(R, G, B, A) {
+ window.runtime.WindowSetBackgroundColour(R, G, B, A);
+}
+
+export function ScreenGetAll() {
+ return window.runtime.ScreenGetAll();
+}
+
+export function BrowserOpenURL(url) {
+ window.runtime.BrowserOpenURL(url);
+}
+
+export function Environment() {
+ return window.runtime.Environment();
+}
+
+export function Quit() {
+ window.runtime.Quit();
+}
+
+export function Hide() {
+ window.runtime.Hide();
+}
+
+export function Show() {
+ window.runtime.Show();
+}
diff --git a/v2/pkg/templates/generate/assets/lit-ts/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/lit-ts/frontend/index.tmpl.html
new file mode 100644
index 000000000..febcb76cb
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/lit-ts/frontend/index.tmpl.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ {{.ProjectName}}
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/assets/lit-ts/frontend/src/my-element.ts b/v2/pkg/templates/generate/assets/lit-ts/frontend/src/my-element.ts
new file mode 100644
index 000000000..27fd71e45
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/lit-ts/frontend/src/my-element.ts
@@ -0,0 +1,102 @@
+import {css, html, LitElement} from 'lit'
+import logo from './assets/images/logo-universal.png'
+import {Greet} from "../wailsjs/go/main/App";
+import {customElement, property} from 'lit/decorators.js'
+
+/**
+ * An example element.
+ *
+ * @slot - This element has a slot
+ * @csspart button - The button
+ */
+@customElement('my-element')
+export class MyElement extends LitElement {
+ static styles = css`
+ #logo {
+ display: block;
+ width: 50%;
+ height: 50%;
+ margin: auto;
+ padding: 10% 0 0;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ background-origin: content-box;
+ }
+
+ .result {
+ height: 20px;
+ line-height: 20px;
+ margin: 1.5rem auto;
+ }
+
+ .input-box .btn {
+ width: 60px;
+ height: 30px;
+ line-height: 30px;
+ border-radius: 3px;
+ border: none;
+ margin: 0 0 0 20px;
+ padding: 0 8px;
+ cursor: pointer;
+ }
+
+ .input-box .btn:hover {
+ background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
+ color: #333333;
+ }
+
+ .input-box .input {
+ border: none;
+ border-radius: 3px;
+ outline: none;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 10px;
+ background-color: rgba(240, 240, 240, 1);
+ -webkit-font-smoothing: antialiased;
+ }
+
+ .input-box .input:hover {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+ }
+
+ .input-box .input:focus {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+ }
+
+ `
+
+ @property()
+ resultText = "Please enter your name below 👇"
+
+ greet() {
+ let thisName = (this.shadowRoot?.getElementById('name') as HTMLInputElement)?.value;
+ if (thisName) {
+ Greet(thisName).then(result => {
+ this.resultText = result
+ });
+ }
+ }
+
+ render() {
+ return html`
+
+
+
${this.resultText}
+
+
+
+
+
+ `
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'my-element': MyElement
+ }
+}
\ No newline at end of file
diff --git a/v2/pkg/templates/generate/assets/lit-ts/frontend/vite.config.ts b/v2/pkg/templates/generate/assets/lit-ts/frontend/vite.config.ts
new file mode 100644
index 000000000..bbb7f5889
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/lit-ts/frontend/vite.config.ts
@@ -0,0 +1,4 @@
+import {defineConfig} from 'vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({})
diff --git a/v2/pkg/templates/generate/assets/lit/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/lit/frontend/index.tmpl.html
new file mode 100644
index 000000000..fbe3eb240
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/lit/frontend/index.tmpl.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ {{.ProjectName}}
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/pkg/templates/generate/assets/lit/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..01c74ee9b
Binary files /dev/null and b/v2/pkg/templates/generate/assets/lit/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/pkg/templates/generate/assets/lit/frontend/src/my-element.js b/v2/pkg/templates/generate/assets/lit/frontend/src/my-element.js
new file mode 100644
index 000000000..ed65e2225
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/lit/frontend/src/my-element.js
@@ -0,0 +1,105 @@
+import {css, html, LitElement} from 'lit'
+import logo from './assets/images/logo-universal.png'
+import {Greet} from "../wailsjs/go/main/App";
+
+/**
+ * An example element.
+ *
+ * @slot - This element has a slot
+ * @csspart button - The button
+ */
+export class MyElement extends LitElement {
+ constructor() {
+ super()
+ this.resultText = "Please enter your name below 👇"
+ }
+
+ static get styles() {
+ return css`
+ #logo {
+ display: block;
+ width: 50%;
+ height: 50%;
+ margin: auto;
+ padding: 10% 0 0;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ background-origin: content-box;
+ }
+
+ .result {
+ height: 20px;
+ line-height: 20px;
+ margin: 1.5rem auto;
+ }
+
+ .input-box .btn {
+ width: 60px;
+ height: 30px;
+ line-height: 30px;
+ border-radius: 3px;
+ border: none;
+ margin: 0 0 0 20px;
+ padding: 0 8px;
+ cursor: pointer;
+ }
+
+ .input-box .btn:hover {
+ background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
+ color: #333333;
+ }
+
+ .input-box .input {
+ border: none;
+ border-radius: 3px;
+ outline: none;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 10px;
+ background-color: rgba(240, 240, 240, 1);
+ -webkit-font-smoothing: antialiased;
+ }
+
+ .input-box .input:hover {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+ }
+
+ .input-box .input:focus {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+ }
+
+ `
+ }
+
+ static get properties() {
+ return {
+ resultText: {type: String},
+ }
+ }
+
+ greet() {
+ let thisName = this.shadowRoot.getElementById('name').value
+ Greet(thisName).then(result => {
+ this.resultText = result
+ });
+ }
+
+ render() {
+ return html`
+
+
+
+ >
+ )
+}
diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..01c74ee9b
Binary files /dev/null and b/v2/pkg/templates/generate/assets/preact/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/main.jsx b/v2/pkg/templates/generate/assets/preact/frontend/src/main.jsx
new file mode 100644
index 000000000..6c42a5949
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/preact/frontend/src/main.jsx
@@ -0,0 +1,5 @@
+import {render} from 'preact';
+import {App} from './app';
+import './style.css';
+
+render(, document.getElementById('app'));
\ No newline at end of file
diff --git a/v2/pkg/templates/generate/assets/preact/frontend/src/style.css b/v2/pkg/templates/generate/assets/preact/frontend/src/style.css
new file mode 100644
index 000000000..3940d6c63
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/preact/frontend/src/style.css
@@ -0,0 +1,26 @@
+html {
+ background-color: rgba(27, 38, 54, 1);
+ text-align: center;
+ color: white;
+}
+
+body {
+ margin: 0;
+ color: white;
+ font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+}
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
+}
+
+#app {
+ height: 100vh;
+ text-align: center;
+}
diff --git a/v2/pkg/templates/generate/assets/react-ts/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/react-ts/frontend/index.tmpl.html
new file mode 100644
index 000000000..a2023cac7
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/react-ts/frontend/index.tmpl.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ {{.ProjectName}}
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.css b/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.css
new file mode 100644
index 000000000..f949d9c18
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.css
@@ -0,0 +1,59 @@
+#app {
+ height: 100vh;
+ text-align: center;
+}
+
+#logo {
+ display: block;
+ width: 50%;
+ height: 50%;
+ margin: auto;
+ padding: 10% 0 0;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ background-origin: content-box;
+}
+
+.result {
+ height: 20px;
+ line-height: 20px;
+ margin: 1.5rem auto;
+}
+
+.input-box .btn {
+ width: 60px;
+ height: 30px;
+ line-height: 30px;
+ border-radius: 3px;
+ border: none;
+ margin: 0 0 0 20px;
+ padding: 0 8px;
+ cursor: pointer;
+}
+
+.input-box .btn:hover {
+ background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
+ color: #333333;
+}
+
+.input-box .input {
+ border: none;
+ border-radius: 3px;
+ outline: none;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 10px;
+ background-color: rgba(240, 240, 240, 1);
+ -webkit-font-smoothing: antialiased;
+}
+
+.input-box .input:hover {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+}
+
+.input-box .input:focus {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+}
\ No newline at end of file
diff --git a/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.tsx b/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.tsx
new file mode 100644
index 000000000..a6e56f9f8
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/react-ts/frontend/src/App.tsx
@@ -0,0 +1,28 @@
+import {useState} from 'react';
+import logo from './assets/images/logo-universal.png';
+import './App.css';
+import {Greet} from "../wailsjs/go/main/App";
+
+function App() {
+ const [resultText, setResultText] = useState("Please enter your name below 👇");
+ const [name, setName] = useState('');
+ const updateName = (e: any) => setName(e.target.value);
+ const updateResultText = (result: string) => setResultText(result);
+
+ function greet() {
+ Greet(name).then(updateResultText);
+ }
+
+ return (
+
+ )
+}
+
+export default App
diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/pkg/templates/generate/assets/react/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/assets/react/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..01c74ee9b
Binary files /dev/null and b/v2/pkg/templates/generate/assets/react/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/main.jsx b/v2/pkg/templates/generate/assets/react/frontend/src/main.jsx
new file mode 100644
index 000000000..e50e105db
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/react/frontend/src/main.jsx
@@ -0,0 +1,14 @@
+import React from 'react'
+import {createRoot} from 'react-dom/client'
+import './style.css'
+import App from './App'
+
+const container = document.getElementById('root')
+
+const root = createRoot(container)
+
+root.render(
+
+
+
+)
diff --git a/v2/pkg/templates/generate/assets/react/frontend/src/style.css b/v2/pkg/templates/generate/assets/react/frontend/src/style.css
new file mode 100644
index 000000000..3940d6c63
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/react/frontend/src/style.css
@@ -0,0 +1,26 @@
+html {
+ background-color: rgba(27, 38, 54, 1);
+ text-align: center;
+ color: white;
+}
+
+body {
+ margin: 0;
+ color: white;
+ font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+}
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
+}
+
+#app {
+ height: 100vh;
+ text-align: center;
+}
diff --git a/v2/pkg/templates/generate/assets/svelte-ts/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/svelte-ts/frontend/index.tmpl.html
new file mode 100644
index 000000000..3dd212f2d
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/svelte-ts/frontend/index.tmpl.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ {{.ProjectName}}
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/assets/svelte-ts/frontend/src/App.svelte b/v2/pkg/templates/generate/assets/svelte-ts/frontend/src/App.svelte
new file mode 100644
index 000000000..1987eb090
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/svelte-ts/frontend/src/App.svelte
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+`;
+document.getElementById('logo').src = logo;
+
+let nameElement = document.getElementById("name");
+nameElement.focus();
+let resultElement = document.getElementById("result");
+
+// Setup the greet function
+window.greet = function () {
+ // Get name
+ let name = nameElement.value;
+
+ // Check if the input is empty
+ if (name === "") return;
+
+ // Call App.Greet(name)
+ try {
+ Greet(name)
+ .then((result) => {
+ // Update result with data back from App.Greet()
+ resultElement.innerText = result;
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ } catch (err) {
+ console.error(err);
+ }
+};
diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/vue-ts/frontend/index.tmpl.html
new file mode 100644
index 000000000..cc259435b
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue-ts/frontend/index.tmpl.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ {{.ProjectName}}
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/src/App.vue b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/App.vue
new file mode 100644
index 000000000..b63d187c5
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/App.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/src/components/HelloWorld.vue b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/components/HelloWorld.vue
new file mode 100644
index 000000000..3ab3df798
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/components/HelloWorld.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
{{ data.resultText }}
+
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/src/main.ts b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/main.ts
new file mode 100644
index 000000000..e57db5948
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue-ts/frontend/src/main.ts
@@ -0,0 +1,4 @@
+import {createApp} from 'vue'
+import App from './App.vue'
+
+createApp(App).mount('#app')
diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/tsconfig.json b/v2/pkg/templates/generate/assets/vue-ts/frontend/tsconfig.json
new file mode 100644
index 000000000..3cc844d92
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue-ts/frontend/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "strict": true,
+ "jsx": "preserve",
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "lib": [
+ "ESNext",
+ "DOM"
+ ],
+ "skipLibCheck": true
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.d.ts",
+ "src/**/*.tsx",
+ "src/**/*.vue"
+ ],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ]
+}
diff --git a/v2/pkg/templates/generate/assets/vue/frontend/index.tmpl.html b/v2/pkg/templates/generate/assets/vue/frontend/index.tmpl.html
new file mode 100644
index 000000000..d45b7a8c4
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue/frontend/index.tmpl.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ {{.ProjectName}}
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/App.vue b/v2/pkg/templates/generate/assets/vue/frontend/src/App.vue
new file mode 100644
index 000000000..15d2f1215
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue/frontend/src/App.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..b1224ec79
Binary files /dev/null and b/v2/pkg/templates/generate/assets/vue/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/components/HelloWorld.vue b/v2/pkg/templates/generate/assets/vue/frontend/src/components/HelloWorld.vue
new file mode 100644
index 000000000..29c023fbe
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue/frontend/src/components/HelloWorld.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
{{ data.resultText }}
+
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/main.js b/v2/pkg/templates/generate/assets/vue/frontend/src/main.js
new file mode 100644
index 000000000..e57db5948
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue/frontend/src/main.js
@@ -0,0 +1,4 @@
+import {createApp} from 'vue'
+import App from './App.vue'
+
+createApp(App).mount('#app')
diff --git a/v2/pkg/templates/generate/assets/vue/frontend/src/style.css b/v2/pkg/templates/generate/assets/vue/frontend/src/style.css
new file mode 100644
index 000000000..3940d6c63
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue/frontend/src/style.css
@@ -0,0 +1,26 @@
+html {
+ background-color: rgba(27, 38, 54, 1);
+ text-align: center;
+ color: white;
+}
+
+body {
+ margin: 0;
+ color: white;
+ font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+}
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
+}
+
+#app {
+ height: 100vh;
+ text-align: center;
+}
diff --git a/v2/pkg/templates/generate/assets/vue/frontend/vite.config.js b/v2/pkg/templates/generate/assets/vue/frontend/vite.config.js
new file mode 100644
index 000000000..a30c338ed
--- /dev/null
+++ b/v2/pkg/templates/generate/assets/vue/frontend/vite.config.js
@@ -0,0 +1,7 @@
+import {defineConfig} from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [vue()]
+})
diff --git a/v2/pkg/templates/generate/generate.go b/v2/pkg/templates/generate/generate.go
new file mode 100644
index 000000000..6842dc196
--- /dev/null
+++ b/v2/pkg/templates/generate/generate.go
@@ -0,0 +1,232 @@
+package main
+
+import (
+ "embed"
+ "os"
+ "strings"
+
+ "github.com/leaanthony/debme"
+ "github.com/leaanthony/gosod"
+ "github.com/wailsapp/wails/v2/internal/s"
+)
+
+//go:embed assets/common/*
+var common embed.FS
+
+//go:embed assets/svelte/*
+var svelte embed.FS
+
+//go:embed assets/svelte-ts/*
+var sveltets embed.FS
+
+//go:embed assets/lit/*
+var lit embed.FS
+
+//go:embed assets/lit-ts/*
+var litts embed.FS
+
+//go:embed assets/vue/*
+var vue embed.FS
+
+//go:embed assets/vue-ts/*
+var vuets embed.FS
+
+//go:embed assets/react/*
+var react embed.FS
+
+//go:embed assets/react-ts/*
+var reactts embed.FS
+
+//go:embed assets/preact/*
+var preact embed.FS
+
+//go:embed assets/preact-ts/*
+var preactts embed.FS
+
+//go:embed assets/vanilla/*
+var vanilla embed.FS
+
+//go:embed assets/vanilla-ts/*
+var vanillats embed.FS
+
+func checkError(err error) {
+ if err != nil {
+ println("\nERROR:", err.Error())
+ os.Exit(1)
+ }
+}
+
+type template struct {
+ Name string
+ ShortName string
+ Description string
+ Assets embed.FS
+ FilesToDelete []string
+ DirsToDelete []string
+}
+
+var templates = []*template{
+ {
+ Name: "Svelte + Vite",
+ ShortName: "Svelte",
+ Description: "Svelte + Vite development server",
+ Assets: svelte,
+ FilesToDelete: []string{"frontend/index.html", "frontend/.gitignore", "frontend/src/app.css", "frontend/src/assets/svelte.svg"},
+ DirsToDelete: []string{"frontend/public", "frontend/src/lib"},
+ },
+ {
+ Name: "Svelte + Vite (Typescript)",
+ ShortName: "Svelte-TS",
+ Description: "Svelte + TS + Vite development server",
+ Assets: sveltets,
+ FilesToDelete: []string{"frontend/index.html", "frontend/.gitignore", "frontend/src/app.css", "frontend/src/assets/svelte.svg"},
+ DirsToDelete: []string{"frontend/public", "frontend/src/lib"},
+ },
+ {
+ Name: "Lit + Vite",
+ ShortName: "Lit",
+ Description: "Lit + Vite development server",
+ Assets: lit,
+ FilesToDelete: []string{"frontend/index.html", "frontend/vite.config.js"},
+ },
+ {
+ Name: "Lit + Vite (Typescript)",
+ ShortName: "Lit-TS",
+ Description: "Lit + TS + Vite development server",
+ Assets: litts,
+ FilesToDelete: []string{"frontend/index.html", "frontend/src/favicon.svg"},
+ },
+ {
+ Name: "Vue + Vite",
+ ShortName: "Vue",
+ Description: "Vue + Vite development server",
+ Assets: vue,
+ FilesToDelete: []string{"frontend/index.html", "frontend/.gitignore"},
+ DirsToDelete: []string{"frontend/src/assets", "frontend/src/components", "frontend/public"},
+ },
+ {
+ Name: "Vue + Vite (Typescript)",
+ ShortName: "Vue-TS",
+ Description: "Vue + Vite development server",
+ Assets: vuets,
+ FilesToDelete: []string{"frontend/index.html", "frontend/.gitignore"},
+ DirsToDelete: []string{"frontend/src/assets", "frontend/src/components", "frontend/public"},
+ },
+ {
+ Name: "React + Vite",
+ ShortName: "React",
+ Description: "React + Vite development server",
+ Assets: react,
+ FilesToDelete: []string{"frontend/src/index.css", "frontend/src/favicon.svg", "frontend/src/logo.svg", "frontend/.gitignore", "frontend/index.html"},
+ },
+ {
+ Name: "React + Vite (Typescript)",
+ ShortName: "React-TS",
+ Description: "React + Vite development server",
+ Assets: reactts,
+ FilesToDelete: []string{"frontend/src/index.css", "frontend/src/favicon.svg", "frontend/src/logo.svg", "frontend/.gitignore", "frontend/index.html"},
+ },
+ {
+ Name: "Preact + Vite",
+ ShortName: "Preact",
+ Description: "Preact + Vite development server",
+ Assets: preact,
+ FilesToDelete: []string{"frontend/src/index.css", "frontend/src/favicon.svg", "frontend/src/logo.jsx", "frontend/.gitignore", "frontend/index.html"},
+ DirsToDelete: []string{"frontend/public"},
+ },
+ {
+ Name: "Preact + Vite (Typescript)",
+ ShortName: "Preact-TS",
+ Description: "Preact + Vite development server",
+ Assets: preactts,
+ FilesToDelete: []string{"frontend/src/index.css", "frontend/src/favicon.svg", "frontend/src/assets/preact.svg", "frontend/src/logo.tsx", "frontend/.gitignore", "frontend/index.html"},
+ DirsToDelete: []string{"frontend/public"},
+ },
+ {
+ Name: "Vanilla + Vite",
+ ShortName: "Vanilla",
+ Description: "Vanilla + Vite development server",
+ Assets: vanilla,
+ FilesToDelete: []string{"frontend/.gitignore", "frontend/index.html", "frontend/favicon.svg", "frontend/main.js", "frontend/style.css"},
+ },
+ {
+ Name: "Vanilla + Vite (Typescript)",
+ ShortName: "Vanilla-TS",
+ Description: "Vanilla + Vite development server",
+ Assets: vanillats,
+ FilesToDelete: []string{"frontend/.gitignore", "frontend/index.html", "frontend/favicon.svg", "frontend/src/main.ts", "frontend/src/style.css"},
+ },
+}
+
+func main() {
+ rebuildRuntime()
+
+ for _, t := range templates {
+ createTemplate(t)
+ }
+
+ // copy plain template
+ s.ECHO("Copying plain template")
+ s.RMDIR("../templates/plain")
+ s.COPYDIR("plain", "../templates/plain")
+
+ s.ECHO(`Until an auto fix is done, add "@babel/types": "^7.17.10" to vue-ts/frontend/package.json`)
+}
+
+func rebuildRuntime() {
+ s.ECHO("Generating Runtime")
+ cwd := s.CWD()
+ const runtimeDir = "../../../internal/frontend/runtime/"
+ const commonDir = "./assets/common/frontend/wailsjs/runtime/"
+ s.CD(runtimeDir)
+ s.EXEC("npm run build")
+ s.ECHO("Copying new files")
+ s.CD("wrapper")
+ s.COPY("package.json", commonDir+"package.json")
+ s.COPY("runtime.js", commonDir+"runtime.js")
+ s.COPY("runtime.d.ts", commonDir+"runtime.d.ts")
+ s.CD(cwd)
+}
+
+func createTemplate(template *template) {
+ cwd := s.CWD()
+ name := template.Name
+ shortName := strings.ToLower(template.ShortName)
+ assets, err := debme.FS(template.Assets, "assets/"+shortName)
+ checkError(err)
+ commonAssets, err := debme.FS(common, "assets/common")
+ checkError(err)
+
+ s.CD("..")
+ s.ENDIR("templates")
+ s.CD("templates")
+ s.RMDIR(shortName)
+ s.COPYDIR("../base", shortName)
+ s.CD(shortName)
+ s.ECHO("Generating vite template: " + shortName)
+ s.EXEC("npm create vite@latest frontend --template " + shortName)
+
+ // Clean up template
+ for _, fileToDelete := range template.FilesToDelete {
+ s.DELETE(fileToDelete)
+ }
+ for _, dirToDelete := range template.DirsToDelete {
+ s.RMDIR(dirToDelete)
+ }
+ s.REPLACEALL("README.md", s.Sub{"$NAME": template.ShortName})
+ s.REPLACEALL("template.json", s.Sub{"$NAME": name, "$SHORTNAME": shortName, "$DESCRIPTION": template.Description})
+
+ // Add common files
+ g := gosod.New(commonAssets)
+ g.SetTemplateFilters([]string{})
+ err = g.Extract(".", nil)
+ checkError(err)
+
+ // Add custom files
+ g = gosod.New(assets)
+ g.SetTemplateFilters([]string{})
+ err = g.Extract(".", nil)
+ checkError(err)
+
+ s.CD(cwd)
+}
diff --git a/v2/pkg/templates/generate/go.sum b/v2/pkg/templates/generate/go.sum
new file mode 100644
index 000000000..69c3ba18a
--- /dev/null
+++ b/v2/pkg/templates/generate/go.sum
@@ -0,0 +1,4 @@
+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
+github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
+github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
diff --git a/v2/pkg/templates/generate/plain/.gitignore.tmpl b/v2/pkg/templates/generate/plain/.gitignore.tmpl
new file mode 100644
index 000000000..b92a6f8bf
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/.gitignore.tmpl
@@ -0,0 +1,12 @@
+# Wails bin directory
+build/bin
+# Wails Windows NSIS support files
+build/windows/installer/wails_tools.nsh
+build/windows/installer/tmp/
+
+# IDEs
+.idea
+.vscode
+
+# The black hole that is...
+node_modules
diff --git a/v2/pkg/templates/generate/plain/README.md b/v2/pkg/templates/generate/plain/README.md
new file mode 100644
index 000000000..9fcd85bdd
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/README.md
@@ -0,0 +1,18 @@
+# README
+
+## About
+
+This template uses plain JS / HTML and CSS.
+
+You can configure the project by editing `wails.json`. More information about the project settings can be found
+here: https://wails.io/docs/reference/project-config
+
+## Live Development
+
+To run in live development mode, run `wails dev` in the project directory. The frontend dev server will run
+on http://localhost:34115. Open this in your browser to connect to your application.
+
+## Building
+
+For a production build, use `wails build`.
+
diff --git a/v2/pkg/templates/generate/plain/app.go b/v2/pkg/templates/generate/plain/app.go
new file mode 100644
index 000000000..224be7156
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/app.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "context"
+ "fmt"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called at application startup
+func (a *App) startup(ctx context.Context) {
+ // Perform your setup here
+ a.ctx = ctx
+}
+
+// domReady is called after front-end resources have been loaded
+func (a App) domReady(ctx context.Context) {
+ // Add your action here
+}
+
+// beforeClose is called when the application is about to quit,
+// either by clicking the window close button or calling runtime.Quit.
+// Returning true will cause the application to continue, false will continue shutdown as normal.
+func (a *App) beforeClose(ctx context.Context) (prevent bool) {
+ return false
+}
+
+// shutdown is called at application termination
+func (a *App) shutdown(ctx context.Context) {
+ // Perform your teardown here
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+ return fmt.Sprintf("Hello %s, It's show time!", name)
+}
diff --git a/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/pkg/templates/generate/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/pkg/templates/generate/plain/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/generate/plain/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..b1224ec79
Binary files /dev/null and b/v2/pkg/templates/generate/plain/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/pkg/templates/generate/plain/frontend/src/index.tmpl.html b/v2/pkg/templates/generate/plain/frontend/src/index.tmpl.html
new file mode 100644
index 000000000..a8a434a37
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/frontend/src/index.tmpl.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ {{.ProjectName}}
+
+
+
+
+
+
+
Please enter your name below 👇
+
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/generate/plain/frontend/src/main.css b/v2/pkg/templates/generate/plain/frontend/src/main.css
new file mode 100644
index 000000000..dab87d09a
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/frontend/src/main.css
@@ -0,0 +1,82 @@
+html {
+ background-color: rgba(27, 38, 54, 1);
+ text-align: center;
+ color: white;
+}
+
+body {
+ margin: 0;
+ color: white;
+ font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+}
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
+}
+
+#app {
+ height: 100vh;
+ text-align: center;
+}
+
+.logo {
+ display: block;
+ width: 50%;
+ height: 50%;
+ margin: auto;
+ padding: 10% 0 0;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: url("./assets/images/logo-universal.png");
+ background-size: 100% 100%;
+ background-origin: content-box;
+}
+
+.result {
+ height: 20px;
+ line-height: 20px;
+ margin: 1.5rem auto;
+}
+
+.input-box .btn {
+ width: 60px;
+ height: 30px;
+ line-height: 30px;
+ border-radius: 3px;
+ border: none;
+ margin: 0 0 0 20px;
+ padding: 0 8px;
+ cursor: pointer;
+}
+
+.input-box .btn:hover {
+ background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
+ color: #333333;
+}
+
+.input-box .input {
+ border: none;
+ border-radius: 3px;
+ outline: none;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 10px;
+ background-color: rgba(240, 240, 240, 1);
+ -webkit-font-smoothing: antialiased;
+}
+
+.input-box .input:hover {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+}
+
+.input-box .input:focus {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+}
diff --git a/v2/pkg/templates/generate/plain/frontend/src/main.js b/v2/pkg/templates/generate/plain/frontend/src/main.js
new file mode 100644
index 000000000..3346d59ff
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/frontend/src/main.js
@@ -0,0 +1,32 @@
+// Get input + focus
+let nameElement = document.getElementById("name");
+nameElement.focus();
+
+// Setup the greet function
+window.greet = function () {
+ // Get name
+ let name = nameElement.value;
+
+ // Check if the input is empty
+ if (name === "") return;
+
+ // Call App.Greet(name)
+ try {
+ window.go.main.App.Greet(name)
+ .then((result) => {
+ // Update result with data back from App.Greet()
+ document.getElementById("result").innerText = result;
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ } catch (err) {
+ console.error(err);
+ }
+};
+
+nameElement.onkeydown = function (e) {
+ if (e.keyCode == 13) {
+ window.greet();
+ }
+};
diff --git a/v2/pkg/templates/generate/plain/go.mod.tmpl b/v2/pkg/templates/generate/plain/go.mod.tmpl
new file mode 100644
index 000000000..f6d0daec4
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/go.mod.tmpl
@@ -0,0 +1,34 @@
+module changeme
+
+go 1.23.0
+
+require github.com/wailsapp/wails/v2 {{.WailsVersion}}
+
+require (
+github.com/andybalholm/brotli v1.0.2 // indirect
+github.com/davecgh/go-spew v1.1.1 // indirect
+github.com/fasthttp/websocket v0.0.0-20200320073529-1554a54587ab // indirect
+github.com/wailsapp/mimetype v1.4.1-beta.1
+github.com/go-ole/go-ole v1.2.5 // indirect
+github.com/gofiber/fiber/v2 v2.17.0 // indirect
+github.com/gofiber/websocket/v2 v2.0.8 // indirect
+github.com/google/uuid v1.1.2 // indirect
+github.com/imdario/mergo v0.3.12 // indirect
+github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5 // indirect
+github.com/klauspost/compress v1.12.2 // indirect
+github.com/leaanthony/debme v1.2.1 // indirect
+github.com/leaanthony/go-ansi-parser v1.0.1 // indirect
+github.com/leaanthony/slicer v1.5.0 // indirect
+github.com/leaanthony/typescriptify-golang-structs v0.1.7 // indirect
+github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect
+github.com/pkg/errors v0.9.1 // indirect
+github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f // indirect
+github.com/tkrajina/go-reflector v0.5.5 // indirect
+github.com/valyala/bytebufferpool v1.0.0 // indirect
+github.com/valyala/fasthttp v1.28.0 // indirect
+github.com/valyala/tcplisten v1.0.0 // indirect
+golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
+golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect
+)
+
+// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}}
diff --git a/v2/pkg/templates/generate/plain/go.sum b/v2/pkg/templates/generate/plain/go.sum
new file mode 100644
index 000000000..3e14e745f
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/go.sum
@@ -0,0 +1,220 @@
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
+github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
+github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/fasthttp/websocket v0.0.0-20200320073529-1554a54587ab h1:9e2joQGp642wHGFP5m86SDptAavrdGBe8/x9DGEEAaI=
+github.com/fasthttp/websocket v0.0.0-20200320073529-1554a54587ab/go.mod h1:smsv/h4PBEBaU0XDTY5UwJTpZv69fQ0FfcLJr21mA6Y=
+github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/flytam/filenamify v1.0.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/wailsapp/mimetype v1.4.1-beta.1 h1:gSnKX7WH+7aA0EEjOGUmpWXTb0Nt5B7/8Dm9wHLrnnY=
+github.com/wailsapp/mimetype v1.4.1-beta.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
+github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
+github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw=
+github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
+github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
+github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/gofiber/fiber/v2 v2.17.0 h1:qP3PkGUbBB0i9iQh5E057XI1yO5CZigUxZhyUFYAFoM=
+github.com/gofiber/fiber/v2 v2.17.0/go.mod h1:iftruuHGkRYGEXVISmdD7HTYWyfS2Bh+Dkfq4n/1Owg=
+github.com/gofiber/websocket/v2 v2.0.8 h1:Hb4y6IxYZVMO0segROODXJiXVgVD3a6i7wnfot8kM6k=
+github.com/gofiber/websocket/v2 v2.0.8/go.mod h1:fv8HSGQX09sauNv9g5Xq8GeGAaahLFYQKKb4ZdT0x2w=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
+github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.12.2 h1:2KCfW3I9M7nSc5wOqXAlW2v2U6v+w6cbjvbfp+OykW8=
+github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
+github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
+github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4=
+github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM=
+github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
+github.com/leaanthony/idgen v1.0.0/go.mod h1:4nBZnt8ml/f/ic/EVQuLxuj817RccT2fyrUaZFxrcVA=
+github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
+github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
+github.com/leaanthony/typescriptify-golang-structs v0.1.7 h1:yoznzWzyxkO/iWdlpq+aPcuJ5Y/hpjq/lmgMFmpjwl0=
+github.com/leaanthony/typescriptify-golang-structs v0.1.7/go.mod h1:cWtOkiVhMF77e6phAXUcfNwYmMwCJ67Sij24lfvi9Js=
+github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
+github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
+github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc=
+github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f h1:PgA+Olipyj258EIEYnpFFONrrCcAIWNUNoFhUfMqAGY=
+github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/tc-hib/winres v0.1.5/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A=
+github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs=
+github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ=
+github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
+github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
+github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tidwall/sjson v1.1.7/go.mod h1:w/yG+ezBeTdUxiKs5NcPicO9diP38nk96QBAbIIGeFs=
+github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ=
+github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
+github.com/valyala/fasthttp v1.26.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA=
+github.com/valyala/fasthttp v1.28.0 h1:ruVmTmZaBR5i67NqnjvvH5gEv0zwHfWtbjoyW98iho4=
+github.com/valyala/fasthttp v1.28.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA=
+github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+github.com/wailsapp/wails/v2 v2.0.0-beta.3 h1:8vhBbnjpYDF6cCUwKadon7J/98UjcP1nrnptUl70Tfg=
+github.com/wailsapp/wails/v2 v2.0.0-beta.3/go.mod h1:aku28riyHF2G5jmx/qtxjLWi7VwpTjhhX/HVLCptWFA=
+github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28=
+github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20=
+github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
+github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/ztrue/tracerr v0.3.0/go.mod h1:qEalzze4VN9O8tnhBXScfCrmoJo10o8TN5ciKjm6Mww=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
+golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
diff --git a/v2/pkg/templates/generate/plain/main.go.tmpl b/v2/pkg/templates/generate/plain/main.go.tmpl
new file mode 100644
index 000000000..9f3e2fffe
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/main.go.tmpl
@@ -0,0 +1,86 @@
+package main
+
+import (
+"embed"
+"log"
+
+"github.com/wailsapp/wails/v2/pkg/options/mac"
+
+"github.com/wailsapp/wails/v2"
+"github.com/wailsapp/wails/v2/pkg/logger"
+"github.com/wailsapp/wails/v2/pkg/options"
+"github.com/wailsapp/wails/v2/pkg/options/assetserver"
+"github.com/wailsapp/wails/v2/pkg/options/windows"
+)
+
+//go:embed frontend/src
+var assets embed.FS
+
+//go:embed build/appicon.png
+var icon []byte
+
+func main() {
+// Create an instance of the app structure
+app := NewApp()
+
+// Create application with options
+err := wails.Run(&options.App{
+Title: "{{.ProjectName}}",
+Width: 1024,
+Height: 768,
+MinWidth: 1024,
+MinHeight: 768,
+MaxWidth: 1280,
+MaxHeight: 800,
+DisableResize: false,
+Fullscreen: false,
+Frameless: false,
+StartHidden: false,
+HideWindowOnClose: false,
+BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+AssetServer: &assetserver.Options{
+ Assets: assets,
+},
+Menu: nil,
+Logger: nil,
+LogLevel: logger.DEBUG,
+OnStartup: app.startup,
+OnDomReady: app.domReady,
+OnBeforeClose: app.beforeClose,
+OnShutdown: app.shutdown,
+WindowStartState: options.Normal,
+Bind: []interface{}{
+app,
+},
+// Windows platform specific options
+Windows: &windows.Options{
+WebviewIsTransparent: false,
+WindowIsTranslucent: false,
+DisableWindowIcon: false,
+// DisableFramelessWindowDecorations: false,
+WebviewUserDataPath: "",
+},
+Mac: &mac.Options{
+TitleBar: &mac.TitleBar{
+TitlebarAppearsTransparent: true,
+HideTitle: false,
+HideTitleBar: false,
+FullSizeContent: false,
+UseToolbar: false,
+HideToolbarSeparator: true,
+},
+Appearance: mac.NSAppearanceNameDarkAqua,
+WebviewIsTransparent: true,
+WindowIsTranslucent: true,
+About: &mac.AboutInfo{
+Title: "Plain Template",
+Message: "Part of the Wails projects",
+Icon: icon,
+},
+},
+})
+
+if err != nil {
+log.Fatal(err)
+}
+}
diff --git a/v2/pkg/templates/generate/plain/template.json b/v2/pkg/templates/generate/plain/template.json
new file mode 100644
index 000000000..fc919bc3b
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/template.json
@@ -0,0 +1,7 @@
+{
+ "name": "Plain HTML/JS/CSS",
+ "shortname": "plain",
+ "author": "Lea Anthony ",
+ "description": "A simple template using only HTML/CSS/JS",
+ "helpurl": "https://github.com/wailsapp/wails"
+}
diff --git a/v2/pkg/templates/generate/plain/wails.tmpl.json b/v2/pkg/templates/generate/plain/wails.tmpl.json
new file mode 100644
index 000000000..0168826bd
--- /dev/null
+++ b/v2/pkg/templates/generate/plain/wails.tmpl.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://wails.io/schemas/config.v2.json",
+ "name": "{{.ProjectName}}",
+ "outputfilename": "{{.BinaryName}}",
+ "wailsjsdir": "./frontend",
+ "author": {
+ "name": "{{.AuthorName}}",
+ "email": "{{.AuthorEmail}}"
+ }
+}
diff --git a/v2/pkg/templates/ides/goland/gitignore.txt b/v2/pkg/templates/ides/goland/gitignore.txt
new file mode 100644
index 000000000..73f69e095
--- /dev/null
+++ b/v2/pkg/templates/ides/goland/gitignore.txt
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/v2/pkg/templates/ides/goland/modules.tmpl.xml b/v2/pkg/templates/ides/goland/modules.tmpl.xml
new file mode 100644
index 000000000..228a50071
--- /dev/null
+++ b/v2/pkg/templates/ides/goland/modules.tmpl.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2/pkg/templates/ides/goland/name.tmpl b/v2/pkg/templates/ides/goland/name.tmpl
new file mode 100644
index 000000000..8c328a5d3
--- /dev/null
+++ b/v2/pkg/templates/ides/goland/name.tmpl
@@ -0,0 +1 @@
+{{.ProjectName}}
\ No newline at end of file
diff --git a/v2/pkg/templates/ides/goland/projectname.iml b/v2/pkg/templates/ides/goland/projectname.iml
new file mode 100644
index 000000000..5e764c4f0
--- /dev/null
+++ b/v2/pkg/templates/ides/goland/projectname.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2/pkg/templates/ides/goland/vcs.xml b/v2/pkg/templates/ides/goland/vcs.xml
new file mode 100644
index 000000000..78f5bc6f7
--- /dev/null
+++ b/v2/pkg/templates/ides/goland/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2/pkg/templates/ides/goland/workspace.tmpl.xml b/v2/pkg/templates/ides/goland/workspace.tmpl.xml
new file mode 100644
index 000000000..27f8881bf
--- /dev/null
+++ b/v2/pkg/templates/ides/goland/workspace.tmpl.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2/pkg/templates/ides/vscode/launch.tmpl.json b/v2/pkg/templates/ides/vscode/launch.tmpl.json
new file mode 100644
index 000000000..0a5437c9e
--- /dev/null
+++ b/v2/pkg/templates/ides/vscode/launch.tmpl.json
@@ -0,0 +1,32 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Wails: Production {{.ProjectName}}",
+ "type": "go",
+ "request": "launch",
+ "mode": "exec",
+ "program": "${workspaceFolder}/{{.PathToDesktopBinary}}",
+ "preLaunchTask": "build",
+ "cwd": "${workspaceFolder}"
+ },
+ {
+ "name": "Wails: Debug {{.ProjectName}}",
+ "type": "go",
+ "request": "launch",
+ "mode": "exec",
+ "program": "${workspaceFolder}/{{.PathToDesktopBinary}}",
+ "preLaunchTask": "build debug",
+ "cwd": "${workspaceFolder}"
+ },
+ {
+ "name": "Wails: Dev {{.ProjectName}}",
+ "type": "go",
+ "request": "launch",
+ "mode": "exec",
+ "program": "${workspaceFolder}/{{.PathToDesktopBinary}}",
+ "preLaunchTask": "build dev",
+ "cwd": "${workspaceFolder}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/v2/pkg/templates/ides/vscode/tasks.tmpl.json b/v2/pkg/templates/ides/vscode/tasks.tmpl.json
new file mode 100644
index 000000000..fdf1d48dd
--- /dev/null
+++ b/v2/pkg/templates/ides/vscode/tasks.tmpl.json
@@ -0,0 +1,111 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "type": "shell",
+ "options": {
+ "cwd": "${workspaceFolder}",
+ "env": {
+ "CGO_ENABLED": "1"
+ }
+ },
+ "osx": {
+ "options": {
+ "env": {
+ "CGO_CFLAGS": "-mmacosx-version-min=10.13",
+ "CGO_LDFLAGS": "-framework UniformTypeIdentifiers -mmacosx-version-min=10.13"
+ }
+ }
+ },
+ "windows": {
+ "options": {
+ "env": {
+ "CGO_ENABLED": "0"
+ }
+ }
+ },
+ "command": "go",
+ "args": [
+ "build",
+ "-tags",
+ "production,desktop",
+ "-gcflags",
+ "all=-N -l",
+ "-o",
+ "{{.PathToDesktopBinary}}"
+ ]
+ },
+ {
+ "label": "build debug",
+ "type": "shell",
+ "options": {
+ "cwd": "${workspaceFolder}",
+ "env": {
+ "CGO_ENABLED": "1"
+ }
+ },
+ "osx": {
+ "options": {
+ "env": {
+ "CGO_CFLAGS": "-mmacosx-version-min=10.13",
+ "CGO_LDFLAGS": "-framework UniformTypeIdentifiers -mmacosx-version-min=10.13"
+ }
+ }
+ },
+ "windows": {
+ "options": {
+ "env": {
+ "CGO_ENABLED": "0"
+ }
+ }
+ },
+ "command": "go",
+ "args": [
+ "build",
+ "-tags",
+ "production,desktop,debug",
+ "-gcflags",
+ "all=-N -l",
+ "-o",
+ "{{.PathToDesktopBinary}}"
+ ]
+ },
+ {
+ "label": "build dev",
+ "type": "shell",
+ "options": {
+ "cwd": "${workspaceFolder}",
+ "env": {
+ "CGO_ENABLED": "1"
+ }
+ },
+ "osx": {
+ "options": {
+ "env": {
+ "CGO_CFLAGS": "-mmacosx-version-min=10.13",
+ "CGO_LDFLAGS": "-framework UniformTypeIdentifiers -mmacosx-version-min=10.13"
+ }
+ }
+ },
+ "windows": {
+ "options": {
+ "env": {
+ "CGO_ENABLED": "0"
+ }
+ }
+ },
+ "command": "go",
+ "args": [
+ "build",
+ "-tags",
+ "dev",
+ "-gcflags",
+ "all=-N -l",
+ "-o",
+ "{{.PathToDesktopBinary}}"
+ ]
+ }
+ ]
+}
+
\ No newline at end of file
diff --git a/v2/pkg/templates/templates.go b/v2/pkg/templates/templates.go
new file mode 100644
index 000000000..e18185520
--- /dev/null
+++ b/v2/pkg/templates/templates.go
@@ -0,0 +1,407 @@
+package templates
+
+import (
+ "embed"
+ "encoding/json"
+ "fmt"
+ gofs "io/fs"
+ "log"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/pkg/errors"
+
+ "github.com/leaanthony/debme"
+ "github.com/leaanthony/gosod"
+ "github.com/wailsapp/wails/v2/internal/fs"
+ "github.com/wailsapp/wails/v2/pkg/clilogger"
+)
+
+//go:embed all:templates
+var templates embed.FS
+
+//go:embed all:ides/*
+var ides embed.FS
+
+// Cahce for the templates
+// We use this because we need different views of the same data
+var templateCache []Template = nil
+
+// Data contains the data we wish to embed during template installation
+type Data struct {
+ ProjectName string
+ BinaryName string
+ WailsVersion string
+ NPMProjectName string
+ AuthorName string
+ AuthorEmail string
+ AuthorNameAndEmail string
+ WailsDirectory string
+ GoSDKPath string
+ WindowsFlags string
+ CGOEnabled string
+ OutputFile string
+}
+
+// Options for installing a template
+type Options struct {
+ ProjectName string
+ TemplateName string
+ BinaryName string
+ TargetDir string
+ Logger *clilogger.CLILogger
+ PathToDesktopBinary string
+ PathToServerBinary string
+ InitGit bool
+ AuthorName string
+ AuthorEmail string
+ IDE string
+ ProjectNameFilename string // The project name but as a valid filename
+ WailsVersion string
+ GoSDKPath string
+ WindowsFlags string
+ CGOEnabled string
+ CGOLDFlags string
+ OutputFile string
+}
+
+// Template holds data relating to a template
+// including the metadata stored in template.json
+type Template struct {
+ // Template details
+ Name string `json:"name"`
+ ShortName string `json:"shortname"`
+ Author string `json:"author"`
+ Description string `json:"description"`
+ HelpURL string `json:"helpurl"`
+
+ // Other data
+ FS gofs.FS `json:"-"`
+}
+
+func parseTemplate(template gofs.FS) (Template, error) {
+ var result Template
+ data, err := gofs.ReadFile(template, "template.json")
+ if err != nil {
+ return result, errors.Wrap(err, "Error parsing template")
+ }
+ err = json.Unmarshal(data, &result)
+ if err != nil {
+ return result, err
+ }
+ result.FS = template
+ return result, nil
+}
+
+// List returns the list of available templates
+func List() ([]Template, error) {
+ // If the cache isn't loaded, load it
+ if templateCache == nil {
+ err := loadTemplateCache()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return templateCache, nil
+}
+
+// getTemplateByShortname returns the template with the given short name
+func getTemplateByShortname(shortname string) (Template, error) {
+ var result Template
+
+ // If the cache isn't loaded, load it
+ if templateCache == nil {
+ err := loadTemplateCache()
+ if err != nil {
+ return result, err
+ }
+ }
+
+ for _, template := range templateCache {
+ if template.ShortName == shortname {
+ return template, nil
+ }
+ }
+
+ return result, fmt.Errorf("shortname '%s' is not a valid template shortname", shortname)
+}
+
+// Loads the template cache
+func loadTemplateCache() error {
+ templatesFS, err := debme.FS(templates, "templates")
+ if err != nil {
+ return err
+ }
+
+ // Get directories
+ files, err := templatesFS.ReadDir(".")
+ if err != nil {
+ return err
+ }
+
+ // Reset cache
+ templateCache = []Template{}
+
+ for _, file := range files {
+ if file.IsDir() {
+ templateFS, err := templatesFS.FS(file.Name())
+ if err != nil {
+ return err
+ }
+ template, err := parseTemplate(templateFS)
+ if err != nil {
+ // Cannot parse this template, continue
+ continue
+ }
+ templateCache = append(templateCache, template)
+ }
+ }
+
+ return nil
+}
+
+// Install the given template. Returns true if the template is remote.
+func Install(options *Options) (bool, *Template, error) {
+ // Get cwd
+ cwd, err := os.Getwd()
+ if err != nil {
+ return false, nil, err
+ }
+
+ // Did the user want to install in current directory?
+ if options.TargetDir == "" {
+ options.TargetDir = filepath.Join(cwd, options.ProjectName)
+ if fs.DirExists(options.TargetDir) {
+ return false, nil, fmt.Errorf("cannot create project directory. Dir exists: %s", options.TargetDir)
+ }
+ } else {
+ // Get the absolute path of the given directory
+ targetDir, err := filepath.Abs(options.TargetDir)
+ if err != nil {
+ return false, nil, err
+ }
+ options.TargetDir = targetDir
+ if fs.DirExists(options.TargetDir) {
+ // Check if directory is non-empty
+ entries, err := os.ReadDir(options.TargetDir)
+ if err != nil {
+ return false, nil, err
+ }
+ if len(entries) > 0 {
+ return false, nil, fmt.Errorf("cannot initialise project in non-empty directory: %s", options.TargetDir)
+ }
+ } else {
+ err := fs.Mkdir(options.TargetDir)
+ if err != nil {
+ return false, nil, err
+ }
+ }
+ }
+
+ // Flag to indicate remote template
+ remoteTemplate := false
+
+ // Is this a shortname?
+ template, err := getTemplateByShortname(options.TemplateName)
+ if err != nil {
+ // Is this a filepath?
+ templatePath, err := filepath.Abs(options.TemplateName)
+ if fs.DirExists(templatePath) {
+ templateFS := os.DirFS(templatePath)
+ template, err = parseTemplate(templateFS)
+ if err != nil {
+ return false, nil, errors.Wrap(err, "Error installing template")
+ }
+ } else {
+ // git clone to temporary dir
+ tempdir, err := gitclone(options)
+ defer func(path string) {
+ err := os.RemoveAll(path)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }(tempdir)
+ if err != nil {
+ return false, nil, err
+ }
+ // Remove the .git directory
+ err = os.RemoveAll(filepath.Join(tempdir, ".git"))
+ if err != nil {
+ return false, nil, err
+ }
+
+ templateFS := os.DirFS(tempdir)
+ template, err = parseTemplate(templateFS)
+ if err != nil {
+ return false, nil, err
+ }
+ remoteTemplate = true
+ }
+ }
+
+ // Use Gosod to install the template
+ installer := gosod.New(template.FS)
+
+ // Ignore template.json files
+ installer.IgnoreFile("template.json")
+
+ // Setup the data.
+ // We use the directory name for the binary name, like Go
+ BinaryName := filepath.Base(options.TargetDir)
+ NPMProjectName := strings.ToLower(strings.ReplaceAll(BinaryName, " ", ""))
+ localWailsDirectory := fs.RelativePath("../../../../../..")
+
+ templateData := &Data{
+ ProjectName: options.ProjectName,
+ BinaryName: filepath.Base(options.TargetDir),
+ NPMProjectName: NPMProjectName,
+ WailsDirectory: localWailsDirectory,
+ AuthorEmail: options.AuthorEmail,
+ AuthorName: options.AuthorName,
+ WailsVersion: options.WailsVersion,
+ GoSDKPath: options.GoSDKPath,
+ }
+
+ // Create a formatted name and email combo.
+ if options.AuthorName != "" {
+ templateData.AuthorNameAndEmail = options.AuthorName + " "
+ }
+ if options.AuthorEmail != "" {
+ templateData.AuthorNameAndEmail += "<" + options.AuthorEmail + ">"
+ }
+ templateData.AuthorNameAndEmail = strings.TrimSpace(templateData.AuthorNameAndEmail)
+
+ installer.RenameFiles(map[string]string{
+ "gitignore.txt": ".gitignore",
+ })
+
+ // Extract the template
+ err = installer.Extract(options.TargetDir, templateData)
+ if err != nil {
+ return false, nil, err
+ }
+
+ err = generateIDEFiles(options)
+ if err != nil {
+ return false, nil, err
+ }
+
+ return remoteTemplate, &template, nil
+}
+
+// Clones the given uri and returns the temporary cloned directory
+func gitclone(options *Options) (string, error) {
+ // Create temporary directory
+ dirname, err := os.MkdirTemp("", "wails-template-*")
+ if err != nil {
+ return "", err
+ }
+
+ // Parse remote template url and version number
+ templateInfo := strings.Split(options.TemplateName, "@")
+ cloneOption := &git.CloneOptions{
+ URL: templateInfo[0],
+ }
+ if len(templateInfo) > 1 {
+ cloneOption.ReferenceName = plumbing.NewTagReferenceName(templateInfo[1])
+ }
+
+ _, err = git.PlainClone(dirname, false, cloneOption)
+
+ return dirname, err
+}
+
+func generateIDEFiles(options *Options) error {
+ switch options.IDE {
+ case "vscode":
+ return generateVSCodeFiles(options)
+ case "goland":
+ return generateGolandFiles(options)
+ }
+
+ return nil
+}
+
+type ideOptions struct {
+ name string
+ targetDir string
+ options *Options
+ renameFiles map[string]string
+ ignoredFiles []string
+}
+
+func generateGolandFiles(options *Options) error {
+ ideoptions := ideOptions{
+ name: "goland",
+ targetDir: filepath.Join(options.TargetDir, ".idea"),
+ options: options,
+ renameFiles: map[string]string{
+ "projectname.iml": options.ProjectNameFilename + ".iml",
+ "gitignore.txt": ".gitignore",
+ "name": ".name",
+ },
+ }
+ if !options.InitGit {
+ ideoptions.ignoredFiles = []string{"vcs.xml"}
+ }
+ err := installIDEFiles(ideoptions)
+ if err != nil {
+ return errors.Wrap(err, "generating Goland IDE files")
+ }
+
+ return nil
+}
+
+func generateVSCodeFiles(options *Options) error {
+ ideoptions := ideOptions{
+ name: "vscode",
+ targetDir: filepath.Join(options.TargetDir, ".vscode"),
+ options: options,
+ }
+ return installIDEFiles(ideoptions)
+}
+
+func installIDEFiles(o ideOptions) error {
+ source, err := debme.FS(ides, "ides/"+o.name)
+ if err != nil {
+ return err
+ }
+
+ // Use gosod to install the template
+ installer := gosod.New(source)
+
+ if o.renameFiles != nil {
+ installer.RenameFiles(o.renameFiles)
+ }
+
+ for _, ignoreFile := range o.ignoredFiles {
+ installer.IgnoreFile(ignoreFile)
+ }
+
+ binaryName := filepath.Base(o.options.TargetDir)
+ o.options.WindowsFlags = ""
+ o.options.CGOEnabled = "1"
+
+ switch runtime.GOOS {
+ case "windows":
+ binaryName += ".exe"
+ o.options.WindowsFlags = " -H windowsgui"
+ o.options.CGOEnabled = "0"
+ case "darwin":
+ o.options.CGOLDFlags = "-framework UniformTypeIdentifiers"
+ }
+
+ o.options.PathToDesktopBinary = filepath.ToSlash(filepath.Join("build", "bin", binaryName))
+
+ err = installer.Extract(o.targetDir, o.options)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/v2/pkg/templates/templates/lit-ts/.gitignore.tmpl b/v2/pkg/templates/templates/lit-ts/.gitignore.tmpl
new file mode 100644
index 000000000..129d52294
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/.gitignore.tmpl
@@ -0,0 +1,3 @@
+build/bin
+node_modules
+frontend/dist
diff --git a/v2/pkg/templates/templates/lit-ts/README.md b/v2/pkg/templates/templates/lit-ts/README.md
new file mode 100644
index 000000000..98d4d0447
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/README.md
@@ -0,0 +1,19 @@
+# README
+
+## About
+
+This is the official Wails Lit-TS template.
+
+You can configure the project by editing `wails.json`. More information about the project settings can be found
+here: https://wails.io/docs/reference/project-config
+
+## Live Development
+
+To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
+server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
+and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
+to this in your browser, and you can call your Go code from devtools.
+
+## Building
+
+To build a redistributable, production mode package, use `wails build`.
diff --git a/v2/pkg/templates/templates/lit-ts/app.tmpl.go b/v2/pkg/templates/templates/lit-ts/app.tmpl.go
new file mode 100644
index 000000000..af53038a1
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/app.tmpl.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "context"
+ "fmt"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called when the app starts. The context is saved
+// so we can call the runtime methods
+func (a *App) startup(ctx context.Context) {
+ a.ctx = ctx
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+ return fmt.Sprintf("Hello %s, It's show time!", name)
+}
diff --git a/cmd/templates/vuebasic/.gitignore b/v2/pkg/templates/templates/lit-ts/frontend/.gitignore.tmpl
similarity index 54%
rename from cmd/templates/vuebasic/.gitignore
rename to v2/pkg/templates/templates/lit-ts/frontend/.gitignore.tmpl
index 185e66319..a547bf36d 100644
--- a/cmd/templates/vuebasic/.gitignore
+++ b/v2/pkg/templates/templates/lit-ts/frontend/.gitignore.tmpl
@@ -1,21 +1,24 @@
-.DS_Store
-node_modules
-/dist
-
-# local env files
-.env.local
-.env.*.local
-
-# Log files
+# Logs
+logs
+*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
.idea
-.vscode
+.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
-*.sw*
+*.sw?
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/dist/gitkeep b/v2/pkg/templates/templates/lit-ts/frontend/dist/gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/index.tmpl.html b/v2/pkg/templates/templates/lit-ts/frontend/index.tmpl.html
new file mode 100644
index 000000000..febcb76cb
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/index.tmpl.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ {{.ProjectName}}
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/package.json b/v2/pkg/templates/templates/lit-ts/frontend/package.json
new file mode 100644
index 000000000..01aa1512c
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "dist/my-element.es.js",
+ "exports": {
+ ".": "./dist/my-element.es.js"
+ },
+ "types": "types/my-element.d.ts",
+ "files": [
+ "dist",
+ "types"
+ ],
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build"
+ },
+ "dependencies": {
+ "lit": "^2.2.8"
+ },
+ "devDependencies": {
+ "typescript": "^4.6.4",
+ "vite": "^3.0.7"
+ }
+}
\ No newline at end of file
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..b1224ec79
Binary files /dev/null and b/v2/pkg/templates/templates/lit-ts/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/my-element.ts b/v2/pkg/templates/templates/lit-ts/frontend/src/my-element.ts
new file mode 100644
index 000000000..af4e9ce20
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/src/my-element.ts
@@ -0,0 +1,103 @@
+import {css, html, LitElement} from 'lit'
+import logo from './assets/images/logo-universal.png'
+import {Greet} from "../wailsjs/go/main/App";
+import {customElement, property} from 'lit/decorators.js'
+import './style.css';
+
+/**
+ * An example element.
+ *
+ * @slot - This element has a slot
+ * @csspart button - The button
+ */
+@customElement('my-element')
+export class MyElement extends LitElement {
+ static styles = css`
+ #logo {
+ display: block;
+ width: 50%;
+ height: 50%;
+ margin: auto;
+ padding: 10% 0 0;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ background-origin: content-box;
+ }
+
+ .result {
+ height: 20px;
+ line-height: 20px;
+ margin: 1.5rem auto;
+ }
+
+ .input-box .btn {
+ width: 60px;
+ height: 30px;
+ line-height: 30px;
+ border-radius: 3px;
+ border: none;
+ margin: 0 0 0 20px;
+ padding: 0 8px;
+ cursor: pointer;
+ }
+
+ .input-box .btn:hover {
+ background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
+ color: #333333;
+ }
+
+ .input-box .input {
+ border: none;
+ border-radius: 3px;
+ outline: none;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 10px;
+ background-color: rgba(240, 240, 240, 1);
+ -webkit-font-smoothing: antialiased;
+ }
+
+ .input-box .input:hover {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+ }
+
+ .input-box .input:focus {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+ }
+
+ `
+
+ @property()
+ resultText = "Please enter your name below 👇"
+
+ greet() {
+ let thisName = (this.shadowRoot?.getElementById('name') as HTMLInputElement)?.value;
+ if (thisName) {
+ Greet(thisName).then(result => {
+ this.resultText = result
+ });
+ }
+ }
+
+ render() {
+ return html`
+
+
+
${this.resultText}
+
+
+
+
+
+ `
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'my-element': MyElement
+ }
+}
\ No newline at end of file
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/style.css b/v2/pkg/templates/templates/lit-ts/frontend/src/style.css
new file mode 100644
index 000000000..3940d6c63
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/src/style.css
@@ -0,0 +1,26 @@
+html {
+ background-color: rgba(27, 38, 54, 1);
+ text-align: center;
+ color: white;
+}
+
+body {
+ margin: 0;
+ color: white;
+ font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+}
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
+}
+
+#app {
+ height: 100vh;
+ text-align: center;
+}
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/src/vite-env.d.ts b/v2/pkg/templates/templates/lit-ts/frontend/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.json b/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.json
new file mode 100644
index 000000000..a28678589
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "module": "ESNext",
+ "lib": [
+ "ES2020",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "outDir": "./types",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "moduleResolution": "Node",
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "experimentalDecorators": true,
+ "forceConsistentCasingInFileNames": true,
+ "useDefineForClassFields": false,
+ "skipLibCheck": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ]
+}
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.node.json b/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.node.json
new file mode 100644
index 000000000..b8afcc8fa
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": [
+ "vite.config.ts"
+ ]
+}
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/vite.config.ts b/v2/pkg/templates/templates/lit-ts/frontend/vite.config.ts
new file mode 100644
index 000000000..bbb7f5889
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/vite.config.ts
@@ -0,0 +1,4 @@
+import {defineConfig} from 'vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({})
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.d.ts
new file mode 100644
index 000000000..43173cfce
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.d.ts
@@ -0,0 +1,4 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Greet(arg1: string): Promise;
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.js
new file mode 100644
index 000000000..0ee085c95
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/go/main/App.js
@@ -0,0 +1,7 @@
+// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Greet(arg1) {
+ return window['go']['main']['App']['Greet'](arg1);
+}
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/package.json
new file mode 100644
index 000000000..1e7c8a5d7
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@wailsapp/runtime",
+ "version": "2.0.0",
+ "description": "Wails Javascript runtime library",
+ "main": "runtime.js",
+ "types": "runtime.d.ts",
+ "scripts": {
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/wailsapp/wails.git"
+ },
+ "keywords": [
+ "Wails",
+ "Javascript",
+ "Go"
+ ],
+ "author": "Lea Anthony ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/wailsapp/wails/issues"
+ },
+ "homepage": "https://github.com/wailsapp/wails#readme"
+}
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.d.ts
new file mode 100644
index 000000000..336fb07aa
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.d.ts
@@ -0,0 +1,211 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export interface Size {
+ w: number;
+ h: number;
+}
+
+export interface Screen {
+ isCurrent: boolean;
+ isPrimary: boolean;
+ width: number
+ height: number
+}
+
+// Environment information such as platform, buildtype, ...
+export interface EnvironmentInfo {
+ buildType: string;
+ platform: string;
+ arch: string;
+}
+
+// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
+// emits the given event. Optional data may be passed with the event.
+// This will trigger any event listeners.
+export function EventsEmit(eventName: string, ...data: any): void;
+
+// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
+export function EventsOn(eventName: string, callback: (...data: any) => void): void;
+
+// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
+// sets up a listener for the given event name, but will only trigger a given number times.
+export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void;
+
+// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
+// sets up a listener for the given event name, but will only trigger once.
+export function EventsOnce(eventName: string, callback: (...data: any) => void): void;
+
+// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff)
+// unregisters the listener for the given event name.
+export function EventsOff(eventName: string): void;
+
+// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
+// unregisters all event listeners.
+export function EventsOffAll(): void;
+
+// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
+// logs the given message as a raw message
+export function LogPrint(message: string): void;
+
+// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
+// logs the given message at the `trace` log level.
+export function LogTrace(message: string): void;
+
+// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
+// logs the given message at the `debug` log level.
+export function LogDebug(message: string): void;
+
+// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
+// logs the given message at the `error` log level.
+export function LogError(message: string): void;
+
+// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
+// logs the given message at the `fatal` log level.
+// The application will quit after calling this method.
+export function LogFatal(message: string): void;
+
+// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
+// logs the given message at the `info` log level.
+export function LogInfo(message: string): void;
+
+// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
+// logs the given message at the `warning` log level.
+export function LogWarning(message: string): void;
+
+// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
+// Forces a reload by the main application as well as connected browsers.
+export function WindowReload(): void;
+
+// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
+// Reloads the application frontend.
+export function WindowReloadApp(): void;
+
+// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
+// Sets the window AlwaysOnTop or not on top.
+export function WindowSetAlwaysOnTop(b: boolean): void;
+
+// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
+// *Windows only*
+// Sets window theme to system default (dark/light).
+export function WindowSetSystemDefaultTheme(): void;
+
+// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
+// *Windows only*
+// Sets window to light theme.
+export function WindowSetLightTheme(): void;
+
+// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
+// *Windows only*
+// Sets window to dark theme.
+export function WindowSetDarkTheme(): void;
+
+// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
+// Centers the window on the monitor the window is currently on.
+export function WindowCenter(): void;
+
+// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
+// Sets the text in the window title bar.
+export function WindowSetTitle(title: string): void;
+
+// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
+// Makes the window full screen.
+export function WindowFullscreen(): void;
+
+// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
+// Restores the previous window dimensions and position prior to full screen.
+export function WindowUnfullscreen(): void;
+
+// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
+// Sets the width and height of the window.
+export function WindowSetSize(width: number, height: number): void;
+
+// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
+// Gets the width and height of the window.
+export function WindowGetSize(): Promise;
+
+// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
+// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMaxSize(width: number, height: number): void;
+
+// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
+// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMinSize(width: number, height: number): void;
+
+// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
+// Sets the window position relative to the monitor the window is currently on.
+export function WindowSetPosition(x: number, y: number): void;
+
+// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
+// Gets the window position relative to the monitor the window is currently on.
+export function WindowGetPosition(): Promise;
+
+// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
+// Hides the window.
+export function WindowHide(): void;
+
+// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
+// Shows the window, if it is currently hidden.
+export function WindowShow(): void;
+
+// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
+// Maximises the window to fill the screen.
+export function WindowMaximise(): void;
+
+// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
+// Toggles between Maximised and UnMaximised.
+export function WindowToggleMaximise(): void;
+
+// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
+// Restores the window to the dimensions and position prior to maximising.
+export function WindowUnmaximise(): void;
+
+// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
+// Minimises the window.
+export function WindowMinimise(): void;
+
+// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
+// Restores the window to the dimensions and position prior to minimising.
+export function WindowUnminimise(): void;
+
+// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
+// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
+export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
+
+// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
+// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
+export function ScreenGetAll(): Promise;
+
+// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
+// Opens the given URL in the system browser.
+export function BrowserOpenURL(url: string): void;
+
+// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
+// Returns information about the environment
+export function Environment(): Promise;
+
+// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
+// Quits the application.
+export function Quit(): void;
+
+// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
+// Hides the application.
+export function Hide(): void;
+
+// [Show](https://wails.io/docs/reference/runtime/intro#show)
+// Shows the application.
+export function Show(): void;
diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.js
new file mode 100644
index 000000000..b5ae16d56
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.js
@@ -0,0 +1,182 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export function LogPrint(message) {
+ window.runtime.LogPrint(message);
+}
+
+export function LogTrace(message) {
+ window.runtime.LogTrace(message);
+}
+
+export function LogDebug(message) {
+ window.runtime.LogDebug(message);
+}
+
+export function LogInfo(message) {
+ window.runtime.LogInfo(message);
+}
+
+export function LogWarning(message) {
+ window.runtime.LogWarning(message);
+}
+
+export function LogError(message) {
+ window.runtime.LogError(message);
+}
+
+export function LogFatal(message) {
+ window.runtime.LogFatal(message);
+}
+
+export function EventsOnMultiple(eventName, callback, maxCallbacks) {
+ window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
+}
+
+export function EventsOn(eventName, callback) {
+ EventsOnMultiple(eventName, callback, -1);
+}
+
+export function EventsOff(eventName) {
+ return window.runtime.EventsOff(eventName);
+}
+
+export function EventsOffAll() {
+ return window.runtime.EventsOffAll();
+}
+
+export function EventsOnce(eventName, callback) {
+ EventsOnMultiple(eventName, callback, 1);
+}
+
+export function EventsEmit(eventName) {
+ let args = [eventName].slice.call(arguments);
+ return window.runtime.EventsEmit.apply(null, args);
+}
+
+export function WindowReload() {
+ window.runtime.WindowReload();
+}
+
+export function WindowReloadApp() {
+ window.runtime.WindowReloadApp();
+}
+
+export function WindowSetAlwaysOnTop(b) {
+ window.runtime.WindowSetAlwaysOnTop(b);
+}
+
+export function WindowSetSystemDefaultTheme() {
+ window.runtime.WindowSetSystemDefaultTheme();
+}
+
+export function WindowSetLightTheme() {
+ window.runtime.WindowSetLightTheme();
+}
+
+export function WindowSetDarkTheme() {
+ window.runtime.WindowSetDarkTheme();
+}
+
+export function WindowCenter() {
+ window.runtime.WindowCenter();
+}
+
+export function WindowSetTitle(title) {
+ window.runtime.WindowSetTitle(title);
+}
+
+export function WindowFullscreen() {
+ window.runtime.WindowFullscreen();
+}
+
+export function WindowUnfullscreen() {
+ window.runtime.WindowUnfullscreen();
+}
+
+export function WindowGetSize() {
+ return window.runtime.WindowGetSize();
+}
+
+export function WindowSetSize(width, height) {
+ window.runtime.WindowSetSize(width, height);
+}
+
+export function WindowSetMaxSize(width, height) {
+ window.runtime.WindowSetMaxSize(width, height);
+}
+
+export function WindowSetMinSize(width, height) {
+ window.runtime.WindowSetMinSize(width, height);
+}
+
+export function WindowSetPosition(x, y) {
+ window.runtime.WindowSetPosition(x, y);
+}
+
+export function WindowGetPosition() {
+ return window.runtime.WindowGetPosition();
+}
+
+export function WindowHide() {
+ window.runtime.WindowHide();
+}
+
+export function WindowShow() {
+ window.runtime.WindowShow();
+}
+
+export function WindowMaximise() {
+ window.runtime.WindowMaximise();
+}
+
+export function WindowToggleMaximise() {
+ window.runtime.WindowToggleMaximise();
+}
+
+export function WindowUnmaximise() {
+ window.runtime.WindowUnmaximise();
+}
+
+export function WindowMinimise() {
+ window.runtime.WindowMinimise();
+}
+
+export function WindowUnminimise() {
+ window.runtime.WindowUnminimise();
+}
+
+export function WindowSetBackgroundColour(R, G, B, A) {
+ window.runtime.WindowSetBackgroundColour(R, G, B, A);
+}
+
+export function ScreenGetAll() {
+ return window.runtime.ScreenGetAll();
+}
+
+export function BrowserOpenURL(url) {
+ window.runtime.BrowserOpenURL(url);
+}
+
+export function Environment() {
+ return window.runtime.Environment();
+}
+
+export function Quit() {
+ window.runtime.Quit();
+}
+
+export function Hide() {
+ window.runtime.Hide();
+}
+
+export function Show() {
+ window.runtime.Show();
+}
diff --git a/v2/pkg/templates/templates/lit-ts/go.mod.tmpl b/v2/pkg/templates/templates/lit-ts/go.mod.tmpl
new file mode 100644
index 000000000..4b34d1668
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/go.mod.tmpl
@@ -0,0 +1,7 @@
+module changeme
+
+go 1.23.0
+
+require github.com/wailsapp/wails/v2 {{.WailsVersion}}
+
+// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}}
\ No newline at end of file
diff --git a/v2/pkg/templates/templates/lit-ts/main.go.tmpl b/v2/pkg/templates/templates/lit-ts/main.go.tmpl
new file mode 100644
index 000000000..e24782be3
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/main.go.tmpl
@@ -0,0 +1,36 @@
+package main
+
+import (
+ "embed"
+
+ "github.com/wailsapp/wails/v2"
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+//go:embed all:frontend/dist
+var assets embed.FS
+
+func main() {
+ // Create an instance of the app structure
+ app := NewApp()
+
+ // Create application with options
+ err := wails.Run(&options.App{
+ Title: "{{.ProjectName}}",
+ Width: 1024,
+ Height: 768,
+ AssetServer: &assetserver.Options{
+ Assets: assets,
+ },
+ BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+ OnStartup: app.startup,
+ Bind: []interface{}{
+ app,
+ },
+ })
+
+ if err != nil {
+ println("Error:", err.Error())
+ }
+}
diff --git a/v2/pkg/templates/templates/lit-ts/template.json b/v2/pkg/templates/templates/lit-ts/template.json
new file mode 100644
index 000000000..7e9beabb7
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/template.json
@@ -0,0 +1,7 @@
+{
+ "name": "Lit + Vite (Typescript)",
+ "shortname": "lit-ts",
+ "author": "Lea Anthony",
+ "description": "Lit + TS + Vite development server",
+ "helpurl": "https://wails.io"
+}
\ No newline at end of file
diff --git a/v2/pkg/templates/templates/lit-ts/wails.tmpl.json b/v2/pkg/templates/templates/lit-ts/wails.tmpl.json
new file mode 100644
index 000000000..c39b2cb7d
--- /dev/null
+++ b/v2/pkg/templates/templates/lit-ts/wails.tmpl.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://wails.io/schemas/config.v2.json",
+ "name": "{{.ProjectName}}",
+ "outputfilename": "{{.BinaryName}}",
+ "frontend:install": "npm install",
+ "frontend:build": "npm run build",
+ "frontend:dev:watcher": "npm run dev",
+ "frontend:dev:serverUrl": "auto",
+ "author": {
+ "name": "{{.AuthorName}}",
+ "email": "{{.AuthorEmail}}"
+ }
+}
diff --git a/v2/pkg/templates/templates/lit/.gitignore.tmpl b/v2/pkg/templates/templates/lit/.gitignore.tmpl
new file mode 100644
index 000000000..129d52294
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/.gitignore.tmpl
@@ -0,0 +1,3 @@
+build/bin
+node_modules
+frontend/dist
diff --git a/v2/pkg/templates/templates/lit/README.md b/v2/pkg/templates/templates/lit/README.md
new file mode 100644
index 000000000..dc8efed65
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/README.md
@@ -0,0 +1,19 @@
+# README
+
+## About
+
+This is the official Wails Lit template.
+
+You can configure the project by editing `wails.json`. More information about the project settings can be found
+here: https://wails.io/docs/reference/project-config
+
+## Live Development
+
+To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
+server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
+and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
+to this in your browser, and you can call your Go code from devtools.
+
+## Building
+
+To build a redistributable, production mode package, use `wails build`.
diff --git a/v2/pkg/templates/templates/lit/app.tmpl.go b/v2/pkg/templates/templates/lit/app.tmpl.go
new file mode 100644
index 000000000..af53038a1
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/app.tmpl.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "context"
+ "fmt"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called when the app starts. The context is saved
+// so we can call the runtime methods
+func (a *App) startup(ctx context.Context) {
+ a.ctx = ctx
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+ return fmt.Sprintf("Hello %s, It's show time!", name)
+}
diff --git a/v2/pkg/templates/templates/lit/frontend/.gitignore.tmpl b/v2/pkg/templates/templates/lit/frontend/.gitignore.tmpl
new file mode 100644
index 000000000..a547bf36d
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/.gitignore.tmpl
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/v2/pkg/templates/templates/lit/frontend/dist/gitkeep b/v2/pkg/templates/templates/lit/frontend/dist/gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/v2/pkg/templates/templates/lit/frontend/index.tmpl.html b/v2/pkg/templates/templates/lit/frontend/index.tmpl.html
new file mode 100644
index 000000000..fbe3eb240
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/index.tmpl.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ {{.ProjectName}}
+
+
+
+
+
+
+
diff --git a/v2/pkg/templates/templates/lit/frontend/package.json b/v2/pkg/templates/templates/lit/frontend/package.json
new file mode 100644
index 000000000..10c9a760e
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "dist/my-element.es.js",
+ "exports": {
+ ".": "./dist/my-element.es.js"
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build"
+ },
+ "dependencies": {
+ "lit": "^2.2.8"
+ },
+ "devDependencies": {
+ "vite": "^3.0.7"
+ }
+}
\ No newline at end of file
diff --git a/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/pkg/templates/templates/lit/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/pkg/templates/templates/lit/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/lit/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..99ac71f5a
Binary files /dev/null and b/v2/pkg/templates/templates/lit/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/pkg/templates/templates/lit/frontend/src/my-element.js b/v2/pkg/templates/templates/lit/frontend/src/my-element.js
new file mode 100644
index 000000000..017632c09
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/src/my-element.js
@@ -0,0 +1,106 @@
+import {css, html, LitElement} from 'lit'
+import logo from './assets/images/logo-universal.png'
+import {Greet} from "../wailsjs/go/main/App";
+import './style.css';
+
+/**
+ * An example element.
+ *
+ * @slot - This element has a slot
+ * @csspart button - The button
+ */
+export class MyElement extends LitElement {
+ constructor() {
+ super()
+ this.resultText = "Please enter your name below 👇"
+ }
+
+ static get styles() {
+ return css`
+ #logo {
+ display: block;
+ width: 50%;
+ height: 50%;
+ margin: auto;
+ padding: 10% 0 0;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ background-origin: content-box;
+ }
+
+ .result {
+ height: 20px;
+ line-height: 20px;
+ margin: 1.5rem auto;
+ }
+
+ .input-box .btn {
+ width: 60px;
+ height: 30px;
+ line-height: 30px;
+ border-radius: 3px;
+ border: none;
+ margin: 0 0 0 20px;
+ padding: 0 8px;
+ cursor: pointer;
+ }
+
+ .input-box .btn:hover {
+ background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
+ color: #333333;
+ }
+
+ .input-box .input {
+ border: none;
+ border-radius: 3px;
+ outline: none;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 10px;
+ background-color: rgba(240, 240, 240, 1);
+ -webkit-font-smoothing: antialiased;
+ }
+
+ .input-box .input:hover {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+ }
+
+ .input-box .input:focus {
+ border: none;
+ background-color: rgba(255, 255, 255, 1);
+ }
+
+ `
+ }
+
+ static get properties() {
+ return {
+ resultText: {type: String},
+ }
+ }
+
+ greet() {
+ let thisName = this.shadowRoot.getElementById('name').value
+ Greet(thisName).then(result => {
+ this.resultText = result
+ });
+ }
+
+ render() {
+ return html`
+
+
+
${this.resultText}
+
+
+
+
+
+ `
+ }
+
+}
+
+window.customElements.define('my-element', MyElement)
diff --git a/v2/pkg/templates/templates/lit/frontend/src/style.css b/v2/pkg/templates/templates/lit/frontend/src/style.css
new file mode 100644
index 000000000..3940d6c63
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/src/style.css
@@ -0,0 +1,26 @@
+html {
+ background-color: rgba(27, 38, 54, 1);
+ text-align: center;
+ color: white;
+}
+
+body {
+ margin: 0;
+ color: white;
+ font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+}
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
+}
+
+#app {
+ height: 100vh;
+ text-align: center;
+}
diff --git a/v2/pkg/templates/templates/lit/frontend/vite.config.js b/v2/pkg/templates/templates/lit/frontend/vite.config.js
new file mode 100644
index 000000000..bbb7f5889
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/vite.config.js
@@ -0,0 +1,4 @@
+import {defineConfig} from 'vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({})
diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.d.ts b/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.d.ts
new file mode 100644
index 000000000..43173cfce
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.d.ts
@@ -0,0 +1,4 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Greet(arg1: string): Promise;
diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.js b/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.js
new file mode 100644
index 000000000..0ee085c95
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/go/main/App.js
@@ -0,0 +1,7 @@
+// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function Greet(arg1) {
+ return window['go']['main']['App']['Greet'](arg1);
+}
diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/package.json b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/package.json
new file mode 100644
index 000000000..1e7c8a5d7
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@wailsapp/runtime",
+ "version": "2.0.0",
+ "description": "Wails Javascript runtime library",
+ "main": "runtime.js",
+ "types": "runtime.d.ts",
+ "scripts": {
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/wailsapp/wails.git"
+ },
+ "keywords": [
+ "Wails",
+ "Javascript",
+ "Go"
+ ],
+ "author": "Lea Anthony ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/wailsapp/wails/issues"
+ },
+ "homepage": "https://github.com/wailsapp/wails#readme"
+}
diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.d.ts
new file mode 100644
index 000000000..336fb07aa
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.d.ts
@@ -0,0 +1,211 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export interface Size {
+ w: number;
+ h: number;
+}
+
+export interface Screen {
+ isCurrent: boolean;
+ isPrimary: boolean;
+ width: number
+ height: number
+}
+
+// Environment information such as platform, buildtype, ...
+export interface EnvironmentInfo {
+ buildType: string;
+ platform: string;
+ arch: string;
+}
+
+// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
+// emits the given event. Optional data may be passed with the event.
+// This will trigger any event listeners.
+export function EventsEmit(eventName: string, ...data: any): void;
+
+// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
+export function EventsOn(eventName: string, callback: (...data: any) => void): void;
+
+// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
+// sets up a listener for the given event name, but will only trigger a given number times.
+export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void;
+
+// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
+// sets up a listener for the given event name, but will only trigger once.
+export function EventsOnce(eventName: string, callback: (...data: any) => void): void;
+
+// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff)
+// unregisters the listener for the given event name.
+export function EventsOff(eventName: string): void;
+
+// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
+// unregisters all event listeners.
+export function EventsOffAll(): void;
+
+// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
+// logs the given message as a raw message
+export function LogPrint(message: string): void;
+
+// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
+// logs the given message at the `trace` log level.
+export function LogTrace(message: string): void;
+
+// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
+// logs the given message at the `debug` log level.
+export function LogDebug(message: string): void;
+
+// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
+// logs the given message at the `error` log level.
+export function LogError(message: string): void;
+
+// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
+// logs the given message at the `fatal` log level.
+// The application will quit after calling this method.
+export function LogFatal(message: string): void;
+
+// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
+// logs the given message at the `info` log level.
+export function LogInfo(message: string): void;
+
+// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
+// logs the given message at the `warning` log level.
+export function LogWarning(message: string): void;
+
+// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
+// Forces a reload by the main application as well as connected browsers.
+export function WindowReload(): void;
+
+// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
+// Reloads the application frontend.
+export function WindowReloadApp(): void;
+
+// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
+// Sets the window AlwaysOnTop or not on top.
+export function WindowSetAlwaysOnTop(b: boolean): void;
+
+// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
+// *Windows only*
+// Sets window theme to system default (dark/light).
+export function WindowSetSystemDefaultTheme(): void;
+
+// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
+// *Windows only*
+// Sets window to light theme.
+export function WindowSetLightTheme(): void;
+
+// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
+// *Windows only*
+// Sets window to dark theme.
+export function WindowSetDarkTheme(): void;
+
+// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
+// Centers the window on the monitor the window is currently on.
+export function WindowCenter(): void;
+
+// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
+// Sets the text in the window title bar.
+export function WindowSetTitle(title: string): void;
+
+// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
+// Makes the window full screen.
+export function WindowFullscreen(): void;
+
+// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
+// Restores the previous window dimensions and position prior to full screen.
+export function WindowUnfullscreen(): void;
+
+// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
+// Sets the width and height of the window.
+export function WindowSetSize(width: number, height: number): void;
+
+// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
+// Gets the width and height of the window.
+export function WindowGetSize(): Promise;
+
+// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
+// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMaxSize(width: number, height: number): void;
+
+// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
+// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMinSize(width: number, height: number): void;
+
+// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
+// Sets the window position relative to the monitor the window is currently on.
+export function WindowSetPosition(x: number, y: number): void;
+
+// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
+// Gets the window position relative to the monitor the window is currently on.
+export function WindowGetPosition(): Promise;
+
+// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
+// Hides the window.
+export function WindowHide(): void;
+
+// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
+// Shows the window, if it is currently hidden.
+export function WindowShow(): void;
+
+// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
+// Maximises the window to fill the screen.
+export function WindowMaximise(): void;
+
+// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
+// Toggles between Maximised and UnMaximised.
+export function WindowToggleMaximise(): void;
+
+// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
+// Restores the window to the dimensions and position prior to maximising.
+export function WindowUnmaximise(): void;
+
+// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
+// Minimises the window.
+export function WindowMinimise(): void;
+
+// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
+// Restores the window to the dimensions and position prior to minimising.
+export function WindowUnminimise(): void;
+
+// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
+// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
+export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
+
+// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
+// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
+export function ScreenGetAll(): Promise;
+
+// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
+// Opens the given URL in the system browser.
+export function BrowserOpenURL(url: string): void;
+
+// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
+// Returns information about the environment
+export function Environment(): Promise;
+
+// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
+// Quits the application.
+export function Quit(): void;
+
+// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
+// Hides the application.
+export function Hide(): void;
+
+// [Show](https://wails.io/docs/reference/runtime/intro#show)
+// Shows the application.
+export function Show(): void;
diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.js
new file mode 100644
index 000000000..b5ae16d56
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.js
@@ -0,0 +1,182 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export function LogPrint(message) {
+ window.runtime.LogPrint(message);
+}
+
+export function LogTrace(message) {
+ window.runtime.LogTrace(message);
+}
+
+export function LogDebug(message) {
+ window.runtime.LogDebug(message);
+}
+
+export function LogInfo(message) {
+ window.runtime.LogInfo(message);
+}
+
+export function LogWarning(message) {
+ window.runtime.LogWarning(message);
+}
+
+export function LogError(message) {
+ window.runtime.LogError(message);
+}
+
+export function LogFatal(message) {
+ window.runtime.LogFatal(message);
+}
+
+export function EventsOnMultiple(eventName, callback, maxCallbacks) {
+ window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
+}
+
+export function EventsOn(eventName, callback) {
+ EventsOnMultiple(eventName, callback, -1);
+}
+
+export function EventsOff(eventName) {
+ return window.runtime.EventsOff(eventName);
+}
+
+export function EventsOffAll() {
+ return window.runtime.EventsOffAll();
+}
+
+export function EventsOnce(eventName, callback) {
+ EventsOnMultiple(eventName, callback, 1);
+}
+
+export function EventsEmit(eventName) {
+ let args = [eventName].slice.call(arguments);
+ return window.runtime.EventsEmit.apply(null, args);
+}
+
+export function WindowReload() {
+ window.runtime.WindowReload();
+}
+
+export function WindowReloadApp() {
+ window.runtime.WindowReloadApp();
+}
+
+export function WindowSetAlwaysOnTop(b) {
+ window.runtime.WindowSetAlwaysOnTop(b);
+}
+
+export function WindowSetSystemDefaultTheme() {
+ window.runtime.WindowSetSystemDefaultTheme();
+}
+
+export function WindowSetLightTheme() {
+ window.runtime.WindowSetLightTheme();
+}
+
+export function WindowSetDarkTheme() {
+ window.runtime.WindowSetDarkTheme();
+}
+
+export function WindowCenter() {
+ window.runtime.WindowCenter();
+}
+
+export function WindowSetTitle(title) {
+ window.runtime.WindowSetTitle(title);
+}
+
+export function WindowFullscreen() {
+ window.runtime.WindowFullscreen();
+}
+
+export function WindowUnfullscreen() {
+ window.runtime.WindowUnfullscreen();
+}
+
+export function WindowGetSize() {
+ return window.runtime.WindowGetSize();
+}
+
+export function WindowSetSize(width, height) {
+ window.runtime.WindowSetSize(width, height);
+}
+
+export function WindowSetMaxSize(width, height) {
+ window.runtime.WindowSetMaxSize(width, height);
+}
+
+export function WindowSetMinSize(width, height) {
+ window.runtime.WindowSetMinSize(width, height);
+}
+
+export function WindowSetPosition(x, y) {
+ window.runtime.WindowSetPosition(x, y);
+}
+
+export function WindowGetPosition() {
+ return window.runtime.WindowGetPosition();
+}
+
+export function WindowHide() {
+ window.runtime.WindowHide();
+}
+
+export function WindowShow() {
+ window.runtime.WindowShow();
+}
+
+export function WindowMaximise() {
+ window.runtime.WindowMaximise();
+}
+
+export function WindowToggleMaximise() {
+ window.runtime.WindowToggleMaximise();
+}
+
+export function WindowUnmaximise() {
+ window.runtime.WindowUnmaximise();
+}
+
+export function WindowMinimise() {
+ window.runtime.WindowMinimise();
+}
+
+export function WindowUnminimise() {
+ window.runtime.WindowUnminimise();
+}
+
+export function WindowSetBackgroundColour(R, G, B, A) {
+ window.runtime.WindowSetBackgroundColour(R, G, B, A);
+}
+
+export function ScreenGetAll() {
+ return window.runtime.ScreenGetAll();
+}
+
+export function BrowserOpenURL(url) {
+ window.runtime.BrowserOpenURL(url);
+}
+
+export function Environment() {
+ return window.runtime.Environment();
+}
+
+export function Quit() {
+ window.runtime.Quit();
+}
+
+export function Hide() {
+ window.runtime.Hide();
+}
+
+export function Show() {
+ window.runtime.Show();
+}
diff --git a/v2/pkg/templates/templates/lit/go.mod.tmpl b/v2/pkg/templates/templates/lit/go.mod.tmpl
new file mode 100644
index 000000000..4b34d1668
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/go.mod.tmpl
@@ -0,0 +1,7 @@
+module changeme
+
+go 1.23.0
+
+require github.com/wailsapp/wails/v2 {{.WailsVersion}}
+
+// replace github.com/wailsapp/wails/v2 {{.WailsVersion}} => {{.WailsDirectory}}
\ No newline at end of file
diff --git a/v2/pkg/templates/templates/lit/main.go.tmpl b/v2/pkg/templates/templates/lit/main.go.tmpl
new file mode 100644
index 000000000..e24782be3
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/main.go.tmpl
@@ -0,0 +1,36 @@
+package main
+
+import (
+ "embed"
+
+ "github.com/wailsapp/wails/v2"
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+//go:embed all:frontend/dist
+var assets embed.FS
+
+func main() {
+ // Create an instance of the app structure
+ app := NewApp()
+
+ // Create application with options
+ err := wails.Run(&options.App{
+ Title: "{{.ProjectName}}",
+ Width: 1024,
+ Height: 768,
+ AssetServer: &assetserver.Options{
+ Assets: assets,
+ },
+ BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+ OnStartup: app.startup,
+ Bind: []interface{}{
+ app,
+ },
+ })
+
+ if err != nil {
+ println("Error:", err.Error())
+ }
+}
diff --git a/v2/pkg/templates/templates/lit/template.json b/v2/pkg/templates/templates/lit/template.json
new file mode 100644
index 000000000..168769e37
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/template.json
@@ -0,0 +1,7 @@
+{
+ "name": "Lit + Vite",
+ "shortname": "lit",
+ "author": "Lea Anthony",
+ "description": "Lit + Vite development server",
+ "helpurl": "https://wails.io"
+}
\ No newline at end of file
diff --git a/v2/pkg/templates/templates/lit/wails.tmpl.json b/v2/pkg/templates/templates/lit/wails.tmpl.json
new file mode 100644
index 000000000..c39b2cb7d
--- /dev/null
+++ b/v2/pkg/templates/templates/lit/wails.tmpl.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://wails.io/schemas/config.v2.json",
+ "name": "{{.ProjectName}}",
+ "outputfilename": "{{.BinaryName}}",
+ "frontend:install": "npm install",
+ "frontend:build": "npm run build",
+ "frontend:dev:watcher": "npm run dev",
+ "frontend:dev:serverUrl": "auto",
+ "author": {
+ "name": "{{.AuthorName}}",
+ "email": "{{.AuthorEmail}}"
+ }
+}
diff --git a/v2/pkg/templates/templates/plain/.gitignore.tmpl b/v2/pkg/templates/templates/plain/.gitignore.tmpl
new file mode 100644
index 000000000..b92a6f8bf
--- /dev/null
+++ b/v2/pkg/templates/templates/plain/.gitignore.tmpl
@@ -0,0 +1,12 @@
+# Wails bin directory
+build/bin
+# Wails Windows NSIS support files
+build/windows/installer/wails_tools.nsh
+build/windows/installer/tmp/
+
+# IDEs
+.idea
+.vscode
+
+# The black hole that is...
+node_modules
diff --git a/v2/pkg/templates/templates/plain/README.md b/v2/pkg/templates/templates/plain/README.md
new file mode 100644
index 000000000..3a71c4e9b
--- /dev/null
+++ b/v2/pkg/templates/templates/plain/README.md
@@ -0,0 +1,19 @@
+# README
+
+## About
+
+This template uses plain JS / HTML and CSS.
+
+You can configure the project by editing `wails.json`. More information about the project settings can be found
+here: https://wails.io/docs/reference/project-config
+
+## Live Development
+
+To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
+server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
+and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
+to this in your browser, and you can call your Go code from devtools.
+
+## Building
+
+To build a redistributable, production mode package, use `wails build`.
diff --git a/v2/pkg/templates/templates/plain/app.go b/v2/pkg/templates/templates/plain/app.go
new file mode 100644
index 000000000..224be7156
--- /dev/null
+++ b/v2/pkg/templates/templates/plain/app.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "context"
+ "fmt"
+)
+
+// App struct
+type App struct {
+ ctx context.Context
+}
+
+// NewApp creates a new App application struct
+func NewApp() *App {
+ return &App{}
+}
+
+// startup is called at application startup
+func (a *App) startup(ctx context.Context) {
+ // Perform your setup here
+ a.ctx = ctx
+}
+
+// domReady is called after front-end resources have been loaded
+func (a App) domReady(ctx context.Context) {
+ // Add your action here
+}
+
+// beforeClose is called when the application is about to quit,
+// either by clicking the window close button or calling runtime.Quit.
+// Returning true will cause the application to continue, false will continue shutdown as normal.
+func (a *App) beforeClose(ctx context.Context) (prevent bool) {
+ return false
+}
+
+// shutdown is called at application termination
+func (a *App) shutdown(ctx context.Context) {
+ // Perform your teardown here
+}
+
+// Greet returns a greeting for the given name
+func (a *App) Greet(name string) string {
+ return fmt.Sprintf("Hello %s, It's show time!", name)
+}
diff --git a/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/OFL.txt b/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/OFL.txt
new file mode 100644
index 000000000..9cac04ce8
--- /dev/null
+++ b/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
new file mode 100644
index 000000000..2f9cc5964
Binary files /dev/null and b/v2/pkg/templates/templates/plain/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ
diff --git a/v2/pkg/templates/templates/plain/frontend/src/assets/images/logo-universal.png b/v2/pkg/templates/templates/plain/frontend/src/assets/images/logo-universal.png
new file mode 100644
index 000000000..99ac71f5a
Binary files /dev/null and b/v2/pkg/templates/templates/plain/frontend/src/assets/images/logo-universal.png differ
diff --git a/v2/pkg/templates/templates/plain/frontend/src/index.tmpl.html b/v2/pkg/templates/templates/plain/frontend/src/index.tmpl.html
new file mode 100644
index 000000000..a8a434a37
--- /dev/null
+++ b/v2/pkg/templates/templates/plain/frontend/src/index.tmpl.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ {{.ProjectName}}
+
+
+
+
+