Fossil SCM

New options to the "fossil http" command: --in FILE, --out FILE, --ipaddr ADDRESS, and --nodelay. Use the --in, --out, and --inaddr options for subprocesses that handle HTTP requests via file I/O. This replaced the older and undocumented form of the "fossil http" command that accepted extra arguments. Use the --nodelay option to prevent lengthy backoffice processing. The use of --nodelay during "fossil ui" on Windows prevents annoying pauses on that platform.

drh 2018-07-22 18:58 trunk
Commit 52943029e201bddac24ed24b79690c1093a58e74baf02063d52346bb25e4c336
--- src/backoffice.c
+++ src/backoffice.c
@@ -69,10 +69,26 @@
6969
sqlite3_uint64 tmCurrent; /* Expiration of the current lease */
7070
sqlite3_uint64 idNext; /* ID for the next lease holder on queue */
7171
sqlite3_uint64 tmNext; /* Expiration of the next lease */
7272
};
7373
#endif
74
+
75
+/*
76
+** Set to prevent backoffice processing from every entering sleep or
77
+** otherwise taking a long time to complete. Set this when a user-visible
78
+** process might need to wait for backoffice to complete.
79
+*/
80
+static int backofficeNoDelay = 0;
81
+
82
+
83
+/*
84
+** Disable the backoffice
85
+*/
86
+void backoffice_no_delay(void){
87
+ backofficeNoDelay = 1;
88
+}
89
+
7490
7591
/*
7692
** Parse a unsigned 64-bit integer from a string. Return a pointer
7793
** to the character of z[] that occurs after the integer.
7894
*/
@@ -227,10 +243,17 @@
227243
fprintf(stderr, "/***** Begin Backoffice Processing %d *****/\n",
228244
getpid());
229245
}
230246
backoffice_work();
231247
break;
248
+ }
249
+ if( backofficeNoDelay ){
250
+ /* If the no-delay flag is set, exit immediately rather than queuing
251
+ ** up. Assume that some future request will come along and handle any
252
+ ** necessary backoffice work. */
253
+ db_end_transaction(0);
254
+ break;
232255
}
233256
/* This process needs to queue up and wait for the current lease
234257
** to expire before continuing. */
235258
x.idNext = idSelf;
236259
x.tmNext = (tmNow>x.tmCurrent ? tmNow : x.tmCurrent) + BKOFCE_LEASE_TIME;
237260
--- src/backoffice.c
+++ src/backoffice.c
@@ -69,10 +69,26 @@
69 sqlite3_uint64 tmCurrent; /* Expiration of the current lease */
70 sqlite3_uint64 idNext; /* ID for the next lease holder on queue */
71 sqlite3_uint64 tmNext; /* Expiration of the next lease */
72 };
73 #endif
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
75 /*
76 ** Parse a unsigned 64-bit integer from a string. Return a pointer
77 ** to the character of z[] that occurs after the integer.
78 */
@@ -227,10 +243,17 @@
227 fprintf(stderr, "/***** Begin Backoffice Processing %d *****/\n",
228 getpid());
229 }
230 backoffice_work();
231 break;
 
 
 
 
 
 
 
