Fossil SCM

Add submenu entries on timeline pages for selecting options such as "tickets only" and "200 entries per page" and so forth.

drh 2008-11-02 18:22 trunk
Commit c9cd128c2c2749739e6423fd668a5682395f7e76
2 files changed +52 -11 +72
+52 -11
--- src/timeline.c
+++ src/timeline.c
@@ -211,10 +211,24 @@
211211
@ FROM event JOIN blob
212212
@ WHERE blob.rid=event.objid
213213
;
214214
return zBaseSql;
215215
}
216
+
217
+/*
218
+** Generate a submenu element with a single parameter change.
219
+*/
220
+static void timeline_submenu(
221
+ HQuery *pUrl, /* Base URL */
222
+ const char *zMenuName, /* Submenu name */
223
+ const char *zParam, /* Parameter value to add or change */
224
+ const char *zValue, /* Value of the new parameter */
225
+ const char *zRemove /* Parameter to omit */
226
+){
227
+ style_submenu_element(zMenuName, zMenuName, "%s",
228
+ url_render(pUrl, zParam, zValue, zRemove, 0));
229
+}
216230
217231
/*
218232
** WEBPAGE: timeline
219233
**
220234
** Query parameters:
@@ -223,11 +237,11 @@
223237
** b=TIMESTAMP before this date.
224238
** n=COUNT number of events in output
225239
** p=RID artifact RID and up to COUNT parents and ancestors
226240
** d=RID artifact RID and up to COUNT descendants
227241
** u=USER only if belonging to this user
228
-** y=TYPE 'ci', 'w', 'tkt'
242
+** y=TYPE 'ci', 'w', 't'
229243
**
230244
** p= and d= can appear individually or together. If either p= or d=
231245
** appear, then u=, y=, a=, and b= are ignored.
232246
**
233247
** If a= and b= appear, only a= is used. If neither appear, the most
@@ -241,13 +255,14 @@
241255
Blob desc; /* Description of the timeline */
242256
int nEntry = atoi(PD("n","20")); /* Max number of entries on timeline */
243257
int p_rid = atoi(PD("p","0")); /* artifact p and its parents */
244258
int d_rid = atoi(PD("d","0")); /* artifact d and its descendants */
245259
const char *zUser = P("u"); /* All entries by this user if not NULL */
246
- const char *zType = P("y"); /* Type of events. All if NULL */
260
+ const char *zType = PD("y","all"); /* Type of events. All if NULL */
247261
const char *zAfter = P("a"); /* Events after this time */
248262
const char *zBefore = P("b"); /* Events before this time */
263
+ HQuery url; /* URL for various branch links */
249264
250265
/* To view the timeline, must have permission to read project data.
251266
*/
252267
login_check_credentials();
253268
if( !g.okRead ){ login_needed(); return; }
@@ -297,18 +312,18 @@
297312
blob_appendf(&desc, " of [%.10s]", zUuid);
298313
}
299314
db_prepare(&q, "SELECT * FROM timeline ORDER BY timestamp DESC");
300315
}else{
301316
int n;
302
- Blob url;
303317
const char *zEType = "event";
304
- const char *zDate;
305
- blob_zero(&url);
306
- blob_appendf(&url, "%s/timeline?n=%d", g.zBaseURL, nEntry);
307
- if( zType ){
318
+ char *zDate;
319
+ char *zNEntry = mprintf("%d", nEntry);
320
+ url_initialize(&url, "timeline");
321
+ url_add_parameter(&url, "n", zNEntry);
322
+ if( zType[0]!='a' ){
308323
blob_appendf(&sql, " AND event.type=%Q", zType);
309
- blob_appendf(&url, "&y=%T", zType);
324
+ url_add_parameter(&url, "y", zType);
310325
if( zType[0]=='c' ){
311326
zEType = "checkin";
312327
}else if( zType[0]=='w' ){
313328
zEType = "wiki edit";
314329
}else if( zType[0]=='t' ){
@@ -315,18 +330,19 @@
315330
zEType = "ticket change";
316331
}
317332
}
318333
if( zUser ){
319334
blob_appendf(&sql, " AND event.user=%Q", zUser);
320
- blob_appendf(&url, "&u=%T", zUser);
335
+ url_add_parameter(&url, "u", zUser);
321336
}
322337
if( zAfter ){
323338
while( isspace(zAfter[0]) ){ zAfter++; }
324339
if( zAfter[0] ){
325340
blob_appendf(&sql,
326341
" AND event.mtime>=(SELECT julianday(%Q, 'utc'))"
327342
" ORDER BY event.mtime ASC", zAfter);
343
+ url_add_parameter(&url, "a", zAfter);
328344
zBefore = 0;
329345
}else{
330346
zAfter = 0;
331347
}
332348
}else if( zBefore ){
@@ -333,10 +349,11 @@
333349
while( isspace(zBefore[0]) ){ zBefore++; }
334350
if( zBefore[0] ){
335351
blob_appendf(&sql,
336352
" AND event.mtime<=(SELECT julianday(%Q, 'utc'))"
337353
" ORDER BY event.mtime DESC", zBefore);
354
+ url_add_parameter(&url, "b", zBefore);
338355
}else{
339356
zBefore = 0;
340357
}
341358
}else{
342359
blob_appendf(&sql, " ORDER BY event.mtime DESC");
@@ -343,10 +360,13 @@
343360
}
344361
blob_appendf(&sql, " LIMIT %d", nEntry);
345362
db_multi_exec("%s", blob_str(&sql));
346363
347364
n = db_int(0, "SELECT count(*) FROM timeline");
365
+ if( n<nEntry && zAfter ){
366
+ cgi_redirect(url_render(&url, "a", 0, "b", 0));
367
+ }
348368
if( zAfter==0 && zBefore==0 ){
349369
blob_appendf(&desc, "%d most recent %ss", n, zEType);
350370
}else{
351371
blob_appendf(&desc, "%d %ss", n, zEType);
352372
}
@@ -359,15 +379,36 @@
359379
blob_appendf(&desc, " occurring on or before %h.<br>", zBefore);
360380
}
361381
if( g.okHistory ){
362382
if( zAfter || n==nEntry ){
363383
zDate = db_text(0, "SELECT min(timestamp) FROM timeline");
364
- blob_appendf(&desc, " <a href='%b&b=%s'>[older]</a>", &url, zDate);
384
+ timeline_submenu(&url, "Older", "b", zDate, "a");
385
+ free(zDate);
365386
}
366387
if( zBefore || (zAfter && n==nEntry) ){
367388
zDate = db_text(0, "SELECT max(timestamp) FROM timeline");
368
- blob_appendf(&desc, " <a href='%b&a=%s'>[more recent]</a>", &url,zDate);
389
+ timeline_submenu(&url, "Newer", "a", zDate, "b");
390
+ free(zDate);
391
+ }else{
392
+ if( zType[0]!='a' ){
393
+ timeline_submenu(&url, "All Types", "y", "all", 0);
394
+ }
395
+ if( zType[0]!='w' ){
396
+ timeline_submenu(&url, "Wiki Only", "y", "w", 0);
397
+ }
398
+ if( zType[0]!='c' ){
399
+ timeline_submenu(&url, "Checkins Only", "y", "ci", 0);
400
+ }
401
+ if( zType[0]!='t' ){
402
+ timeline_submenu(&url, "Tickets Only", "y", "t", 0);
403
+ }
404
+ }
405
+ if( nEntry>20 ){
406
+ timeline_submenu(&url, "20 Events", "n", "20", 0);
407
+ }
408
+ if( nEntry<200 ){
409
+ timeline_submenu(&url, "200 Events", "n", "200", 0);
369410
}
370411
}
371412
}
372413
blob_zero(&sql);
373414
db_prepare(&q, "SELECT * FROM timeline ORDER BY timestamp DESC");
374415
--- src/timeline.c
+++ src/timeline.c
@@ -211,10 +211,24 @@
211 @ FROM event JOIN blob
212 @ WHERE blob.rid=event.objid
213 ;
214 return zBaseSql;
215 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
217 /*
218 ** WEBPAGE: timeline
219 **
220 ** Query parameters:
@@ -223,11 +237,11 @@
223 ** b=TIMESTAMP before this date.
224 ** n=COUNT number of events in output
225 ** p=RID artifact RID and up to COUNT parents and ancestors
226 ** d=RID artifact RID and up to COUNT descendants
227 ** u=USER only if belonging to this user
228 ** y=TYPE 'ci', 'w', 'tkt'
229 **
230 ** p= and d= can appear individually or together. If either p= or d=
231 ** appear, then u=, y=, a=, and b= are ignored.
232 **
233 ** If a= and b= appear, only a= is used. If neither appear, the most
@@ -241,13 +255,14 @@
241 Blob desc; /* Description of the timeline */
242 int nEntry = atoi(PD("n","20")); /* Max number of entries on timeline */
243 int p_rid = atoi(PD("p","0")); /* artifact p and its parents */
244 int d_rid = atoi(PD("d","0")); /* artifact d and its descendants */
245 const char *zUser = P("u"); /* All entries by this user if not NULL */
246 const char *zType = P("y"); /* Type of events. All if NULL */
247 const char *zAfter = P("a"); /* Events after this time */
248 const char *zBefore = P("b"); /* Events before this time */
 
