Skip to content

Commit 9636e64

Browse files
committed
Improve mobile key input reliability
Replace sticky Ctrl defaults with explicit Prefix and Alt+Enter controls, and add a drawer combo picker for explicit Ctrl/Alt sends without depending on soft keyboard modifiers. Route combo sends through the send queue to preserve action order under async hooks.
1 parent 74e8d6f commit 9636e64

19 files changed

Lines changed: 684 additions & 35 deletions

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ This project has not been published yet. On first release, rename this section t
2828
- Added: `floatingButtons` config — always-visible buttons on touch devices using the same `ControlButton` schema as toolbar/drawer. Renders in the help overlay when non-empty.
2929
- Breaking: `floatingButtons` changed from a flat `ControlButton[]` to `FloatingButtonGroup[]`. Each group has a `position` (`'top-left' | 'top-right' | 'top-centre' | 'bottom-left' | 'bottom-right' | 'bottom-centre' | 'centre-left' | 'centre-right'`), optional `direction` (`'row' | 'column'`, default `'row'`), and a `buttons` array. Migrate: `floatingButtons: [btn]` → `floatingButtons: [{ position: 'top-left', buttons: [btn] }]`.
3030
- Changed: Tailscale Serve guide now recommends always rebuilding the overlay before start; removed version-hash cache-key snippet to prevent stale build issues.
31+
- Changed: default toolbar now uses explicit tmux `Prefix` (`C-b`) and `Alt+Enter` buttons for more predictable mobile input.
32+
- Added: new drawer `Combo` action (`combo-picker`) that opens a small combo input modal for explicit Ctrl/Alt key sends.

‎README.md‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Turns a ttyd web terminal into a touch-optimised tmux client with toolbar, gestu
88

