using System.Collections.Frozen;
using System.Text;
namespace DodoSSH.Client.Shell.Keyboard;
///
/// Every key the phone ships with, in the groups the quick-access keyboard draws them in.
///
///
///
/// The ids are a stored format. The bar and the group order are written to the settings file by id,
/// so renaming one silently drops it from every bar it was on. Labels, hints and bytes can change freely —
/// that is the point of storing ids — but an id, once shipped, stays.
///
///
/// Cursor and function keys are written as the sequences xterm sends in its default mode — CSI for
/// the arrows, SS3 for F1 to F4 — for the reason TerminalInputSurface gives: nothing on this side
/// tracks the remote's application-cursor mode, and CSI is what bash, zsh and every readline accept in
/// both modes.
///
///
internal static class TerminalKeyCatalog
{
/// What every custom key's id starts with, so one can never collide with a shipped key.
internal const string CustomPrefix = "custom:";
/// The id of the group custom keys are drawn in.
internal const string CustomGroupId = "custom";
/// The most keys the bar holds.
///
/// Two rows of . A third row would cost the terminal another five lines on a
/// phone, and anything used rarely enough not to make the first sixteen belongs on the quick-access
/// keyboard, one tap further away.
///
internal const int MaximumBarKeys = 16;
/// The most keys one row of the bar divides its width between.
///
/// Eight across 360dp is a 41dp target after margins — just under Android's 48 and roughly what every
/// terminal on the platform ships. Nine would be 36, which is where the old scrolling row's misses began.
///
internal const int BarRowWidth = 8;
private static TerminalKey Send(string id, string label, string hint, params byte[] bytes) =>
new(id, label, TerminalKeyKind.Send, bytes, hint);
private static TerminalKey Text(string id, string label, string hint, string text) =>
new(id, label, TerminalKeyKind.Send, Encoding.UTF8.GetBytes(text), hint);
private static byte[] Csi(string tail) => [0x1B, (byte)'[', .. Encoding.ASCII.GetBytes(tail)];
private static byte[] Ss3(char final) => [0x1B, (byte)'O', (byte)final];
/// Ctrl-B and then a key: one press of a tmux binding under the default prefix.
private static TerminalKey Tmux(string id, string key, string hint) =>
new(id, $"ctrl+B {key}", TerminalKeyKind.Send, [0x02, .. Encoding.ASCII.GetBytes(key)], hint);
///
/// Escape first, then the command: whatever mode vi was in, this is the one it is meant for.
///
private static TerminalKey Vim(string id, string label, string command, string hint) =>
new(id, label, TerminalKeyKind.Send, [0x1B, .. Encoding.ASCII.GetBytes(command)], hint);
private static TerminalKey Control(char letter) =>
Send(
$"ctrl-{char.ToLowerInvariant(letter)}",
$"^{letter}",
ControlHints.GetValueOrDefault(letter, $"Ctrl+{letter}"),
(byte)(letter & 0x1F));
private static readonly Dictionary ControlHints = new()
{
['C'] = "Interrupt the running command",
['D'] = "End of input; logs out at an empty prompt",
['Z'] = "Suspend the running command",
['L'] = "Clear the screen",
['R'] = "Search command history",
['A'] = "Start of the line",
['E'] = "End of the line",
['U'] = "Delete to the start of the line",
['W'] = "Delete the word before the cursor",
['K'] = "Delete to the end of the line",
};
/// The groups as shipped, in their shipped order.
internal static IReadOnlyList Groups { get; } =
[
new("essentials", "ESSENTIALS",
[
Send("esc", "esc", "Escape", 0x1B),
Send("tab", "tab", "Complete a command or path", 0x09),
Send("shift-tab", "shift+tab", "Backwards through completions and forms", Csi("Z")),
new("ctrl", "ctrl", TerminalKeyKind.Control, [], "Hold Ctrl for the next key"),
new("alt", "alt", TerminalKeyKind.Alt, [], "Hold Alt for the next key"),
Send("enter", "⏎", "Enter", 0x0D),
new("paste", "paste", TerminalKeyKind.Paste, [], "Paste the clipboard"),
Send("ins", "ins", "Insert", Csi("2~")),
Send("del", "del", "Delete forwards", Csi("3~")),
]),
new("cursor", "CURSOR",
[
Send("left", "←", "Left", Csi("D")),
Send("down", "↓", "Down; the next command in history", Csi("B")),
Send("up", "↑", "Up; the previous command in history", Csi("A")),
Send("right", "→", "Right", Csi("C")),
Send("home", "home", "Start of the line", Csi("H")),
Send("end", "end", "End of the line", Csi("F")),
Send("pgup", "pgup", "Page up", Csi("5~")),
Send("pgdn", "pgdn", "Page down", Csi("6~")),
]),
new("control", "CONTROL",
[
Control('C'), Control('D'), Control('Z'), Control('L'), Control('R'),
Control('A'), Control('E'), Control('U'), Control('W'), Control('K'),
]),
new("symbols", "SYMBOLS",
[
Text("pipe", "|", "Pipe", "|"),
Text("dash", "-", "Hyphen", "-"),
Text("underscore", "_", "Underscore", "_"),
Text("tilde", "~", "Home directory", "~"),
Text("slash", "/", "Slash", "/"),
Text("backslash", "\\", "Backslash", "\\"),
Text("backtick", "`", "Backtick", "`"),
Text("star", "*", "Asterisk", "*"),
Text("amp", "&", "Ampersand", "&"),
Text("dollar", "$", "Dollar", "$"),
Text("lt", "<", "Less than", "<"),
Text("gt", ">", "Greater than", ">"),
Text("lbrace", "{", "Left brace", "{"),
Text("rbrace", "}", "Right brace", "}"),
Text("lbracket", "[", "Left bracket", "["),
Text("rbracket", "]", "Right bracket", "]"),
]),
new("tmux", "TMUX",
[
Send("tmux-prefix", "^B", "The tmux prefix", 0x02),
Tmux("tmux-new", "c", "New window"),
Tmux("tmux-next", "n", "Next window"),
Tmux("tmux-prev", "p", "Previous window"),
Tmux("tmux-split-v", "%", "Split side by side"),
Tmux("tmux-split-h", "\"", "Split top and bottom"),
Tmux("tmux-other", "o", "Next pane"),
Tmux("tmux-kill", "x", "Close the pane"),
Tmux("tmux-detach", "d", "Detach"),
Tmux("tmux-copy", "[", "Scroll and copy mode"),
]),
new("vim", "VIM",
[
Vim("vim-write", ":w", ":w\r", "Save"),
Vim("vim-quit", ":q", ":q\r", "Quit"),
Vim("vim-write-quit", ":wq", ":wq\r", "Save and quit"),
Vim("vim-force-quit", ":q!", ":q!\r", "Quit without saving"),
Vim("vim-top", "gg", "gg", "First line"),
Vim("vim-bottom", "G", "G", "Last line"),
]),
new("function", "FUNCTION",
[
Send("f1", "F1", "F1", Ss3('P')),
Send("f2", "F2", "F2", Ss3('Q')),
Send("f3", "F3", "F3", Ss3('R')),
Send("f4", "F4", "F4", Ss3('S')),
Send("f5", "F5", "F5", Csi("15~")),
Send("f6", "F6", "F6", Csi("17~")),
Send("f7", "F7", "F7", Csi("18~")),
Send("f8", "F8", "F8", Csi("19~")),
Send("f9", "F9", "F9", Csi("20~")),
Send("f10", "F10", "F10", Csi("21~")),
Send("f11", "F11", "F11", Csi("23~")),
Send("f12", "F12", "F12", Csi("24~")),
]),
];
///
/// The bar as shipped: the keys this head's old two fixed rows carried, plus Alt and paste.
///
///
/// Modifiers and punctuation on the first row, cursor keys on the second — the split the old keypad's
/// remarks argue for, which is unchanged: the arrows are the keys pressed repeatedly and in sequence, so
/// they get the row nearest the thumb and, being fewer, the widest targets. Paste joins them because a
/// long press on the terminal is now the cursor gesture, and was the only way to paste before.
///
internal static IReadOnlyList DefaultBar { get; } =
[
"esc", "tab", "ctrl", "alt", "pipe", "dash", "slash", "tilde",
"left", "down", "up", "right", "paste",
];
/// Every shipped key by id.
internal static FrozenDictionary ById { get; } =
Groups.SelectMany(g => g.Keys).ToFrozenDictionary(k => k.Id, StringComparer.Ordinal);
/// Builds the key a stored custom entry describes, or null when its sequence does not parse.
internal static TerminalKey? FromCustom(string id, string label, string sequence) =>
KeySequence.TryParse(sequence, out var bytes) && bytes.Length > 0
? new TerminalKey(id, label, TerminalKeyKind.Send, bytes, $"Sends {sequence}")
: null;
}