Compare commits

..

35 Commits

Author SHA1 Message Date
Lea Anthony
c5e76c50b0 v2.0.0-alpha.64 2021-04-13 21:07:24 +10:00
Lea Anthony
e40226ff7a ANSI support for tray labels and submenus 2021-04-13 21:05:56 +10:00
Lea Anthony
53a3638fa8 ANSI support for tray labels 2021-04-13 05:43:37 +10:00
Lea Anthony
1344638c52 Add OS detection 2021-04-12 06:25:15 +10:00
Lea Anthony
6fdc87454a Initial support for desktop notifications 2021-04-07 06:32:21 +10:00
Lea Anthony
c36f9501a9 Dialog warning on JS Error. Remove shutdown hack. Optional dialog callback. 2021-04-06 20:18:29 +10:00
Lea Anthony
c23b43c352 Improve vanilla app JS 2021-04-04 15:47:03 +10:00
Lea Anthony
a76851463b Font smoothing for vanilla app 2021-04-04 15:45:24 +10:00
Lea Anthony
e17b432c8f Tidy up templates 2021-04-04 15:36:55 +10:00
Lea Anthony
5d444cd6dd Support user tags 2021-04-04 13:42:48 +10:00
Lea Anthony
be43049fc6 Remove debug code 2021-04-04 05:25:21 +10:00
Lea Anthony
2e01710412 v2.0.0-alpha.63 2021-04-04 05:15:09 +10:00
Lea Anthony
1b0193161c Improvements for handling Info.plist 2021-04-04 05:14:16 +10:00
Lea Anthony
1b377fb575 Support enter as alias for return 2021-04-03 16:52:54 +11:00
Lea Anthony
1203ae64b8 v2.0.0-alpha.62 2021-03-31 15:44:28 +11:00
Lea Anthony
26ed8002b9 Menu bar font fix 2021-03-31 15:43:28 +11:00
Lea Anthony
cf23bffc67 v2.0.0-alpha.61 2021-03-30 22:22:48 +11:00
Lea Anthony
d70f6fffe7 Remove autorelease of colour. Fixes high sierra. 2021-03-30 22:21:49 +11:00
Lea Anthony
54c99fc386 Move some methods to main thread 2021-03-29 20:45:50 +11:00
Lea Anthony
86c1ea5e6a Initial support for compression 2021-03-27 20:59:14 +11:00
Lea Anthony
b394c1914c v2.0.0-alpha.60 2021-03-26 20:54:42 +11:00
Lea Anthony
91c2ddf155 Allow styling of menu items that have submenus 2021-03-26 20:54:19 +11:00
Lea Anthony
712ad96d2a v2.0.0-alpha.59 2021-03-26 18:30:00 +11:00
Lea Anthony
86b4a4f2f5 Don't clean directory when doing universal builds 2021-03-26 18:29:28 +11:00
Lea Anthony
4b9786abc9 v2.0.0-alpha.58 2021-03-26 18:13:10 +11:00
Lea Anthony
fd96ebc050 Better verbose output 2021-03-26 18:12:42 +11:00
Lea Anthony
939e0f5975 Use default broken image for invalid images 2021-03-26 17:51:49 +11:00
Lea Anthony
6a7a288a0f Limit StartsAtLogin to app bundles 2021-03-26 16:52:59 +11:00
Lea Anthony
0564d0aa98 v2.0.0-alpha.57 2021-03-26 16:32:10 +11:00
Lea Anthony
3a136a73ca Add package for mac functions 2021-03-26 15:57:30 +11:00
Lea Anthony
50c219307f Add clean flag 2021-03-26 14:10:25 +11:00
Lea Anthony
de3038b302 v2.0.0-alpha.56 2021-03-25 21:13:40 +11:00
Lea Anthony
6eb4b0a419 Fix packaging universal builds 2021-03-25 21:12:29 +11:00
Lea Anthony
41d2158375 Support building arm64 & universal binaries. 2021-03-25 21:12:29 +11:00
Lea Anthony
5d7f57e80b Initial ARM support! 2021-03-25 21:12:29 +11:00
58 changed files with 556 additions and 1151 deletions

View File

@@ -39,6 +39,9 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
compilerCommand := "go"
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand)
compress := false
command.BoolFlag("compress", "Compress final binary", &compress)
// Setup Platform flag
platform := runtime.GOOS
command.StringFlag("platform", "Platform to target", &platform)
@@ -51,6 +54,10 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
ldflags := ""
command.StringFlag("ldflags", "optional ldflags", &ldflags)
// tags to pass to `go`
tags := ""
command.StringFlag("tags", "tags to pass to Go compiler (quoted and space separated)", &tags)
// Log to file
//logFile := ""
//command.StringFlag("l", "Log to file", &logFile)
@@ -63,6 +70,10 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
outputFilename := ""
command.StringFlag("o", "Output filename", &outputFilename)
// Clean build directory
cleanBuildDirectory := false
command.BoolFlag("clean", "Clean the build directory before building", &cleanBuildDirectory)
appleIdentity := ""
if runtime.GOOS == "darwin" {
command.StringFlag("sign", "Signs your app with the given identity.", &appleIdentity)
@@ -110,18 +121,35 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
return fmt.Errorf("platform %s is not supported", platform)
}
if compress && platform == "darwin/universal" {
println("Warning: compress flag unsupported for universal binaries. Ignoring.")
compress = false
}
// Tags
userTags := []string{}
for _, tag := range strings.Split(tags, " ") {
thisTag := strings.TrimSpace(tag)
if thisTag != "" {
userTags = append(userTags, thisTag)
}
}
// Create BuildOptions
buildOptions := &build.Options{
Logger: logger,
OutputType: outputType,
OutputFile: outputFilename,
Mode: mode,
Pack: pack,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: keepAssets,
AppleIdentity: appleIdentity,
Verbosity: verbosity,
Logger: logger,
OutputType: outputType,
OutputFile: outputFilename,
CleanBuildDirectory: cleanBuildDirectory,
Mode: mode,
Pack: pack,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: keepAssets,
AppleIdentity: appleIdentity,
Verbosity: verbosity,
Compress: compress,
UserTags: userTags,
}
// Calculate platform and arch
@@ -142,14 +170,18 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
}
// Write out the system information
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, "App Type: \t%s\n", buildOptions.OutputType)
fmt.Fprintf(w, "Platform: \t%s\n", buildOptions.Platform)
fmt.Fprintf(w, "Arch: \t%s\n", buildOptions.Arch)
fmt.Fprintf(w, "Compiler: \t%s\n", buildOptions.Compiler)
fmt.Fprintf(w, "Compress: \t%t\n", buildOptions.Compress)
fmt.Fprintf(w, "Build Mode: \t%s\n", buildModeText)
fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack)
fmt.Fprintf(w, "Clean Build Dir: \t%t\n", buildOptions.CleanBuildDirectory)
fmt.Fprintf(w, "KeepAssets: \t%t\n", buildOptions.KeepAssets)
fmt.Fprintf(w, "LDFlags: \t\"%s\"\n", buildOptions.LDFlags)
fmt.Fprintf(w, "Tags: \t[%s]\n", strings.Join(buildOptions.UserTags, ","))
if len(buildOptions.OutputFile) > 0 {
fmt.Fprintf(w, "Output File: \t%s\n", buildOptions.OutputFile)
}

View File

@@ -67,5 +67,6 @@ func main() {
err = app.Run()
if err != nil {
println("\n\nERROR: " + err.Error())
os.Exit(1)
}
}

View File

@@ -1,3 +1,3 @@
package main
var version = "v2.0.0-alpha.55"
var version = "v2.0.0-alpha.64"

View File

