Skip to content

Commit 8ce566f

Browse files
committed
Add key binding customizations and documentation for feature
1 parent 48514e5 commit 8ce566f

4 files changed

Lines changed: 179 additions & 15 deletions

File tree

aider/tui/app.py

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import queue
66

77
from textual.app import App, ComposeResult
8-
from textual.binding import Binding
8+
9+
# from textual.binding import Binding
910
from textual.containers import Vertical
1011
from textual.theme import Theme
1112

@@ -28,8 +29,8 @@ class TUI(App):
2829

2930
BINDINGS = [
3031
# Binding("ctrl+c", "quit", "Quit", show=True),
31-
Binding("ctrl+l", "clear_output", "Clear", show=True),
32-
Binding("escape", "interrupt", "Interrupt", show=True),
32+
# Binding("ctrl+l", "clear_output", "Clear", show=True),
33+
# Binding("escape", "interrupt", "Interrupt", show=True),
3334
]
3435

3536
def __init__(self, coder_worker, output_queue, input_queue, args):
@@ -67,6 +68,42 @@ def __init__(self, coder_worker, output_queue, input_queue, args):
6768
},
6869
)
6970

71+
self.bind(
72+
self.tui_config["key_bindings"]["newline"], "noop", description="New Line", show=True
73+
)
74+
self.bind(
75+
self.tui_config["key_bindings"]["submit"], "noop", description="Submit", show=True
76+
)
77+
self.bind(
78+
self.tui_config["key_bindings"]["cycle_forward"],
79+
"noop",
80+
description="Cycle Forward",
81+
show=True,
82+
)
83+
self.bind(
84+
self.tui_config["key_bindings"]["cycle_backward"],
85+
"noop",
86+
description="Cycle Backward",
87+
show=True,
88+
)
89+
self.bind(
90+
self.tui_config["key_bindings"]["cancel"], "noop", description="Cancel", show=True
91+
)
92+
93+
self.bind(
94+
self.tui_config["key_bindings"]["focus"],
95+
"focus_input",
96+
description="Focus Input",
97+
show=True,
98+
)
99+
self.bind(
100+
self.tui_config["key_bindings"]["stop"], "interrupt", description="Interrupt", show=True
101+
)
102+
self.bind(
103+
self.tui_config["key_bindings"]["clear"], "clear_output", description="Clear", show=True
104+
)
105+
self.bind(self.tui_config["key_bindings"]["focus"], "quit", description="Quit", show=True)
106+
70107
self.register_theme(BASE_THEME)
71108
self.theme = "aider"
72109

@@ -101,6 +138,12 @@ def _get_config(self):
101138
if "other" not in config:
102139
config["other"] = {}
103140

141+
if "key_bindings" not in config:
142+
config["key_bindings"] = {}
143+
144+
coder = self.worker.coder
145+
is_multiline = coder.args.multiline
146+
104147
# Ensure colors dict has all expected keys with default values
105148
default_colors = {
106149
"primary": "#00ff5f",
@@ -120,6 +163,18 @@ def _get_config(self):
120163
},
121164
}
122165

166+
default_key_bindings = {
167+
"newline": "enter" if is_multiline else "shift+enter",
168+
"submit": "shift+enter" if is_multiline else "enter",
169+
"stop": "escape",
170+
"cycle_forward": "tab",
171+
"cycle_backward": "shift+tab",
172+
"focus": "ctrl+f",
173+
"cancel": "ctrl+c",
174+
"clear": "ctrl+l",
175+
"quit": "ctrl+q",
176+
}
177+
123178
# Merge default colors with user-provided colors
124179
for key, default_value in default_colors.items():
125180
if key not in config["colors"]:
@@ -132,6 +187,13 @@ def _get_config(self):
132187
if var_key not in config["colors"]["variables"]:
133188
config["colors"]["variables"][var_key] = var_default
134189

190+
for key, default_value in default_key_bindings.items():
191+
if key not in config["key_bindings"]:
192+
config["key_bindings"][key] = self._encode_keys(default_value)
193+
194+
for key, value in config["key_bindings"].items():
195+
config["key_bindings"][key] = self._encode_keys(value)
196+
135197
return config
136198

