Fossil SCM

Add the option to sort files by size in the tree-view.

drh 2023-07-22 14:29 filesize-listings
Commit dedae5a1239eed64ebde12224356dfc216f2a08813431f268ac9732a0a62df82
1 file changed +36 -16
+36 -16
--- src/browse.c
+++ src/browse.c
@@ -442,10 +442,11 @@
442442
FileTreeNode *pLastChild; /* Last child on the pChild list */
443443
char *zName; /* Name of this entry. The "tail" */
444444
char *zFullName; /* Full pathname of this entry */
445445
char *zUuid; /* Artifact hash of this file. May be NULL. */
446446
double mtime; /* Modification time for this entry */
447
+ double sortBy; /* Either mtime or size, depending on desired sort order */
447448
int iSize; /* Size for this entry */
448449
unsigned nFullName; /* Length of zFullName */
449450
unsigned iLevel; /* Levels of parent directories */
450451
};
451452
@@ -473,11 +474,12 @@
473474
static void tree_add_node(
474475
FileTree *pTree, /* Tree into which nodes are added */
475476
const char *zPath, /* The full pathname of file to add */
476477
const char *zUuid, /* Hash of the file. Might be NULL. */
477478
double mtime, /* Modification time for this entry */
478
- int size /* Size for this entry */
479
+ int size, /* Size for this entry */
480
+ int sortOrder /* 0: filename, 1: mtime, 2: size */
479481
){
480482
int i;
481483
FileTreeNode *pParent; /* Parent (directory) of the next node to insert */
482484
//fossil_print("<pre>zPath %s zUuid %s mtime %f size %d</pre>\n",zPath,zUuid,mtime,size);
483485
/* Make pParent point to the most recent ancestor of zPath, or
@@ -528,10 +530,15 @@
528530
if( pTree->pLastTop ) pTree->pLastTop->pSibling = pNew;
529531
pTree->pLastTop = pNew;
530532
}
531533
pNew->mtime = mtime;
532534
pNew->iSize = size;
535
+ if( sortOrder ){
536
+ pNew->sortBy = sortOrder==1 ? mtime : (double)size;
537
+ }else{
538
+ pNew->sortBy = 0.0;
539
+ }
533540
while( zPath[i]=='/' ){ i++; }
534541
pParent = pNew;
535542
}
536543
while( pParent && pParent->pParent ){
537544
if( pParent->pParent->mtime < pParent->mtime ){
@@ -540,19 +547,22 @@
540547
pParent = pParent->pParent;
541548
}
542549
}
543550
544551
/* Comparison function for two FileTreeNode objects. Sort first by
545
-** mtime (larger numbers first) and then by zName (smaller names first).
552
+** sortBy (larger numbers first) and then by zName (smaller names first).
553
+**
554
+** The sortBy field will be the same as mtime in order to sort by time,
555
+** or the same as iSize to sort by file size.
546556
**
547557
** Return negative if pLeft<pRight.
548558
** Return positive if pLeft>pRight.
549559
** Return zero if pLeft==pRight.
550560
*/
551561
static int compareNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
552
- if( pLeft->mtime>pRight->mtime ) return -1;
553
- if( pLeft->mtime<pRight->mtime ) return +1;
562
+ if( pLeft->sortBy>pRight->sortBy ) return -1;
563
+ if( pLeft->sortBy<pRight->sortBy ) return +1;
554564
return fossil_stricmp(pLeft->zName, pRight->zName);
555565
}
556566
557567
/* Merge together two sorted lists of FileTreeNode objects */
558568
static FileTreeNode *mergeNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
@@ -574,12 +584,12 @@
574584
pEnd->pSibling = pRight;
575585
}
576586
return base.pSibling;
577587
}
578588
579
-/* Sort a list of FileTreeNode objects in mtime order. */
580
-static FileTreeNode *sortNodesByMtime(FileTreeNode *p){
589
+/* Sort a list of FileTreeNode objects in sortmtime order. */
590
+static FileTreeNode *sortNodes(FileTreeNode *p){
581591
FileTreeNode *a[30];
582592
FileTreeNode *pX;
583593
int i;
584594
585595
memset(a, 0, sizeof(a));
@@ -607,16 +617,16 @@
607617
** FileTreeNode.pLastChild
608618
** FileTreeNode.pNext
609619
**
610620
** Use relinkTree to reconnect the pNext pointers.
611621
*/
612
-static FileTreeNode *sortTreeByMtime(FileTreeNode *p){
622
+static FileTreeNode *sortTree(FileTreeNode *p){
613623
FileTreeNode *pX;
614624
for(pX=p; pX; pX=pX->pSibling){
615
- if( pX->pChild ) pX->pChild = sortTreeByMtime(pX->pChild);
625
+ if( pX->pChild ) pX->pChild = sortTree(pX->pChild);
616626
}
617
- return sortNodesByMtime(p);
627
+ return sortNodes(p);
618628
}
619629
620630
/* Reconstruct the FileTree by reconnecting the FileTreeNode.pNext
621631
** fields in sequential order.
622632
*/
@@ -651,11 +661,11 @@
651661
** name=PATH Directory to display. Optional
652662
** ci=LABEL Show only files in this check-in. Optional.
653663
** re=REGEXP Show only files matching REGEXP. Optional.
654664
** expand Begin with the tree fully expanded.
655665
** nofiles Show directories (folders) only. Omit files.
656
-** mtime Order directory elements by decreasing mtime
666
+** sort 0: by filename, 1: by mtime, 2: by size
657667
*/
658668
void page_tree(void){
659669
char *zD = fossil_strdup(P("name"));
660670
int nD = zD ? strlen(zD)+1 : 0;
661671
const char *zCI = P("ci");
@@ -664,10 +674,11 @@
664674
Blob dirname;
665675
Manifest *pM = 0;
666676
double rNow = 0;
667677
char *zNow = 0;
668678
int useMtime = atoi(PD("mtime","0"));
679
+ int sortOrder = atoi(PD("sort",useMtime?"1":"0"));
669680
int linkTrunk = 1; /* include link to "trunk" */
670681
int linkTip = 1; /* include link to "tip" */
671682
const char *zRE; /* the value for the re=REGEXP query parameter */
672683
const char *zObjType; /* "files" by default or "folders" for "nofiles" */
673684
char *zREx = ""; /* Extra parameters for path hyperlinks */
@@ -768,11 +779,18 @@
768779
style_submenu_element("Top-Level", "%s",
769780
url_render(&sURI, "name", 0, 0, 0));
770781
}else if( zRE ){
771782
blob_appendf(&dirname, "matching \"%s\"", zRE);
772783
}
773
- style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0);
784
+ {
785
+ static const char *const sort_orders[] = {
786
+ "0", "Sort By Filename",
787
+ "1", "Sort By Age",
788
+ "2", "Sort By Size"
789
+ };
790
+ style_submenu_multichoice("sort", 3, sort_orders, 0);
791
+ }
774792
if( zCI ){
775793
style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
776794
if( nD==0 && !showDirOnly ){
777795
style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
778796
}
@@ -806,11 +824,11 @@
806824
double mtime = db_column_double(&q,3);
807825
if( nD>0 && (fossil_strncmp(zFile, zD, nD-1)!=0 || zFile[nD-1]!='/') ){
808826
continue;
809827
}
810828
if( pRE && re_match(pRE, (const unsigned char*)zFile, -1)==0 ) continue;
811
- tree_add_node(&sTree, zFile, zUuid, mtime, size);
829
+ tree_add_node(&sTree, zFile, zUuid, mtime, size, sortOrder);
812830
}
813831
db_finalize(&q);
814832
}else{
815833
Stmt q;
816834
db_prepare(&q,
@@ -829,11 +847,11 @@
829847
double mtime = db_column_double(&q,3);
830848
if( nD>0 && (fossil_strncmp(zName, zD, nD-1)!=0 || zName[nD-1]!='/') ){
831849
continue;
832850
}
833851
if( pRE && re_match(pRE, (const u8*)zName, -1)==0 ) continue;
834
- tree_add_node(&sTree, zName, zUuid, mtime, size);
852
+ tree_add_node(&sTree, zName, zUuid, mtime, size, sortOrder);
835853
}
836854
db_finalize(&q);
837855
}
838856
style_submenu_checkbox("nofiles", "Folders Only", 0, 0);
839857
@@ -859,12 +877,14 @@
859877
}
860878
}else{
861879
int n = db_int(0, "SELECT count(*) FROM plink");
862880
@ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname))
863881
}
864
- if( useMtime ){
882
+ if( sortOrder==1 ){
865883
@ sorted by modification time</h2>
884
+ }else if( sortOrder==2 ){
885
+ @ sorted by size</h2>
866886
}else{
867887
@ sorted by filename</h2>
868888
}
869889
870890
if( zNow ){
@@ -894,12 +914,12 @@
894914
if( zNow ){
895915
@ <div class="filetreeage">%s(zNow)</div>
896916
}
897917
@ </div>
898918
@ <ul>
899
- if( useMtime ){
900
- p = sortTreeByMtime(sTree.pFirst);
919
+ if( sortOrder ){
920
+ p = sortTree(sTree.pFirst);
901921
memset(&sTree, 0, sizeof(sTree));
902922
relinkTree(&sTree, p);
903923
}
904924
for(p=sTree.pFirst, nDir=0; p; p=p->pNext){
905925
const char *zLastClass = p->pSibling==0 ? " last" : "";
906926
--- src/browse.c
+++ src/browse.c
@@ -442,10 +442,11 @@
442 FileTreeNode *pLastChild; /* Last child on the pChild list */
443 char *zName; /* Name of this entry. The "tail" */
444 char *zFullName; /* Full pathname of this entry */
445 char *zUuid; /* Artifact hash of this file. May be NULL. */
446 double mtime; /* Modification time for this entry */
 
447 int iSize; /* Size for this entry */
448 unsigned nFullName; /* Length of zFullName */
449 unsigned iLevel; /* Levels of parent directories */
450 };
451
@@ -473,11 +474,12 @@
473 static void tree_add_node(
474 FileTree *pTree, /* Tree into which nodes are added */
475 const char *zPath, /* The full pathname of file to add */
476 const char *zUuid, /* Hash of the file. Might be NULL. */
477 double mtime, /* Modification time for this entry */
478 int size /* Size for this entry */
 
479 ){
480 int i;
481 FileTreeNode *pParent; /* Parent (directory) of the next node to insert */
482 //fossil_print("<pre>zPath %s zUuid %s mtime %f size %d</pre>\n",zPath,zUuid,mtime,size);
483 /* Make pParent point to the most recent ancestor of zPath, or
@@ -528,10 +530,15 @@
528 if( pTree->pLastTop ) pTree->pLastTop->pSibling = pNew;
529 pTree->pLastTop = pNew;
530 }
531 pNew->mtime = mtime;
532 pNew->iSize = size;
 
 
 
 
 
533 while( zPath[i]=='/' ){ i++; }
534 pParent = pNew;
535 }
536 while( pParent && pParent->pParent ){
537 if( pParent->pParent->mtime < pParent->mtime ){
@@ -540,19 +547,22 @@
540 pParent = pParent->pParent;
541 }
542 }
543
544 /* Comparison function for two FileTreeNode objects. Sort first by
545 ** mtime (larger numbers first) and then by zName (smaller names first).
 
 
 
546 **
547 ** Return negative if pLeft<pRight.
548 ** Return positive if pLeft>pRight.
549 ** Return zero if pLeft==pRight.
550 */
551 static int compareNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
552 if( pLeft->mtime>pRight->mtime ) return -1;
553 if( pLeft->mtime<pRight->mtime ) return +1;
554 return fossil_stricmp(pLeft->zName, pRight->zName);
555 }
556
557 /* Merge together two sorted lists of FileTreeNode objects */
558 static FileTreeNode *mergeNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
@@ -574,12 +584,12 @@
574 pEnd->pSibling = pRight;
575 }
576 return base.pSibling;
577 }
578
579 /* Sort a list of FileTreeNode objects in mtime order. */
580 static FileTreeNode *sortNodesByMtime(FileTreeNode *p){
581 FileTreeNode *a[30];
582 FileTreeNode *pX;
583 int i;
584
585 memset(a, 0, sizeof(a));
@@ -607,16 +617,16 @@
607 ** FileTreeNode.pLastChild
608 ** FileTreeNode.pNext
609 **
610 ** Use relinkTree to reconnect the pNext pointers.
611 */
612 static FileTreeNode *sortTreeByMtime(FileTreeNode *p){
613 FileTreeNode *pX;
614 for(pX=p; pX; pX=pX->pSibling){
615 if( pX->pChild ) pX->pChild = sortTreeByMtime(pX->pChild);
616 }
617 return sortNodesByMtime(p);
618 }
619
620 /* Reconstruct the FileTree by reconnecting the FileTreeNode.pNext
621 ** fields in sequential order.
622 */
@@ -651,11 +661,11 @@
651 ** name=PATH Directory to display. Optional
652 ** ci=LABEL Show only files in this check-in. Optional.
653 ** re=REGEXP Show only files matching REGEXP. Optional.
654 ** expand Begin with the tree fully expanded.
655 ** nofiles Show directories (folders) only. Omit files.
656 ** mtime Order directory elements by decreasing mtime
657 */
658 void page_tree(void){
659 char *zD = fossil_strdup(P("name"));
660 int nD = zD ? strlen(zD)+1 : 0;
661 const char *zCI = P("ci");
@@ -664,10 +674,11 @@
664 Blob dirname;
665 Manifest *pM = 0;
666 double rNow = 0;
667 char *zNow = 0;
668 int useMtime = atoi(PD("mtime","0"));
 
669 int linkTrunk = 1; /* include link to "trunk" */
670 int linkTip = 1; /* include link to "tip" */
671 const char *zRE; /* the value for the re=REGEXP query parameter */
672 const char *zObjType; /* "files" by default or "folders" for "nofiles" */
673 char *zREx = ""; /* Extra parameters for path hyperlinks */
@@ -768,11 +779,18 @@
768 style_submenu_element("Top-Level", "%s",
769 url_render(&sURI, "name", 0, 0, 0));
770 }else if( zRE ){
771 blob_appendf(&dirname, "matching \"%s\"", zRE);
772 }
773 style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0);
 
 
 
 
 
 
 
774 if( zCI ){
775 style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
776 if( nD==0 && !showDirOnly ){
777 style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
778 }
@@ -806,11 +824,11 @@
806 double mtime = db_column_double(&q,3);
807 if( nD>0 && (fossil_strncmp(zFile, zD, nD-1)!=0 || zFile[nD-1]!='/') ){
808 continue;
809 }
810 if( pRE && re_match(pRE, (const unsigned char*)zFile, -1)==0 ) continue;
811 tree_add_node(&sTree, zFile, zUuid, mtime, size);
812 }
813 db_finalize(&q);
814 }else{
815 Stmt q;
816 db_prepare(&q,
@@ -829,11 +847,11 @@
829 double mtime = db_column_double(&q,3);
830 if( nD>0 && (fossil_strncmp(zName, zD, nD-1)!=0 || zName[nD-1]!='/') ){
831 continue;
832 }
833 if( pRE && re_match(pRE, (const u8*)zName, -1)==0 ) continue;
834 tree_add_node(&sTree, zName, zUuid, mtime, size);
835 }
836 db_finalize(&q);
837 }
838 style_submenu_checkbox("nofiles", "Folders Only", 0, 0);
839
@@ -859,12 +877,14 @@
859 }
860 }else{
861 int n = db_int(0, "SELECT count(*) FROM plink");
862 @ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname))
863 }
864 if( useMtime ){
865 @ sorted by modification time</h2>
 
 
866 }else{
867 @ sorted by filename</h2>
868 }
869
870 if( zNow ){
@@ -894,12 +914,12 @@
894 if( zNow ){
895 @ <div class="filetreeage">%s(zNow)</div>
896 }
897 @ </div>
898 @ <ul>
899 if( useMtime ){
900 p = sortTreeByMtime(sTree.pFirst);
901 memset(&sTree, 0, sizeof(sTree));
902 relinkTree(&sTree, p);
903 }
904 for(p=sTree.pFirst, nDir=0; p; p=p->pNext){
905 const char *zLastClass = p->pSibling==0 ? " last" : "";
906
--- src/browse.c
+++ src/browse.c
@@ -442,10 +442,11 @@
442 FileTreeNode *pLastChild; /* Last child on the pChild list */
443 char *zName; /* Name of this entry. The "tail" */
444 char *zFullName; /* Full pathname of this entry */
445 char *zUuid; /* Artifact hash of this file. May be NULL. */
446 double mtime; /* Modification time for this entry */
447 double sortBy; /* Either mtime or size, depending on desired sort order */
448 int iSize; /* Size for this entry */
449 unsigned nFullName; /* Length of zFullName */
450 unsigned iLevel; /* Levels of parent directories */
451 };
452
@@ -473,11 +474,12 @@
474 static void tree_add_node(
475 FileTree *pTree, /* Tree into which nodes are added */
476 const char *zPath, /* The full pathname of file to add */
477 const char *zUuid, /* Hash of the file. Might be NULL. */
478 double mtime, /* Modification time for this entry */
479 int size, /* Size for this entry */
480 int sortOrder /* 0: filename, 1: mtime, 2: size */
481 ){
482 int i;
483 FileTreeNode *pParent; /* Parent (directory) of the next node to insert */
484 //fossil_print("<pre>zPath %s zUuid %s mtime %f size %d</pre>\n",zPath,zUuid,mtime,size);
485 /* Make pParent point to the most recent ancestor of zPath, or
@@ -528,10 +530,15 @@
530 if( pTree->pLastTop ) pTree->pLastTop->pSibling = pNew;
531 pTree->pLastTop = pNew;
532 }
533 pNew->mtime = mtime;
534 pNew->iSize = size;
535 if( sortOrder ){
536 pNew->sortBy = sortOrder==1 ? mtime : (double)size;
537 }else{
538 pNew->sortBy = 0.0;
539 }
540 while( zPath[i]=='/' ){ i++; }
541 pParent = pNew;
542 }
543 while( pParent && pParent->pParent ){
544 if( pParent->pParent->mtime < pParent->mtime ){
@@ -540,19 +547,22 @@
547 pParent = pParent->pParent;
548 }
549 }
550
551 /* Comparison function for two FileTreeNode objects. Sort first by
552 ** sortBy (larger numbers first) and then by zName (smaller names first).
553 **
554 ** The sortBy field will be the same as mtime in order to sort by time,
555 ** or the same as iSize to sort by file size.
556 **
557 ** Return negative if pLeft<pRight.
558 ** Return positive if pLeft>pRight.
559 ** Return zero if pLeft==pRight.
560 */
561 static int compareNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
562 if( pLeft->sortBy>pRight->sortBy ) return -1;
563 if( pLeft->sortBy<pRight->sortBy ) return +1;
564 return fossil_stricmp(pLeft->zName, pRight->zName);
565 }
566
567 /* Merge together two sorted lists of FileTreeNode objects */
568 static FileTreeNode *mergeNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
@@ -574,12 +584,12 @@
584 pEnd->pSibling = pRight;
585 }
586 return base.pSibling;
587 }
588
589 /* Sort a list of FileTreeNode objects in sortmtime order. */
590 static FileTreeNode *sortNodes(FileTreeNode *p){
591 FileTreeNode *a[30];
592 FileTreeNode *pX;
593 int i;
594
595 memset(a, 0, sizeof(a));
@@ -607,16 +617,16 @@
617 ** FileTreeNode.pLastChild
618 ** FileTreeNode.pNext
619 **
620 ** Use relinkTree to reconnect the pNext pointers.
621 */
622 static FileTreeNode *sortTree(FileTreeNode *p){
623 FileTreeNode *pX;
624 for(pX=p; pX; pX=pX->pSibling){
625 if( pX->pChild ) pX->pChild = sortTree(pX->pChild);
626 }
627 return sortNodes(p);
628 }
629
630 /* Reconstruct the FileTree by reconnecting the FileTreeNode.pNext
631 ** fields in sequential order.
632 */
@@ -651,11 +661,11 @@
661 ** name=PATH Directory to display. Optional
662 ** ci=LABEL Show only files in this check-in. Optional.
663 ** re=REGEXP Show only files matching REGEXP. Optional.
664 ** expand Begin with the tree fully expanded.
665 ** nofiles Show directories (folders) only. Omit files.
666 ** sort 0: by filename, 1: by mtime, 2: by size
667 */
668 void page_tree(void){
669 char *zD = fossil_strdup(P("name"));
670 int nD = zD ? strlen(zD)+1 : 0;
671 const char *zCI = P("ci");
@@ -664,10 +674,11 @@
674 Blob dirname;
675 Manifest *pM = 0;
676 double rNow = 0;
677 char *zNow = 0;
678 int useMtime = atoi(PD("mtime","0"));
679 int sortOrder = atoi(PD("sort",useMtime?"1":"0"));
680 int linkTrunk = 1; /* include link to "trunk" */
681 int linkTip = 1; /* include link to "tip" */
682 const char *zRE; /* the value for the re=REGEXP query parameter */
683 const char *zObjType; /* "files" by default or "folders" for "nofiles" */
684 char *zREx = ""; /* Extra parameters for path hyperlinks */
@@ -768,11 +779,18 @@
779 style_submenu_element("Top-Level", "%s",
780 url_render(&sURI, "name", 0, 0, 0));
781 }else if( zRE ){
782 blob_appendf(&dirname, "matching \"%s\"", zRE);
783 }
784 {
785 static const char *const sort_orders[] = {
786 "0", "Sort By Filename",
787 "1", "Sort By Age",
788 "2", "Sort By Size"
789 };
790 style_submenu_multichoice("sort", 3, sort_orders, 0);
791 }
792 if( zCI ){
793 style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
794 if( nD==0 && !showDirOnly ){
795 style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
796 }
@@ -806,11 +824,11 @@
824 double mtime = db_column_double(&q,3);
825 if( nD>0 && (fossil_strncmp(zFile, zD, nD-1)!=0 || zFile[nD-1]!='/') ){
826 continue;
827 }
828 if( pRE && re_match(pRE, (const unsigned char*)zFile, -1)==0 ) continue;
829 tree_add_node(&sTree, zFile, zUuid, mtime, size, sortOrder);
830 }
831 db_finalize(&q);
832 }else{
833 Stmt q;
834 db_prepare(&q,
@@ -829,11 +847,11 @@
847 double mtime = db_column_double(&q,3);
848 if( nD>0 && (fossil_strncmp(zName, zD, nD-1)!=0 || zName[nD-1]!='/') ){
849 continue;
850 }
851 if( pRE && re_match(pRE, (const u8*)zName, -1)==0 ) continue;
852 tree_add_node(&sTree, zName, zUuid, mtime, size, sortOrder);
853 }
854 db_finalize(&q);
855 }
856 style_submenu_checkbox("nofiles", "Folders Only", 0, 0);
857
@@ -859,12 +877,14 @@
877 }
878 }else{
879 int n = db_int(0, "SELECT count(*) FROM plink");
880 @ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname))
881 }
882 if( sortOrder==1 ){
883 @ sorted by modification time</h2>
884 }else if( sortOrder==2 ){
885 @ sorted by size</h2>
886 }else{
887 @ sorted by filename</h2>
888 }
889
890 if( zNow ){
@@ -894,12 +914,12 @@
914 if( zNow ){
915 @ <div class="filetreeage">%s(zNow)</div>
916 }
917 @ </div>
918 @ <ul>
919 if( sortOrder ){
920 p = sortTree(sTree.pFirst);
921 memset(&sTree, 0, sizeof(sTree));
922 relinkTree(&sTree, p);
923 }
924 for(p=sTree.pFirst, nDir=0; p; p=p->pNext){
925 const char *zLastClass = p->pSibling==0 ? " last" : "";
926

Keyboard Shortcuts

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