@@ -4,13 +4,13 @@ go 1.16
require (
github.com/Masterminds/semver v1.5.0
github.com/davecgh/go-spew v1.1.1
github.com/fatih/structtag v1.2.0
github.com/fsnotify/fsnotify v1.4.9
github.com/gorilla/websocket v1.4.1
github.com/imdario/mergo v0.3.11
github.com/jackmordaunt/icns v1.0.0
github.com/leaanthony/clir v1.0.4
github.com/leaanthony/go-ansi-parser v1.0.1
github.com/leaanthony/gosod v0.0.4
github.com/leaanthony/slicer v1.5.0
github.com/matryer/is v1.4.0

View File

@@ -1,7 +1,6 @@
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
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/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
@@ -42,6 +41,8 @@ github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eT
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/leaanthony/clir v1.0.4 h1:Dov2y9zWJmZr7CjaCe86lKa4b5CSxskGAt2yBkoDyiU=
github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
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 v0.0.4 h1:v4hepo4IyL8E8c9qzDsvYcA0KGh7Npf8As74K5ibQpI=
github.com/leaanthony/gosod v0.0.4/go.mod h1:nGMCb1PJfXwBDbOAike78jEYlpqge+xUKFf0iBKjKxU=
github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=

View File

@@ -24,6 +24,9 @@ struct hashmap_s dialogIconCache;
// Dispatch Method
typedef void (^dispatchMethod)(void);
// Message Dialog
void MessageDialog(struct Application *app, char *callbackID, char *type, char *title, char *message, char *icon, char *button1, char *button2, char *button3, char *button4, char *defaultButton, char *cancelButton);
TrayMenuStore *TrayMenuStoreSingleton;
// dispatch will execute the given `func` pointer
@@ -80,6 +83,11 @@ void ShowMouse() {
msg_reg(c("NSCursor"), s("unhide"));
}
OSVersion getOSVersion() {
id processInfo = msg_reg(c("NSProcessInfo"), s("processInfo"));
return GET_OSVERSION(processInfo);
}
struct Application {
// Cocoa data
@@ -295,7 +303,11 @@ void messageHandler(id self, SEL cmd, id contentController, id message) {
const char *name = (const char *)msg_reg(msg_reg(message, s("name")), s("UTF8String"));
if( strcmp(name, "error") == 0 ) {
printf("There was a Javascript error. Please open the devtools for more information.\n");
Show(app);
// Show app if we are in debug mode
if( debug ) {
Show(app);
MessageDialog(app, "", "error", "Javascript Error", "There was a Javascript error. Please open the devtools for more information.", "", "", "", "","","","");
}
} else if( strcmp(name, "completed") == 0) {
// Delete handler
msg_id(app->manager, s("removeScriptMessageHandlerForName:"), str("completed"));
@@ -691,7 +703,7 @@ void processDialogButton(id alert, char *buttonTitle, char *cancelButton, char *
}
}
extern void MessageDialog(struct Application *app, char *callbackID, char *type, char *title, char *message, char *icon, char *button1, char *button2, char *button3, char *button4, char *defaultButton, char *cancelButton) {
void MessageDialog(struct Application *app, char *callbackID, char *type, char *title, char *message, char *icon, char *button1, char *button2, char *button3, char *button4, char *defaultButton, char *cancelButton) {
// Guard against calling during shutdown
if( app->shuttingDown ) return;
@@ -791,18 +803,20 @@ extern void MessageDialog(struct Application *app, char *callbackID, char *type,
buttonPressed = button4;
}
// Construct callback message. Format "DM<callbackID>|<selected button index>"
const char *callback = concat("DM", callbackID);
const char *header = concat(callback, "|");
const char *responseMessage = concat(header, buttonPressed);
if ( STR_HAS_CHARS(callbackID) ) {
// Construct callback message. Format "DM<callbackID>|<selected button index>"
const char *callback = concat("DM", callbackID);
const char *header = concat(callback, "|");
const char *responseMessage = concat(header, buttonPressed);
// Send message to backend
app->sendMessageToBackend(responseMessage);
// Send message to backend
app->sendMessageToBackend(responseMessage);
// Free memory
MEMFREE(header);
MEMFREE(callback);
MEMFREE(responseMessage);
// Free memory
MEMFREE(header);
MEMFREE(callback);
MEMFREE(responseMessage);
}
);
}
@@ -1049,22 +1063,26 @@ void SetDebug(void *applicationPointer, int flag) {
void AddContextMenu(struct Application *app, const char *contextMenuJSON) {
// Guard against calling during shutdown
if( app->shuttingDown ) return;
AddContextMenuToStore(app->contextMenuStore, contextMenuJSON);
ON_MAIN_THREAD (
AddContextMenuToStore(app->contextMenuStore, contextMenuJSON);
);
}
void UpdateContextMenu(struct Application *app, const char* contextMenuJSON) {
// Guard against calling during shutdown
if( app->shuttingDown ) return;
UpdateContextMenuInStore(app->contextMenuStore, contextMenuJSON);
ON_MAIN_THREAD(
UpdateContextMenuInStore(app->contextMenuStore, contextMenuJSON);
);
}
void AddTrayMenu(struct Application *app, const char *trayMenuJSON) {
// Guard against calling during shutdown
if( app->shuttingDown ) return;
AddTrayMenuToStore(TrayMenuStoreSingleton, trayMenuJSON);
ON_MAIN_THREAD(
AddTrayMenuToStore(TrayMenuStoreSingleton, trayMenuJSON);
);
}
void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
@@ -1372,211 +1390,6 @@ void parseMenuRole(struct Application *app, id parentMenu, JsonNode *item) {
}
id parseTextMenuItem(struct Application *app, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char *menuCallback) {
id item = ALLOC("NSMenuItem");
id wrappedId = ((id(*)(id, SEL, const char*))objc_msgSend)(c("NSValue"), s("valueWithPointer:"), menuid);
msg_id(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s(menuCallback), key);
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
// Process modifiers
if( modifiers != NULL ) {
unsigned long modifierFlags = parseModifiers(modifiers);
msg_int(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
}
msg_id(parentMenu, s("addItem:"), item);
return item;
}
id parseCheckboxMenuItem(struct Application *app, id parentmenu, const char
*title, const char *menuid, bool disabled, bool checked, const char *key,
struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
hashmap_put(menuItemMap, (char*)menuid, strlen(menuid), item);
id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)menuid);
msg_id(item, s("setRepresentedObject:"), wrappedId);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title), s(checkboxCallbackFunction), str(key));
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
msg_int(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg_id(parentmenu, s("addItem:"), item);
return item;
}
id parseRadioMenuItem(struct Application *app, id parentmenu, const char *title,
const char *menuid, bool disabled, bool checked, const char *acceleratorkey,
struct hashmap_s *menuItemMap, const char *radioCallbackFunction) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
hashmap_put(menuItemMap, (char*)menuid, strlen(menuid), item);
id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)menuid);
msg_id(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title), s(radioCallbackFunction), key);
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
msg_int(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg_id(parentmenu, s("addItem:"), item);
return item;
}
void parseMenuItem(struct Application *app, id parentMenu, JsonNode *item,
struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char
*radioCallbackFunction, const char *menuCallbackFunction) {
// Check if this item is hidden and if so, exit early!
bool hidden = false;
getJSONBool(item, "Hidden", &hidden);
if( hidden ) {
return;
}
// Get the role
JsonNode *role = json_find_member(item, "Role");
if( role != NULL ) {
parseMenuRole(app, parentMenu, role);
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg_id(thisMenuItem, s("setSubmenu:"), thisMenu);
msg_id(parentMenu, s("addItem:"), thisMenuItem);
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenu) {
// Get item label
parseMenuItem(app, thisMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction, menuCallbackFunction);
}
return;
}
// This is a user menu. Get the common data
// Get the label
const char *label = getJSONString(item, "Label");
if ( label == NULL) {
label = "(empty)";
}
const char *menuid = getJSONString(item, "ID");
if ( menuid == NULL) {
menuid = "";
}
bool disabled = false;
getJSONBool(item, "Disabled", &disabled);
// Get the Accelerator
JsonNode *accelerator = json_find_member(item, "Accelerator");
const char *acceleratorkey = NULL;
const char **modifiers = NULL;
// If we have an accelerator
if( accelerator != NULL ) {
// Get the key
acceleratorkey = getJSONString(accelerator, "Key");
// Check if there are modifiers
JsonNode *modifiersList = json_find_member(accelerator, "Modifiers");
if ( modifiersList != NULL ) {
// Allocate an array of strings
int noOfModifiers = json_array_length(modifiersList);
// Do we have any?
if (noOfModifiers > 0) {
modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1));
JsonNode *modifier;
int count = 0;
// Iterate the modifiers and save a reference to them in our new array
json_foreach(modifier, modifiersList) {
// Get modifier name
modifiers[count] = modifier->string_;
count++;
}
// Null terminate the modifier list
modifiers[count] = NULL;
}
}
}
// Get the Type
JsonNode *type = json_find_member(item, "Type");
if( type != NULL ) {
if( STREQ(type->string_, "Text")) {
parseTextMenuItem(app, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, menuCallbackFunction);
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);
}
else if ( STREQ(type->string_, "Checkbox")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
parseCheckboxMenuItem(app, parentMenu, label, menuid, disabled, checked, "", menuItemMap, checkboxCallbackFunction);
}
else if ( STREQ(type->string_, "Radio")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
parseRadioMenuItem(app, parentMenu, label, menuid, disabled, checked, "", menuItemMap, radioCallbackFunction);
}
if ( modifiers != NULL ) {
free(modifiers);
}
return;
}
}
void parseMenu(struct Application *app, id parentMenu, JsonNode *menu, struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char *radioCallbackFunction, const char *menuCallbackFunction) {
JsonNode *items = json_find_member(menu, "Items");
if( items == NULL ) {
// Parse error!
Fatal(app, "Unable to find Items in Menu");
return;
}
// Iterate items
JsonNode *item;
json_foreach(item, items) {
// Get item label
parseMenuItem(app, parentMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction, menuCallbackFunction);
}
}
void dumpMemberList(const char *name, id *memberList) {
void *member = memberList[0];
int count = 0;
@@ -1589,43 +1402,6 @@ void dumpMemberList(const char *name, id *memberList) {
printf("]\n");
}
void processRadioGroup(JsonNode *radioGroup, struct hashmap_s *menuItemMap,
struct hashmap_s *radioGroupMap) {
int groupLength;
getJSONInt(radioGroup, "Length", &groupLength);
JsonNode *members = json_find_member(radioGroup, "Members");
JsonNode *member;
// Allocate array
size_t arrayLength = sizeof(id)*(groupLength+1);
id memberList[arrayLength];
// Build the radio group items
int count=0;
json_foreach(member, members) {
// Get menu by id
id menuItem = (id)hashmap_get(menuItemMap, (char*)member->string_, strlen(member->string_));
// Save Member
memberList[count] = menuItem;
count = count + 1;
}
// Null terminate array
memberList[groupLength] = 0;
// dumpMemberList("memberList", memberList);
// Store the members
json_foreach(member, members) {
// Copy the memberList
char *newMemberList = (char *)malloc(arrayLength);
memcpy(newMemberList, memberList, arrayLength);
// add group to each member of group
hashmap_put(radioGroupMap, member->string_, strlen(member->string_), newMemberList);
}
}
// updateMenu replaces the current menu with the given one
void updateMenu(struct Application *app, const char *menuAsJSON) {
Debug(app, "Menu is now: %s", menuAsJSON);
@@ -1649,7 +1425,9 @@ void SetApplicationMenu(struct Application *app, const char *menuAsJSON) {
}
// Update menu
updateMenu(app, menuAsJSON);
ON_MAIN_THREAD (
updateMenu(app, menuAsJSON);
);
}
void processDialogIcons(struct hashmap_s *hashmap, const unsigned char *dialogIcons[]) {
@@ -1789,7 +1567,7 @@ void Run(struct Application *app, int argc, char **argv) {
// Disable damn smart quotes
// Credit: https://stackoverflow.com/a/31640511
id userDefaults = msg_reg(c("NSUserDefaults"), s("standardUserDefaults"));
((id(*)(id, SEL, id, id))objc_msgSend)(userDefaults, s("setBool:forKey:"), NO, str("NSAutomaticQuoteSubstitutionEnabled"));
((id(*)(id, SEL, BOOL, id))objc_msgSend)(userDefaults, s("setBool:forKey:"), false, str("NSAutomaticQuoteSubstitutionEnabled"));
// Setup drag message handler
msg_id_id(manager, s("addScriptMessageHandler:name:"), app->delegate, str("windowDrag"));
@@ -1934,9 +1712,24 @@ void HasURLHandlers(struct Application* app) {
void Quit(struct Application *app) {
Debug(app, "Quit Called");
msg_id(app->application, s("stop:"), NULL);
SetSize(app, 0, 0);
Show(app);
Hide(app);
}
id createImageFromBase64Data(const char *data, bool isTemplateImage) {
id nsdata = ALLOC("NSData");
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(nsdata, s("initWithBase64EncodedString:options:"), str(data), 0);
// If it's not valid base64 data, use the broken image
if ( imageData == NULL ) {
imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(nsdata, s("initWithBase64EncodedString:options:"), str(BrokenImage), 0);
}
id result = ALLOC("NSImage");
msg_id(result, s("initWithData:"), imageData);
if( isTemplateImage ) {
msg_bool(result, s("setTemplate:"), YES);
}
return result;
}
void* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose) {

View File

@@ -11,6 +11,12 @@
#include "hashmap.h"
#include "stdlib.h"
typedef struct {
long maj;
long min;
long patch;
} OSVersion;
// Macros to make it slightly more sane
#define msg objc_msgSend
#define msg_reg ((id(*)(id, SEL))objc_msgSend)
@@ -37,11 +43,13 @@
#if defined (__aarch64__)
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend)(receiver, s("bounds"))
#define GET_OSVERSION(receiver) ((OSVersion(*)(id, SEL))objc_msgSend)(processInfo, s("operatingSystemVersion"));
#endif
#if defined (__x86_64__)
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("bounds"))
#define GET_OSVERSION(receiver) ((OSVersion(*)(id, SEL))objc_msgSend_stret)(processInfo, s("operatingSystemVersion"));
#endif
#define GET_BACKINGSCALEFACTOR(receiver) ((CGFloat(*)(id, SEL))objc_msgSend)(receiver, s("backingScaleFactor"))
@@ -117,6 +125,8 @@
#define NSAlertSecondButtonReturn 1001
#define NSAlertThirdButtonReturn 1002
#define BrokenImage "iVBORw0KGgoAAAANSUhEUgAAABAAAAASCAMAAABl5a5YAAABj1BMVEWopan///+koqSWk5P9/v3///////////+AgACMiovz8/PB0fG9z+3i4+WysbGBfX1Erh80rACLiYqBxolEsDhHlDEbqQDDx+CNho7W1tj4+/bw+O3P5Mn4/f/W1tbK6sX////b2dn////////////8/Pz6+vro6Ojj4+P////G1PL////EzNydmp2cmZnd3eDF1PHs8v/o8P/Q3vrS3vfE0vCdmpqZkpr19/3N2vXI1vPH1fOgnqDg6frP3PbCytvHx8irqq6HhIZtuGtjnlZetU1Xs0NWskBNsi7w9v/d6P7w9P3S4Pzr8Pvl7PrY5PrU4PjQ3fjD1Ozo6Om30NjGzNi7ubm34K+UxKmbnaWXlJeUjpSPi4tppF1TtjxSsTf2+f7L2PTr7e3H2+3V7+q+0uXg4OPg4eLR1uG7z+Hg4ODGzODV2N7V1trP5dmxzs65vcfFxMWq0cKxxr+/vr+0s7apxbWaxrCv2qao05+dlp2Uuo2Dn4F8vIB6xnyAoHmAym9zqGpctENLryNFsgoblJpnAAAAKnRSTlP+hP7+5ZRmYgL+/f39/f39/f38/Pz8/Pv69+7j083My8GocnBPTTMWEgjxeITOAAABEklEQVQY0y3KZXuCYBiG4ceYuu7u3nyVAaKOMBBQ7O5Yd3f3fvheDnd9u8/jBkGwNxP6sjOWVQvY/ftrzfT6bd3yEhCnYZqiaYoKiwX/gXkFiHySTcUTLJMsZ9v8nQvgssWYOEKedKpcOO6CUXD5IlGEY5hLUbyDAAZ6HRf1bnkoavOsFQibg+Q4nuNYL+ON5PHD5nBaraRVyxnzGf6BJzUi2QQCQgMyk8tleL7dg1owpJ17D5IkvV100EingeOopPyo6vfAuXF+9hbDTknZCIaUoeK4efKwG4iT6xDewd7imGlid7gGwv37b6Oh9jwaTdOf/Tc1qH7UZVmuP6G5qZfBr9cAGNy4KiDd4tXIs7tS+QO9aUKvPAIKuQAAAABJRU5ErkJggg=="
struct Application;
int releaseNSObject(void *const context, struct hashmap_element_s *const e);
void TitlebarAppearsTransparent(struct Application* app);
@@ -139,4 +149,6 @@ void* lookupStringConstant(id constantName);
void HasURLHandlers(struct Application* app);
id createImageFromBase64Data(const char *data, bool isTemplateImage);
#endif

View File

@@ -189,6 +189,9 @@ id processAcceleratorKey(const char *key) {
if( STREQ(key, "return") ) {
return strunicode(0x000d);
}
if( STREQ(key, "enter") ) {
return strunicode(0x000d);
}
if( STREQ(key, "escape") ) {
return strunicode(0x001b);
}
@@ -576,32 +579,127 @@ id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const c
return item;
}
// getColour returns the colour from a styledLabel based on the key
const char* getColour(JsonNode *styledLabelEntry, const char* key) {
JsonNode* colEntry = getJSONObject(styledLabelEntry, key);
if( colEntry == NULL ) {
return NULL;
}
return getJSONString(colEntry, "hex");
}
id createAttributedStringFromStyledLabel(JsonNode *styledLabel, const char* fontName, int fontSize) {
// Create result
id attributedString = ALLOC_INIT("NSMutableAttributedString");
msg_reg(attributedString, s("autorelease"));
// Create new Dictionary
id dictionary = ALLOC_INIT("NSMutableDictionary");
msg_reg(dictionary, s("autorelease"));
// Use default font
CGFloat fontSizeFloat = (CGFloat)fontSize;
id font = ((id(*)(id, SEL, CGFloat))objc_msgSend)(c("NSFont"), s("menuBarFontOfSize:"), fontSizeFloat);
// Check user supplied font
if( STR_HAS_CHARS(fontName) ) {
id fontNameAsNSString = str(fontName);
id userFont = ((id(*)(id, SEL, id, CGFloat))objc_msgSend)(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
if( userFont != NULL ) {
font = userFont;
}
}
id fan = lookupStringConstant(str("NSFontAttributeName"));
id NSForegroundColorAttributeName = lookupStringConstant(str("NSForegroundColorAttributeName"));
id NSBackgroundColorAttributeName = lookupStringConstant(str("NSBackgroundColorAttributeName"));
// Loop over styled text creating NSAttributedText and appending to result
JsonNode *styledLabelEntry;
json_foreach(styledLabelEntry, styledLabel) {
// Clear dictionary
msg_reg(dictionary, s("removeAllObjects"));
// Add font to dictionary
msg_id_id(dictionary, s("setObject:forKey:"), font, fan);
// Get Text
const char* thisLabel = mustJSONString(styledLabelEntry, "Label");
// Get foreground colour
const char *hexColour = getColour(styledLabelEntry, "FgCol");
if( hexColour != NULL) {
unsigned short r, g, b, a;
// white by default
r = g = b = a = 255;
int count = sscanf(hexColour, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a);
if (count > 0) {
id colour = ((id(*)(id, SEL, CGFloat, CGFloat, CGFloat, CGFloat))objc_msgSend)(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"),
(CGFloat)r / (CGFloat)255.0,
(CGFloat)g / (CGFloat)255.0,
(CGFloat)b / (CGFloat)255.0,
(CGFloat)a / (CGFloat)255.0);
msg_id_id(dictionary, s("setObject:forKey:"), colour, NSForegroundColorAttributeName);
}
}
// Get background colour
hexColour = getColour(styledLabelEntry, "BgCol");
if( hexColour != NULL) {
unsigned short r, g, b, a;
// white by default
r = g = b = a = 255;
int count = sscanf(hexColour, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a);
if (count > 0) {
id colour = ((id(*)(id, SEL, CGFloat, CGFloat, CGFloat, CGFloat))objc_msgSend)(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"),
(CGFloat)r / (CGFloat)255.0,
(CGFloat)g / (CGFloat)255.0,
(CGFloat)b / (CGFloat)255.0,
(CGFloat)a / (CGFloat)255.0);
msg_id_id(dictionary, s("setObject:forKey:"), colour, NSForegroundColorAttributeName);
}
}
// Create AttributedText
id thisString = ALLOC("NSMutableAttributedString");
msg_reg(thisString, s("autorelease"));
msg_id_id(thisString, s("initWithString:attributes:"), str(thisLabel), dictionary);
// Append text to result
msg_id(attributedString, s("appendAttributedString:"), thisString);
}
return attributedString;
}
id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA) {
// Process Menu Item attributes
// Create new Dictionary
id dictionary = ALLOC_INIT("NSMutableDictionary");
// Process font
CGFloat fontSizeFloat = (CGFloat)fontSize;
// Check if valid
id fontNameAsNSString = str(fontName);
id font = ((id(*)(id, SEL, id, CGFloat))objc_msgSend)(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
if( font == NULL ) {
bool supportsMonospacedDigitSystemFont = (bool) ((id(*)(id, SEL, SEL))objc_msgSend)(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:"));
if( supportsMonospacedDigitSystemFont ) {
font = ((id(*)(id, SEL, CGFloat, CGFloat))objc_msgSend)(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, (CGFloat)NSFontWeightRegular);
} else {
font = ((id(*)(id, SEL, CGFloat))objc_msgSend)(c("NSFont"), s("menuFontOfSize:"), fontSizeFloat);
// Use default font
id font = ((id(*)(id, SEL, CGFloat))objc_msgSend)(c("NSFont"), s("menuBarFontOfSize:"), fontSizeFloat);
// Check user supplied font
if( STR_HAS_CHARS(fontName) ) {
id fontNameAsNSString = str(fontName);
id userFont = ((id(*)(id, SEL, id, CGFloat))objc_msgSend)(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
if( userFont != NULL ) {
font = userFont;
}
}
// Add font to dictionary
id fan = lookupStringConstant(str("NSFontAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), font, fan);
id offset = msg_float(c("NSNumber"), s("numberWithFloat:"), (float)0.0);
id offsetAttrName = lookupStringConstant(str("NSBaselineOffsetAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), offset, offsetAttrName);
// RGBA
if( RGBA != NULL && strlen(RGBA) > 0) {
unsigned short r, g, b, a;
@@ -617,18 +715,17 @@ id createAttributedString(const char* title, const char* fontName, int fontSize,
(CGFloat)a / (CGFloat)255.0);
id NSForegroundColorAttributeName = lookupStringConstant(str("NSForegroundColorAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), colour, NSForegroundColorAttributeName);
msg_reg(colour, s("autorelease"));
}
}
id attributedString = ALLOC("NSMutableAttributedString");
msg_id_id(attributedString, s("initWithString:attributes:"), str(title), dictionary);
msg_reg(attributedString, s("autorelease"));
msg_reg(dictionary, s("release"));
msg_reg(dictionary, s("autorelease"));
return attributedString;
}
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage, bool alternate) {
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage, bool alternate, JsonNode* styledLabel) {
id item = ALLOC("NSMenuItem");
// Create a MenuItemCallbackData
@@ -651,17 +748,16 @@ id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char
// Process image
if( image != NULL && strlen(image) > 0) {
id data = ALLOC("NSData");
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(data, s("initWithBase64EncodedString:options:"), str(image), 0);
id nsimage = ALLOC("NSImage");
msg_id(nsimage, s("initWithData:"), imageData);
if( templateImage ) {
msg_bool(nsimage, s("setTemplate:"), YES);
}
id nsimage = createImageFromBase64Data(image, templateImage);
msg_id(item, s("setImage:"), nsimage);
}
id attributedString = createAttributedString(title, fontName, fontSize, RGBA);
id attributedString = NULL;
if( styledLabel != NULL) {
attributedString = createAttributedStringFromStyledLabel(styledLabel, fontName, fontSize);
} else {
attributedString = createAttributedString(title, fontName, fontSize, RGBA);
}
msg_id(item, s("setAttributedTitle:"), attributedString);
//msg_id(item, s("setTitle:"), str(title));
@@ -701,38 +797,6 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg_id(thisMenuItem, s("setSubmenu:"), thisMenu);
msg_id(parentMenu, s("addItem:"), thisMenuItem);
JsonNode *submenuItems = json_find_member(submenu, "Items");
// If we have no items, just return
if ( submenuItems == NULL ) {
return;
}
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenuItems) {
// Get item label
processMenuItem(menu, thisMenu, item);
}
return;
}
// This is a user menu. Get the common data
// Get the label
const char *label = getJSONString(item, "Label");
@@ -740,6 +804,8 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
label = "(empty)";
}
// Check for a styled label
JsonNode *styledLabel = getJSONObject(item, "StyledLabel");
// Is this an alternate menu item?
bool alternate = false;
@@ -798,9 +864,36 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
// Get the Type
JsonNode *type = json_find_member(item, "Type");
if( type != NULL ) {
if( STREQ(type->string_, "Text") || STREQ(type->string_, "Submenu")) {
id thisMenuItem = processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage, alternate, styledLabel);
if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage, alternate);
// Check if this node has a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenu = createMenu(str(name));
msg_id(thisMenuItem, s("setSubmenu:"), thisMenu);
JsonNode *submenuItems = json_find_member(submenu, "Items");
// If we have no items, just return
if ( submenuItems == NULL ) {
return;
}
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenuItems) {
// Get item label
processMenuItem(menu, thisMenu, item);
}
}
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);
@@ -819,7 +912,6 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
processRadioMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
}
}
if ( modifiers != NULL ) {

View File

@@ -105,12 +105,13 @@ id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage, bool alternate);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage, bool alternate, JsonNode* styledLabel);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData);
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup);
id GetMenu(Menu *menu);
id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA);
id createAttributedStringFromStyledLabel(JsonNode *styledLabel, const char* fontName, int fontSize);
#endif //ASSETS_C_MENU_DARWIN_H

