@briancray/belte 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/belte.ts +25 -12
- package/package.json +2 -1
- package/src/appEntry.ts +124 -0
- package/src/belteResolverPlugin.ts +217 -194
- package/src/build.ts +6 -67
- package/src/buildCli.ts +36 -63
- package/src/buildDisconnected.ts +127 -0
- package/src/bundleApp.ts +123 -0
- package/src/bundleDisconnectedEntry.ts +17 -0
- package/src/cliEntry.ts +3 -9
- package/src/compile.ts +4 -15
- package/src/controlServerWorker.ts +261 -0
- package/src/dedupeSveltePlugin.ts +66 -0
- package/src/discoveryEntry.ts +12 -11
- package/src/lib/browser/cache.ts +3 -6
- package/src/lib/browser/page.svelte.ts +19 -21
- package/src/lib/browser/socketChannel.ts +11 -1
- package/src/lib/browser/types/Pages.ts +1 -1
- package/src/lib/bundle/BundleMenu.ts +11 -0
- package/src/lib/bundle/BundleMenuItem.ts +24 -0
- package/src/lib/bundle/BundleWindow.ts +20 -0
- package/src/lib/bundle/bindConnectedFlag.ts +29 -0
- package/src/lib/bundle/bindRequestNavigate.ts +31 -0
- package/src/lib/bundle/buildWebviewLib.ts +111 -0
- package/src/lib/bundle/disconnected.css +9 -0
- package/src/lib/bundle/disconnected.svelte +192 -0
- package/src/lib/bundle/ensureWebviewLib.ts +20 -0
- package/src/lib/bundle/exitWithParent.ts +28 -0
- package/src/lib/bundle/findFreePort.ts +14 -0
- package/src/lib/bundle/infoPlist.ts +46 -0
- package/src/lib/bundle/installMacMenu.ts +39 -0
- package/src/lib/bundle/listenLocalControlServer.ts +19 -0
- package/src/lib/bundle/native/belteMenu.mm +298 -0
- package/src/lib/bundle/native/webview.h +4557 -0
- package/src/lib/bundle/onMenu.ts +26 -0
- package/src/lib/bundle/openWebview.ts +81 -0
- package/src/lib/bundle/pngToIcns.ts +47 -0
- package/src/lib/bundle/probeBelteServer.ts +34 -0
- package/src/lib/bundle/resolveServerBinary.ts +12 -0
- package/src/lib/bundle/resolveWebviewLib.ts +51 -0
- package/src/lib/bundle/serverBinaryFilename.ts +8 -0
- package/src/lib/bundle/stableLocalPort.ts +19 -0
- package/src/lib/bundle/waitForServer.ts +23 -0
- package/src/lib/bundle/webviewBuildRevision.ts +9 -0
- package/src/lib/bundle/webviewCachePath.ts +23 -0
- package/src/lib/bundle/webviewLibName.ts +11 -0
- package/src/lib/bundle/webviewVersion.ts +7 -0
- package/src/lib/cli/createClient.ts +34 -36
- package/src/lib/cli/printHelp.ts +45 -2
- package/src/lib/cli/runCli.ts +12 -3
- package/src/lib/mcp/createMcpResourceServer.ts +1 -1
- package/src/lib/mcp/dispatchMcpRequest.ts +53 -73
- package/src/lib/server/AppModule.ts +2 -2
- package/src/lib/server/cli/handleCliDownload.ts +4 -5
- package/src/lib/server/cli/handleCliInstall.ts +17 -0
- package/src/lib/server/error.ts +23 -9
- package/src/lib/server/json.ts +5 -5
- package/src/lib/server/jsonl.ts +10 -5
- package/src/lib/server/prompts/definePrompt.ts +6 -6
- package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
- package/src/lib/server/prompts/types/Prompt.ts +8 -9
- package/src/lib/server/prompts/types/PromptOptions.ts +7 -12
- package/src/lib/server/prompts/types/PromptRegistryEntry.ts +3 -5
- package/src/lib/server/prompts/types/PromptRoutes.ts +4 -4
- package/src/lib/server/redirect.ts +13 -8
- package/src/lib/server/rpc/defineVerb.ts +4 -3
- package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
- package/src/lib/server/rpc/types/RemoteFunction.ts +1 -1
- package/src/lib/server/rpc/types/RemoteHandler.ts +4 -0
- package/src/lib/server/runtime/acceptsZstd.ts +8 -0
- package/src/lib/server/runtime/buildOpenApiSpec.ts +2 -0
- package/src/lib/server/runtime/cacheControlForAsset.ts +7 -2
- package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +6 -20
- package/src/lib/server/runtime/createServer.ts +50 -58
- package/src/lib/server/runtime/registryManifests.ts +33 -15
- package/src/lib/server/runtime/types/RequestStore.ts +2 -3
- package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +10 -7
- package/src/lib/server/sse.ts +10 -5
- package/src/lib/shared/cacheControlValues.ts +10 -2
- package/src/lib/shared/canonicalJson.ts +1 -5
- package/src/lib/shared/createCacheStore.ts +29 -20
- package/src/lib/shared/exitOnBuildFailure.ts +17 -0
- package/src/lib/shared/fileStem.ts +9 -0
- package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
- package/src/lib/shared/keyForRemoteCall.ts +7 -5
- package/src/lib/shared/parsePromptMarkdown.ts +34 -0
- package/src/lib/shared/promptNameForFile.ts +5 -5
- package/src/lib/shared/subscribableFromResponse.ts +104 -215
- package/src/lib/shared/types/PromptArgument.ts +12 -0
- package/src/lib/shared/writeRoutesDts.ts +5 -7
- package/src/serverBuildPlugins.ts +25 -0
- package/src/serverEntry.ts +4 -0
- package/template/package.json +2 -1
- package/src/lib/server/prompt.ts +0 -30
- package/src/lib/server/prompts/types/PromptMessage.ts +0 -10
- package/src/lib/shared/preparePromptModule.ts +0 -36
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// belte's native macOS shim, compiled into the same dylib as the vendored
|
|
2
|
+
// webview header. The upstream webview library creates a bare NSWindow with no
|
|
3
|
+
// application menu bar, so a non-bundled webview app has no Quit item and the
|
|
4
|
+
// standard Edit shortcuts (Cmd-C/V/X/A/Z) never reach the WKWebView. This
|
|
5
|
+
// installs the conventional menu so the window behaves like a normal Mac app,
|
|
6
|
+
// the built-in File menu (Start / Disconnect), and the bundle's custom menus
|
|
7
|
+
// whose items emit events into the page.
|
|
8
|
+
#import <Cocoa/Cocoa.h>
|
|
9
|
+
#include <cstdlib>
|
|
10
|
+
#include <cstring>
|
|
11
|
+
|
|
12
|
+
// webview C entry points, also in this dylib — used to drive the live window.
|
|
13
|
+
extern "C" int webview_navigate(void *w, const char *url);
|
|
14
|
+
extern "C" int webview_eval(void *w, const char *js);
|
|
15
|
+
// Marshals a callback onto the UI thread; the only safe way to touch the window
|
|
16
|
+
// from another thread (the launcher runs its control server off the main thread).
|
|
17
|
+
extern "C" int webview_dispatch(void *w, void (*fn)(void *w, void *arg), void *arg);
|
|
18
|
+
|
|
19
|
+
// Exported despite -fvisibility=hidden so belte's FFI layer can resolve it.
|
|
20
|
+
#define BELTE_EXPORT __attribute__((visibility("default")))
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
Process-wide connection flag the menu validation reads. The launcher owns every
|
|
24
|
+
connection transition and flips this via belte_set_connected, so the Server menu
|
|
25
|
+
items enable/disable correctly: menu validation runs each time a menu opens, so
|
|
26
|
+
just storing the bool is enough — no explicit revalidation needed.
|
|
27
|
+
*/
|
|
28
|
+
static int g_belte_connected = 0;
|
|
29
|
+
|
|
30
|
+
// Roles for the built-in Server menu items, parsed from each item's `role` field.
|
|
31
|
+
// `none` is any non-Server navigate item (always enabled).
|
|
32
|
+
typedef enum {
|
|
33
|
+
BelteRoleNone = 0,
|
|
34
|
+
BelteRoleStart,
|
|
35
|
+
BelteRoleDisconnect,
|
|
36
|
+
} BelteRole;
|
|
37
|
+
|
|
38
|
+
// Sets the connection flag the Server menu's validateMenuItem: reads.
|
|
39
|
+
extern "C" BELTE_EXPORT void belte_set_connected(int connected) {
|
|
40
|
+
g_belte_connected = connected;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Runs on the UI thread via webview_dispatch: navigate, then free the copy made
|
|
44
|
+
// below (the original buffer is long gone — the dispatch is asynchronous).
|
|
45
|
+
static void belteDispatchNavigate(void *w, void *arg) {
|
|
46
|
+
char *url = (char *)arg;
|
|
47
|
+
webview_navigate(w, url);
|
|
48
|
+
free(url);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
Navigate the live window to `url` from any thread. The launcher's control server
|
|
53
|
+
runs off the main thread, so it can't call webview_navigate (a Cocoa/UI call)
|
|
54
|
+
directly; this hops onto the UI thread via webview_dispatch. The URL is copied
|
|
55
|
+
synchronously here because the dispatch runs later, after the caller's buffer is
|
|
56
|
+
gone — belteDispatchNavigate frees the copy once it has navigated.
|
|
57
|
+
*/
|
|
58
|
+
extern "C" BELTE_EXPORT void belte_request_navigate(void *w, const char *url) {
|
|
59
|
+
webview_dispatch(w, belteDispatchNavigate, strdup(url));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Maps a JSON `role` string to its enum; unknown/absent → none.
|
|
63
|
+
static BelteRole roleFromString(NSString *role) {
|
|
64
|
+
if ([role isEqualToString:@"start"]) {
|
|
65
|
+
return BelteRoleStart;
|
|
66
|
+
}
|
|
67
|
+
if ([role isEqualToString:@"disconnect"]) {
|
|
68
|
+
return BelteRoleDisconnect;
|
|
69
|
+
}
|
|
70
|
+
return BelteRoleNone;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Builds a JS string literal for `value`, safely escaped via NSJSONSerialization.
|
|
74
|
+
static NSString *jsString(NSString *value) {
|
|
75
|
+
NSData *json = [NSJSONSerialization dataWithJSONObject:value
|
|
76
|
+
options:NSJSONWritingFragmentsAllowed
|
|
77
|
+
error:nil];
|
|
78
|
+
return [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
Action target for a menu item. Owns the webview handle so a click can drive the
|
|
83
|
+
live window: a `navigate` item repoints the window at a URL (the Server menu's
|
|
84
|
+
Start/Disconnect, gated by `role`), a `emit` item dispatches a belte:menu
|
|
85
|
+
event into the page. Lives for the whole process (NSMenuItem does not retain its
|
|
86
|
+
target), matching the never-released menu objects below.
|
|
87
|
+
*/
|
|
88
|
+
@interface BelteMenuAction : NSObject {
|
|
89
|
+
@public
|
|
90
|
+
void *webviewHandle;
|
|
91
|
+
NSString *navigateUrl; // target URL for a navigate item; nil otherwise
|
|
92
|
+
NSString *emitName; // event name for an emit item; nil otherwise
|
|
93
|
+
BelteRole role; // gating role for a Server navigate item; none otherwise
|
|
94
|
+
}
|
|
95
|
+
- (void)navigateTo:(id)sender;
|
|
96
|
+
- (void)emit:(id)sender;
|
|
97
|
+
- (BOOL)validateMenuItem:(NSMenuItem *)item;
|
|
98
|
+
@end
|
|
99
|
+
|
|
100
|
+
@implementation BelteMenuAction
|
|
101
|
+
// Points the live webview at this item's URL (already on the UI thread here).
|
|
102
|
+
- (void)navigateTo:(id)sender {
|
|
103
|
+
if (navigateUrl != nil) {
|
|
104
|
+
webview_navigate(webviewHandle, [navigateUrl UTF8String]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Dispatches a belte:menu CustomEvent (detail `{ name }`) into the page, so the
|
|
109
|
+
// app's own code handles it — including computing args for a parameterised call.
|
|
110
|
+
- (void)emit:(id)sender {
|
|
111
|
+
NSString *js = [NSString
|
|
112
|
+
stringWithFormat:
|
|
113
|
+
@"window.dispatchEvent(new CustomEvent('belte:menu',{detail:{name:%@}}))",
|
|
114
|
+
jsString(emitName)];
|
|
115
|
+
webview_eval(webviewHandle, [js UTF8String]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/*
|
|
119
|
+
Drives the enabled state per connection. Server navigate items follow the truth
|
|
120
|
+
table — Start only when disconnected, Disconnect only when connected — emit items
|
|
121
|
+
only fire into a loaded page (so connected), and plain navigate items (role none)
|
|
122
|
+
are always enabled.
|
|
123
|
+
*/
|
|
124
|
+
- (BOOL)validateMenuItem:(NSMenuItem *)item {
|
|
125
|
+
if (navigateUrl != nil) {
|
|
126
|
+
switch (role) {
|
|
127
|
+
case BelteRoleStart:
|
|
128
|
+
return g_belte_connected == 0;
|
|
129
|
+
case BelteRoleDisconnect:
|
|
130
|
+
return g_belte_connected != 0;
|
|
131
|
+
default:
|
|
132
|
+
return YES;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (emitName != nil) {
|
|
136
|
+
return g_belte_connected != 0;
|
|
137
|
+
}
|
|
138
|
+
return YES;
|
|
139
|
+
}
|
|
140
|
+
@end
|
|
141
|
+
|
|
142
|
+
// Builds one bundle submenu from its JSON description and items, or nil when
|
|
143
|
+
// the description is malformed. Each item is a separator, a `navigate` item
|
|
144
|
+
// (optionally roled), or an `emit` item.
|
|
145
|
+
static NSMenu *buildBundleMenu(NSDictionary *menuDef, void *webview_handle) {
|
|
146
|
+
if (![menuDef isKindOfClass:[NSDictionary class]]) {
|
|
147
|
+
return nil;
|
|
148
|
+
}
|
|
149
|
+
NSMenu *menu = [[NSMenu alloc] initWithTitle:menuDef[@"label"] ?: @""];
|
|
150
|
+
for (NSDictionary *item in menuDef[@"items"]) {
|
|
151
|
+
if ([item[@"separator"] boolValue]) {
|
|
152
|
+
[menu addItem:[NSMenuItem separatorItem]];
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
NSString *navigate = item[@"navigate"];
|
|
156
|
+
NSString *emitName = item[@"emit"];
|
|
157
|
+
BelteMenuAction *target = [[BelteMenuAction alloc] init];
|
|
158
|
+
target->webviewHandle = webview_handle;
|
|
159
|
+
SEL action;
|
|
160
|
+
if ([navigate isKindOfClass:[NSString class]]) {
|
|
161
|
+
target->navigateUrl = [navigate copy];
|
|
162
|
+
target->role = roleFromString(item[@"role"]);
|
|
163
|
+
action = @selector(navigateTo:);
|
|
164
|
+
} else if ([emitName isKindOfClass:[NSString class]]) {
|
|
165
|
+
target->emitName = [emitName copy];
|
|
166
|
+
action = @selector(emit:);
|
|
167
|
+
} else {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
NSMenuItem *menuItem = [menu addItemWithTitle:(item[@"label"] ?: @"")
|
|
171
|
+
action:action
|
|
172
|
+
keyEquivalent:(item[@"shortcut"] ?: @"")];
|
|
173
|
+
[menuItem setTarget:target];
|
|
174
|
+
}
|
|
175
|
+
return menu;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/*
|
|
179
|
+
Builds and installs the macOS main menu on the shared application: App, File (the
|
|
180
|
+
launcher's built-in Start/Disconnect), Edit, the bundle's custom menus, and
|
|
181
|
+
Window.
|
|
182
|
+
|
|
183
|
+
Safe to call after webview_create — which has already created the
|
|
184
|
+
NSApplication — and must run before webview_run so the menu is present when the
|
|
185
|
+
run loop starts. `webview_handle` is the value returned by webview_create,
|
|
186
|
+
captured so the menu can drive it. `config_json` is a JSON object
|
|
187
|
+
`{ "appName": string, "fileMenu"?: { label, items }, "menu"?: [{ label, items }] }`,
|
|
188
|
+
where each item is `{ separator: true }`, `{ label, shortcut?, navigate, role? }`,
|
|
189
|
+
or `{ label, shortcut?, emit }`; pass NULL for the standard menus only.
|
|
190
|
+
|
|
191
|
+
Compiled without ARC (matching the webview header's manual memory model): the
|
|
192
|
+
menu objects and action targets intentionally live for the whole process, so
|
|
193
|
+
the +1 alloc counts are never released.
|
|
194
|
+
*/
|
|
195
|
+
extern "C" BELTE_EXPORT void belte_install_app_menu(void *webview_handle,
|
|
196
|
+
const char *config_json) {
|
|
197
|
+
@autoreleasepool {
|
|
198
|
+
NSDictionary *config = nil;
|
|
199
|
+
if (config_json) {
|
|
200
|
+
NSData *data = [[NSString stringWithUTF8String:config_json]
|
|
201
|
+
dataUsingEncoding:NSUTF8StringEncoding];
|
|
202
|
+
id parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
203
|
+
if ([parsed isKindOfClass:[NSDictionary class]]) {
|
|
204
|
+
config = parsed;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
NSApplication *app = [NSApplication sharedApplication];
|
|
208
|
+
NSString *appName = config[@"appName"] ?: @"App";
|
|
209
|
+
|
|
210
|
+
NSMenu *mainMenu = [[NSMenu alloc] init];
|
|
211
|
+
|
|
212
|
+
// Application menu — the bold first menu. Its title is ignored by macOS
|
|
213
|
+
// (the process/bundle name wins), but its items are what users reach.
|
|
214
|
+
NSMenuItem *appMenuItem = [[NSMenuItem alloc] init];
|
|
215
|
+
[mainMenu addItem:appMenuItem];
|
|
216
|
+
NSMenu *appMenu = [[NSMenu alloc] init];
|
|
217
|
+
[appMenuItem setSubmenu:appMenu];
|
|
218
|
+
|
|
219
|
+
[appMenu addItemWithTitle:[@"About " stringByAppendingString:appName]
|
|
220
|
+
action:@selector(orderFrontStandardAboutPanel:)
|
|
221
|
+
keyEquivalent:@""];
|
|
222
|
+
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
223
|
+
[appMenu addItemWithTitle:[@"Hide " stringByAppendingString:appName]
|
|
224
|
+
action:@selector(hide:)
|
|
225
|
+
keyEquivalent:@"h"];
|
|
226
|
+
NSMenuItem *hideOthers = [appMenu addItemWithTitle:@"Hide Others"
|
|
227
|
+
action:@selector(hideOtherApplications:)
|
|
228
|
+
keyEquivalent:@"h"];
|
|
229
|
+
[hideOthers setKeyEquivalentModifierMask:(NSEventModifierFlagCommand |
|
|
230
|
+
NSEventModifierFlagOption)];
|
|
231
|
+
[appMenu addItemWithTitle:@"Show All"
|
|
232
|
+
action:@selector(unhideAllApplications:)
|
|
233
|
+
keyEquivalent:@""];
|
|
234
|
+
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
235
|
+
[appMenu addItemWithTitle:[@"Quit " stringByAppendingString:appName]
|
|
236
|
+
action:@selector(terminate:)
|
|
237
|
+
keyEquivalent:@"q"];
|
|
238
|
+
|
|
239
|
+
// File menu — the launcher's Start/Disconnect, in the conventional
|
|
240
|
+
// slot right after the App menu and before Edit. Built from the same item
|
|
241
|
+
// shape as the custom menus, so `role` gating applies.
|
|
242
|
+
NSMenu *fileMenu = buildBundleMenu(config[@"fileMenu"], webview_handle);
|
|
243
|
+
if (fileMenu) {
|
|
244
|
+
NSMenuItem *fileMenuItem = [[NSMenuItem alloc] init];
|
|
245
|
+
[mainMenu addItem:fileMenuItem];
|
|
246
|
+
[fileMenuItem setSubmenu:fileMenu];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Edit menu — the actions resolve against the first responder, which is
|
|
250
|
+
// the WKWebView, so Cmd-Z/X/C/V/A operate on the page's text fields.
|
|
251
|
+
NSMenuItem *editMenuItem = [[NSMenuItem alloc] init];
|
|
252
|
+
[mainMenu addItem:editMenuItem];
|
|
253
|
+
NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"];
|
|
254
|
+
[editMenuItem setSubmenu:editMenu];
|
|
255
|
+
[editMenu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"];
|
|
256
|
+
NSMenuItem *redo = [editMenu addItemWithTitle:@"Redo"
|
|
257
|
+
action:@selector(redo:)
|
|
258
|
+
keyEquivalent:@"z"];
|
|
259
|
+
[redo setKeyEquivalentModifierMask:(NSEventModifierFlagCommand |
|
|
260
|
+
NSEventModifierFlagShift)];
|
|
261
|
+
[editMenu addItem:[NSMenuItem separatorItem]];
|
|
262
|
+
[editMenu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"];
|
|
263
|
+
[editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"];
|
|
264
|
+
[editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"];
|
|
265
|
+
[editMenu addItemWithTitle:@"Select All"
|
|
266
|
+
action:@selector(selectAll:)
|
|
267
|
+
keyEquivalent:@"a"];
|
|
268
|
+
|
|
269
|
+
// Bundle's menus, inserted between Edit and Window — the launcher passes
|
|
270
|
+
// its built-in Server menu first, followed by the app's custom menus.
|
|
271
|
+
for (NSDictionary *menuDef in config[@"menu"]) {
|
|
272
|
+
NSMenu *bundleMenu = buildBundleMenu(menuDef, webview_handle);
|
|
273
|
+
if (bundleMenu) {
|
|
274
|
+
NSMenuItem *bundleMenuItem = [[NSMenuItem alloc] init];
|
|
275
|
+
[mainMenu addItem:bundleMenuItem];
|
|
276
|
+
[bundleMenuItem setSubmenu:bundleMenu];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Window menu — Minimize/Zoom/Close, registered so macOS tracks the
|
|
281
|
+
// app's windows in it automatically.
|
|
282
|
+
NSMenuItem *windowMenuItem = [[NSMenuItem alloc] init];
|
|
283
|
+
[mainMenu addItem:windowMenuItem];
|
|
284
|
+
NSMenu *windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
|
|
285
|
+
[windowMenuItem setSubmenu:windowMenu];
|
|
286
|
+
[windowMenu addItemWithTitle:@"Minimize"
|
|
287
|
+
action:@selector(performMiniaturize:)
|
|
288
|
+
keyEquivalent:@"m"];
|
|
289
|
+
[windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
|
|
290
|
+
[windowMenu addItem:[NSMenuItem separatorItem]];
|
|
291
|
+
[windowMenu addItemWithTitle:@"Close"
|
|
292
|
+
action:@selector(performClose:)
|
|
293
|
+
keyEquivalent:@"w"];
|
|
294
|
+
[app setWindowsMenu:windowMenu];
|
|
295
|
+
|
|
296
|
+
[app setMainMenu:mainMenu];
|
|
297
|
+
}
|
|
298
|
+
}
|