232 }
233 /* This process needs to queue up and wait for the current lease
234 ** to expire before continuing. */
235 x.idNext = idSelf;
236 x.tmNext = (tmNow>x.tmCurrent ? tmNow : x.tmCurrent) + BKOFCE_LEASE_TIME;
237
--- src/backoffice.c
+++ src/backoffice.c
@@ -69,10 +69,26 @@
69 sqlite3_uint64 tmCurrent; /* Expiration of the current lease */
70 sqlite3_uint64 idNext; /* ID for the next lease holder on queue */
71 sqlite3_uint64 tmNext; /* Expiration of the next lease */
72 };
73 #endif
74
75 /*
76 ** Set to prevent backoffice processing from every entering sleep or
77 ** otherwise taking a long time to complete. Set this when a user-visible
78 ** process might need to wait for backoffice to complete.
79 */
80 static int backofficeNoDelay = 0;
81
82
83 /*
84 ** Disable the backoffice
85 */
86 void backoffice_no_delay(void){
87 backofficeNoDelay = 1;
88 }
89
90
91 /*
92 ** Parse a unsigned 64-bit integer from a string. Return a pointer
93 ** to the character of z[] that occurs after the integer.
94 */
@@ -227,10 +243,17 @@
243 fprintf(stderr, "/***** Begin Backoffice Processing %d *****/\n",
244 getpid());
245 }
246 backoffice_work();
247 break;
248 }
249 if( backofficeNoDelay ){
250 /* If the no-delay flag is set, exit immediately rather than queuing
251 ** up. Assume that some future request will come along and handle any
252 ** necessary backoffice work. */
253 db_end_transaction(0);
254 break;
255 }
256 /* This process needs to queue up and wait for the current lease
257 ** to expire before continuing. */
258 x.idNext = idSelf;
259 x.tmNext = (tmNow>x.tmCurrent ? tmNow : x.tmCurrent) + BKOFCE_LEASE_TIME;
260
--- src/http_transport.c
+++ src/http_transport.c
@@ -269,11 +269,12 @@
269269
*/
270270
void transport_flip(UrlData *pUrlData){
271271
if( pUrlData->isFile ){
272272
char *zCmd;
273273
fclose(transport.pFile);
274
- zCmd = mprintf("\"%s\" http \"%s\" \"%s\" 127.0.0.1 \"%s\" --localauth",
274
+ zCmd = mprintf("\"%s\" http --in \"%s\" --out \"%s\" --ipaddr 127.0.0.1"
275
+ " \"%s\" --localauth --nodelay",
275276
g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name
276277
);
277278
fossil_system(zCmd);
278279
free(zCmd);
279280
transport.pFile = fossil_fopen(transport.zInFile, "rb");
280281
--- src/http_transport.c
+++ src/http_transport.c
@@ -269,11 +269,12 @@
269 */
270 void transport_flip(UrlData *pUrlData){
271 if( pUrlData->isFile ){
272 char *zCmd;
273 fclose(transport.pFile);
274 zCmd = mprintf("\"%s\" http \"%s\" \"%s\" 127.0.0.1 \"%s\" --localauth",
 
275 g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name
276 );
277 fossil_system(zCmd);
278 free(zCmd);
279 transport.pFile = fossil_fopen(transport.zInFile, "rb");
280
--- src/http_transport.c
+++ src/http_transport.c
@@ -269,11 +269,12 @@
269 */
270 void transport_flip(UrlData *pUrlData){
271 if( pUrlData->isFile ){
272 char *zCmd;
273 fclose(transport.pFile);
274 zCmd = mprintf("\"%s\" http --in \"%s\" --out \"%s\" --ipaddr 127.0.0.1"
275 " \"%s\" --localauth --nodelay",
276 g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name
277 );
278 fossil_system(zCmd);
279 free(zCmd);
280 transport.pFile = fossil_fopen(transport.zInFile, "rb");
281
+25 -20
--- src/main.c
+++ src/main.c
@@ -2254,16 +2254,10 @@
22542254
}
22552255
}
22562256
#endif
22572257
22582258
/*
2259
-** undocumented format:
2260
-**
2261
-** fossil http INFILE OUTFILE IPADDR ?REPOSITORY?
2262
-**
2263
-** The argv==6 form (with no options) is used by the win32 server only.
2264
-**
22652259
** COMMAND: http*
22662260
**
22672261
** Usage: %fossil http ?REPOSITORY? ?OPTIONS?
22682262
**
22692263
** Handle a single HTTP request appearing on stdin. The resulting webpage
@@ -2297,14 +2291,18 @@
22972291
** --baseurl URL base URL (useful with reverse proxies)
22982292
** --files GLOB comma-separate glob patterns for static file to serve
22992293
** --localauth enable automatic login for local connections
23002294
** --host NAME specify hostname of the server
23012295
** --https signal a request coming in via https
2302
-** --nocompress Do not compress HTTP replies
2296
+** --in FILE Take input from FILE instead of standard input
2297
+** --ipaddr ADDR Assume the request comes from the given IP address
2298
+** --nocompress do not compress HTTP replies
2299
+** --nodelay omit backoffice processing if it would delay process exit
23032300
** --nojail drop root privilege but do not enter the chroot jail
23042301
** --nossl signal that no SSL connections are available
23052302
** --notfound URL use URL as "HTTP 404, object not found" page.
2303
+** --out FILE write results to FILE instead of to standard output
23062304
** --repolist If REPOSITORY is directory, URL "/" lists all repos
23072305
** --scgi Interpret input as SCGI rather than HTTP
23082306
** --skin LABEL Use override skin LABEL
23092307
** --th-trace trace TH1 execution (for debugging purposes)
23102308
** --usepidkey Use saved encryption key from parent process. This is
@@ -2316,10 +2314,12 @@
23162314
const char *zIpAddr = 0;
23172315
const char *zNotFound;
23182316
const char *zHost;
23192317
const char *zAltBase;
23202318
const char *zFileGlob;
2319
+ const char *zInFile;
2320
+ const char *zOutFile;
23212321
int useSCGI;
23222322
int noJail;
23232323
int allowRepoList;
23242324
#if defined(_WIN32) && USE_SEE
23252325
const char *zPidKey;
@@ -2344,12 +2344,28 @@
23442344
noJail = find_option("nojail",0,0)!=0;
23452345
allowRepoList = find_option("repolist",0,0)!=0;
23462346
g.useLocalauth = find_option("localauth", 0, 0)!=0;
23472347
g.sslNotAvailable = find_option("nossl", 0, 0)!=0;
23482348
g.fNoHttpCompress = find_option("nocompress",0,0)!=0;
2349
+ zInFile = find_option("in",0,1);
2350
+ if( zInFile ){
2351
+ g.httpIn = fossil_fopen(zInFile, "rb");
2352
+ if( g.httpIn==0 ) fossil_fatal("cannot open \"%s\" for reading", zInFile);
2353
+ }else{
2354
+ g.httpIn = stdin;
2355
+ }
2356
+ zOutFile = find_option("out",0,1);
2357
+ if( zOutFile ){
2358
+ g.httpOut = fossil_fopen(zOutFile, "wb");
2359
+ if( g.httpOut==0 ) fossil_fatal("cannot open \"%s\" for writing", zOutFile);
2360
+ }else{
2361
+ g.httpOut = stdout;
2362
+ }
2363
+ zIpAddr = find_option("ipaddr",0,1);
23492364
useSCGI = find_option("scgi", 0, 0)!=0;
23502365
zAltBase = find_option("baseurl", 0, 1);
2366
+ if( find_option("nodelay",0,0)!=0 ) backoffice_no_delay();
23512367
if( zAltBase ) set_base_url(zAltBase);
23522368
if( find_option("https",0,0)!=0 ){
23532369
zIpAddr = fossil_getenv("REMOTE_HOST"); /* From stunnel */
23542370
cgi_replace_parameter("HTTPS","on");
23552371
}
@@ -2368,25 +2384,14 @@
23682384
#endif
23692385
23702386
/* We should be done with options.. */
23712387
verify_all_options();
23722388
2373
- if( g.argc!=2 && g.argc!=3 && g.argc!=5 && g.argc!=6 ){
2374
- fossil_panic("no repository specified");
2375
- }
2389
+ if( g.argc!=2 && g.argc!=3 ) usage("?REPOSITORY?");
23762390
g.cgiOutput = 1;
23772391
g.fullHttpReply = 1;
2378
- if( g.argc>=5 ){
2379
- g.httpIn = fossil_fopen(g.argv[2], "rb");
2380
- g.httpOut = fossil_fopen(g.argv[3], "wb");
2381
- zIpAddr = g.argv[4];
2382
- find_server_repository(5, 0);
2383
- }else{
2384
- g.httpIn = stdin;
2385
- g.httpOut = stdout;
2386
- find_server_repository(2, 0);
2387
- }
2392
+ find_server_repository(2, 0);
23882393
if( zIpAddr==0 ){
23892394
zIpAddr = cgi_ssh_remote_addr(0);
23902395
if( zIpAddr && zIpAddr[0] ){
23912396
g.fSshClient |= CGI_SSH_CLIENT;
23922397
}
23932398
--- src/main.c
+++ src/main.c
@@ -2254,16 +2254,10 @@
2254 }
2255 }
2256 #endif
2257
2258 /*
2259 ** undocumented format:
2260 **
2261 ** fossil http INFILE OUTFILE IPADDR ?REPOSITORY?
2262 **
2263 ** The argv==6 form (with no options) is used by the win32 server only.
2264 **
2265 ** COMMAND: http*
2266 **
2267 ** Usage: %fossil http ?REPOSITORY? ?OPTIONS?
2268 **
2269 ** Handle a single HTTP request appearing on stdin. The resulting webpage
@@ -2297,14 +2291,18 @@
2297 ** --baseurl URL base URL (useful with reverse proxies)
2298 ** --files GLOB comma-separate glob patterns for static file to serve
2299 ** --localauth enable automatic login for local connections
2300 ** --host NAME specify hostname of the server
2301 ** --https signal a request coming in via https
2302 ** --nocompress Do not compress HTTP replies
 
 
 
2303 ** --nojail drop root privilege but do not enter the chroot jail
2304 ** --nossl signal that no SSL connections are available
2305 ** --notfound URL use URL as "HTTP 404, object not found" page.
 
2306 ** --repolist If REPOSITORY is directory, URL "/" lists all repos
2307 ** --scgi Interpret input as SCGI rather than HTTP
2308 ** --skin LABEL Use override skin LABEL
2309 ** --th-trace trace TH1 execution (for debugging purposes)
2310 ** --usepidkey Use saved encryption key from parent process. This is
@@ -2316,10 +2314,12 @@
2316 const char *zIpAddr = 0;
2317 const char *zNotFound;
2318 const char *zHost;
2319 const char *zAltBase;
2320 const char *zFileGlob;
 
 
2321 int useSCGI;
2322 int noJail;
2323 int allowRepoList;
2324 #if defined(_WIN32) && USE_SEE
2325 const char *zPidKey;
@@ -2344,12 +2344,28 @@
2344 noJail = find_option("nojail",0,0)!=0;
2345 allowRepoList = find_option("repolist",0,0)!=0;
2346 g.useLocalauth = find_option("localauth", 0, 0)!=0;
2347 g.sslNotAvailable = find_option("nossl", 0, 0)!=0;
2348 g.fNoHttpCompress = find_option("nocompress",0,0)!=0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2349 useSCGI = find_option("scgi", 0, 0)!=0;
2350 zAltBase = find_option("baseurl", 0, 1);
 
2351 if( zAltBase ) set_base_url(zAltBase);
2352 if( find_option("https",0,0)!=0 ){
2353 zIpAddr = fossil_getenv("REMOTE_HOST"); /* From stunnel */
2354 cgi_replace_parameter("HTTPS","on");
2355 }
@@ -2368,25 +2384,14 @@
2368 #endif
2369
2370 /* We should be done with options.. */
2371 verify_all_options();
2372
2373 if( g.argc!=2 && g.argc!=3 && g.argc!=5 && g.argc!=6 ){
2374 fossil_panic("no repository specified");
2375 }
2376 g.cgiOutput = 1;
2377 g.fullHttpReply = 1;
2378 if( g.argc>=5 ){
2379 g.httpIn = fossil_fopen(g.argv[2], "rb");
2380 g.httpOut = fossil_fopen(g.argv[3], "wb");
2381 zIpAddr = g.argv[4];
2382 find_server_repository(5, 0);
2383 }else{
2384 g.httpIn = stdin;
2385 g.httpOut = stdout;
2386 find_server_repository(2, 0);
2387 }
2388 if( zIpAddr==0 ){
2389 zIpAddr = cgi_ssh_remote_addr(0);
2390 if( zIpAddr && zIpAddr[0] ){
2391 g.fSshClient |= CGI_SSH_CLIENT;
2392 }
2393
--- src/main.c
+++ src/main.c
@@ -2254,16 +2254,10 @@
2254 }
2255 }
2256 #endif
2257
2258 /*
 
 
 
 
 
 
2259 ** COMMAND: http*
2260 **
2261 ** Usage: %fossil http ?REPOSITORY? ?OPTIONS?
2262 **
2263 ** Handle a single HTTP request appearing on stdin. The resulting webpage
@@ -2297,14 +2291,18 @@
2291 ** --baseurl URL base URL (useful with reverse proxies)
2292 ** --files GLOB comma-separate glob patterns for static file to serve
2293 ** --localauth enable automatic login for local connections
2294 ** --host NAME specify hostname of the server
2295 ** --https signal a request coming in via https
2296 ** --in FILE Take input from FILE instead of standard input
2297 ** --ipaddr ADDR Assume the request comes from the given IP address
2298 ** --nocompress do not compress HTTP replies
2299 ** --nodelay omit backoffice processing if it would delay process exit
2300 ** --nojail drop root privilege but do not enter the chroot jail
2301 ** --nossl signal that no SSL connections are available
2302 ** --notfound URL use URL as "HTTP 404, object not found" page.
2303 ** --out FILE write results to FILE instead of to standard output
2304 ** --repolist If REPOSITORY is directory, URL "/" lists all repos
2305 ** --scgi Interpret input as SCGI rather than HTTP
2306 ** --skin LABEL Use override skin LABEL
2307 ** --th-trace trace TH1 execution (for debugging purposes)
2308 ** --usepidkey Use saved encryption key from parent process. This is
@@ -2316,10 +2314,12 @@
2314 const char *zIpAddr = 0;
2315 const char *zNotFound;
2316 const char *zHost;
2317 const char *zAltBase;
2318 const char *zFileGlob;
2319 const char *zInFile;
2320 const char *zOutFile;
2321 int useSCGI;
2322 int noJail;
2323 int allowRepoList;
2324 #if defined(_WIN32) && USE_SEE
2325 const char *zPidKey;
@@ -2344,12 +2344,28 @@
2344 noJail = find_option("nojail",0,0)!=0;
2345 allowRepoList = find_option("repolist",0,0)!=0;
2346 g.useLocalauth = find_option("localauth", 0, 0)!=0;
2347 g.sslNotAvailable = find_option("nossl", 0, 0)!=0;
2348 g.fNoHttpCompress = find_option("nocompress",0,0)!=0;
2349 zInFile = find_option("in",0,1);
2350 if( zInFile ){
2351 g.httpIn = fossil_fopen(zInFile, "rb");
2352 if( g.httpIn==0 ) fossil_fatal("cannot open \"%s\" for reading", zInFile);
2353 }else{
2354 g.httpIn = stdin;
2355 }
2356 zOutFile = find_option("out",0,1);
2357 if( zOutFile ){
2358 g.httpOut = fossil_fopen(zOutFile, "wb");
2359 if( g.httpOut==0 ) fossil_fatal("cannot open \"%s\" for writing", zOutFile);
2360 }else{
2361 g.httpOut = stdout;
2362 }
2363 zIpAddr = find_option("ipaddr",0,1);
2364 useSCGI = find_option("scgi", 0, 0)!=0;
2365 zAltBase = find_option("baseurl", 0, 1);
2366 if( find_option("nodelay",0,0)!=0 ) backoffice_no_delay();
2367 if( zAltBase ) set_base_url(zAltBase);
2368 if( find_option("https",0,0)!=0 ){
2369 zIpAddr = fossil_getenv("REMOTE_HOST"); /* From stunnel */
2370 cgi_replace_parameter("HTTPS","on");
2371 }
@@ -2368,25 +2384,14 @@
2384 #endif
2385
2386 /* We should be done with options.. */
2387 verify_all_options();
2388
2389 if( g.argc!=2 && g.argc!=3 ) usage("?REPOSITORY?");
 
 
2390 g.cgiOutput = 1;
2391 g.fullHttpReply = 1;
2392 find_server_repository(2, 0);
 
 
 
 
 
 
 
 
 