249
250 /* To view the timeline, must have permission to read project data.
251 */
252 login_check_credentials();
253 if( !g.okRead ){ login_needed(); return; }
@@ -297,18 +312,18 @@
297 blob_appendf(&desc, " of [%.10s]", zUuid);
298 }
299 db_prepare(&q, "SELECT * FROM timeline ORDER BY timestamp DESC");
300 }else{
301 int n;
302 Blob url;
303 const char *zEType = "event";
304 const char *zDate;
305 blob_zero(&url);
306 blob_appendf(&url, "%s/timeline?n=%d", g.zBaseURL, nEntry);
307 if( zType ){
 
308 blob_appendf(&sql, " AND event.type=%Q", zType);
309 blob_appendf(&url, "&y=%T", zType);
310 if( zType[0]=='c' ){
311 zEType = "checkin";
312 }else if( zType[0]=='w' ){
313 zEType = "wiki edit";
314 }else if( zType[0]=='t' ){
@@ -315,18 +330,19 @@
315 zEType = "ticket change";
316 }
317 }
318 if( zUser ){
319 blob_appendf(&sql, " AND event.user=%Q", zUser);
320 blob_appendf(&url, "&u=%T", zUser);
321 }
322 if( zAfter ){
323 while( isspace(zAfter[0]) ){ zAfter++; }
324 if( zAfter[0] ){
325 blob_appendf(&sql,
326 " AND event.mtime>=(SELECT julianday(%Q, 'utc'))"
327 " ORDER BY event.mtime ASC", zAfter);
 
328 zBefore = 0;
329 }else{
330 zAfter = 0;
331 }
332 }else if( zBefore ){
@@ -333,10 +349,11 @@
333 while( isspace(zBefore[0]) ){ zBefore++; }
334 if( zBefore[0] ){
335 blob_appendf(&sql,
336 " AND event.mtime<=(SELECT julianday(%Q, 'utc'))"
337 " ORDER BY event.mtime DESC", zBefore);
 
338 }else{
339 zBefore = 0;
340 }
341 }else{
342 blob_appendf(&sql, " ORDER BY event.mtime DESC");
@@ -343,10 +360,13 @@
343 }
344 blob_appendf(&sql, " LIMIT %d", nEntry);
345 db_multi_exec("%s", blob_str(&sql));
346
347 n = db_int(0, "SELECT count(*) FROM timeline");
 
 
 
348 if( zAfter==0 && zBefore==0 ){
349 blob_appendf(&desc, "%d most recent %ss", n, zEType);
350 }else{
351 blob_appendf(&desc, "%d %ss", n, zEType);
352 }
@@ -359,15 +379,36 @@
359 blob_appendf(&desc, " occurring on or before %h.<br>", zBefore);
360 }
361 if( g.okHistory ){
362 if( zAfter || n==nEntry ){
363 zDate = db_text(0, "SELECT min(timestamp) FROM timeline");
364 blob_appendf(&desc, " <a href='%b&b=%s'>[older]</a>", &url, zDate);
 
365 }
366 if( zBefore || (zAfter && n==nEntry) ){
367 zDate = db_text(0, "SELECT max(timestamp) FROM timeline");
368 blob_appendf(&desc, " <a href='%b&a=%s'>[more recent]</a>", &url,zDate);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369 }
370 }
371 }
372 blob_zero(&sql);
373 db_prepare(&q, "SELECT * FROM timeline ORDER BY timestamp DESC");
374
--- src/timeline.c
+++ src/timeline.c
@@ -211,10 +211,24 @@
211 @ FROM event JOIN blob
212 @ WHERE blob.rid=event.objid
213 ;
214 return zBaseSql;
215 }
216
217 /*
218 ** Generate a submenu element with a single parameter change.
219 */
220 static void timeline_submenu(
221 HQuery *pUrl, /* Base URL */
222 const char *zMenuName, /* Submenu name */
223 const char *zParam, /* Parameter value to add or change */
224 const char *zValue, /* Value of the new parameter */
225 const char *zRemove /* Parameter to omit */
226 ){
227 style_submenu_element(zMenuName, zMenuName, "%s",
228 url_render(pUrl, zParam, zValue, zRemove, 0));
229 }
230
231 /*
232 ** WEBPAGE: timeline
233 **
234 ** Query parameters:
@@ -223,11 +237,11 @@
237 ** b=TIMESTAMP before this date.
238 ** n=COUNT number of events in output
239 ** p=RID artifact RID and up to COUNT parents and ancestors
240 ** d=RID artifact RID and up to COUNT descendants
241 ** u=USER only if belonging to this user
242 ** y=TYPE 'ci', 'w', 't'
243 **
244 ** p= and d= can appear individually or together. If either p= or d=
245 ** appear, then u=, y=, a=, and b= are ignored.
246 **
247 ** If a= and b= appear, only a= is used. If neither appear, the most
@@ -241,13 +255,14 @@
255 Blob desc; /* Description of the timeline */
256 int nEntry = atoi(PD("n","20")); /* Max number of entries on timeline */
257 int p_rid = atoi(PD("p","0")); /* artifact p and its parents */
258 int d_rid = atoi(PD("d","0")); /* artifact d and its descendants */
259 const char *zUser = P("u"); /* All entries by this user if not NULL */
260 const char *zType = PD("y","all"); /* Type of events. All if NULL */
261 const char *zAfter = P("a"); /* Events after this time */
262 const char *zBefore = P("b"); /* Events before this time */
263 HQuery url; /* URL for various branch links */
264
265 /* To view the timeline, must have permission to read project data.
266 */
267 login_check_credentials();
268 if( !g.okRead ){ login_needed(); return; }
@@ -297,18 +312,18 @@
312 blob_appendf(&desc, " of [%.10s]", zUuid);
313 }
314 db_prepare(&q, "SELECT * FROM timeline ORDER BY timestamp DESC");
315 }else{
316 int n;
 
317 const char *zEType = "event";
318 char *zDate;
319 char *zNEntry = mprintf("%d", nEntry);
320 url_initialize(&url, "timeline");
321 url_add_parameter(&url, "n", zNEntry);
322 if( zType[0]!='a' ){
323 blob_appendf(&sql, " AND event.type=%Q", zType);
324 url_add_parameter(&url, "y", zType);
325 if( zType[0]=='c' ){
326 zEType = "checkin";
327 }else if( zType[0]=='w' ){
328 zEType = "wiki edit";
329 }else if( zType[0]=='t' ){
@@ -315,18 +330,19 @@
330 zEType = "ticket change";
331 }
332 }
333 if( zUser ){
334 blob_appendf(&sql, " AND event.user=%Q", zUser);
335 url_add_parameter(&url, "u", zUser);
336 }
337 if( zAfter ){
338 while( isspace(zAfter[0]) ){ zAfter++; }
339 if( zAfter[0] ){
340 blob_appendf(&sql,
341 " AND event.mtime>=(SELECT julianday(%Q, 'utc'))"
342 " ORDER BY event.mtime ASC", zAfter);
343 url_add_parameter(&url, "a", zAfter);
344 zBefore = 0;
345 }else{
346 zAfter = 0;
347 }
348 }else if( zBefore ){
@@ -333,10 +349,11 @@
349 while( isspace(zBefore[0]) ){ zBefore++; }
350 if( zBefore[0] ){
351 blob_appendf(&sql,
352 " AND event.mtime<=(SELECT julianday(%Q, 'utc'))"
353 " ORDER BY event.mtime DESC", zBefore);
354 url_add_parameter(&url, "b", zBefore);
355 }else{
356 zBefore = 0;
357 }
358 }else{
359 blob_appendf(&sql, " ORDER BY event.mtime DESC");
@@ -343,10 +360,13 @@
360 }
361 blob_appendf(&sql, " LIMIT %d", nEntry);
362 db_multi_exec("%s", blob_str(&sql));
363
364 n = db_int(0, "SELECT count(*) FROM timeline");
365 if( n<nEntry && zAfter ){
366 cgi_redirect(url_render(&url, "a", 0, "b", 0));
367 }
368 if( zAfter==0 && zBefore==0 ){
369 blob_appendf(&desc, "%d most recent %ss", n, zEType);
370 }else{
371 blob_appendf(&desc, "%d %ss", n, zEType);
372 }
@@ -359,15 +379,36 @@
379 blob_appendf(&desc, " occurring on or before %h.<br>", zBefore);
380 }
381 if( g.okHistory ){
382 if( zAfter || n==nEntry ){
383 zDate = db_text(0, "SELECT min(timestamp) FROM timeline");
384 timeline_submenu(&url, "Older", "b", zDate, "a");
385 free(zDate);
386 }
387 if( zBefore || (zAfter && n==nEntry) ){
388 zDate = db_text(0, "SELECT max(timestamp) FROM timeline");
389 timeline_submenu(&url, "Newer", "a", zDate, "b");
390 free(zDate);
391 }else{
392 if( zType[0]!='a' ){
393 timeline_submenu(&url, "All Types", "y", "all", 0);
394 }
395 if( zType[0]!='w' ){
396 timeline_submenu(&url, "Wiki Only", "y", "w", 0);
397 }
398 if( zType[0]!='c' ){
399 timeline_submenu(&url, "Checkins Only", "y", "ci", 0);
400 }
401 if( zType[0]!='t' ){
402 timeline_submenu(&url, "Tickets Only", "y", "t", 0);
403 }
404 }
405 if( nEntry>20 ){
406 timeline_submenu(&url, "20 Events", "n", "20", 0);
407 }
408 if( nEntry<200 ){
409 timeline_submenu(&url, "200 Events", "n", "200", 0);
410 }
411 }
412 }
413 blob_zero(&sql);
414 db_prepare(&q, "SELECT * FROM timeline ORDER BY timestamp DESC");
415
+72
--- src/url.c
+++ src/url.c
@@ -173,5 +173,77 @@
173173
url_parse(zProxy);
174174
g.urlPath = zOriginalUrl;
175175
g.urlHostname = zOriginalHost;
176176
}
177177
}
178
+
179
+#if INTERFACE
180
+/*
181
+** An instance of this object is used to build a URL with query parameters.
182
+*/
183
+struct HQuery {
184
+ Blob url; /* The URL */
185
+ const char *zBase; /* The base URL */
186
+ int nParam; /* Number of parameters. Max 10 */
187
+ const char *azName[10]; /* Parameter names */
188
+ const char *azValue[10]; /* Parameter values */
189
+};
190
+#endif
191
+
192
+/*
193
+** Initialize the URL object.
194
+*/
195
+void url_initialize(HQuery *p, const char *zBase){
196
+ blob_zero(&p->url);
197
+ p->zBase = zBase;
198
+ p->nParam = 0;
199
+}
200
+
201
+/*
202
+** Add a fixed parameter to an HQuery.
203
+*/
204
+void url_add_parameter(HQuery *p, const char *zName, const char *zValue){
205
+ assert( p->nParam < count(p->azName) );
206
+ assert( p->nParam < count(p->azValue) );
207
+ p->azName[p->nParam] = zName;
208
+ p->azValue[p->nParam] = zValue;
209
+ p->nParam++;
210
+}
211
+
212
+/*
213
+** Render the URL with a parameter override.
214
+*/
215
+char *url_render(
216
+ HQuery *p, /* Base URL */
217
+ const char *zName1, /* First override */
218
+ const char *zValue1, /* First override value */
219
+ const char *zName2, /* Second override */
220
+ const char *zValue2 /* Second override value */
221
+){
222
+ const char *zSep = "?";
223
+ int i;
224
+
225
+ blob_reset(&p->url);
226
+ blob_appendf(&p->url, "%s/%s", g.zBaseURL, p->zBase);
227
+ for(i=0; i<p->nParam; i++){
228
+ const char *z = p->azValue[i];
229
+ if( zName1 && strcmp(zName1,p->azName[i])==0 ){
230
+ zName1 = 0;
231
+ z = zValue1;
232
+ if( z==0 ) continue;
233
+ }
234
+ if( zName2 && strcmp(zName2,p->azName[i])==0 ){
235
+ zName2 = 0;
236
+ z = zValue2;
237
+ if( z==0 ) continue;
238
+ }
239
+ blob_appendf(&p->url, "%s%s=%T", zSep, p->azName[i], z);
240
+ zSep = "&";
241
+ }
242
+ if( zName1 && zValue1 ){
243
+ blob_appendf(&p->url, "%s%s=%T", zSep, zName1, zValue1);
244
+ }
245
+ if( zName2 && zValue2 ){
246
+ blob_appendf(&p->url, "%s%s=%T", zSep, zName2, zValue2);
247
+ }
248
+ return blob_str(&p->url);
249
+}
178250
--- src/url.c
+++ src/url.c
@@ -173,5 +173,77 @@
173 url_parse(zProxy);
174 g.urlPath = zOriginalUrl;
175 g.urlHostname = zOriginalHost;
176 }
177 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
--- src/url.c
+++ src/url.c
@@ -173,5 +173,77 @@
173 url_parse(zProxy);
174 g.urlPath = zOriginalUrl;
175 g.urlHostname = zOriginalHost;
176 }
177 }
178
179 #if INTERFACE
180 /*
181 ** An instance of this object is used to build a URL with query parameters.
182 */
183 struct HQuery {
184 Blob url; /* The URL */
185 const char *zBase; /* The base URL */
186 int nParam; /* Number of parameters. Max 10 */
187 const char *azName[10]; /* Parameter names */
188 const char *azValue[10]; /* Parameter values */
189 };
190 #endif
191
192 /*
193 ** Initialize the URL object.
194 */
195 void url_initialize(HQuery *p, const char *zBase){
196 blob_zero(&p->url);
197 p->zBase = zBase;
198 p->nParam = 0;
199 }
200
201 /*
202 ** Add a fixed parameter to an HQuery.
203 */
204 void url_add_parameter(HQuery *p, const char *zName, const char *zValue){
205 assert( p->nParam < count(p->azName) );
206 assert( p->nParam < count(p->azValue) );
207 p->azName[p->nParam] = zName;
208 p->azValue[p->nParam] = zValue;
209 p->nParam++;
210 }
211
212 /*
213 ** Render the URL with a parameter override.
214 */
215 char *url_render(
216 HQuery *p, /* Base URL */
217 const char *zName1, /* First override */
218 const char *zValue1, /* First override value */
219 const char *zName2, /* Second override */
220 const char *zValue2 /* Second override value */
221 ){
222 const char *zSep = "?";
223 int i;
224
225 blob_reset(&p->url);
226 blob_appendf(&p->url, "%s/%s", g.zBaseURL, p->zBase);
227 for(i=0; i<p->nParam; i++){
228 const char *z = p->azValue[i];
229 if( zName1 && strcmp(zName1,p->azName[i])==0 ){
230 zName1 = 0;
231 z = zValue1;
232 if( z==0 ) continue;
233 }
234 if( zName2 && strcmp(zName2,p->azName[i])==0 ){
235 zName2 = 0;
236 z = zValue2;
237 if( z==0 ) continue;
238 }
239 blob_appendf(&p->url, "%s%s=%T", zSep, p->azName[i], z);
240 zSep = "&";
241 }
242 if( zName1 && zValue1 ){
243 blob_appendf(&p->url, "%s%s=%T", zSep, zName1, zValue1);
244 }
245 if( zName2 && zValue2 ){
246 blob_appendf(&p->url, "%s%s=%T", zSep, zName2, zValue2);
247 }
248 return blob_str(&p->url);
249 }
250

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button