View File

@@ -42,6 +42,8 @@ TrayMenu* NewTrayMenu(const char* menuJSON) {
result->disabled = false;
getJSONBool(processedJSON, "Disabled", &result->disabled);
result->styledLabel = getJSONObject(processedJSON, "StyledLabel");
// Create the menu
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->menu = NewMenu(processedMenu);
@@ -63,7 +65,7 @@ void DumpTrayMenu(TrayMenu* trayMenu) {
}
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled) {
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled, JsonNode *styledLabel) {
// Exit early if NULL
if( trayMenu->label == NULL ) {
@@ -71,7 +73,12 @@ void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName
}
// Update button label
id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
id attributedString = createAttributedString(label, fontName, fontSize, RGBA);
id attributedString = NULL;
if( styledLabel != NULL) {
attributedString = createAttributedStringFromStyledLabel(styledLabel, fontName, fontSize);
} else {
attributedString = createAttributedString(label, fontName, fontSize, RGBA);
}
if( tooltip != NULL ) {
msg_id(statusBarButton, s("setToolTip:"), str(tooltip));
@@ -102,14 +109,7 @@ void UpdateTrayIcon(TrayMenu *trayMenu) {
// If we don't have the image in the icon cache then assume it's base64 encoded image data
if (trayImage == NULL) {
id data = ALLOC("NSData");
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(data, s("initWithBase64EncodedString:options:"), str(trayMenu->icon), 0);
trayImage = ALLOC("NSImage");
msg_id(trayImage, s("initWithData:"), imageData);
if( trayMenu->templateImage ) {
msg_bool(trayImage, s("setTemplate:"), YES);
}
trayImage = createImageFromBase64Data(trayMenu->icon, trayMenu->templateImage);
}
msg_int(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
@@ -132,7 +132,7 @@ void ShowTrayMenu(TrayMenu* trayMenu) {
UpdateTrayIcon(trayMenu);
// Update the label if needed
UpdateTrayLabel(trayMenu, trayMenu->label, trayMenu->fontName, trayMenu->fontSize, trayMenu->RGBA, trayMenu->tooltip, trayMenu->disabled);
UpdateTrayLabel(trayMenu, trayMenu->label, trayMenu->fontName, trayMenu->fontSize, trayMenu->RGBA, trayMenu->tooltip, trayMenu->disabled, trayMenu->styledLabel);
// Update the menu
id menu = GetMenu(trayMenu->menu);
@@ -168,6 +168,7 @@ void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) {
// Copy the other data
currentMenu->ID = newMenu->ID;
currentMenu->label = newMenu->label;
currentMenu->styledLabel = newMenu->styledLabel;
currentMenu->trayIconPosition = newMenu->trayIconPosition;
currentMenu->icon = newMenu->icon;
@@ -227,7 +228,7 @@ void LoadTrayIcons() {
int length = atoi((const char *)lengthAsString);
// Create the icon and add to the hashmap
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(c("NSData"), s("dataWithBytes:length:"), data, length);
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(c("NSData"), s("dataWithBytes:length:"), (id)data, length);
id trayImage = ALLOC("NSImage");
msg_id(trayImage, s("initWithData:"), imageData);
hashmap_put(&trayIconCache, (const char *)name, strlen((const char *)name), trayImage);

View File

@@ -29,6 +29,8 @@ typedef struct {
JsonNode* processedJSON;
JsonNode* styledLabel;
id delegate;
} TrayMenu;
@@ -38,7 +40,7 @@ void DumpTrayMenu(TrayMenu* trayMenu);
void ShowTrayMenu(TrayMenu* trayMenu);
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu);
void UpdateTrayIcon(TrayMenu *trayMenu);
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled);
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled, JsonNode *styledLabel);
void LoadTrayIcons();
void UnloadTrayIcons();

View File

@@ -127,7 +127,9 @@ void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
bool disabled = false;
getJSONBool(parsedUpdate, "Disabled", &disabled);
UpdateTrayLabel(menu, Label, fontName, fontSize, RGBA, tooltip, disabled);
JsonNode *styledLabel = getJSONObject(parsedUpdate, "StyledLabel");
UpdateTrayLabel(menu, Label, fontName, fontSize, RGBA, tooltip, disabled, styledLabel);
}

View File

@@ -2,6 +2,9 @@ package menumanager
import (
"encoding/json"
"strings"
"github.com/leaanthony/go-ansi-parser"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
@@ -41,11 +44,25 @@ type ProcessedMenuItem struct {
// 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,
@@ -63,6 +80,7 @@ func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *Pr
MacTemplateImage: menuItem.MacTemplateImage,
MacAlternate: menuItem.MacAlternate,
Tooltip: menuItem.Tooltip,
StyledLabel: styledLabel,
}
if menuItem.SubMenu != nil {

View File

@@ -4,8 +4,11 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"
"sync"
"github.com/leaanthony/go-ansi-parser"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/pkg/menu"
)
@@ -36,6 +39,7 @@ type TrayMenu struct {
menu *menu.Menu
ProcessedMenu *WailsMenu
trayMenu *menu.TrayMenu
StyledLabel []*ansi.StyledText `json:",omitempty"`
}
func (t *TrayMenu) AsJSON() (string, error) {
@@ -48,6 +52,16 @@ func (t *TrayMenu) AsJSON() (string, error) {
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,
@@ -60,6 +74,7 @@ func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
RGBA: trayMenu.RGBA,
menuItemMap: NewMenuItemMap(),
trayMenu: trayMenu,
StyledLabel: styledLabel,
}
result.menuItemMap.AddMenu(trayMenu.Menu)
@@ -150,14 +165,25 @@ func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
type LabelUpdate struct {
ID string
Label string
FontName string
Label string `json:",omitempty"`
FontName string `json:",omitempty"`
FontSize int
RGBA string
RGBA string `json:",omitempty"`
Disabled bool
Tooltip string
Image string
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{
@@ -170,6 +196,7 @@ func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
Image: trayMenu.Image,
MacTemplateImage: trayMenu.MacTemplateImage,
RGBA: trayMenu.RGBA,
StyledLabel: styledLabel,
}
data, err := json.Marshal(update)

View File

@@ -1,6 +0,0 @@
{
"name": "Angular",
"shortname": "angular",
"author": "bh90210 <ktc@pm.me>",
"description": "Angular projects w/ @angular/cli - Note: in order to reach the cli use npx like this: npx ng"
}

View File

@@ -1,6 +0,0 @@
{
"name": "React JS",
"shortname": "react",
"author": "bh90210 <ktc@pm.me>",
"description": "Create React App v3 standar tooling"
}

View File

@@ -1,35 +0,0 @@
package main
import (
"fmt"
"github.com/wailsapp/wails/v2"
)
// Basic application struct
type Basic struct {
runtime *wails.Runtime
}
// newBasic creates a new Basic application struct
func newBasic() *Basic {
return &Basic{}
}
// WailsInit is called at application startup
func (b *Basic) WailsInit(runtime *wails.Runtime) error {
// Perform your setup here
b.runtime = runtime
runtime.Window.SetTitle("{{.ProjectName}}")
return nil
}
// WailsShutdown is called at application termination
func (b *Basic) WailsShutdown() {
// Perform your teardown here
}
// Greet returns a greeting for the given name
func (b *Basic) Greet(name string) string {
return fmt.Sprintf("Hello %s!", name)
}

View File

@@ -1,4 +0,0 @@
/node_modules/
/public/build/
.DS_Store

View File

@@ -1,93 +0,0 @@
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
---
# svelte app
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
```bash
npx degit sveltejs/template svelte-app
cd svelte-app
```
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
## Get started
Install the dependencies...
```bash
cd svelte-app
npm install
```
...then start [Rollup](https://rollupjs.org):
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
## Building and running in production mode
To create an optimised version of the app:
```bash
npm run build
```
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
## Single-page app mode
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
```js
"start": "sirv public --single"
```
## Deploying to the web
### With [now](https://zeit.co/now)
Install `now` if you haven't already:
```bash
npm install -g now
```
Then, from within your project folder:
```bash
cd public
now deploy --name my-project
```
As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon.
### With [surge](https://surge.sh/)
Install `surge` if you haven't already:
```bash
npm install -g surge
```
Then, from within your project folder:
```bash
npm run build
surge public my-project.surge.sh
```

View File

@@ -1,24 +0,0 @@
{
"name": "svelte-app",
"version": "1.0.0",
"scripts": {
"build": "npx rollup -c",
"dev": "npx rollup -c -w",
"start": "npx sirv public",
"start:dev": "npx sirv public --single --host 0.0.0.0 --dev"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.1",
"focus-visible": "^5.2.0",
"rollup": "^2.35.1",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.2",
"svelte": "^3.31.1",
"svelte-mui": "^0.3.3-5"
},
"dependencies": {
"sirv-cli": "^0.4.4"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,24 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="svelte-mui" />
<meta name="application-name" content="svelte-mui" />
<meta name="theme-color" content="#212121" />
<title>Svelte app</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/bundle.css'>
<script defer src='/bundle.js'></script>
</head>
<body>
<noscript>Please enable JavaScript.</noscript>
</body>
</html>

View File

@@ -1,71 +0,0 @@
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
const production = !process.env.ROLLUP_WATCH;
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/bundle.js'
},
plugins: [
svelte({
// enable run-time checks when not in production
dev: !production,
// we'll extract any component CSS out into
// a separate file - better for performance
css: css => {
css.write('public/bundle.css');
}
}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
};
function serve() {
let started = false;
return {
writeBundle() {
if (!started) {
started = true;
require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
}
}
};
}

View File

@@ -1,51 +0,0 @@
<h1>Hello {name}!</h1>
<Snackbar bind:visible bg="#f44336">
{response}
<span slot="action">
<Button color="#ff0" on:click={() => (visible = false)}>Close</Button>
</span>
</Snackbar>
<Textfield
autocomplete="off"
label="Message"
required
bind:value
message=""
/>
<Button
outlined
shaped
color="Red"
on:click={() => {
window.backend.main.Basic.Greet(value).then((result) => {
response = result;
visible = true;
});
}}
>
Hello
</Button>
<script>
export let name;
let value = '';
let response = '';
// optional import focus-visible polyfill only once
import 'focus-visible';
// import any components
import { Button, Checkbox, Snackbar, Textfield } from 'svelte-mui';
let checked = true;
let visible = false;
</script>
<style>
h1 {
color: purple;
}
</style>

View File

@@ -1,10 +0,0 @@
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
name: 'Wails User'
}
});
export default app;

View File

@@ -1,9 +0,0 @@
module test
go 1.13
require (
github.com/wailsapp/wails/v2 v2.0.0-alpha
)
replace github.com/wailsapp/wails/v2 v2.0.0-alpha => {{.WailsDirectory}}

View File

@@ -1,22 +0,0 @@
package main
import (
"github.com/wailsapp/wails/v2"
"log"
)
func main() {
// Create application with options
app, err := wails.CreateApp("{{.ProjectName}}", 1024, 768)
if err != nil {
log.Fatal(err)
}
app.Bind(newBasic())
err = app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@@ -1,7 +0,0 @@
{
"name": "Svelte + MUI3",
"shortname": "svelte-mui",
"author": "Travis McLane <travis.mclane@gmail.com>",
"description": "A simple template using Svelte + Mui3",
"helpurl": "https://wails.app/help/templates/svelte-mui3"
}

View File

@@ -1,9 +0,0 @@
{
"name": "{{.ProjectName}}",
"outputfilename": "{{.BinaryName}}",
"html": "frontend/public/index.html",
"js": "frontend/public/bundle.js",
"css": "frontend/public/bundle.css",
"frontend:build": "npm run build",
"frontend:install": "npm install"
}

View File

@@ -23,6 +23,7 @@ button {
border-radius: 3px;
outline: none;
height: 20px;
-webkit-font-smoothing: antialiased;
}
#logo {

View File

@@ -1,12 +1,12 @@
// Get input + focus
var nameElement = document.getElementById("name");
let nameElement = document.getElementById("name");
nameElement.focus();
// Stup the greet function
// Setup the greet function
window.greet = function () {
// Get name
var name = nameElement.value;
let name = nameElement.value;
// Call Basic.Greet(name)
window.backend.main.Basic.Greet(name).then((result) => {

View File

@@ -1,6 +1,6 @@
module test
go 1.13
go 1.16
require (
github.com/wailsapp/wails/v2 v2.0.0-alpha

View File

@@ -1,6 +0,0 @@
{
"name": "Vue2/Webpack Basic",
"shortname": "vue",
"author": "Lea Anthony<lea.anthony@gmail.com>",
"description": "A basic template using Vue 2 and bundled using Webpack 4"
}

View File

@@ -1,6 +0,0 @@
{
"name": "Vuetify1.5/Webpack Basic",
"shortname": "vuetify1",
"author": "Lea Anthony <lea.anthony@gmail.com>",
"description": "Basic template using Vuetify v1.5 and bundled using Webpack"
}

View File

@@ -1,35 +0,0 @@
package main
import (
"fmt"
"github.com/wailsapp/wails/v2"
)
// Basic application struct
type Basic struct {
runtime *wails.Runtime
}
// newBasic creates a new Basic application struct
func newBasic() *Basic {
return &Basic{}
}
// WailsInit is called at application startup
func (b *Basic) WailsInit(runtime *wails.Runtime) error {
// Perform your setup here
b.runtime = runtime
runtime.Window.SetTitle("{{.ProjectName}}")
return nil
}
// WailsShutdown is called at application termination
func (b *Basic) WailsShutdown() {
// Perform your teardown here
}
// Greet returns a greeting for the given name
func (b *Basic) Greet(name string) string {
return fmt.Sprintf("Hello %s!", name)
}

View File

@@ -1,5 +0,0 @@
module.exports = {
presets: [
[ '@vue/app', { useBuiltIns: 'entry' } ]
]
};

View File

@@ -1,52 +0,0 @@
{
"name": "vuetify2",
"author": "{{.AuthorNameAndEmail}}",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.1",
"regenerator-runtime": "^0.13.3",
"vue": "^2.5.22",
"vuetify": "^2.2.17"
},
"devDependencies": {
"@mdi/font": "^4.3.95",
"@vue/cli-plugin-babel": "^3.4.0",
"@vue/cli-plugin-eslint": "^3.4.0",
"@vue/cli-service": "^3.4.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.8.0",
"eslint-plugin-vue": "^5.0.0",
"eventsource-polyfill": "^0.9.6",
"vue-template-compiler": "^2.5.21",
"webpack-hot-middleware": "^2.24.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><title>Wails</title></head><body><div id=app></div></body></html>

View File

@@ -1,60 +0,0 @@
<template>
<v-app id="inspire">
<v-navigation-drawer v-model="drawer" clipped fixed app>
<v-list dense>
<v-list-item>
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-icon>
<v-icon>mdi-settings</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Settings</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-app-bar app fixed clipped-left>
<v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title>Application</v-toolbar-title>
</v-app-bar>
<v-content>
<v-container fluid class="px-0">
<v-layout justify-center align-center class="px-0">
<hello-world></hello-world>
</v-layout>
</v-container>
</v-content>
<v-footer app fixed>
<span style="margin-left:1em">&copy; You</span>
</v-footer>
</v-app>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue"
export default {
data: () => ({
drawer: false
}),
components: {
HelloWorld
},
props: {
source: String
}
}
</script>
<style>
.logo {
width: 16em;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

View File

@@ -1,85 +0,0 @@
<template>
<v-container fluid class="px-0">
<v-layout>
<v-flex xs12 sm6 offset-sm3>
<v-card raised="raised" class="pa-4 ma-4">
<v-layout justify-center align-center class="pa-4 ma-4">
<v-img :src="require('../assets/images/logo.png')"></v-img>
</v-layout>
<v-card-actions>
<v-layout justify-center align-center class="px-0">
<v-btn color="blue" @click="getMessage">Press Me</v-btn>
</v-layout>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
<div class="text-xs-center">
<v-dialog v-model="dialog" width="500">
<v-card>
<v-card-title class="headline" primary-title>Message from Go</v-card-title>
<v-card-text>{{message}}</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="dialog = false">Awesome</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</v-container>
</template>
<script>
export default {
data () {
return {
message: " ",
raised: true,
dialog: false
}
},
methods: {
getMessage: function () {
var self = this
window.backend.main.Basic.Greet("Hello from Go!").then(result => {
self.message = result
self.dialog = true
})
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
margin-top: 2em;
position: relative;
min-height: 5rem;
width: 100%;
}
a:hover {
font-size: 1.7em;
border-color: blue;
background-color: blue;
color: white;
border: 3px solid white;
border-radius: 10px;
padding: 9px;
cursor: pointer;
transition: 500ms;
}
a {
font-size: 1.7em;
border-color: white;
background-color: #121212;
color: white;
border: 3px solid white;
border-radius: 10px;
padding: 9px;
cursor: pointer;
}
</style>

View File

@@ -1,27 +0,0 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import '@mdi/font/css/materialdesignicons.css';
import Vue from 'vue';
import Vuetify from 'vuetify';
import 'vuetify/dist/vuetify.min.css';
Vue.use(Vuetify);
import App from './App.vue';
Vue.config.productionTip = false;
Vue.config.devtools = true;
Wails.Init(() => {
new Vue({
vuetify: new Vuetify({
icons: {
iconfont: 'mdi'
},
theme: {
dark: true
}
}),
render: h => h(App)
}).$mount('#app');
});

View File

@@ -1,60 +0,0 @@
let cssConfig = {};
if (process.env.NODE_ENV == 'production') {
cssConfig = {
extract: {
filename: '[name].css',
chunkFilename: '[name].css'
}
};
}
module.exports = {
indexPath: 'index.html',
publicPath: 'public',
// disable hashes in filenames
filenameHashing: false,
// delete HTML related webpack plugins
chainWebpack: config => {
config.plugins.delete('preload');
config.plugins.delete('prefetch');
config
.plugin('html')
.tap(args => {
args[0].template = 'public/index.html';
args[0].inject = false;
args[0].cache = false;
args[0].minify = false;
args[0].filename = 'index.html';
return args;
});
let limit = 9999999999999999;
config.module
.rule('images')
.test(/\.(png|gif|jpg)(\?.*)?$/i)
.use('url-loader')
.loader('url-loader')
.tap(options => Object.assign(options, { limit: limit }));
config.module
.rule('fonts')
.test(/\.(woff2?|eot|ttf|otf|svg)(\?.*)?$/i)
.use('url-loader')
.loader('url-loader')
.options({
limit: limit
});
},
css: cssConfig,
configureWebpack: {
output: {
filename: '[name].js'
},
optimization: {
splitChunks: false
}
},
devServer: {
disableHostCheck: true
}
};

View File

@@ -1,9 +0,0 @@
module test
go 1.13
require (
github.com/wailsapp/wails/v2 v2.0.0-alpha
)
replace github.com/wailsapp/wails/v2 v2.0.0-alpha => {{.WailsDirectory}}

View File

@@ -1,15 +0,0 @@
package main
import (
"github.com/wailsapp/wails/v2"
)
func main() {
// Create application with options
app := wails.CreateApp("{{.ProjectName}}", 1024, 768)
app.Bind(newBasic())
app.Run()
}

View File

@@ -1,7 +0,0 @@
{
"name": "Vuetify 2 ",
"shortname": "vuetify2",
"author": "Michael Hipp <michael@redmule.com>",
"description": "A simple template using only HTML/CSS/JS",
"helpurl": "https://wails.app/help/templates/vuetify2"
}

View File

@@ -1,13 +0,0 @@
{
"name": "{{.ProjectName}}",
"outputfilename": "{{.BinaryName}}",
"html": "frontend/dist/index.html",
"js": "frontend/dist/app.js",
"css": "frontend/dist/app.css",
"frontend:build": "npm run build",
"frontend:install": "npm install",
"author": {
"name": "{{.AuthorName}}",
"email": "{{.AuthorEmail}}"
}
}

View File

@@ -10,6 +10,8 @@ import (
"runtime"
"strings"
"github.com/pkg/errors"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/assetdb"
"github.com/wailsapp/wails/v2/internal/fs"
@@ -157,9 +159,11 @@ func (b *BaseBuilder) OutputFilename(options *Options) string {
// CompileProject compiles the project
func (b *BaseBuilder) CompileProject(options *Options) error {
verbose := options.Verbosity == VERBOSE
// Run go mod tidy first
cmd := exec.Command(options.Compiler, "mod", "tidy")
if options.Verbosity == VERBOSE {
if verbose {
println("")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
@@ -183,14 +187,14 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// potentially try and see if the assets have changed but will
// this take as much time as a `-a` build?
commands.Add("-a")
commands.Add("-x")
var tags slicer.StringSlicer
tags.Add(options.OutputType)
tags.AddSlice(options.UserTags)
if options.Mode == Debug {
tags.Add("debug")
}
tags.Deduplicate()
// Add the output type build tag
commands.Add("-tags")
@@ -212,10 +216,12 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// Get application build directory
appDir := options.BuildDirectory
//err = cleanBuildDirectory(options)
//if err != nil {
// return err
//}
if options.CleanBuildDirectory {
err = cleanBuildDirectory(options)
if err != nil {
return err
}
}
if options.LDFlags != "" {
commands.Add("-ldflags")
@@ -233,7 +239,8 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// Create the command
cmd = exec.Command(options.Compiler, commands.AsSlice()...)
if options.Verbosity == VERBOSE {
if verbose {
println(" Build command:", commands.Join(" "))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
@@ -270,14 +277,44 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
return "1"
})
if verbose {
println(" Environment:", strings.Join(cmd.Env, " "))
}
// Setup buffers
var stdo, stde bytes.Buffer
cmd.Stdout = &stdo
cmd.Stderr = &stde
// Run command
err = cmd.Run()
// Format error if we have one
if err != nil {
return err
return fmt.Errorf("%s\n%s", err, string(stde.Bytes()))
}
if !options.Compress {
return nil
}
// Do we have upx installed?
if !shell.CommandExists("upx") {
println("Warning: Cannot compress binary: upx not found")
return nil
}
if verbose {
println(" Compressing with:", "upx", "--best", "--no-color", "--no-progress", options.CompiledBinary)
}
output, err := exec.Command(options.BuildDirectory, "upx", "--best", "--no-color", "--no-progress", options.CompiledBinary).Output()
if err != nil {
return errors.Wrap(err, "Error during compression:")
}
if verbose {
println(output)
}
return nil
}
@@ -391,7 +428,7 @@ func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error {
outputLogger.Print("Installing frontend dependencies: ")
if verbose {
outputLogger.Println("")
outputLogger.Println("\tCommand: " + b.projectData.InstallCommand)
outputLogger.Println(" Install command: '" + b.projectData.InstallCommand + "'")
}
if err := b.NpmInstallUsingCommand(frontendDir, b.projectData.InstallCommand, verbose); err != nil {
return err
@@ -410,7 +447,7 @@ func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error {
cmd := strings.Split(b.projectData.BuildCommand, " ")
if verbose {
outputLogger.Println("")
outputLogger.Println("\tCommand: '" + strings.Join(cmd, " ") + "'")
outputLogger.Println(" Build command: '" + strings.Join(cmd, " ") + "'")
}
stdout, stderr, err := shell.RunCommand(frontendDir, cmd[0], cmd[1:]...)
if verbose || err != nil {

View File

@@ -28,22 +28,25 @@ var modeMap = []string{"Debug", "Production"}
// Options contains all the build options as well as the project data
type Options struct {
LDFlags string // Optional flags to pass to linker
Logger *clilogger.CLILogger // All output to the logger
OutputType string // EG: desktop, server....
Mode Mode // release or debug
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
IgnoreFrontend bool // Indicates if the frontend does not need building
OutputFile string // Override the output filename
BuildDirectory string // Directory to use for building the application
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)
AppleIdentity string
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 debug
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
IgnoreFrontend bool // Indicates if the frontend does not need building
OutputFile string // Override the output filename
BuildDirectory string // Directory to use for building the application
CleanBuildDirectory bool // Indicates if the build 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
AppleIdentity string
}
// GetModeAsString returns the current mode as a string
@@ -129,6 +132,11 @@ func Build(options *Options) (string, error) {
// Build amd64 first
options.Arch = "amd64"
options.OutputFile = amd64Filename
options.CleanBuildDirectory = false
if options.Verbosity == VERBOSE {
println()
println(" Building AMD64 Target:", filepath.Join(options.BuildDirectory, options.OutputFile))
}
err = builder.CompileProject(options)
if err != nil {
return "", err
@@ -136,11 +144,18 @@ func Build(options *Options) (string, error) {
// Build arm64
options.Arch = "arm64"
options.OutputFile = arm64Filename
options.CleanBuildDirectory = false
if options.Verbosity == VERBOSE {
println(" Building ARM64 Target:", filepath.Join(options.BuildDirectory, options.OutputFile))
}
err = builder.CompileProject(options)
if err != nil {
return "", err
}
// Run lipo
if options.Verbosity == VERBOSE {
println(" Running lipo: ", "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename)
}
_, stderr, err := shell.RunCommand(options.BuildDirectory, "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename)
if err != nil {
return "", fmt.Errorf("%s - %s", err.Error(), stderr)

View File

@@ -49,4 +49,4 @@ Example:
## Mac
The `mac` directory holds files specific to Mac builds, such as `info.plist`. These may be edited and used as part of the build.
The `mac` directory holds files specific to Mac builds, such as `Info.plist`. These may be edited and used as part of the build.

View File

@@ -42,7 +42,7 @@ func packageApplication(options *Options) error {
return errors.Wrap(err, "Cannot move file: "+options.ProjectData.OutputFilename)
}
// Generate info.plist
// Generate Info.plist
err = processPList(options, contentsDirectory)
if err != nil {
return err
@@ -68,7 +68,7 @@ func packageApplication(options *Options) error {
func processPList(options *Options, contentsDirectory string) error {
// Check if plist already exists in project dir
plistFile := filepath.Join(options.ProjectData.AssetsDir, "mac", "info.plist")
plistFile := filepath.Join(options.ProjectData.AssetsDir, "mac", "Info.plist")
// If the file doesn't exist, generate it
if !fs.FileExists(plistFile) {
@@ -79,7 +79,7 @@ func processPList(options *Options, contentsDirectory string) error {
}
// Copy it to the contents directory
targetFile := filepath.Join(contentsDirectory, "info.plist")
targetFile := filepath.Join(contentsDirectory, "Info.plist")
return fs.CopyFile(plistFile, targetFile)
}
@@ -88,11 +88,11 @@ func generateDefaultPlist(options *Options, targetPlistFile string) error {
exe := defaultString(options.OutputFile, name)
version := "1.0.0"
author := defaultString(options.ProjectData.Author.Name, "Anonymous")
packageID := strings.Join([]string{"wails", name, version}, ".")
packageID := strings.Join([]string{"wails", name}, ".")
plistData := newPlistData(name, exe, packageID, version, author)
tmpl := template.New("infoPlist")
plistTemplate := fs.RelativePath("./internal/packager/darwin/info.plist")
plistTemplate := fs.RelativePath("./internal/packager/darwin/Info.plist")
infoPlist, err := ioutil.ReadFile(plistTemplate)
if err != nil {
return errors.Wrap(err, "Cannot open plist template")

View File

@@ -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 {
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
}

View File

@@ -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"
)
// 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 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 {
errors.Wrap(err, stde)
}
return nil
}

View File

@@ -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)
}
})
}
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/leaanthony/slicer"
)
var namedKeys = slicer.String([]string{"backspace", "tab", "return", "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"})
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) {

View File

@@ -5,7 +5,6 @@ import (
"go/ast"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/fatih/structtag"
)
@@ -225,7 +224,6 @@ func (p *Parser) parseField(file *ast.File, field *ast.Field, pkg *Package) ([]*
}
default:
spew.Dump(t)
return nil, fmt.Errorf("unsupported field found in struct: %+v", t)
}

View File

@@ -1 +1 @@
info.plist
Info.plist