99
- **`webmux serve`** — one command to run webmux: builds overlay in memory, manages ttyd, serves manifest + icons
1010
- **PWA installable** — "Add to Home Screen" on iOS and Android with proper icon and standalone mode
11-
- **Two-row toolbar** — Esc, Ctrl (sticky modifier), Tab, arrows, C-c, Enter, tmux prev/next, paste, drawer toggle
11+
- **Two-row toolbar** — Esc, Prefix (`C-b`), Tab, arrows, C-c, Enter, Alt+Enter, paste, drawer toggle
1212
- **Config-driven controls** — unified button model across toolbar + drawer with ids and descriptions
1313
- **Swipe gestures** — configurable swipe left/right with custom data and help overlay labels
1414
- **Floating buttons** — always-visible quick-action buttons (top-left, touch only) for zoom, pane cycling, etc.
@@ -92,10 +92,11 @@ export default defineConfig({
9292
toolbar: {
9393
row1: [
9494
{ id: 'esc', label: 'Esc', description: 'Send Escape key', action: { type: 'send', data: '\x1b' } },
95-
{ id: 'ctrl-mod', label: 'Ctrl', description: 'Sticky Ctrl modifier', action: { type: 'ctrl-modifier' } },
95+
{ id: 'tmux-prefix', label: 'Prefix', description: 'Send tmux prefix key (Ctrl-B)', action: { type: 'send', data: '\x02' } },
9696
// ...
9797
],
9898
row2: [
99+
{ id: 'alt-enter', label: 'M-↵', description: 'Send Alt+Enter (ESC + Enter)', action: { type: 'send', data: '\x1b\r' } },
99100
{ id: 'drawer-toggle', label: '☰ More', description: 'Open command drawer', action: { type: 'drawer-toggle' } },
100101
{ id: 'paste', label: 'Paste', description: 'Paste from clipboard', action: { type: 'paste' } },
101102
// ...
@@ -105,6 +106,7 @@ export default defineConfig({
105106
buttons: [
106107
{ id: 'tmux-new-window', label: '+ Win', description: 'Create tmux window', action: { type: 'send', data: '\x02c' } },
107108
{ id: 'tmux-split-vertical', label: 'Split |', description: 'Split pane vertically', action: { type: 'send', data: '\x02|' } },
109+
{ id: 'combo-picker', label: 'Combo', description: 'Open combo sender (Ctrl/Alt + key)', action: { type: 'combo-picker' } },
108110
// ...
109111
],
110112
},
@@ -129,8 +131,12 @@ export default defineConfig({
129131
widthThreshold: 768, // px — default matches common phone/tablet breakpoint
130132
},
131133
floatingButtons: [
132-
// Always-visible buttons in the top-left corner (touch devices only)
133-
{ id: 'zoom', label: 'Zoom', description: 'Toggle pane zoom', action: { type: 'send', data: '\x02z' } },
134+
{
135+
position: 'top-left',
136+
buttons: [
137+
{ id: 'zoom', label: 'Zoom', description: 'Toggle pane zoom', action: { type: 'send', data: '\x02z' } },
138+
],
139+
},
134140
],
135141
})
136142
```

‎src/actions/registry.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export interface ActionExecutionContext {
77
readonly sendText: (data: string) => Promise<void>
88
readonly sendRawText?: (data: string) => Promise<void>
99
readonly openDrawer?: () => void
10+
readonly openComboPicker?: (options: {
11+
readonly sendText: (data: string) => Promise<void>
12+
readonly focusIfNeeded: () => void
13+
}) => void
1014
readonly toggleCtrlModifier?: () => void
1115
}
1216

@@ -104,5 +108,24 @@ export function createDefaultActionRegistry(): ActionRegistry {
104108
}
105109
})
106110

111+
registry.register('combo-picker', (_action, context) => {
112+
if (context.openComboPicker) {
113+
context.openComboPicker({
114+
sendText: async (data: string) => {
115+
await registry.execute(
116+
{ type: 'send', data },
117+
{
118+
...context,
119+
sendText: context.sendRawText ?? context.sendText,
120+
},
121+
)
122+
},
123+
focusIfNeeded: context.focusIfNeeded,
124+
})
125+
} else {
126+
context.focusIfNeeded()
127+
}
128+
})
129+
107130
return registry
108131
}

‎src/config-validate.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ function validateAction(value: unknown, path: string, issues: ValidationIssue[])
211211
pushIssue(
212212
issues,
213213
`${path}.type`,
214-
`'send' | 'ctrl-modifier' | 'paste' | 'drawer-toggle'`,
214+
`'send' | 'ctrl-modifier' | 'paste' | 'combo-picker' | 'drawer-toggle'`,
215215
actionType,
216216
)
217217
return
@@ -221,12 +221,13 @@ function validateAction(value: unknown, path: string, issues: ValidationIssue[])
221221
actionType !== 'send' &&
222222
actionType !== 'ctrl-modifier' &&
223223
actionType !== 'paste' &&
224+
actionType !== 'combo-picker' &&
224225
actionType !== 'drawer-toggle'
225226
) {
226227
pushIssue(
227228
issues,
228229
`${path}.type`,
229-
`'send' | 'ctrl-modifier' | 'paste' | 'drawer-toggle'`,
230+
`'send' | 'ctrl-modifier' | 'paste' | 'combo-picker' | 'drawer-toggle'`,
230231
actionType,
231232
)
232233
return

‎src/config.ts‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const defaultGestures: WebmuxConfig['gestures'] = {
2525
scroll: { enabled: true, sensitivity: 40, strategy: 'wheel', wheelIntervalMs: 24 },
2626
}
2727

28-
/** Default row 1 buttons (modifiers + nav) */
28+
/** Default row 1 buttons (prefix + nav) */
2929
const defaultRow1: WebmuxConfig['toolbar']['row1'] = [
3030
{
3131
id: 'esc',
@@ -34,10 +34,10 @@ const defaultRow1: WebmuxConfig['toolbar']['row1'] = [
3434
action: { type: 'send', data: '\x1b' },
3535
},
3636
{
37-
id: 'ctrl-mod',
38-
label: 'Ctrl',
39-
description: 'Sticky Ctrl modifier for the next typed key',
40-
action: { type: 'ctrl-modifier' },
37+
id: 'tmux-prefix',
38+
label: 'Prefix',
39+
description: 'Send tmux prefix key (Ctrl-B)',
40+
action: { type: 'send', data: '\x02' },
4141
},
4242
{ id: 'tab', label: 'Tab', description: 'Send Tab key', action: { type: 'send', data: '\t' } },
4343
{
@@ -86,7 +86,12 @@ const defaultRow1: WebmuxConfig['toolbar']['row1'] = [
8686

8787
/** Default row 2 buttons */
8888
const defaultRow2: WebmuxConfig['toolbar']['row2'] = [
89-
{ id: 'q', label: 'q', description: 'Send q key', action: { type: 'send', data: 'q' } },
89+
{
90+
id: 'alt-enter',
91+
label: 'M-↵',
92+
description: 'Send Alt+Enter (ESC + Enter)',
93+
action: { type: 'send', data: '\x1b\r', keyLabel: 'Alt+Enter' },
94+
},
9095
{
9196
id: 'ctrl-d',
9297
label: 'C-d',
@@ -194,6 +199,12 @@ export const defaultDrawerButtons: readonly ControlButton[] = [
194199
description: 'Kill current pane (with confirm)',
195200
action: { type: 'send', data: '\x02x' },
196201
},
202+
{
203+
id: 'combo-picker',
204+
label: 'Combo',
205+
description: 'Open combo sender (Ctrl/Alt + key)',
206+
action: { type: 'combo-picker' },
207+
},
197208
]
198209

199210
/** Default mobile configuration */

0 commit comments

Comments
 (0)