summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2020-07-24 13:19:38 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2020-07-24 13:19:38 +0300
commit0f0b4250ca460d58edb61cc0dd509ba1980c3272 (patch)
treee8e9be30b2ec7942c4796cf93cbbd42e0c272e6a
parentdd73346aad190239ed133d0c4835dbaa05d1b0cd (diff)
Added GmRequest for handling the request
This feels a little bit too complex, with GmRequest observing TlsRequest and then notifying its own audience. There are still some issues with cancelling requests as well.
-rw-r--r--CMakeLists.txt8
-rw-r--r--src/gemini.h2
-rw-r--r--src/gmdocument.c4
-rw-r--r--src/gmdocument.h2
-rw-r--r--src/gmrequest.c240
-rw-r--r--src/gmrequest.h23
-rw-r--r--src/gmutil.c25
-rw-r--r--src/gmutil.h16
-rw-r--r--src/ui/documentwidget.c295
-rw-r--r--src/ui/window.c4
10 files changed, 491 insertions, 128 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 280dcdf8..be56af23 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -33,8 +33,12 @@ set (SOURCES
33 src/app.c 33 src/app.c
34 src/app.h 34 src/app.h
35 src/gemini.h 35 src/gemini.h
36 src/gmdocument.h
37 src/gmdocument.c 36 src/gmdocument.c
37 src/gmdocument.h
38 src/gmrequest.c
39 src/gmrequest.h
40 src/gmutil.c
41 src/gmutil.h
38 src/stb_image.h 42 src/stb_image.h
39 src/stb_truetype.h 43 src/stb_truetype.h
40 # User interface: 44 # User interface:
@@ -42,8 +46,8 @@ set (SOURCES
42 src/ui/color.h 46 src/ui/color.h
43 src/ui/command.c 47 src/ui/command.c
44 src/ui/command.h 48 src/ui/command.h
45 src/ui/documentwidget.h
46 src/ui/documentwidget.c 49 src/ui/documentwidget.c
50 src/ui/documentwidget.h
47 src/ui/metrics.c 51 src/ui/metrics.c
48 src/ui/metrics.h 52 src/ui/metrics.h
49 src/ui/paint.c 53 src/ui/paint.c
diff --git a/src/gemini.h b/src/gemini.h
index dd281e20..3c00d425 100644
--- a/src/gemini.h
+++ b/src/gemini.h
@@ -2,6 +2,8 @@
2 2
3/* Response status codes. */ 3/* Response status codes. */
4enum iGmStatusCode { 4enum iGmStatusCode {
5 failedToOpenFile_GmStatusCode = -2,
6 invalidHeader_GmStatusCode = -1,
5 none_GmStatusCode = 0, 7 none_GmStatusCode = 0,
6 input_GmStatusCode = 10, 8 input_GmStatusCode = 10,
7 sensitiveInput_GmStatusCode = 11, 9 sensitiveInput_GmStatusCode = 11,
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 3dd2d08f..8355a0eb 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -133,11 +133,11 @@ static void clearLinks_GmDocument_(iGmDocument *d) {
133} 133}
134 134
135static void doLayout_GmDocument_(iGmDocument *d) { 135static void doLayout_GmDocument_(iGmDocument *d) {
136 clear_Array(&d->layout);
137 clearLinks_GmDocument_(d);
136 if (d->size.x <= 0 || isEmpty_String(&d->source)) { 138 if (d->size.x <= 0 || isEmpty_String(&d->source)) {
137 return; 139 return;
138 } 140 }
139 clear_Array(&d->layout);
140 clearLinks_GmDocument_(d);
141 iBool isPreformat = iFalse; 141 iBool isPreformat = iFalse;
142 iInt2 pos = zero_I2(); 142 iInt2 pos = zero_I2();
143 const iRangecc content = range_String(&d->source); 143 const iRangecc content = range_String(&d->source);
diff --git a/src/gmdocument.h b/src/gmdocument.h
index b5102044..959c75ea 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -17,9 +17,7 @@ struct Impl_GmRun {
17 iGmLinkId linkId; /* zero for non-links */ 17 iGmLinkId linkId; /* zero for non-links */
18}; 18};
19 19
20iDeclareType(GmDocument)
21iDeclareClass(GmDocument) 20iDeclareClass(GmDocument)
22
23iDeclareObjectConstruction(GmDocument) 21iDeclareObjectConstruction(GmDocument)
24 22
25void setWidth_GmDocument (iGmDocument *, int width); 23void setWidth_GmDocument (iGmDocument *, int width);
diff --git a/src/gmrequest.c b/src/gmrequest.c
new file mode 100644
index 00000000..2bda65e9
--- /dev/null
+++ b/src/gmrequest.c
@@ -0,0 +1,240 @@
1#include "gmrequest.h"
2#include "gmutil.h"
3
4#include <the_Foundation/file.h>
5#include <the_Foundation/tlsrequest.h>
6#include <the_Foundation/mutex.h>
7
8#include <SDL_timer.h>
9
10static const int BODY_TIMEOUT = 1500; /* ms */
11
12enum iGmRequestState {
13 initialized_GmRequestState,
14 receivingHeader_GmRequestState,
15 receivingBody_GmRequestState,
16 finished_GmRequestState
17};
18
19struct Impl_GmRequest {
20 iObject object;
21 iMutex mutex;
22 enum iGmRequestState state;
23 iString url;
24 iTlsRequest *req;
25 enum iGmStatusCode code;
26 iString header;
27 iBlock body; /* rest of the received data */
28 uint32_t timeoutId; /* in case server doesn't close the connection */
29 iAudience *updated;
30 iAudience *finished;
31};
32
33iDefineObjectConstruction(GmRequest)
34iDefineAudienceGetter(GmRequest, updated)
35iDefineAudienceGetter(GmRequest, finished)
36
37void init_GmRequest(iGmRequest *d) {
38 init_Mutex(&d->mutex);
39 d->state = initialized_GmRequestState;
40 init_String(&d->url);
41 d->req = NULL;
42 d->code = none_GmStatusCode;
43 init_String(&d->header);
44 init_Block(&d->body, 0);
45 d->timeoutId = 0;
46 d->updated = NULL;
47 d->finished = NULL;
48}
49
50void deinit_GmRequest(iGmRequest *d) {
51 lock_Mutex(&d->mutex);
52 if (d->timeoutId) {
53 SDL_RemoveTimer(d->timeoutId);
54 }
55 if (d->req) {
56 if (!isFinished_GmRequest(d)) {
57 iDisconnectObject(TlsRequest, d->req, readyRead, d);
58 iDisconnectObject(TlsRequest, d->req, finished, d);
59 cancel_TlsRequest(d->req);
60 d->state = finished_GmRequestState;
61 iRelease(d->req);
62 d->req = NULL;
63 }
64 }
65 unlock_Mutex(&d->mutex);
66 delete_Audience(d->finished);
67 delete_Audience(d->updated);
68 deinit_Block(&d->body);
69 deinit_String(&d->header);
70 deinit_String(&d->url);
71 deinit_Mutex(&d->mutex);
72}
73
74void setUrl_GmRequest(iGmRequest *d, const iString *url) {
75 set_String(&d->url, url);
76}
77
78static uint32_t timedOutWhileReceivingBody_GmRequest_(uint32_t interval, void *obj) {
79 iGmRequest *d = obj;
80 iGuardMutex(&d->mutex, cancel_TlsRequest(d->req));
81 iUnused(interval);
82 return 0;
83}
84
85static void restartTimeout_GmRequest_(iGmRequest *d) {
86 /* Note: `d` is currently locked. */
87 if (d->timeoutId) {
88 SDL_RemoveTimer(d->timeoutId);
89 }
90 d->timeoutId = SDL_AddTimer(BODY_TIMEOUT, timedOutWhileReceivingBody_GmRequest_, d);
91}
92
93static void readIncoming_GmRequest_(iAnyObject *obj) {
94 iGmRequest *d = (iGmRequest *) obj;
95 iBool notifyUpdate = iFalse;
96 iBool notifyDone = iFalse;
97 lock_Mutex(&d->mutex);
98 iAssert(d->state != finished_GmRequestState); /* notifications out of order? */
99 iBlock *data = readAll_TlsRequest(d->req);
100 fflush(stdout);
101 if (d->state == receivingHeader_GmRequestState) {
102 appendCStrN_String(&d->header, constData_Block(data), size_Block(data));
103 /* Check if the header line is complete. */
104 size_t endPos = indexOfCStr_String(&d->header, "\r\n");
105 if (endPos != iInvalidPos) {
106 /* Move remainder to the body. */
107 setData_Block(&d->body,
108 constBegin_String(&d->header) + endPos + 2,
109 size_String(&d->header) - endPos - 2);
110 remove_Block(&d->header.chars, endPos, iInvalidSize);
111 /* parse and remove the code */
112 if (size_String(&d->header) < 3) {
113 clear_String(&d->header);
114 d->code = invalidHeader_GmStatusCode;
115 d->state = finished_GmRequestState;
116 notifyDone = iTrue;
117 }
118 const int code = toInt_String(&d->header);
119 if (code == 0 || cstr_String(&d->header)[2] != ' ') {
120 clear_String(&d->header);
121 d->code = invalidHeader_GmStatusCode;
122 d->state = finished_GmRequestState;
123 notifyDone = iTrue;
124 }
125 remove_Block(&d->header.chars, 0, 3); /* just the meta */
126 d->code = code;
127 d->state = receivingBody_GmRequestState;
128 notifyUpdate = iTrue;
129 /* Start a timeout for the remainder of the response, in case the connection
130 remains open. */
131 restartTimeout_GmRequest_(d);
132 }
133 }
134 else if (d->state == receivingBody_GmRequestState) {
135 append_Block(&d->body, data);
136 restartTimeout_GmRequest_(d);
137 notifyUpdate = iTrue;
138 }
139 delete_Block(data);
140 unlock_Mutex(&d->mutex);
141 if (notifyUpdate) {
142 iNotifyAudience(d, updated, GmRequestUpdated);
143 }
144 if (notifyDone) {
145 iNotifyAudience(d, finished, GmRequestFinished);
146 }
147}
148
149static void requestFinished_GmRequest_(iAnyObject *obj) {
150 iGmRequest *d = (iGmRequest *) obj;
151 lock_Mutex(&d->mutex);
152 /* There shouldn't be anything left to read. */ {
153 iBlock *data = readAll_TlsRequest(d->req);
154 iAssert(isEmpty_Block(data));
155 delete_Block(data);
156 }
157 SDL_RemoveTimer(d->timeoutId);
158 d->timeoutId = 0;
159 iReleaseLater(d->req);
160 d->req = NULL;
161 d->state = finished_GmRequestState;
162 unlock_Mutex(&d->mutex);
163 iNotifyAudience(d, finished, GmRequestFinished);
164}
165
166void submit_GmRequest(iGmRequest *d) {
167 iAssert(d->state == initialized_GmRequestState);
168 if (d->state != initialized_GmRequestState) {
169 return;
170 }
171 d->code = none_GmStatusCode;
172 clear_String(&d->header);
173 clear_Block(&d->body);
174 iUrl url;
175 init_Url(&url, &d->url);
176 if (!cmpCStrSc_Rangecc(&url.protocol, "file", &iCaseInsensitive)) {
177 iFile *f = new_File(collect_String(newRange_String(url.path)));
178 if (open_File(f, readOnly_FileMode)) {
179 /* TODO: Check supported file types: images, audio */
180 d->code = success_GmStatusCode;
181 setCStr_String(&d->header, "text/gemini; charset=utf-8");
182 set_Block(&d->body, collect_Block(readAll_File(f)));
183 iNotifyAudience(d, updated, GmRequestUpdated);
184 }
185 else {
186 d->code = failedToOpenFile_GmStatusCode;
187 }
188 iRelease(f);
189 d->state = finished_GmRequestState;
190 iNotifyAudience(d, finished, GmRequestFinished);
191 return;
192 }
193 d->state = receivingHeader_GmRequestState;
194 d->req = new_TlsRequest();
195 iConnect(TlsRequest, d->req, readyRead, d, readIncoming_GmRequest_);
196 iConnect(TlsRequest, d->req, finished, d, requestFinished_GmRequest_);
197 uint16_t port = toInt_String(collect_String(newRange_String(url.port)));
198 if (port == 0) {
199 port = 1965; /* default Gemini port */
200 }
201 setUrl_TlsRequest(d->req, collect_String(newRange_String(url.host)), port);
202 setContent_TlsRequest(d->req,
203 utf8_String(collectNewFormat_String("%s\r\n", cstr_String(&d->url))));
204 submit_TlsRequest(d->req);
205}
206
207iBool isFinished_GmRequest(const iGmRequest *d) {
208 iBool done;
209 iGuardMutex(&d->mutex, done = (d->state == finished_GmRequestState));
210 return done;
211}
212
213const char *error_GmRequest(const iGmRequest *d) {
214 if (d->code == failedToOpenFile_GmStatusCode) {
215 return "Failed to open file";
216 }
217 if (d->code == invalidHeader_GmStatusCode) {
218 return "Received invalid header (not Gemini?)";
219 }
220 return NULL; /* TDOO: detailed error string */
221}
222
223enum iGmStatusCode status_GmRequest(const iGmRequest *d) {
224 return d->code;
225}
226
227const iString *meta_GmRequest(const iGmRequest *d) {
228 if (d->state >= receivingBody_GmRequestState) {
229 return &d->header;
230 }
231 return collectNew_String();
232}
233
234const iBlock *body_GmRequest(const iGmRequest *d) {
235 iBlock *body;
236 iGuardMutex(&d->mutex, body = collect_Block(copy_Block(&d->body)));
237 return body;
238}
239
240iDefineClass(GmRequest)
diff --git a/src/gmrequest.h b/src/gmrequest.h
new file mode 100644
index 00000000..e7d5916b
--- /dev/null
+++ b/src/gmrequest.h
@@ -0,0 +1,23 @@
1#pragma once
2
3#include <the_Foundation/audience.h>
4#include <the_Foundation/object.h>
5
6#include "gemini.h"
7
8iDeclareClass(GmRequest)
9iDeclareObjectConstruction(GmRequest)
10
11iDeclareNotifyFunc(GmRequest, Updated)
12iDeclareNotifyFunc(GmRequest, Finished)
13iDeclareAudienceGetter(GmRequest, updated)
14iDeclareAudienceGetter(GmRequest, finished)
15
16void setUrl_GmRequest (iGmRequest *, const iString *url);
17void submit_GmRequest (iGmRequest *);
18
19iBool isFinished_GmRequest (const iGmRequest *);
20const char * error_GmRequest (const iGmRequest *); /* NULL if successful */
21enum iGmStatusCode status_GmRequest (const iGmRequest *);
22const iString * meta_GmRequest (const iGmRequest *);
23const iBlock * body_GmRequest (const iGmRequest *);
diff --git a/src/gmutil.c b/src/gmutil.c
new file mode 100644
index 00000000..07861523
--- /dev/null
+++ b/src/gmutil.c
@@ -0,0 +1,25 @@
1#include "gmutil.h"
2
3#include <the_Foundation/regexp.h>
4#include <the_Foundation/object.h>
5
6void init_Url(iUrl *d, const iString *text) {
7 iRegExp *pattern =
8 new_RegExp("(.+)://([^/:?]*)(:[0-9]+)?([^?]*)(\\?.*)?", caseInsensitive_RegExpOption);
9 iRegExpMatch m;
10 if (matchString_RegExp(pattern, text, &m)) {
11 d->protocol = capturedRange_RegExpMatch(&m, 1);
12 d->host = capturedRange_RegExpMatch(&m, 2);
13 d->port = capturedRange_RegExpMatch(&m, 3);
14 if (!isEmpty_Range(&d->port)) {
15 /* Don't include the colon. */
16 d->port.start++;
17 }
18 d->path = capturedRange_RegExpMatch(&m, 4);
19 d->query = capturedRange_RegExpMatch(&m, 5);
20 }
21 else {
22 iZap(*d);
23 }
24 iRelease(pattern);
25}
diff --git a/src/gmutil.h b/src/gmutil.h
new file mode 100644
index 00000000..264ad8a8
--- /dev/null
+++ b/src/gmutil.h
@@ -0,0 +1,16 @@
1#pragma once
2
3#include <the_Foundation/range.h>
4#include <the_Foundation/string.h>
5
6iDeclareType(Url)
7
8struct Impl_Url {
9 iRangecc protocol;
10 iRangecc host;
11 iRangecc port;
12 iRangecc path;
13 iRangecc query;
14};
15
16void init_Url(iUrl *d, const iString *text);
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 71399d3b..121ca7cf 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -6,16 +6,20 @@
6#include "app.h" 6#include "app.h"
7#include "../gemini.h" 7#include "../gemini.h"
8#include "../gmdocument.h" 8#include "../gmdocument.h"
9#include "../gmrequest.h"
10#include "../gmutil.h"
9 11
10#include <the_Foundation/file.h> 12#include <the_Foundation/file.h>
11#include <the_Foundation/path.h> 13#include <the_Foundation/path.h>
12#include <the_Foundation/ptrarray.h> 14#include <the_Foundation/ptrarray.h>
13#include <the_Foundation/regexp.h> 15#include <the_Foundation/regexp.h>
14#include <the_Foundation/tlsrequest.h> 16
17#include <SDL_timer.h>
15 18
16enum iDocumentState { 19enum iDocumentState {
17 blank_DocumentState, 20 blank_DocumentState,
18 fetching_DocumentState, 21 fetching_DocumentState,
22 receivedPartialResponse_DocumentState,
19 layout_DocumentState, 23 layout_DocumentState,
20 ready_DocumentState, 24 ready_DocumentState,
21}; 25};
@@ -24,9 +28,8 @@ struct Impl_DocumentWidget {
24 iWidget widget; 28 iWidget widget;
25 enum iDocumentState state; 29 enum iDocumentState state;
26 iString *url; 30 iString *url;
27 iTlsRequest *request; 31 iGmRequest *request;
28 int statusCode; 32 iAtomicInt isSourcePending; /* request has new content, need to parse it */
29 iString *newSource;
30 iGmDocument *doc; 33 iGmDocument *doc;
31 int pageMargin; 34 int pageMargin;
32 int scrollY; 35 int scrollY;
@@ -36,37 +39,6 @@ struct Impl_DocumentWidget {
36 iScrollWidget *scroll; 39 iScrollWidget *scroll;
37}; 40};
38 41
39iDeclareType(Url)
40
41struct Impl_Url {
42 iRangecc protocol;
43 iRangecc host;
44 iRangecc port;
45 iRangecc path;
46 iRangecc query;
47};
48
49void init_Url(iUrl *d, const iString *text) {
50 iRegExp *pattern =
51 new_RegExp("(.+)://([^/:?]*)(:[0-9]+)?([^?]*)(\\?.*)?", caseInsensitive_RegExpOption);
52 iRegExpMatch m;
53 if (matchString_RegExp(pattern, text, &m)) {
54 d->protocol = capturedRange_RegExpMatch(&m, 1);
55 d->host = capturedRange_RegExpMatch(&m, 2);
56 d->port = capturedRange_RegExpMatch(&m, 3);
57 if (!isEmpty_Range(&d->port)) {
58 /* Don't include the colon. */
59 d->port.start++;
60 }
61 d->path = capturedRange_RegExpMatch(&m, 4);
62 d->query = capturedRange_RegExpMatch(&m, 5);
63 }
64 else {
65 iZap(*d);
66 }
67 iRelease(pattern);
68}
69
70iDefineObjectConstruction(DocumentWidget) 42iDefineObjectConstruction(DocumentWidget)
71 43
72void init_DocumentWidget(iDocumentWidget *d) { 44void init_DocumentWidget(iDocumentWidget *d) {
@@ -75,9 +47,13 @@ void init_DocumentWidget(iDocumentWidget *d) {
75 setId_Widget(w, "document"); 47 setId_Widget(w, "document");
76 d->state = blank_DocumentState; 48 d->state = blank_DocumentState;
77 d->url = new_String(); 49 d->url = new_String();
78 d->statusCode = 0; 50// d->statusCode = 0;
79 d->request = NULL; 51 d->request = NULL;
80 d->newSource = new_String(); 52 d->isSourcePending = iFalse;
53// d->requestTimeout = 0;
54// d->readPending = iFalse;
55// d->newSource = new_String();
56// d->needSourceUpdate = iFalse;
81 d->doc = new_GmDocument(); 57 d->doc = new_GmDocument();
82 d->pageMargin = 5; 58 d->pageMargin = 5;
83 d->scrollY = 0; 59 d->scrollY = 0;
@@ -90,7 +66,8 @@ void init_DocumentWidget(iDocumentWidget *d) {
90void deinit_DocumentWidget(iDocumentWidget *d) { 66void deinit_DocumentWidget(iDocumentWidget *d) {
91 deinit_PtrArray(&d->visibleLinks); 67 deinit_PtrArray(&d->visibleLinks);
92 delete_String(d->url); 68 delete_String(d->url);
93 delete_String(d->newSource); 69// delete_String(d->newSource);
70 iRelease(d->request);
94 iRelease(d->doc); 71 iRelease(d->doc);
95} 72}
96 73
@@ -111,90 +88,63 @@ static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) {
111 return rect; 88 return rect;
112} 89}
113 90
114#if 0
115void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
116 /* TODO: lock source during update */
117 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d));
118 d->state = ready_DocumentState;
119}
120#endif
121
122static iRangecc getLine_(iRangecc text) { 91static iRangecc getLine_(iRangecc text) {
123 iRangecc line = { text.start, text.start }; 92 iRangecc line = { text.start, text.start };
124 for (; *line.end != '\n' && line.end != text.end; line.end++) {} 93 for (; *line.end != '\n' && line.end != text.end; line.end++) {}
125 return line; 94 return line;
126} 95}
127 96
128static void requestFinished_DocumentWidget_(iAnyObject *obj) { 97static void requestUpdated_DocumentWidget_(iAnyObject *obj) {
98#if 0
129 iDocumentWidget *d = obj; 99 iDocumentWidget *d = obj;
130 iBlock *response = readAll_TlsRequest(d->request); 100 iBlock *response = readAll_TlsRequest(d->request);
131 iRangecc responseRange = { constBegin_Block(response), constEnd_Block(response) }; 101 if (d->state == fetching_DocumentState) {
132 iRangecc respLine = getLine_(responseRange); 102 iRangecc responseRange = { constBegin_Block(response), constEnd_Block(response) };
133 responseRange.start = respLine.end + 1; 103 iRangecc respLine = getLine_(responseRange);
134 /* First line is the status code. */ { 104 responseRange.start = respLine.end + 1;
135 iString *line = newRange_String(respLine); 105 /* First line is the status code. */ {
136 trim_String(line); 106 iString *line = newRange_String(respLine);
137 d->statusCode = toInt_String(line); 107 trim_String(line);
138 printf("response (%02d): %s\n", d->statusCode, cstr_String(line)); 108 d->statusCode = toInt_String(line);
139 /* TODO: post a command with the status code */ 109 printf("response (%02d): %s\n", d->statusCode, cstr_String(line));
140 switch (d->statusCode) { 110 /* TODO: post a command with the status code */
141 case redirectPermanent_GmStatusCode: 111 switch (d->statusCode) {
142 case redirectTemporary_GmStatusCode: 112 case redirectPermanent_GmStatusCode:
143 postCommandf_App("open url:%s", cstr_String(line) + 3); 113 case redirectTemporary_GmStatusCode:
144 break; 114 postCommandf_App("open url:%s", cstr_String(line) + 3);
115 break;
116 }
117 delete_String(line);
145 } 118 }
146 delete_String(line); 119 setCStrN_String(d->newSource, responseRange.start, size_Range(&responseRange));
120 d->requestTimeout = SDL_AddTimer(2000, requestTimedOut_DocumentWidget_, d);
121 d->state = receivedPartialResponse_DocumentState;
147 } 122 }
148 setCStrN_String(d->newSource, responseRange.start, size_Range(&responseRange)); 123 else if (d->state == receivedPartialResponse_DocumentState) {
149 delete_Block(response); 124 appendCStr_String(d->newSource, cstr_Block(response));
150 iReleaseLater(d->request); 125 d->needSourceUpdate = iTrue;
151 d->request = NULL;
152 fflush(stdout);
153 refresh_Widget(constAs_Widget(d));
154}
155
156static void fetch_DocumentWidget_(iDocumentWidget *d) {
157 iAssert(!d->request);
158 d->state = fetching_DocumentState;
159 d->statusCode = 0;
160 iUrl url;
161 init_Url(&url, d->url);
162 if (!cmpCStrSc_Rangecc(&url.protocol, "file", &iCaseInsensitive)) {
163 iFile *f = new_File(collect_String(newRange_String(url.path)));
164 if (open_File(f, readOnly_FileMode)) {
165 setBlock_String(d->newSource, collect_Block(readAll_File(f)));
166 refresh_Widget(constAs_Widget(d));
167 }
168 iRelease(f);
169 return;
170 } 126 }
171 d->request = new_TlsRequest(); 127 delete_Block(response);
172 uint16_t port = toInt_String(collect_String(newRange_String(url.port))); 128 refresh_Widget(as_Widget(d));
173 if (port == 0) { 129#endif
174 port = 1965; /* default Gemini port */ 130 iDocumentWidget *d = obj;
131 const int wasPending = exchange_Atomic(&d->isSourcePending, iTrue);
132 if (!wasPending) {
133 postCommand_Widget(obj, "document.request.updated request:%p", d->request);
175 } 134 }
176 setUrl_TlsRequest(d->request, collect_String(newRange_String(url.host)), port);
177 /* The request string is an UTF-8 encoded absolute URL. */
178 iString *content = collectNew_String();
179 append_String(content, d->url);
180 appendCStr_String(content, "\r\n");
181 setContent_TlsRequest(d->request, utf8_String(content));
182 iConnect(TlsRequest, d->request, finished, d, requestFinished_DocumentWidget_);
183 submit_TlsRequest(d->request);
184} 135}
185 136
186void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { 137static void requestFinished_DocumentWidget_(iAnyObject *obj) {
187 iString *newUrl = new_String(); 138 iDocumentWidget *d = obj;
188 if (indexOfCStr_String(url, "://") == iInvalidPos && !startsWithCase_String(url, "gemini:")) { 139 /*
189 /* Prepend default protocol. */ 140 iReleaseLater(d->request);
190 setCStr_String(newUrl, "gemini://"); 141 d->request = NULL;
191 } 142 if (d->requestTimeout) {
192 append_String(newUrl, url); 143 SDL_RemoveTimer(d->requestTimeout);
193 if (cmpStringSc_String(d->url, newUrl, &iCaseInsensitive)) { 144 d->requestTimeout = 0;
194 set_String(d->url, newUrl);
195 fetch_DocumentWidget_(d);
196 } 145 }
197 delete_String(newUrl); 146 refresh_Widget(constAs_Widget(d));*/
147 postCommand_Widget(obj, "document.request.finished request:%p", d->request);
198} 148}
199 149
200static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) { 150static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) {
@@ -227,6 +177,54 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
227 render_GmDocument(d->doc, visRange, addVisibleLink_DocumentWidget_, d); 177 render_GmDocument(d->doc, visRange, addVisibleLink_DocumentWidget_, d);
228} 178}
229 179
180static void updateSource_DocumentWidget_(iDocumentWidget *d) {
181 /* Update the document? */
182 // if (d->needSourceUpdate) {
183 /* TODO: Do this in the background. However, that requires a text metrics calculator
184 that does not try to cache the glyph bitmaps. */
185 iString str;
186 initBlock_String(&str, body_GmRequest(d->request));
187 setSource_GmDocument(d->doc, &str, documentWidth_DocumentWidget_(d));
188 deinit_String(&str);
189 updateVisible_DocumentWidget_(d);
190 refresh_Widget(as_Widget(d));
191 // d->state = ready_DocumentState;
192 // if (!d->request) {
193 // d->needSourceUpdate = iFalse;
194 // postCommandf_App("document.changed url:%s", cstr_String(d->url));
195 // }
196 // }
197}
198
199static void fetch_DocumentWidget_(iDocumentWidget *d) {
200 /* Forget the previous request. */
201 if (d->request) {
202 iRelease(d->request);
203 d->request = NULL;
204 }
205 d->state = fetching_DocumentState;
206 set_Atomic(&d->isSourcePending, iFalse);
207 d->request = new_GmRequest();
208 setUrl_GmRequest(d->request, d->url);
209 iConnect(GmRequest, d->request, updated, d, requestUpdated_DocumentWidget_);
210 iConnect(GmRequest, d->request, finished, d, requestFinished_DocumentWidget_);
211 submit_GmRequest(d->request);
212}
213
214void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) {
215 iString *newUrl = new_String();
216 if (indexOfCStr_String(url, "://") == iInvalidPos && !startsWithCase_String(url, "gemini:")) {
217 /* Prepend default protocol. */
218 setCStr_String(newUrl, "gemini://");
219 }
220 append_String(newUrl, url);
221 if (cmpStringSc_String(d->url, newUrl, &iCaseInsensitive)) {
222 set_String(d->url, newUrl);
223 fetch_DocumentWidget_(d);
224 }
225 delete_String(newUrl);
226}
227
230static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) { 228static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) {
231 d->scrollY += offset; 229 d->scrollY += offset;
232 if (d->scrollY < 0) { 230 if (d->scrollY < 0) {
@@ -296,6 +294,61 @@ static const iString *absoluteUrl_DocumentWidget_(const iDocumentWidget *d, cons
296 return collect_String(absolute); 294 return collect_String(absolute);
297} 295}
298 296
297static void readResponse_DocumentWidget_(iDocumentWidget *d) {
298#if 0
299 d->readPending = iFalse;
300 iBlock *response = collect_Block(readAll_TlsRequest(d->request));
301 if (isEmpty_Block(response)) {
302 return;
303 }
304 if (d->state == fetching_DocumentState) {
305 /* TODO: Bug here is that the first read may occur before the first line is
306 available, so nothing gets done. Should ensure that the status code is
307 read successully. */
308 iRangecc responseRange = { constBegin_Block(response), constEnd_Block(response) };
309 iRangecc respLine = getLine_(responseRange);
310 responseRange.start = respLine.end + 1;
311 /* First line is the status code. */ {
312 iString *line = collect_String(newRange_String(respLine));
313 trim_String(line);
314 d->statusCode = toInt_String(line);
315 printf("response (%02d): %s\n", d->statusCode, cstr_String(line));
316 /* TODO: post a command with the status code */
317 switch (d->statusCode) {
318 case redirectPermanent_GmStatusCode:
319 case redirectTemporary_GmStatusCode:
320 postCommandf_App("open url:%s", cstr_String(line) + 3);
321 return;
322 }
323 }
324 setCStrN_String(d->newSource, responseRange.start, size_Range(&responseRange));
325 d->requestTimeout = SDL_AddTimer(2000, requestTimedOut_DocumentWidget_, d);
326 d->state = receivedPartialResponse_DocumentState;
327 d->scrollY = 0;
328 }
329 else if (d->state == receivedPartialResponse_DocumentState) {
330 appendCStr_String(d->newSource, cstr_Block(response));
331 }
332#endif
333 updateSource_DocumentWidget_(d);
334}
335
336static void checkResponseCode_DocumentWidget_(iDocumentWidget *d) {
337 if (d->state == fetching_DocumentState) {
338 d->state = receivedPartialResponse_DocumentState;
339 d->scrollY = 0;
340 switch (status_GmRequest(d->request)) {
341 case redirectTemporary_GmStatusCode:
342 case redirectPermanent_GmStatusCode:
343 postCommandf_App("open url:%s", cstr_String(meta_GmRequest(d->request)));
344 iReleasePtr(&d->request);
345 break;
346 default:
347 break;
348 }
349 }
350}
351
299static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { 352static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
300 iWidget *w = as_Widget(d); 353 iWidget *w = as_Widget(d);
301 if (isResize_UserEvent(ev)) { 354 if (isResize_UserEvent(ev)) {
@@ -303,6 +356,20 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
303 updateVisible_DocumentWidget_(d); 356 updateVisible_DocumentWidget_(d);
304 refresh_Widget(w); 357 refresh_Widget(w);
305 } 358 }
359 else if (isCommand_Widget(w, ev, "document.request.updated") &&
360 pointerLabel_Command(command_UserEvent(ev), "request") == d->request) {
361 updateSource_DocumentWidget_(d);
362 checkResponseCode_DocumentWidget_(d);
363 return iTrue;
364 }
365 else if (isCommand_Widget(w, ev, "document.request.finished") &&
366 pointerLabel_Command(command_UserEvent(ev), "request") == d->request) {
367 updateSource_DocumentWidget_(d);
368 checkResponseCode_DocumentWidget_(d);
369 d->state = ready_DocumentState;
370 iReleasePtr(&d->request);
371 return iTrue;
372 }
306 else if (isCommand_Widget(w, ev, "scroll.moved")) { 373 else if (isCommand_Widget(w, ev, "scroll.moved")) {
307 d->scrollY = arg_Command(command_UserEvent(ev)); 374 d->scrollY = arg_Command(command_UserEvent(ev));
308 updateVisible_DocumentWidget_(d); 375 updateVisible_DocumentWidget_(d);
@@ -417,19 +484,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
417 iDrawContext ctx = { .widget = d, .bounds = documentBounds_DocumentWidget_(d) }; 484 iDrawContext ctx = { .widget = d, .bounds = documentBounds_DocumentWidget_(d) };
418 init_Paint(&ctx.paint); 485 init_Paint(&ctx.paint);
419 fillRect_Paint(&ctx.paint, bounds, gray25_ColorId); 486 fillRect_Paint(&ctx.paint, bounds, gray25_ColorId);
420 /* Update the document? */ 487// if (d->state != ready_DocumentState) return;
421 if (!isEmpty_String(d->newSource)) {
422 iDocumentWidget *m = iConstCast(iDocumentWidget *, d);
423 /* TODO: Do this in the background. However, that requires a text metrics calculator
424 that does not try to cache the glyph bitmaps. */
425 setSource_GmDocument(m->doc, m->newSource, width_Rect(ctx.bounds));
426 postCommandf_App("document.changed url:%s", cstr_String(d->url));
427 clear_String(m->newSource);
428 m->scrollY = 0;
429 m->state = ready_DocumentState;
430 updateVisible_DocumentWidget_(m);
431 }
432 if (d->state != ready_DocumentState) return;
433 setClip_Paint(&ctx.paint, bounds); 488 setClip_Paint(&ctx.paint, bounds);
434 render_GmDocument(d->doc, visibleRange_DocumentWidget_(d), drawRun_DrawContext_, &ctx); 489 render_GmDocument(d->doc, visibleRange_DocumentWidget_(d), drawRun_DrawContext_, &ctx);
435 clearClip_Paint(&ctx.paint); 490 clearClip_Paint(&ctx.paint);
diff --git a/src/ui/window.c b/src/ui/window.c
index 43233010..2cd30459 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -49,7 +49,7 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
49 } 49 }
50 return iTrue; 50 return iTrue;
51 } 51 }
52 else if (equal_Command(cmd, "setfocus")) { 52 else if (equal_Command(cmd, "focus.set")) {
53 setFocus_Widget(findWidget_App(cstr_String(string_Command(cmd, "id")))); 53 setFocus_Widget(findWidget_App(cstr_String(string_Command(cmd, "id"))));
54 return iTrue; 54 return iTrue;
55 } 55 }
@@ -273,7 +273,7 @@ static void setupUserInterface_Window(iWindow *d) {
273#endif 273#endif
274 /* Glboal keyboard shortcuts. */ { 274 /* Glboal keyboard shortcuts. */ {
275 // addAction_Widget(d->root, SDLK_LEFTBRACKET, KMOD_SHIFT | KMOD_PRIMARY, "tabs.prev"); 275 // addAction_Widget(d->root, SDLK_LEFTBRACKET, KMOD_SHIFT | KMOD_PRIMARY, "tabs.prev");
276 addAction_Widget(d->root, 'l', KMOD_PRIMARY, "setfocus id:url"); 276 addAction_Widget(d->root, 'l', KMOD_PRIMARY, "focus.set id:url");
277 } 277 }
278} 278}
279 279