Fossil SCM

Add the "Bugs" menu element on the default header. Progress on implementing bug tracking.

drh 2008-05-17 21:15 trunk
Commit 86ed68ba348bd71c9b50f57f9ce4116b50ef50af
2 files changed +87 -69 +4 -1
+87 -69
--- src/report.c
+++ src/report.c
@@ -34,39 +34,52 @@
3434
/*
3535
** WEBPAGE: /reportlist
3636
*/
3737
void view_list(void){
3838
Stmt q;
39
+ int rn = 0;
3940
4041
login_check_credentials();
41
- if( !g.okRdTkt ){ login_needed(); return; }
42
- style_header("Available Report Formats");
43
- db_prepare(&q, "SELECT rn, title, owner FROM reportfmt ORDER BY title");
44
- @ <p>Choose a report format from the following list:</p>
45
- @ <ol>
46
- while( db_step(&q)==SQLITE_ROW ){
47
- int rn = db_column_int(&q, 0);
48
- const char *zTitle = db_column_text(&q, 1);
49
- const char *zOwner = db_column_text(&q, 2);
50
- @ <li><a href="rptview?rn=%d(rn)"
51
- @ rel="nofollow">%h(zTitle)</a>&nbsp;&nbsp;&nbsp;
52
- if( g.okWrite && zOwner && zOwner[0] ){
53
- @ (by <i>%h(zOwner)</i>)
54
- }
55
- if( g.okWrTkt ){
56
- @ [<a href="rptedit?rn=%d(rn)&amp;copy=1" rel="nofollow">copy</a>]
57
- }
58
- if( g.okAdmin || (g.okWrTkt && zOwner && strcmp(g.zLogin,zOwner)==0) ){
59
- @ [<a href="rptedit?rn=%d(rn)" rel="nofollow">edit</a>]
60
- }
61
- @ [<a href="rptsql?rn=%d(rn)" rel="nofollow">sql</a>]
62
- @ </li>
63
- }
64
- if( g.okWrTkt ){
65
- @ <li><a href="rptnew">Create a new report format</a></li>
42
+ if( !g.okRdTkt && !g.okNewTkt ){ login_needed(); return; }
43
+ style_header("Bug Report Main Menu");
44
+ if( g.okNewTkt ){
45
+ @ <p>Enter a new bug report:</p>
46
+ @ <ol><li value="0"><a href="tktnew">New bug report</a></li></ol>
47
+ @
48
+ }
49
+ if( !g.okRdTkt ){
50
+ @ <p>You are not authorized to view existing bug reports.</p>
51
+ }else{
52
+ db_prepare(&q, "SELECT rn, title, owner FROM reportfmt ORDER BY title");
53
+ @ <p>Choose a report format from the following list:</p>
54
+ @ <ol>
55
+ while( db_step(&q)==SQLITE_ROW ){
56
+ rn = db_column_int(&q, 0);
57
+ const char *zTitle = db_column_text(&q, 1);
58
+ const char *zOwner = db_column_text(&q, 2);
59
+ @ <li value="%d(rn)"><a href="rptview?rn=%d(rn)"
60
+ @ rel="nofollow">%h(zTitle)</a>&nbsp;&nbsp;&nbsp;
61
+ if( g.okWrite && zOwner && zOwner[0] ){
62
+ @ (by <i>%h(zOwner)</i>)
63
+ }
64
+ if( g.okWrTkt ){
65
+ @ [<a href="rptedit?rn=%d(rn)&amp;copy=1" rel="nofollow">copy</a>]
66
+ }
67
+ if( g.okAdmin || (g.okWrTkt && zOwner && strcmp(g.zLogin,zOwner)==0) ){
68
+ @ [<a href="rptedit?rn=%d(rn)" rel="nofollow">edit</a>]
69
+ }
70
+ @ [<a href="rptsql?rn=%d(rn)" rel="nofollow">sql</a>]
71
+ @ </li>
72
+ }
6673
}
6774
@ </ol>
75
+ if( g.okWrTkt ){
76
+ @ <p>Create a new bug report display format:</p>
77
+ @ <ol>
78
+ @ <li value="%d(rn+1)"><a href="rptnew">New report format</a></li>
79
+ @ </ol>
80
+ }
6881
style_footer();
6982
}
7083
7184
/*
7285
** Remove whitespace from both ends of a string.
@@ -644,12 +657,16 @@
644657
645658
/*
646659
** The state of the report generation.
647660
*/
648661
struct GenerateHTML {
649
- int rn; /* Report number */
650
- int nCount; /* Row number */
662
+ int rn; /* Report number */
663
+ int nCount; /* Row number */
664
+ int nCol; /* Number of columns */
665
+ int isMultirow; /* True if multiple table rows per query result row */
666
+ int iNewRow; /* Index of first column that goes on separate row */
667
+ int iBg; /* Index of column that defines background color */
651668
};
652669
653670
/*
654671
** The callback function for db_query
655672
*/
@@ -661,106 +678,107 @@
661678
){
662679
struct GenerateHTML *pState = (struct GenerateHTML*)pUser;
663680
int i;
664681
int tn; /* Ticket number. (value of column named '#') */
665682
int rn; /* Report number */
666
- int ncol; /* Number of columns in the table */
667
- int multirow; /* True if multiple table rows per line of data */
668
- int newrowidx; /* Index of first column that goes on a separate row */
669
- int iBg = -1; /* Index of column that determines background color */
670683
char *zBg = 0; /* Use this background color */
671684
char zPage[30]; /* Text version of the ticket number */
672685
673686
/* Get the report number
674687
*/
675688
rn = pState->rn;
676689
677
- /* Figure out the number of columns, the column that determines background
678
- ** color, and whether or not this row of data is represented by multiple
679
- ** rows in the table.
680
- */
681
- ncol = 0;
682
- multirow = 0;
683
- newrowidx = -1;
684
- for(i=0; i<nArg; i++){
685
- if( azName[i][0]=='b' && strcmp(azName[i],"bgcolor")==0 ){
686
- zBg = azArg ? azArg[i] : 0;
687
- iBg = i;
688
- continue;
689
- }
690
- if( g.okWrite && azName[i][0]=='#' ){
691
- ncol++;
692
- }
693
- if( !multirow ){
694
- if( azName[i][0]=='_' ){
695
- multirow = 1;
696
- newrowidx = i;
697
- }else{
698
- ncol++;
699
- }
700
- }
701
- }
702
-
703
- /* The first time this routine is called, output a table header
704
- */
705
- if( pState->nCount==0 ){
690
+ /* Do initialization
691
+ */
692
+ if( pState->nCount==0 ){
693
+ /* Figure out the number of columns, the column that determines background
694
+ ** color, and whether or not this row of data is represented by multiple
695
+ ** rows in the table.
696
+ */
697
+ pState->nCol = 0;
698
+ pState->isMultirow = 0;
699
+ pState->iNewRow = -1;
700
+ pState->iBg = -1;
701
+ for(i=0; i<nArg; i++){
702
+ if( azName[i][0]=='b' && strcmp(azName[i],"bgcolor")==0 ){
703
+ pState->iBg = i;
704
+ continue;
705
+ }
706
+ if( g.okWrite && azName[i][0]=='#' ){
707
+ pState->nCol++;
708
+ }
709
+ if( !pState->isMultirow ){
710
+ if( azName[i][0]=='_' ){
711
+ pState->isMultirow = 1;
712
+ pState->iNewRow = i;
713
+ }else{
714
+ pState->nCol++;
715
+ }
716
+ }
717
+ }
718
+
719
+ /* The first time this routine is called, output a table header
720
+ */
706721
@ <tr>
707722
tn = -1;
708723
for(i=0; i<nArg; i++){
709724
char *zName = azName[i];
710
- if( i==iBg ) continue;
711
- if( newrowidx>=0 && i>=newrowidx ){
725
+ if( i==pState->iBg ) continue;
726
+ if( pState->iNewRow>=0 && i>=pState->iNewRow ){
712727
if( g.okWrite && tn>=0 ){
713728
@ <th>&nbsp;</th>
714729
tn = -1;
715730
}
716731
if( zName[0]=='_' ) zName++;
717
- @ </tr><tr><th colspan=%d(ncol)>%h(zName)</th>
732
+ @ </tr><tr><th colspan=%d(pState->nCol)>%h(zName)</th>
718733
}else{
719734
if( zName[0]=='#' ){
720735
tn = i;
736
+ }else{
737
+ @ <th>%h(zName)</th>
721738
}
722739
}
723740
}
724741
if( g.okWrite && tn>=0 ){
725742
@ <th>&nbsp;</th>
726743
}
727744
@ </tr>
728745
}
729746
if( azArg==0 ){
730
- @ <tr><td colspan="%d(ncol)">
747
+ @ <tr><td colspan="%d(pState->nCol)">
731748
@ <i>No records match the report criteria</i>
732749
@ </td></tr>
733750
return 0;
734751
}
735752
++pState->nCount;
736753
737754
/* Output the separator above each entry in a table which has multiple lines
738755
** per database entry.
739756
*/
740
- if( newrowidx>=0 ){
741
- @ <tr><td colspan=%d(ncol)><font size=1>&nbsp;</font></td></tr>
757
+ if( pState->iNewRow>=0 ){
758
+ @ <tr><td colspan=%d(pState->nCol)><font size=1>&nbsp;</font></td></tr>
742759
}
743760
744761
/* Output the data for this entry from the database
745762
*/
763
+ zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0;
746764
if( zBg==0 ) zBg = "white";
747765
@ <tr bgcolor="%h(zBg)">
748766
tn = 0;
749767
zPage[0] = 0;
750768
for(i=0; i<nArg; i++){
751769
char *zData;
752
- if( i==iBg ) continue;
770
+ if( i==pState->iBg ) continue;
753771
zData = azArg[i];
754772
if( zData==0 ) zData = "";
755
- if( newrowidx>=0 && i>=newrowidx ){
773
+ if( pState->iNewRow>=0 && i>=pState->iNewRow ){
756774
if( tn>0 && g.okWrite ){
757775
@ <td valign="top"><a href="tktedit?tn=%d(tn),%d(rn)">edit</a></td>
758776
tn = 0;
759777
}
760778
if( zData[0] ){
761
- @ </tr><tr bgcolor="%h(zBg)"><td colspan=%d(ncol)>
779
+ @ </tr><tr bgcolor="%h(zBg)"><td colspan=%d(pState->nCol)>
762780
@ %h(zData)
763781
}
764782
}else if( azName[i][0]=='#' ){
765783
tn = atoi(zData);
766784
@ <td valign="top"><a href="tktview?tn=%d(tn),%d(rn)">%h(zData)</a></td>
767785
--- src/report.c
+++ src/report.c
@@ -34,39 +34,52 @@
34 /*
35 ** WEBPAGE: /reportlist
36 */
37 void view_list(void){
38 Stmt q;
 
39
40 login_check_credentials();
41 if( !g.okRdTkt ){ login_needed(); return; }
42 style_header("Available Report Formats");
43 db_prepare(&q, "SELECT rn, title, owner FROM reportfmt ORDER BY title");
44 @ <p>Choose a report format from the following list:</p>
45 @ <ol>
46 while( db_step(&q)==SQLITE_ROW ){
47 int rn = db_column_int(&q, 0);
48 const char *zTitle = db_column_text(&q, 1);
49 const char *zOwner = db_column_text(&q, 2);
50 @ <li><a href="rptview?rn=%d(rn)"
51 @ rel="nofollow">%h(zTitle)</a>&nbsp;&nbsp;&nbsp;
52 if( g.okWrite && zOwner && zOwner[0] ){
53 @ (by <i>%h(zOwner)</i>)
54 }
55 if( g.okWrTkt ){
56 @ [<a href="rptedit?rn=%d(rn)&amp;copy=1" rel="nofollow">copy</a>]
57 }
58 if( g.okAdmin || (g.okWrTkt && zOwner && strcmp(g.zLogin,zOwner)==0) ){
59 @ [<a href="rptedit?rn=%d(rn)" rel="nofollow">edit</a>]
60 }
61 @ [<a href="rptsql?rn=%d(rn)" rel="nofollow">sql</a>]
62 @ </li>
63 }
64 if( g.okWrTkt ){
65 @ <li><a href="rptnew">Create a new report format</a></li>
 
 
 
 
 
 
66 }
67 @ </ol>
 
 
 
 
 
 
68 style_footer();
69 }
70
71 /*
72 ** Remove whitespace from both ends of a string.
@@ -644,12 +657,16 @@
644
645 /*
646 ** The state of the report generation.
647 */
648 struct GenerateHTML {
649 int rn; /* Report number */
650 int nCount; /* Row number */
 
 
 
 
651 };
652
653 /*
654 ** The callback function for db_query
655 */
@@ -661,106 +678,107 @@
661 ){
662 struct GenerateHTML *pState = (struct GenerateHTML*)pUser;
663 int i;
664 int tn; /* Ticket number. (value of column named '#') */
665 int rn; /* Report number */
666 int ncol; /* Number of columns in the table */
667 int multirow; /* True if multiple table rows per line of data */
668 int newrowidx; /* Index of first column that goes on a separate row */
669 int iBg = -1; /* Index of column that determines background color */
670 char *zBg = 0; /* Use this background color */
671 char zPage[30]; /* Text version of the ticket number */
672
673 /* Get the report number
674 */
675 rn = pState->rn;
676
677 /* Figure out the number of columns, the column that determines background
678 ** color, and whether or not this row of data is represented by multiple
679 ** rows in the table.
680 */
681 ncol = 0;
682 multirow = 0;
683 newrowidx = -1;
684 for(i=0; i<nArg; i++){
685 if( azName[i][0]=='b' && strcmp(azName[i],"bgcolor")==0 ){
686 zBg = azArg ? azArg[i] : 0;
687 iBg = i;
688 continue;
689 }
690 if( g.okWrite && azName[i][0]=='#' ){
691 ncol++;
692 }
693 if( !multirow ){
694 if( azName[i][0]=='_' ){
695 multirow = 1;
696 newrowidx = i;
697 }else{
698 ncol++;
699 }
700 }
701 }
702
703 /* The first time this routine is called, output a table header
704 */
705 if( pState->nCount==0 ){
 
 
706 @ <tr>
707 tn = -1;
708 for(i=0; i<nArg; i++){
709 char *zName = azName[i];
710 if( i==iBg ) continue;
711 if( newrowidx>=0 && i>=newrowidx ){
712 if( g.okWrite && tn>=0 ){
713 @ <th>&nbsp;</th>
714 tn = -1;
715 }
716 if( zName[0]=='_' ) zName++;
717 @ </tr><tr><th colspan=%d(ncol)>%h(zName)</th>
718 }else{
719 if( zName[0]=='#' ){
720 tn = i;
 
 
721 }
722 }
723 }
724 if( g.okWrite && tn>=0 ){
725 @ <th>&nbsp;</th>
726 }
727 @ </tr>
728 }
729 if( azArg==0 ){
730 @ <tr><td colspan="%d(ncol)">
731 @ <i>No records match the report criteria</i>
732 @ </td></tr>
733 return 0;
734 }
735 ++pState->nCount;
736
737 /* Output the separator above each entry in a table which has multiple lines
738 ** per database entry.
739 */
740 if( newrowidx>=0 ){
741 @ <tr><td colspan=%d(ncol)><font size=1>&nbsp;</font></td></tr>
742 }
743
744 /* Output the data for this entry from the database
745 */
 
746 if( zBg==0 ) zBg = "white";
747 @ <tr bgcolor="%h(zBg)">
748 tn = 0;
749 zPage[0] = 0;
750 for(i=0; i<nArg; i++){
751 char *zData;
752 if( i==iBg ) continue;
753 zData = azArg[i];
754 if( zData==0 ) zData = "";
755 if( newrowidx>=0 && i>=newrowidx ){
756 if( tn>0 && g.okWrite ){
757 @ <td valign="top"><a href="tktedit?tn=%d(tn),%d(rn)">edit</a></td>
758 tn = 0;
759 }
760 if( zData[0] ){
761 @ </tr><tr bgcolor="%h(zBg)"><td colspan=%d(ncol)>
762 @ %h(zData)
763 }
764 }else if( azName[i][0]=='#' ){
765 tn = atoi(zData);
766 @ <td valign="top"><a href="tktview?tn=%d(tn),%d(rn)">%h(zData)</a></td>
767
--- src/report.c
+++ src/report.c
@@ -34,39 +34,52 @@
34 /*
35 ** WEBPAGE: /reportlist
36 */
37 void view_list(void){
38 Stmt q;
39 int rn = 0;
40
41 login_check_credentials();
42 if( !g.okRdTkt && !g.okNewTkt ){ login_needed(); return; }
43 style_header("Bug Report Main Menu");
44 if( g.okNewTkt ){
45 @ <p>Enter a new bug report:</p>
46 @ <ol><li value="0"><a href="tktnew">New bug report</a></li></ol>
47 @
48 }
49 if( !g.okRdTkt ){
50 @ <p>You are not authorized to view existing bug reports.</p>
51 }else{
52 db_prepare(&q, "SELECT rn, title, owner FROM reportfmt ORDER BY title");
53 @ <p>Choose a report format from the following list:</p>
54 @ <ol>
55 while( db_step(&q)==SQLITE_ROW ){
56 rn = db_column_int(&q, 0);
57 const char *zTitle = db_column_text(&q, 1);
58 const char *zOwner = db_column_text(&q, 2);
59 @ <li value="%d(rn)"><a href="rptview?rn=%d(rn)"
60 @ rel="nofollow">%h(zTitle)</a>&nbsp;&nbsp;&nbsp;
61 if( g.okWrite && zOwner && zOwner[0] ){
62 @ (by <i>%h(zOwner)</i>)
63 }
64 if( g.okWrTkt ){
65 @ [<a href="rptedit?rn=%d(rn)&amp;copy=1" rel="nofollow">copy</a>]
66 }
67 if( g.okAdmin || (g.okWrTkt && zOwner && strcmp(g.zLogin,zOwner)==0) ){
68 @ [<a href="rptedit?rn=%d(rn)" rel="nofollow">edit</a>]
69 }
70 @ [<a href="rptsql?rn=%d(rn)" rel="nofollow">sql</a>]
71 @ </li>
72 }
73 }
74 @ </ol>
75 if( g.okWrTkt ){
76 @ <p>Create a new bug report display format:</p>
77 @ <ol>
78 @ <li value="%d(rn+1)"><a href="rptnew">New report format</a></li>
79 @ </ol>
80 }
81 style_footer();
82 }
83
84 /*
85 ** Remove whitespace from both ends of a string.
@@ -644,12 +657,16 @@
657
658 /*
659 ** The state of the report generation.
660 */
661 struct GenerateHTML {
662 int rn; /* Report number */
663 int nCount; /* Row number */
664 int nCol; /* Number of columns */
665 int isMultirow; /* True if multiple table rows per query result row */
666 int iNewRow; /* Index of first column that goes on separate row */
667 int iBg; /* Index of column that defines background color */
668 };
669
670 /*
671 ** The callback function for db_query
672 */
@@ -661,106 +678,107 @@
678 ){
679 struct GenerateHTML *pState = (struct GenerateHTML*)pUser;
680 int i;
681 int tn; /* Ticket number. (value of column named '#') */
682 int rn; /* Report number */
 
 
 
 
683 char *zBg = 0; /* Use this background color */
684 char zPage[30]; /* Text version of the ticket number */
685
686 /* Get the report number
687 */
688 rn = pState->rn;
689
690 /* Do initialization
691 */
692 if( pState->nCount==0 ){
693 /* Figure out the number of columns, the column that determines background
694 ** color, and whether or not this row of data is represented by multiple
695 ** rows in the table.
696 */
697 pState->nCol = 0;
698 pState->isMultirow = 0;
699 pState->iNewRow = -1;
700 pState->iBg = -1;
701 for(i=0; i<nArg; i++){
702 if( azName[i][0]=='b' && strcmp(azName[i],"bgcolor")==0 ){
703 pState->iBg = i;
704 continue;
705 }
706 if( g.okWrite && azName[i][0]=='#' ){
707 pState->nCol++;
708 }
709 if( !pState->isMultirow ){
710 if( azName[i][0]=='_' ){
711 pState->isMultirow = 1;
712 pState->iNewRow = i;
713 }else{
714 pState->nCol++;
715 }
716 }
717 }
718
719 /* The first time this routine is called, output a table header
720 */
721 @ <tr>
722 tn = -1;
723 for(i=0; i<nArg; i++){
724 char *zName = azName[i];
725 if( i==pState->iBg ) continue;
726 if( pState->iNewRow>=0 && i>=pState->iNewRow ){
727 if( g.okWrite && tn>=0 ){
728 @ <th>&nbsp;</th>
729 tn = -1;
730 }
731 if( zName[0]=='_' ) zName++;
732 @ </tr><tr><th colspan=%d(pState->nCol)>%h(zName)</th>
733 }else{
734 if( zName[0]=='#' ){
735 tn = i;
736 }else{
737 @ <th>%h(zName)</th>
738 }
739 }
740 }
741 if( g.okWrite && tn>=0 ){
742 @ <th>&nbsp;</th>
743 }
744 @ </tr>
745 }
746 if( azArg==0 ){
747 @ <tr><td colspan="%d(pState->nCol)">
748 @ <i>No records match the report criteria</i>
749 @ </td></tr>
750 return 0;
751 }
752 ++pState->nCount;
753
754 /* Output the separator above each entry in a table which has multiple lines
755 ** per database entry.
756 */
757 if( pState->iNewRow>=0 ){
758 @ <tr><td colspan=%d(pState->nCol)><font size=1>&nbsp;</font></td></tr>
759 }
760
761 /* Output the data for this entry from the database
762 */
763 zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0;
764 if( zBg==0 ) zBg = "white";
765 @ <tr bgcolor="%h(zBg)">
766 tn = 0;
767 zPage[0] = 0;
768 for(i=0; i<nArg; i++){
769 char *zData;
770 if( i==pState->iBg ) continue;
771 zData = azArg[i];
772 if( zData==0 ) zData = "";
773 if( pState->iNewRow>=0 && i>=pState->iNewRow ){
774 if( tn>0 && g.okWrite ){
775 @ <td valign="top"><a href="tktedit?tn=%d(tn),%d(rn)">edit</a></td>
776 tn = 0;
777 }
778 if( zData[0] ){
779 @ </tr><tr bgcolor="%h(zBg)"><td colspan=%d(pState->nCol)>
780 @ %h(zData)
781 }
782 }else if( azName[i][0]=='#' ){
783 tn = atoi(zData);
784 @ <td valign="top"><a href="tktview?tn=%d(tn),%d(rn)">%h(zData)</a></td>
785
+4 -1
--- src/style.c
+++ src/style.c
@@ -171,11 +171,14 @@
171171
@ html "<a href='$baseurl/dir'>Files</a>"
172172
@ }
173173
@ if {[hascap o]} {
174174
@ html "<a href='$baseurl/leaves'>Leaves</a>"
175175
@ html "<a href='$baseurl/timeline'>Timeline</a>"
176
-@ html "<a href='$baseurl/tagview'>Tags</a>"
176
+@ # html "<a href='$baseurl/tagview'>Tags</a>"
177
+@ }
178
+@ if {[hascap hr]} {
179
+@ html "<a href='$baseurl/reportlist'>Bugs</a>"
177180
@ }
178181
@ if {[hascap j]} {
179182
@ html "<a href='$baseurl/wiki'>Wiki</a>"
180183
@ }
181184
@ if {[hascap s]} {
182185
--- src/style.c
+++ src/style.c
@@ -171,11 +171,14 @@
171 @ html "<a href='$baseurl/dir'>Files</a>"
172 @ }
173 @ if {[hascap o]} {
174 @ html "<a href='$baseurl/leaves'>Leaves</a>"
175 @ html "<a href='$baseurl/timeline'>Timeline</a>"
176 @ html "<a href='$baseurl/tagview'>Tags</a>"
 
 
 
177 @ }
178 @ if {[hascap j]} {
179 @ html "<a href='$baseurl/wiki'>Wiki</a>"
180 @ }
181 @ if {[hascap s]} {
182
--- src/style.c
+++ src/style.c
@@ -171,11 +171,14 @@
171 @ html "<a href='$baseurl/dir'>Files</a>"
172 @ }
173 @ if {[hascap o]} {
174 @ html "<a href='$baseurl/leaves'>Leaves</a>"
175 @ html "<a href='$baseurl/timeline'>Timeline</a>"
176 @ # html "<a href='$baseurl/tagview'>Tags</a>"
177 @ }
178 @ if {[hascap hr]} {
179 @ html "<a href='$baseurl/reportlist'>Bugs</a>"
180 @ }
181 @ if {[hascap j]} {
182 @ html "<a href='$baseurl/wiki'>Wiki</a>"
183 @ }
184 @ if {[hascap s]} {
185

Keyboard Shortcuts

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