summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--res/MacOSXBundleInfo.plist.in15
-rw-r--r--res/about/help.gmi30
-rw-r--r--src/app.c57
-rw-r--r--src/app.h1
-rw-r--r--src/gmrequest.c3
-rw-r--r--src/macos.m53
-rw-r--r--src/main.c2
-rw-r--r--src/ui/window.c7
8 files changed, 140 insertions, 28 deletions
diff --git a/res/MacOSXBundleInfo.plist.in b/res/MacOSXBundleInfo.plist.in
index 07a596aa..4dc685bd 100644
--- a/res/MacOSXBundleInfo.plist.in
+++ b/res/MacOSXBundleInfo.plist.in
@@ -38,8 +38,8 @@
38 <key>CFBundleTypeExtensions</key> 38 <key>CFBundleTypeExtensions</key>
39 <array> 39 <array>
40 <string>gmi</string> 40 <string>gmi</string>
41 <string>gemini</string> 41 <string>gemini</string>
42 </array> 42 </array>
43 <key>CFBundleTypeIconFile</key> 43 <key>CFBundleTypeIconFile</key>
44 <string>text-gemini.icns</string> 44 <string>text-gemini.icns</string>
45 <key>CFBundleTypeName</key> 45 <key>CFBundleTypeName</key>
@@ -52,5 +52,16 @@
52 <false/> 52 <false/>
53 </dict> 53 </dict>
54 </array> 54 </array>
55 <key>CFBundleURLTypes</key>
56 <array>
57 <dict>
58 <key>CFBundleURLName</key>
59 <string>Gemini</string>
60 <key>CFBundleURLSchemes</key>
61 <array>
62 <string>gemini</string>
63 </array>
64 </dict>
65 </array>
55</dict> 66</dict>
56</plist> 67</plist>
diff --git a/res/about/help.gmi b/res/about/help.gmi
index c8750140..1be94132 100644
--- a/res/about/help.gmi
+++ b/res/about/help.gmi
@@ -17,23 +17,22 @@ Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a
17 17
18### Features 18### Features
19 19
20* UI inspired by Safari on macOS
21* Light and dark UI themes
22* Multiple tabs
23* Sidebar for page outline, managing bookmarks and identities, and viewing history
24* Smart suggestions when typing the URL — search bookmarks, history, identities
25* Scaling factor for the UI (for arbitrary monitor DPI)
26* Beautiful typography using Unicode fonts 20* Beautiful typography using Unicode fonts
27* Autogenerated page style and Unicode icon for each Gemini domain 21* Autogenerated page style and Unicode icon for each Gemini domain
22* Smart suggestions when typing the URL — search bookmarks, history, identities
23* Sidebar for page outline, managing bookmarks and identities, and viewing history
24* Multiple tabs
25* Identity management — create and use TLS client certificates
26* Light and dark UI themes
27* Select and copy text with the mouse
28* Find text on the page
28* Open image links inline on the same page 29* Open image links inline on the same page
30* Open links via keyboard shortcuts
29* Instant back/forward navigation 31* Instant back/forward navigation
30* Smooth scrolling 32* Smooth scrolling
31* Scaling page content (50%...200%) 33* Scaling page content (50%...200%)
32* Select and copy text with the mouse 34* Scaling factor for the UI (for arbitrary monitor DPI)
33* Find text on the page
34* Identity management — create and use TLS client certificates
35* Persistent app state — tabs and history are restored on next run 35* Persistent app state — tabs and history are restored on next run
36* Opening links via keyboard shortcuts
37 36
38## What is Gemini 37## What is Gemini
39 38
@@ -54,16 +53,21 @@ One way to browse Gemini content is via web browser extensions or proxies that t
54 53
55# User interface 54# User interface
56 55
57The user interface has been designed to have a feel similar to Safari on macOS.
58
59## URL entry and quick search 56## URL entry and quick search
60 57
61The URL input field is in its typical location in the navigation bar. It can be accessed quickly by pressing ${CTRL+}L. 58The URL input field is in its typical location in the navigation bar. It can be accessed quickly by pressing ${CTRL+}L.
62 59
63As you enter text, Lagrange starts looking for matches in bookmarks, history, content of cached pages, and identities. Search terms are case insensitive, and if many words are entered, they are all required to appear in the specified order in any matched content. Search of cached pages is limited to the (small-ish) set of pages that Lagrange keeps in memory for back navigation. 60As you enter text, Lagrange starts looking for matches in bookmarks, history, content of cached pages, and identities. Search terms are case insensitive, and if many words are entered, they are all required to appear in the specified order in any matched content. Search of cached pages is limited to the (small) set of pages that Lagrange keeps in memory for back navigation.
64 61
65## Sidebar 62## Sidebar
66 63
64The sidebar can be toggled via menus or by pressing ${SHIFT+}${CTRL+}L. It has four tabs:
65
66* Bookmarks: List of bookmarks that you've created. These appear first in search results for quick and easy access.
67* History: Chronological list of visited URLs. This is not a full history of all the URLs you've accessed over time — only unique URLs are shown at the latest access time.
68* Identities: TLS client certificates.
69* Outline: List of the headings in the currently open tab. Useful when reading longer documents.
70
67## Navigation 71## Navigation
68 72
69### Opening links using the keyboard 73### Opening links using the keyboard
diff --git a/src/app.c b/src/app.c
index 0144c6d7..03e9b036 100644
--- a/src/app.c
+++ b/src/app.c
@@ -88,6 +88,9 @@ struct Impl_App {
88 iBool running; 88 iBool running;
89 iBool pendingRefresh; 89 iBool pendingRefresh;
90 int tabEnum; 90 int tabEnum;
91 iStringList *launchCommands;
92 iBool isFinishedLaunching;
93 iTime lastDropTime; /* for detecting drops of multiple items */
91 /* Preferences: */ 94 /* Preferences: */
92 iBool commandEcho; /* --echo */ 95 iBool commandEcho; /* --echo */
93 iBool retainWindowSize; 96 iBool retainWindowSize;
@@ -283,6 +286,9 @@ static void saveState_App_(const iApp *d) {
283} 286}
284 287
285static void init_App_(iApp *d, int argc, char **argv) { 288static void init_App_(iApp *d, int argc, char **argv) {
289 d->isFinishedLaunching = iFalse;
290 d->launchCommands = new_StringList();
291 iZap(d->lastDropTime);
286 init_CommandLine(&d->args, argc, argv); 292 init_CommandLine(&d->args, argc, argv);
287 init_SortedArray(&d->tickers, sizeof(iTicker), cmp_Ticker_); 293 init_SortedArray(&d->tickers, sizeof(iTicker), cmp_Ticker_);
288 d->lastTickerTime = SDL_GetTicks(); 294 d->lastTickerTime = SDL_GetTicks();
@@ -304,6 +310,9 @@ static void init_App_(iApp *d, int argc, char **argv) {
304 init_String(&d->gopherProxy); 310 init_String(&d->gopherProxy);
305 init_String(&d->httpProxy); 311 init_String(&d->httpProxy);
306 setThemePalette_Color(d->theme); 312 setThemePalette_Color(d->theme);
313#if defined (iPlatformApple)
314 setupApplication_MacOS();
315#endif
307 loadPrefs_App_(d); 316 loadPrefs_App_(d);
308 load_Visited(d->visited, dataDir_App_); 317 load_Visited(d->visited, dataDir_App_);
309 load_Bookmarks(d->bookmarks, dataDir_App_); 318 load_Bookmarks(d->bookmarks, dataDir_App_);
@@ -324,6 +333,12 @@ static void init_App_(iApp *d, int argc, char **argv) {
324 postCommand_App("navigate.home"); 333 postCommand_App("navigate.home");
325 } 334 }
326 postCommand_App("window.unfreeze"); 335 postCommand_App("window.unfreeze");
336 d->isFinishedLaunching = iTrue;
337 /* Run any commands that were pending completion of launch. */ {
338 iForEach(StringList, i, d->launchCommands) {
339 postCommandString_App(i.value);
340 }
341 }
327} 342}
328 343
329static void deinit_App(iApp *d) { 344static void deinit_App(iApp *d) {
@@ -340,6 +355,7 @@ static void deinit_App(iApp *d) {
340 delete_Window(d->window); 355 delete_Window(d->window);
341 d->window = NULL; 356 d->window = NULL;
342 deinit_CommandLine(&d->args); 357 deinit_CommandLine(&d->args);
358 iRelease(d->launchCommands);
343} 359}
344 360
345const iString *execPath_App(void) { 361const iString *execPath_App(void) {
@@ -350,6 +366,21 @@ const iString *dataDir_App(void) {
350 return collect_String(cleanedCStr_Path(dataDir_App_)); 366 return collect_String(cleanedCStr_Path(dataDir_App_));
351} 367}
352 368
369const iString *debugInfo_App(void) {
370 iApp *d = &app_;
371 iString *msg = collectNew_String();
372 format_String(msg, "# Debug information\n");
373 appendFormat_String(msg, "## Launch arguments\n");
374 iConstForEach(StringList, i, args_CommandLine(&d->args)) {
375 appendFormat_String(msg, "* %zu: %s\n", i.pos, cstr_String(i.value));
376 }
377 appendFormat_String(msg, "## Launch commands\n");
378 iConstForEach(StringList, j, d->launchCommands) {
379 appendFormat_String(msg, "%s\n", cstr_String(j.value));
380 }
381 return msg;
382}
383
353iLocalDef iBool isWaitingAllowed_App_(const iApp *d) { 384iLocalDef iBool isWaitingAllowed_App_(const iApp *d) {
354 return !d->pendingRefresh && isEmpty_SortedArray(&d->tickers); 385 return !d->pendingRefresh && isEmpty_SortedArray(&d->tickers);
355} 386}
@@ -365,9 +396,22 @@ void processEvents_App(enum iAppEventMode eventMode) {
365 case SDL_QUIT: 396 case SDL_QUIT:
366 d->running = iFalse; 397 d->running = iFalse;
367 goto backToMainLoop; 398 goto backToMainLoop;
368 case SDL_DROPFILE: 399 case SDL_DROPFILE: {
369 postCommandf_App("open url:file://%s", ev.drop.file); 400 iBool newTab = iFalse;
401 if (elapsedSeconds_Time(&d->lastDropTime) < 0.1) {
402 /* Each additional drop gets a new tab. */
403 newTab = iTrue;
404 }
405 d->lastDropTime = now_Time();
406 if (startsWithCase_CStr(ev.drop.file, "gemini:") ||
407 startsWithCase_CStr(ev.drop.file, "file:")) {
408 postCommandf_App("~open newtab:%d url:%s", newTab, ev.drop.file);
409 }
410 else {
411 postCommandf_App("~open newtab:%d url:file://%s", newTab, ev.drop.file);
412 }
370 break; 413 break;
414 }
371 default: { 415 default: {
372 iBool wasUsed = processEvent_Window(d->window, &ev); 416 iBool wasUsed = processEvent_Window(d->window, &ev);
373 if (ev.type == SDL_USEREVENT && ev.user.code == command_UserEventCode) { 417 if (ev.type == SDL_USEREVENT && ev.user.code == command_UserEventCode) {
@@ -488,12 +532,21 @@ void postRefresh_App(void) {
488} 532}
489 533
490void postCommand_App(const char *command) { 534void postCommand_App(const char *command) {
535 iApp *d = &app_;
491 iAssert(command); 536 iAssert(command);
492 SDL_Event ev; 537 SDL_Event ev;
493 if (*command == '!') { 538 if (*command == '!') {
494 /* Global command; this is global context so just ignore. */ 539 /* Global command; this is global context so just ignore. */
495 command++; 540 command++;
496 } 541 }
542 if (*command == '~') {
543 /* Requires launch to be finished; defer it if needed. */
544 command++;
545 if (!d->isFinishedLaunching) {
546 pushBackCStr_StringList(d->launchCommands, command);
547 return;
548 }
549 }
497 ev.user.type = SDL_USEREVENT; 550 ev.user.type = SDL_USEREVENT;
498 ev.user.code = command_UserEventCode; 551 ev.user.code = command_UserEventCode;
499 ev.user.windowID = get_Window() ? SDL_GetWindowID(get_Window()->win) : 0; 552 ev.user.windowID = get_Window() ? SDL_GetWindowID(get_Window()->win) : 0;
diff --git a/src/app.h b/src/app.h
index da86c37e..69b8d274 100644
--- a/src/app.h
+++ b/src/app.h
@@ -48,6 +48,7 @@ enum iUserEventCode {
48 48
49const iString *execPath_App (void); 49const iString *execPath_App (void);
50const iString *dataDir_App (void); 50const iString *dataDir_App (void);
51const iString *debugInfo_App (void);
51 52
52int run_App (int argc, char **argv); 53int run_App (int argc, char **argv);
53void processEvents_App (enum iAppEventMode mode); 54void processEvents_App (enum iAppEventMode mode);
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 137e8303..843d2f46 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -308,6 +308,9 @@ static const iBlock *aboutPageSource_(iRangecc path) {
308 if (equalCase_Rangecc(path, "version")) { 308 if (equalCase_Rangecc(path, "version")) {
309 return &blobVersion_Embedded; 309 return &blobVersion_Embedded;
310 } 310 }
311 if (equalCase_Rangecc(path, "debug")) {
312 return utf8_String(debugInfo_App());
313 }
311 return src; 314 return src;
312} 315}
313 316
diff --git a/src/macos.m b/src/macos.m
index d44be7d0..bbbaaa19 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -121,8 +121,9 @@ enum iTouchBarVariant {
121} 121}
122- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl; 122- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl;
123//- (NSTouchBar *)makeTouchBar; 123//- (NSTouchBar *)makeTouchBar;
124/* SDL needs to do its own thing. */ 124- (BOOL)application:(NSApplication *)app openFile:(NSString *)filename;
125- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename; 125- (void)application:(NSApplication *)app openFiles:(NSArray<NSString *> *)filenames;
126- (void)application:(NSApplication *)app openURLs:(NSArray<NSURL *> *)urls;
126- (void)applicationDidFinishLaunching:(NSNotification *)notifications; 127- (void)applicationDidFinishLaunching:(NSNotification *)notifications;
127@end 128@end
128 129
@@ -151,7 +152,7 @@ enum iTouchBarVariant {
151static void appearanceChanged_MacOS_(NSString *name) { 152static void appearanceChanged_MacOS_(NSString *name) {
152 const iBool isDark = [name containsString:@"Dark"]; 153 const iBool isDark = [name containsString:@"Dark"];
153 const iBool isHighContrast = [name containsString:@"HighContrast"]; 154 const iBool isHighContrast = [name containsString:@"HighContrast"];
154 postCommandf_App("os.theme.changed dark:%d contrast:%d", isDark ? 1 : 0, isHighContrast ? 1 : 0); 155 postCommandf_App("~os.theme.changed dark:%d contrast:%d", isDark ? 1 : 0, isHighContrast ? 1 : 0);
155// printf("Effective appearance changed: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); 156// printf("Effective appearance changed: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]);
156// fflush(stdout); 157// fflush(stdout);
157} 158}
@@ -170,8 +171,23 @@ static void appearanceChanged_MacOS_(NSString *name) {
170 [menuCommands setObject:command forKey:[menuItem title]]; 171 [menuCommands setObject:command forKey:[menuItem title]];
171} 172}
172 173
173- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { 174- (BOOL)application:(NSApplication *)app openFile:(NSString *)filename {
174 return [sdlDelegate application:theApplication openFile:filename]; 175 return [sdlDelegate application:app openFile:filename];
176}
177
178- (void)application:(NSApplication *)app openFiles:(NSArray<NSString *> *)filenames {
179 /* TODO: According to AppKit docs, this method won't be called when openURLs is defined. */
180 for (NSString *fn in filenames) {
181 NSLog(@"openFiles: %@", fn);
182 [self application:app openFile:fn];
183 }
184}
185
186- (void)application:(NSApplication *)app openURLs:(NSArray<NSURL *> *)urls {
187 for (NSURL *url in urls) {
188 NSLog(@"openURLs: %@", [url absoluteString]);
189 [sdlDelegate application:app openFile:[url absoluteString]];
190 }
175} 191}
176 192
177- (void)applicationDidFinishLaunching:(NSNotification *)notification { 193- (void)applicationDidFinishLaunching:(NSNotification *)notification {
@@ -397,9 +413,30 @@ void enableMomentumScroll_MacOS(void) {
397 forKey: @"AppleMomentumScrollSupported"]; 413 forKey: @"AppleMomentumScrollSupported"];
398} 414}
399 415
416@interface UrlHandler : NSObject
417- (void)handleURLEvent:(NSAppleEventDescriptor*)event
418 withReplyEvent:(NSAppleEventDescriptor*)replyEvent;
419@end
420
421@implementation UrlHandler
422- (void)handleURLEvent:(NSAppleEventDescriptor*)event
423 withReplyEvent:(NSAppleEventDescriptor*)replyEvent {
424 NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
425 postCommandf_App("~open url:%s", [url cStringUsingEncoding:NSUTF8StringEncoding]);
426}
427@end
428
429void registerURLHandler_MacOS(void) {
430 UrlHandler *handler = [[UrlHandler alloc] init];
431 [[NSAppleEventManager sharedAppleEventManager]
432 setEventHandler:handler
433 andSelector:@selector(handleURLEvent:withReplyEvent:)
434 forEventClass:kInternetEventClass
435 andEventID:kAEGetURL];
436}
437
400void setupApplication_MacOS(void) { 438void setupApplication_MacOS(void) {
401 NSApplication *app = [NSApplication sharedApplication]; 439 NSApplication *app = [NSApplication sharedApplication];
402 //appearanceChanged_MacOS_([[app effectiveAppearance] name]);
403 /* Our delegate will override SDL's delegate. */ 440 /* Our delegate will override SDL's delegate. */
404 MyDelegate *myDel = [[MyDelegate alloc] initWithSDLDelegate:app.delegate]; 441 MyDelegate *myDel = [[MyDelegate alloc] initWithSDLDelegate:app.delegate];
405 [myDel setAppearance:[[app effectiveAppearance] name]]; 442 [myDel setAppearance:[[app effectiveAppearance] name]];
@@ -502,3 +539,7 @@ void handleCommand_MacOS(const char *cmd) {
502 } 539 }
503#endif 540#endif
504} 541}
542
543void log_MacOS(const char *msg) {
544 NSLog(@"%s", msg);
545}
diff --git a/src/main.c b/src/main.c
index c13b75d1..247c4b40 100644
--- a/src/main.c
+++ b/src/main.c
@@ -31,11 +31,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
31 31
32#if defined (iPlatformApple) 32#if defined (iPlatformApple)
33extern void enableMomentumScroll_MacOS(void); 33extern void enableMomentumScroll_MacOS(void);
34extern void registerURLHandler_MacOS(void);
34#endif 35#endif
35 36
36int main(int argc, char **argv) { 37int main(int argc, char **argv) {
37#if defined (iPlatformApple) 38#if defined (iPlatformApple)
38 enableMomentumScroll_MacOS(); 39 enableMomentumScroll_MacOS();
40 registerURLHandler_MacOS();
39#endif 41#endif
40#if defined (iPlatformMsys) 42#if defined (iPlatformMsys)
41 /* MSYS runtime takes care of WinMain. */ 43 /* MSYS runtime takes care of WinMain. */
diff --git a/src/ui/window.c b/src/ui/window.c
index 87ca2614..0a63a941 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -138,8 +138,8 @@ static const iMenuItem viewMenuItems[] = {
138 { "Show Page Outline", '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1" }, 138 { "Show Page Outline", '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1" },
139 { "Toggle Sidebar", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, 139 { "Toggle Sidebar", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" },
140 { "---", 0, 0, NULL }, 140 { "---", 0, 0, NULL },
141 { "Go Back", navigateBack_KeyShortcut, "navigate.back" }, 141 { "Go Back", SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" },
142 { "Go Forward", navigateForward_KeyShortcut, "navigate.forward" }, 142 { "Go Forward", SDLK_RIGHTBRACKET, KMOD_PRIMARY, "navigate.forward" },
143 { "Reload Page", reload_KeyShortcut, "navigate.reload" }, 143 { "Reload Page", reload_KeyShortcut, "navigate.reload" },
144 { "---", 0, 0, NULL }, 144 { "---", 0, 0, NULL },
145 { "Zoom In", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" }, 145 { "Zoom In", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" },
@@ -546,9 +546,6 @@ void init_Window(iWindow *d, iRect rect) {
546 d->presentTime = 0.0; 546 d->presentTime = 0.0;
547 setId_Widget(d->root, "root"); 547 setId_Widget(d->root, "root");
548 init_Text(d->render); 548 init_Text(d->render);
549#if defined (iPlatformApple) && !defined (iPlatformIOS)
550 setupApplication_MacOS();
551#endif
552 setupUserInterface_Window(d); 549 setupUserInterface_Window(d);
553 updateRootSize_Window_(d); 550 updateRootSize_Window_(d);
554} 551}