137199
def compose(self) -> ComposeResult:
@@ -205,9 +267,11 @@ def update_key_hints(self, generating=False):
205267
try:
206268
hints = self.query_one(KeyHints)
207269
if generating:
208-
hints.update("escape to cancel")
270+
stop = self.app._decode_keys(self.app.tui_config["key_bindings"]["stop"])
271+
hints.update(f"{stop} to cancel")
209272
else:
210-
hints.update("ctrl+s to submit")
273+
submit = self.app._decode_keys(self.app.tui_config["key_bindings"]["submit"])
274+
hints.update(f"{submit} to submit")
211275
except Exception:
212276
pass
213277

@@ -381,6 +445,11 @@ def on_input_area_submit(self, message: InputArea.Submit):
381445

382446
self.input_queue.put({"text": user_input})
383447

448+
def action_focus_input(self) -> None:
449+
"""Find the input widget and set focus to it."""
450+
input_area = self.query_one("#input", InputArea)
451+
input_area.focus()
452+
384453
def action_clear_output(self):
385454
"""Clear all output."""
386455
output_container = self.query_one("#output", OutputContainer)
@@ -413,6 +482,21 @@ def action_quit(self):
413482
# Delay exit to allow status bar to render
414483
self.set_timer(0.3, self._do_quit)
415484

485+
def action_noop(self):
486+
pass
487+
488+
def _encode_keys(self, key):
489+
if key == "shift+enter":
490+
return "ctrl+j"
491+
492+
return key
493+
494+
def _decode_keys(self, key):
495+
if key == "ctrl+j":
496+
return "shift+enter"
497+
498+
return key
499+
416500
def _do_quit(self):
417501
"""Perform the actual quit after UI updates."""
418502
self.worker.stop()
@@ -653,6 +737,14 @@ def on_input_area_completion_cycle(self, message: InputArea.CompletionCycle):
653737
except Exception:
654738
pass
655739

740+
def on_input_area_completion_cycle_previous(self, message: InputArea.CompletionCyclePrevious):
741+
"""Handle Tab to cycle through completions."""
742+
try:
743+
completion_bar = self.query_one("#completion-bar", CompletionBar)
744+
completion_bar.cycle_previous()
745+
except Exception:
746+
pass
747+
656748
def on_input_area_completion_accept(self, message: InputArea.CompletionAccept):
657749
"""Handle Enter to accept current completion."""
658750
try:

aider/tui/widgets/completion_bar.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,15 @@ def cycle_next(self) -> None:
280280
self.selected_index = (self.selected_index + 1) % len(self.suggestions)
281281
self._update_selection()
282282

283+
def cycle_previous(self) -> None:
284+
"""Cycle to next suggestion."""
285+
if self.suggestions:
286+
if not self.selected_index:
287+
self.selected_index = len(self.suggestions) - 1
288+
else:
289+
self.selected_index = (self.selected_index - 1) % len(self.suggestions)
290+
self._update_selection()
291+
283292
def select_current(self) -> None:
284293
"""Select current suggestion and dismiss."""
285294
if self.suggestions:

aider/tui/widgets/input_area.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ class CompletionCycle(Message):
2727

2828
pass
2929

30+
class CompletionCyclePrevious(Message):
31+
"""User wants to cycle through completions backwards."""
32+
33+
pass
34+
3035
class CompletionAccept(Message):
3136
"""User wants to accept current completion."""
3237

@@ -54,7 +59,12 @@ def __init__(self, history_file: str = None, **kwargs):
5459
# Let's assume kwargs might handle it or we set it.
5560
# Actually, let's just set the default if it's empty.
5661
if not self.placeholder:
57-
self.placeholder = "> Type your message... (ctrl+s to submit, enter for new line)"
62+
submit = self.app._decode_keys(self.app.tui_config["key_bindings"]["submit"])
63+
newline = self.app._decode_keys(self.app.tui_config["key_bindings"]["newline"])
64+
65+
self.placeholder = (
66+
f"> Type your message... ({submit} to submit, {newline} for new line)"
67+
)
5868

5969
self.files = []
6070
self.commands = []
@@ -198,32 +208,38 @@ def on_key(self, event) -> None:
198208
if self.disabled:
199209
return
200210

201-
if event.key == "ctrl+c":
211+
if event.key == self.app.tui_config["key_bindings"]["cancel"]:
202212
event.stop()
203213
event.prevent_default()
204214
if self.text.strip():
205215
self.save_to_history(self.text)
206216
self.text = ""
207217
return
208218

209-
if event.key == "ctrl+s":
219+
if event.key == self.app.tui_config["key_bindings"]["submit"]:
210220
# Submit message
211221
event.stop()
212222
event.prevent_default()
213223
self.post_message(self.Submit(self.text))
214224
return
215225