2393 if( zIpAddr==0 ){
2394 zIpAddr = cgi_ssh_remote_addr(0);
2395 if( zIpAddr && zIpAddr[0] ){
2396 g.fSshClient |= CGI_SSH_CLIENT;
2397 }
2398
+4 -3
--- src/winhttp.c
+++ src/winhttp.c
@@ -387,24 +387,25 @@
387387
** with the local Fossil server started via the "ui" command.
388388
*/
389389
zIp = SocketAddr_toString(&p->addr);
390390
if( (p->flags & HTTP_SERVER_HAD_CHECKOUT)==0 ){
391391
assert( g.zRepositoryName && g.zRepositoryName[0] );
392
- sqlite3_snprintf(sizeof(zCmd), zCmd, "%s%s\n%s\n%s\n%s",
392
+ sqlite3_snprintf(sizeof(zCmd), zCmd, "%s--in %s\n--out %s\n--ipaddr %s\n%s",
393393
get_utf8_bom(0), zRequestFName, zReplyFName, zIp, g.zRepositoryName
394394
);
395395
}else{
396
- sqlite3_snprintf(sizeof(zCmd), zCmd, "%s%s\n%s\n%s",
396
+ sqlite3_snprintf(sizeof(zCmd), zCmd, "%s--in %s\n--out %s\n--ipaddr %s",
397397
get_utf8_bom(0), zRequestFName, zReplyFName, zIp
398398
);
399399
}
400400
fossil_free(zIp);
401401
aux = fossil_fopen(zCmdFName, "wb");
402402
if( aux==0 ) goto end_request;
403403
fwrite(zCmd, 1, strlen(zCmd), aux);
404404
405
- sqlite3_snprintf(sizeof(zCmd), zCmd, "\"%s\" http -args \"%s\" --nossl%s",
405
+ sqlite3_snprintf(sizeof(zCmd), zCmd,
406
+ "\"%s\" http -args \"%s\" --nossl --nodelay%s",
406407
g.nameOfExe, zCmdFName, p->zOptions
407408
);
408409
in = fossil_fopen(zReplyFName, "w+b");
409410
fflush(out);
410411
fflush(aux);
411412
--- src/winhttp.c
+++ src/winhttp.c
@@ -387,24 +387,25 @@
387 ** with the local Fossil server started via the "ui" command.
388 */
389 zIp = SocketAddr_toString(&p->addr);
390 if( (p->flags & HTTP_SERVER_HAD_CHECKOUT)==0 ){
391 assert( g.zRepositoryName && g.zRepositoryName[0] );
392 sqlite3_snprintf(sizeof(zCmd), zCmd, "%s%s\n%s\n%s\n%s",
393 get_utf8_bom(0), zRequestFName, zReplyFName, zIp, g.zRepositoryName
394 );
395 }else{
396 sqlite3_snprintf(sizeof(zCmd), zCmd, "%s%s\n%s\n%s",
397 get_utf8_bom(0), zRequestFName, zReplyFName, zIp
398 );
399 }
400 fossil_free(zIp);
401 aux = fossil_fopen(zCmdFName, "wb");
402 if( aux==0 ) goto end_request;
403 fwrite(zCmd, 1, strlen(zCmd), aux);
404
405 sqlite3_snprintf(sizeof(zCmd), zCmd, "\"%s\" http -args \"%s\" --nossl%s",
 
406 g.nameOfExe, zCmdFName, p->zOptions
407 );
408 in = fossil_fopen(zReplyFName, "w+b");
409 fflush(out);
410 fflush(aux);
411
--- src/winhttp.c
+++ src/winhttp.c
@@ -387,24 +387,25 @@
387 ** with the local Fossil server started via the "ui" command.
388 */
389 zIp = SocketAddr_toString(&p->addr);
390 if( (p->flags & HTTP_SERVER_HAD_CHECKOUT)==0 ){
391 assert( g.zRepositoryName && g.zRepositoryName[0] );
392 sqlite3_snprintf(sizeof(zCmd), zCmd, "%s--in %s\n--out %s\n--ipaddr %s\n%s",
393 get_utf8_bom(0), zRequestFName, zReplyFName, zIp, g.zRepositoryName
394 );
395 }else{
396 sqlite3_snprintf(sizeof(zCmd), zCmd, "%s--in %s\n--out %s\n--ipaddr %s",
397 get_utf8_bom(0), zRequestFName, zReplyFName, zIp
398 );
399 }
400 fossil_free(zIp);
401 aux = fossil_fopen(zCmdFName, "wb");
402 if( aux==0 ) goto end_request;
403 fwrite(zCmd, 1, strlen(zCmd), aux);
404
405 sqlite3_snprintf(sizeof(zCmd), zCmd,
406 "\"%s\" http -args \"%s\" --nossl --nodelay%s",
407 g.nameOfExe, zCmdFName, p->zOptions
408 );
409 in = fossil_fopen(zReplyFName, "w+b");
410 fflush(out);
411 fflush(aux);
412

Keyboard Shortcuts

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