Navegador

feat: shell completions for bash, zsh, and fish CLI: navegador completions <shell> [--install]. Generates eval lines for tab completion, with optional install to shell rc file. Closes #60

lmata 2026-03-23 05:04 trunk
Commit 67dab811171e9d0c621c33a839cdc17cecf13b1443a84fb8d61d6ffcadb9a881
--- a/navegador/completions.py
+++ b/navegador/completions.py
@@ -0,0 +1,50 @@
1
+"""
2
+Shell completions — generate tab-completion scripts for bash, zsh, and fish.
3
+
4
+Usage:
5
+ Print the eval line to add to your shell rc file:
6
+ navegador completions bash
7
+ navegador completions zsh
8
+ navegador completions fish
9
+
10
+ Install automatically:
11
+ navegador completions bash --install
12
+ navegador completions zsh --install
13
+ navegador completions fish --install
14
+"""
15
+
16
+from __future__ import annotations
17
+
18
+from pathlib import Path
19
+
20
+SUPPORTED_SHELLS = ["bash", "zsh", "fish"]
21
+
22
+# The env-var source command for each shell
23
+_SOURCE_COMMANDS: dict[str, str] = {
24
+ "bash": "_NAVEGADOR_COMPLETE=bash_source navegador",
25
+ "zsh": "_NAVEGADOR_COMPLETE=zsh_source navegador",
26
+ "fish": "_NAVEGADOR_COMPLETE=fish_source navegador",
27
+}
28
+
29
+# The eval/source wrapper for each shell
30
+_EVAL_LINES: dict[str, str] = {
31
+ "bash": 'eval "$(_NAVEGADOR_COMPLETE=bash_source navegador)"',
32
+ "zsh": 'eval "$(_NAVEGADOR_COMPLETE=zsh_source navegador)"',
33
+ "fish": "_NAVEGADOR_COMPLETE=fish_source navegador | source",
34
+}
35
+
36
+# Default rc file paths for each shell (relative to $HOME)
37
+_RC_PATHS: dict[str, str] = {
38
+ "bash": "~/.bashrc",
39
+ "zsh": "~/.zshrc",
40
+ "fish": "~/.config/fish/config.fish",
41
+}
42
+
43
+
44
+def get_eval_line(shell: str) -> str:
45
+ """Return the eval/source line to add to the shell rc file.
46
+
47
+ Raises ValueError for unsupported shells.
48
+ """
49
+ if shell not in SUPPORTED_SHELLS:
50
+ raise ValueError(f"Unsupported shell: {shell!r}. Choose from: {', '.join(SUPPORTED_SHELL
--- a/navegador/completions.py
+++ b/navegador/completions.py
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/completions.py
+++ b/navegador/completions.py
@@ -0,0 +1,50 @@
1 """
2 Shell completions — generate tab-completion scripts for bash, zsh, and fish.
3
4 Usage:
5 Print the eval line to add to your shell rc file:
6 navegador completions bash
7 navegador completions zsh
8 navegador completions fish
9
10 Install automatically:
11 navegador completions bash --install
12 navegador completions zsh --install
13 navegador completions fish --install
14 """
15
16 from __future__ import annotations
17
18 from pathlib import Path
19
20 SUPPORTED_SHELLS = ["bash", "zsh", "fish"]
21
22 # The env-var source command for each shell
23 _SOURCE_COMMANDS: dict[str, str] = {
24 "bash": "_NAVEGADOR_COMPLETE=bash_source navegador",
25 "zsh": "_NAVEGADOR_COMPLETE=zsh_source navegador",
26 "fish": "_NAVEGADOR_COMPLETE=fish_source navegador",
27 }
28
29 # The eval/source wrapper for each shell
30 _EVAL_LINES: dict[str, str] = {
31 "bash": 'eval "$(_NAVEGADOR_COMPLETE=bash_source navegador)"',
32 "zsh": 'eval "$(_NAVEGADOR_COMPLETE=zsh_source navegador)"',
33 "fish": "_NAVEGADOR_COMPLETE=fish_source navegador | source",
34 }
35
36 # Default rc file paths for each shell (relative to $HOME)
37 _RC_PATHS: dict[str, str] = {
38 "bash": "~/.bashrc",
39 "zsh": "~/.zshrc",
40 "fish": "~/.config/fish/config.fish",
41 }
42
43
44 def get_eval_line(shell: str) -> str:
45 """Return the eval/source line to add to the shell rc file.
46
47 Raises ValueError for unsupported shells.
48 """
49 if shell not in SUPPORTED_SHELLS:
50 raise ValueError(f"Unsupported shell: {shell!r}. Choose from: {', '.join(SUPPORTED_SHELL
--- a/tests/test_completions.py
+++ b/tests/test_completions.py
@@ -0,0 +1,235 @@
1
+"""Tests for shell completions — module API and CLI command."""
2
+
3
+from pathlib import Path
4
+
5
+import pytest
6
+from click.testing import CliRunner
7
+
8
+from navegador.cli.commands import main
9
+from navegador.completions import (
10
+ SUPPORTED_SHELLS,
11
+ get_eval_line,
12
+ get_rc_path,
13
+ install_completion,
14
+)
15
+
16
+# ── get_eval_line ─────────────────────────────────────────────────────────────
17
+
18
+
19
+class TestGetEvalLine:
20
+ def test_bash_contains_bash_source(self):
21
+ line = get_eval_line("bash")
22
+ assert "bash_source" in line
23
+ assert "navegador" in line
24
+
25
+ def test_zsh_contains_zsh_source(self):
26
+ line = get_eval_line("zsh")
27
+ assert "zsh_source" in line
28
+ assert "navegador" in line
29
+
30
+ def test_fish_contains_fish_source(self):
31
+ line = get_eval_line("fish")
32
+ assert "fish_source" in line
33
+ assert "navegador" in line
34
+
35
+ def test_bash_uses_eval(self):
36
+ line = get_eval_line("bash")
37
+ assert line.startswith("eval ")
38
+
39
+ def test_zsh_uses_eval(self):
40
+ line = get_eval_line("zsh")
41
+ assert line.startswith("eval ")
42
+
43
+ def test_fish_uses_pipe_source(self):
44
+ line = get_eval_line("fish")
45
+ assert "| source" in line
46
+
47
+ def test_invalid_shell_raises_value_error(self):
48
+ with pytest.raises(ValueError, match="Unsupported shell"):
49
+ get_eval_line("powershell")
50
+
51
+ def test_all_supported_shells_return_string(self):
52
+ for shell in SUPPORTED_SHELLS:
53
+ assert isinstance(get_eval_line(shell), str)
54
+
55
+
56
+# ── get_rc_path ───────────────────────────────────────────────────────────────
57
+
58
+
59
+class TestGetRcPath:
60
+ def test_bash_returns_bashrc(self):
61
+ assert get_rc_path("bash") == "~/.bashrc"
62
+
63
+ def test_zsh_returns_zshrc(self):
64
+ assert get_rc_path("zsh") == "~/.zshrc"
65
+
66
+ def test_fish_returns_config_fish(self):
67
+ assert "config.fish" in get_rc_path("fish")
68
+
69
+ def test_invalid_shell_raises_value_error(self):
70
+ with pytest.raises(ValueError, match="Unsupported shell"):
71
+ get_rc_path("ksh")
72
+
73
+ def test_all_supported_shells_return_string(self):
74
+ for shell in SUPPORTED_SHELLS:
75
+ assert isinstance(get_rc_path(shell), str)
76
+
77
+
78
+# ── install_completion ────────────────────────────────────────────────────────
79
+
80
+
81
+class TestInstallCompletion:
82
+ def test_creates_file_with_eval_line(self, tmp_path):
83
+ rc = tmp_path / ".bashrc"
84
+ result = install_completion("bash", rc_path=str(rc))
85
+ assert result.exists()
86
+ content = rc.read_text()
87
+ assert "bash_source" in content
88
+ assert "navegador" in content
89
+
90
+ def test_creates_parent_dirs_for_fish(self, tmp_path):
91
+ rc = tmp_path / ".config" / "fish" / "config.fish"
92
+ result = install_completion("fish", rc_path=str(rc))
93
+ assert result.exists()
94
+ assert rc.parent.is_dir()
95
+
96
+ def test_returns_path_object(self, tmp_path):
97
+ rc = tmp_path / ".zshrc"
98
+ result = install_completion("zsh", rc_path=str(rc))
99
+ assert isinstance(result, Path)
100
+
101
+ def test_idempotent_does_not_duplicate(self, tmp_path):
102
+ rc = tmp_path / ".bashrc"
103
+ install_completion("bash", rc_path=str(rc))
104
+ install_completion("bash", rc_path=str(rc))
105
+ content = rc.read_text()
106
+ # The eval line should appear exactly once
107
+ line = "eval \"$(_NAVEGADOR_COMPLETE=bash_source navegador)\""
108
+ assert content.count(line) == 1
109
+
110
+ def test_appends_to_existing_file(self, tmp_path):
111
+ rc = tmp_path / ".zshrc"
112
+ rc.write_text("# existing content\nexport FOO=bar\n")
113
+ install_completion("zsh", rc_path=str(rc))
114
+ content = rc.read_text()
115
+ assert "existing content" in content
116
+ assert "zsh_source" in content
117
+
118
+ def test_invalid_shell_raises_value_error(self, tmp_path):
119
+ with pytest.raises(ValueError, match="Unsupported shell"):
120
+ install_completion("tcsh", rc_path=str(tmp_path / ".tcshrc"))
121
+
122
+ def test_zsh_eval_line_in_file(self, tmp_path):
123
+ rc = tmp_path / ".zshrc"
124
+ install_completion("zsh", rc_path=str(rc))
125
+ assert "zsh_source" in rc.read_text()
126
+
127
+ def test_fish_source_line_in_file(self, tmp_path):
128
+ rc = tmp_path / "config.fish"
129
+ install_completion("fish", rc_path=str(rc))
130
+ assert "fish_source" in rc.read_text()
131
+ assert "| source" in rc.read_text()
132
+
133
+
134
+# ── CLI: navegador completions <shell> ────────────────────────────────────────
135
+
136
+
137
+class TestCompletionsCommand:
138
+ # Basic output
139
+
140
+ def test_bash_outputs_eval_line(self):
141
+ runner = CliRunner()
142
+ result = runner.invoke(main, ["completions", "bash"])
143
+ assert result.exit_code == 0
144
+ assert "bash_source" in result.output
145
+
146
+ def test_zsh_outputs_eval_line(self):
147
+ runner = CliRunner()
148
+ result = runner.invoke(main, ["completions", "zsh"])
149
+ assert result.exit_code == 0
150
+ assert "zsh_source" in result.output
151
+
152
+ def test_fish_outputs_source_line(self):
153
+ runner = CliRunner()
154
+ result = runner.invoke(main, ["completions", "fish"])
155
+ assert result.exit_code == 0
156
+ assert "fish_source" in result.output
157
+
158
+ def test_output_mentions_rc_file(self):
159
+ runner = CliRunner()
160
+ result = runner.invoke(main, ["completions", "bash"])
161
+ assert result.exit_code == 0
162
+ assert ".bashrc" in result.output
163
+
164
+ def test_output_mentions_install_hint(self):
165
+ runner = CliRunner()
166
+ result = runner.invoke(main, ["completions", "zsh"])
167
+ assert result.exit_code == 0
168
+ assert "--install" in result.output
169
+
170
+ # Invalid shell
171
+
172
+ def test_invalid_shell_exits_nonzero(self):
173
+ runner = CliRunner()
174
+ result = runner.invoke(main, ["completions", "powershell"])
175
+ assert result.exit_code != 0
176
+
177
+ def test_invalid_shell_shows_error(self):
178
+ runner = CliRunner()
179
+ result = runner.invoke(main, ["completions", "powershell"])
180
+ assert result.exit_code != 0
181
+ # Click's Choice type reports the invalid value
182
+ assert "powershell" in result.output or "invalid" in result.output.lower()
183
+
184
+ # --install flag
185
+
186
+ def test_install_bash_creates_file(self):
187
+ runner = CliRunner()
188
+ with runner.isolated_filesystem() as tmp:
189
+ rc = str(Path(tmp) / ".bashrc")
190
+ result = runner.invoke(main, ["completions", "bash", "--install", "--rc-path", rc])
191
+ assert result.exit_code == 0
192
+ assert Path(rc).exists()
193
+ assert "bash_source" in Path(rc).read_text()
194
+
195
+ def test_install_zsh_creates_file(self):
196
+ runner = CliRunner()
197
+ with runner.isolated_filesystem() as tmp:
198
+ rc = str(Path(tmp) / ".zshrc")
199
+ result = runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc])
200
+ assert result.exit_code == 0
201
+ assert Path(rc).exists()
202
+
203
+ def test_install_fish_creates_file(self):
204
+ runner = CliRunner()
205
+ with runner.isolated_filesystem() as tmp:
206
+ rc = str(Path(tmp) / "config.fish")
207
+ result = runner.invoke(main, ["completions", "fish", "--install", "--rc-path", rc])
208
+ assert result.exit_code == 0
209
+ assert Path(rc).exists()
210
+
211
+ def test_install_shows_success_message(self):
212
+ runner = CliRunner()
213
+ with runner.isolated_filesystem() as tmp:
214
+ rc = str(Path(tmp) / ".bashrc")
215
+ result = runner.invoke(main, ["completions", "bash", "--install", "--rc-path", rc])
216
+ assert result.exit_code == 0
217
+ assert "installed" in result.output.lower() or "Completion" in result.output
218
+
219
+ def test_install_shows_restart_hint(self):
220
+ runner = CliRunner()
221
+ with runner.isolated_filesystem() as tmp:
222
+ rc = str(Path(tmp) / ".zshrc")
223
+ result = runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc])
224
+ assert result.exit_code == 0
225
+ assert "source" in result.output or "restart" in result.output.lower()
226
+
227
+ def test_install_idempotent_via_cli(self):
228
+ runner = CliRunner()
229
+ with runner.isolated_filesystem() as tmp:
230
+ rc = str(Path(tmp) / ".zshrc")
231
+ runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc])
232
+ runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc])
233
+ content = Path(rc).read_text()
234
+ line = 'eval "$(_NAVEGADOR_COMPLETE=zsh_source navegador)"'
235
+ assert content.count(line) == 1
--- a/tests/test_completions.py
+++ b/tests/test_completions.py
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_completions.py
+++ b/tests/test_completions.py
@@ -0,0 +1,235 @@
1 """Tests for shell completions — module API and CLI command."""
2
3 from pathlib import Path
4
5 import pytest
6 from click.testing import CliRunner
7
8 from navegador.cli.commands import main
9 from navegador.completions import (
10 SUPPORTED_SHELLS,
11 get_eval_line,
12 get_rc_path,
13 install_completion,
14 )
15
16 # ── get_eval_line ─────────────────────────────────────────────────────────────
17
18
19 class TestGetEvalLine:
20 def test_bash_contains_bash_source(self):
21 line = get_eval_line("bash")
22 assert "bash_source" in line
23 assert "navegador" in line
24
25 def test_zsh_contains_zsh_source(self):
26 line = get_eval_line("zsh")
27 assert "zsh_source" in line
28 assert "navegador" in line
29
30 def test_fish_contains_fish_source(self):
31 line = get_eval_line("fish")
32 assert "fish_source" in line
33 assert "navegador" in line
34
35 def test_bash_uses_eval(self):
36 line = get_eval_line("bash")
37 assert line.startswith("eval ")
38
39 def test_zsh_uses_eval(self):
40 line = get_eval_line("zsh")
41 assert line.startswith("eval ")
42
43 def test_fish_uses_pipe_source(self):
44 line = get_eval_line("fish")
45 assert "| source" in line
46
47 def test_invalid_shell_raises_value_error(self):
48 with pytest.raises(ValueError, match="Unsupported shell"):
49 get_eval_line("powershell")
50
51 def test_all_supported_shells_return_string(self):
52 for shell in SUPPORTED_SHELLS:
53 assert isinstance(get_eval_line(shell), str)
54
55
56 # ── get_rc_path ───────────────────────────────────────────────────────────────
57
58
59 class TestGetRcPath:
60 def test_bash_returns_bashrc(self):
61 assert get_rc_path("bash") == "~/.bashrc"
62
63 def test_zsh_returns_zshrc(self):
64 assert get_rc_path("zsh") == "~/.zshrc"
65
66 def test_fish_returns_config_fish(self):
67 assert "config.fish" in get_rc_path("fish")
68
69 def test_invalid_shell_raises_value_error(self):
70 with pytest.raises(ValueError, match="Unsupported shell"):
71 get_rc_path("ksh")
72
73 def test_all_supported_shells_return_string(self):
74 for shell in SUPPORTED_SHELLS:
75 assert isinstance(get_rc_path(shell), str)
76
77
78 # ── install_completion ────────────────────────────────────────────────────────
79
80
81 class TestInstallCompletion:
82 def test_creates_file_with_eval_line(self, tmp_path):
83 rc = tmp_path / ".bashrc"
84 result = install_completion("bash", rc_path=str(rc))
85 assert result.exists()
86 content = rc.read_text()
87 assert "bash_source" in content
88 assert "navegador" in content
89
90 def test_creates_parent_dirs_for_fish(self, tmp_path):
91 rc = tmp_path / ".config" / "fish" / "config.fish"
92 result = install_completion("fish", rc_path=str(rc))
93 assert result.exists()
94 assert rc.parent.is_dir()
95
96 def test_returns_path_object(self, tmp_path):
97 rc = tmp_path / ".zshrc"
98 result = install_completion("zsh", rc_path=str(rc))
99 assert isinstance(result, Path)
100
101 def test_idempotent_does_not_duplicate(self, tmp_path):
102 rc = tmp_path / ".bashrc"
103 install_completion("bash", rc_path=str(rc))
104 install_completion("bash", rc_path=str(rc))
105 content = rc.read_text()
106 # The eval line should appear exactly once
107 line = "eval \"$(_NAVEGADOR_COMPLETE=bash_source navegador)\""
108 assert content.count(line) == 1
109
110 def test_appends_to_existing_file(self, tmp_path):
111 rc = tmp_path / ".zshrc"
112 rc.write_text("# existing content\nexport FOO=bar\n")
113 install_completion("zsh", rc_path=str(rc))
114 content = rc.read_text()
115 assert "existing content" in content
116 assert "zsh_source" in content
117
118 def test_invalid_shell_raises_value_error(self, tmp_path):
119 with pytest.raises(ValueError, match="Unsupported shell"):
120 install_completion("tcsh", rc_path=str(tmp_path / ".tcshrc"))
121
122 def test_zsh_eval_line_in_file(self, tmp_path):
123 rc = tmp_path / ".zshrc"
124 install_completion("zsh", rc_path=str(rc))
125 assert "zsh_source" in rc.read_text()
126
127 def test_fish_source_line_in_file(self, tmp_path):
128 rc = tmp_path / "config.fish"
129 install_completion("fish", rc_path=str(rc))
130 assert "fish_source" in rc.read_text()
131 assert "| source" in rc.read_text()
132
133
134 # ── CLI: navegador completions <shell> ────────────────────────────────────────
135
136
137 class TestCompletionsCommand:
138 # Basic output
139
140 def test_bash_outputs_eval_line(self):
141 runner = CliRunner()
142 result = runner.invoke(main, ["completions", "bash"])
143 assert result.exit_code == 0
144 assert "bash_source" in result.output
145
146 def test_zsh_outputs_eval_line(self):
147 runner = CliRunner()
148 result = runner.invoke(main, ["completions", "zsh"])
149 assert result.exit_code == 0
150 assert "zsh_source" in result.output
151
152 def test_fish_outputs_source_line(self):
153 runner = CliRunner()
154 result = runner.invoke(main, ["completions", "fish"])
155 assert result.exit_code == 0
156 assert "fish_source" in result.output
157
158 def test_output_mentions_rc_file(self):
159 runner = CliRunner()
160 result = runner.invoke(main, ["completions", "bash"])
161 assert result.exit_code == 0
162 assert ".bashrc" in result.output
163
164 def test_output_mentions_install_hint(self):
165 runner = CliRunner()
166 result = runner.invoke(main, ["completions", "zsh"])
167 assert result.exit_code == 0
168 assert "--install" in result.output
169
170 # Invalid shell
171
172 def test_invalid_shell_exits_nonzero(self):
173 runner = CliRunner()
174 result = runner.invoke(main, ["completions", "powershell"])
175 assert result.exit_code != 0
176
177 def test_invalid_shell_shows_error(self):
178 runner = CliRunner()
179 result = runner.invoke(main, ["completions", "powershell"])
180 assert result.exit_code != 0
181 # Click's Choice type reports the invalid value
182 assert "powershell" in result.output or "invalid" in result.output.lower()
183
184 # --install flag
185
186 def test_install_bash_creates_file(self):
187 runner = CliRunner()
188 with runner.isolated_filesystem() as tmp:
189 rc = str(Path(tmp) / ".bashrc")
190 result = runner.invoke(main, ["completions", "bash", "--install", "--rc-path", rc])
191 assert result.exit_code == 0
192 assert Path(rc).exists()
193 assert "bash_source" in Path(rc).read_text()
194
195 def test_install_zsh_creates_file(self):
196 runner = CliRunner()
197 with runner.isolated_filesystem() as tmp:
198 rc = str(Path(tmp) / ".zshrc")
199 result = runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc])
200 assert result.exit_code == 0
201 assert Path(rc).exists()
202
203 def test_install_fish_creates_file(self):
204 runner = CliRunner()
205 with runner.isolated_filesystem() as tmp:
206 rc = str(Path(tmp) / "config.fish")
207 result = runner.invoke(main, ["completions", "fish", "--install", "--rc-path", rc])
208 assert result.exit_code == 0
209 assert Path(rc).exists()
210
211 def test_install_shows_success_message(self):
212 runner = CliRunner()
213 with runner.isolated_filesystem() as tmp:
214 rc = str(Path(tmp) / ".bashrc")
215 result = runner.invoke(main, ["completions", "bash", "--install", "--rc-path", rc])
216 assert result.exit_code == 0
217 assert "installed" in result.output.lower() or "Completion" in result.output
218
219 def test_install_shows_restart_hint(self):
220 runner = CliRunner()
221 with runner.isolated_filesystem() as tmp:
222 rc = str(Path(tmp) / ".zshrc")
223 result = runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc])
224 assert result.exit_code == 0
225 assert "source" in result.output or "restart" in result.output.lower()
226
227 def test_install_idempotent_via_cli(self):
228 runner = CliRunner()
229 with runner.isolated_filesystem() as tmp:
230 rc = str(Path(tmp) / ".zshrc")
231 runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc])
232 runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc])
233 content = Path(rc).read_text()
234 line = 'eval "$(_NAVEGADOR_COMPLETE=zsh_source navegador)"'
235 assert content.count(line) == 1

Keyboard Shortcuts

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