216-
if event.key == "enter":
226+
if event.key == self.app.tui_config["key_bindings"]["newline"]:
217227
if self.completion_active:
218228
# Accept completion
219229
self.post_message(self.CompletionAccept())
220230
event.stop()
221231
event.prevent_default()
222232
return
223233
else:
234+
if self.app.tui_config["key_bindings"]["newline"] != "enter":
235+
self.insert("\n")
236+
237+
current_row, current_col = self.cursor_location
238+
self.cursor_location = (current_row + 1, 0)
239+
224240
return
225241

226-
if event.key == "tab":
242+
if event.key == self.app.tui_config["key_bindings"]["cycle_forward"]:
227243
event.stop()
228244
event.prevent_default()
229245
if self.completion_active:
@@ -232,7 +248,16 @@ def on_key(self, event) -> None:
232248
else:
233249
# Request completions
234250
self.post_message(self.CompletionRequested(self.text))
235-
elif event.key == "escape" and self.completion_active:
251+
elif event.key == self.app.tui_config["key_bindings"]["cycle_backward"]:
252+
event.stop()
253+
event.prevent_default()
254+
if self.completion_active:
255+
# Cycle through completions
256+
self.post_message(self.CompletionCyclePrevious())
257+
else:
258+
# Request completions
259+
self.post_message(self.CompletionRequested(self.text))
260+
elif event.key == self.app.tui_config["key_bindings"]["stop"] and self.completion_active:
236261
event.stop()
237262
event.prevent_default()
238263
self.post_message(self.CompletionDismiss())

aider/website/docs/config/tui.md

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ tui: true
2525
2626
### Complete Configuration Example
2727
28-
Complete configuration example in YAML configuration file (`.aider.conf.yml` or `~/.aider.conf.yml`). The base theme is pretty nice but if you want different colors, do you thing:
28+
Complete configuration example in YAML configuration file (`.aider.conf.yml` or `~/.aider.conf.yml`). The base theme is pretty nice but if you want different colors and key bindings, do you thing:
2929

3030
```yaml
3131
tui: true
@@ -41,15 +41,53 @@ tui-config:
4141
error: "#ff3333"
4242
surface: "transparent"
4343
panel: "transparent"
44-
dark: true
45-
variables:
46-
input-cursor-foreground: "#00ff87"
44+
input-cursor-foreground: "#00ff87"
4745
other:
4846
dark: true
4947
input-cursor-text-style: "underline"
48+
key_bindings:
49+
newline: "enter"
50+
submit: "shift+enter"
51+
stop: "escape"
52+
cycle_forward: "tab"
53+
cycle_backward: "shift+tab"
54+
focus: "ctrl+f"
55+
cancel: "ctrl+c"
56+
clear: "ctrl+l"
57+
quit: "ctrl+q"
5058
5159
```
5260

61+
### Key Command Configuration
62+
63+
The TUI provides customizable key bindings for all major actions. The default key bindings are:
64+
65+
| Action | Default Key | Description |
66+
|--------|-------------|-------------|
67+
| New Line | `enter` (multiline mode) / `shift+enter` (single-line mode) | Insert a new line in the input area |
68+
| Submit | `shift+enter` (multiline mode) / `enter` (single-line mode) | Submit the current input |
69+
| Cancel | `ctrl+c` | Stop and stash current input prompt |
70+
| Stop | `escape` | Interrupt the current LLM response or task |
71+
| Cycle Forward | `tab` | Cycle forward through completion suggestions |
72+
| Cycle Backward | `shift+tab` | Cycle backward through completion suggestions |
73+
| Focus | `ctrl+f` | Focus the input area |
74+
| Clear | `ctrl+l` | Clear the output area |
75+
| Quit | `ctrl+q` | Exit the TUI |
76+
77+
#### Customizing Key Bindings
78+
79+
You can customize any key binding by adding a `key_bindings` section to your `tui-config`. For example, to change the quit key to `ctrl+x`:
80+
81+
```yaml
82+
tui-config:
83+
key_bindings:
84+
quit: "ctrl+x"
85+
```
86+
87+
All key bindings use Textual's key syntax:
88+
- Single keys: `enter`, `escape`, `tab`
89+
- Modifier combinations: `ctrl+c`, `shift+enter`, etc.
90+
5391
## Benefits
5492

5593
- **Improved Productivity**: Reduced context switching with all information visible at once

0 commit comments

Comments
 (0)