ConsoleEmulatorComponent.razor 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. @using RackPeek.Domain
  2. @inject IConsoleEmulator Console
  3. @inject IJSRuntime JS
  4. <div class="bg-black text-green-400 font-mono rounded-xl shadow-inner shadow-green-900/40
  5. p-4 h-[500px] cursor-text flex flex-col text-xs"
  6. @onclick="HandleContainerClick">
  7. <!-- Scrollable output -->
  8. <div class="flex-1 overflow-y-auto overflow-x-auto text-xs pr-2"
  9. @ref="_outputDiv">
  10. @for (var index = 0; index < _lines.Count; index++)
  11. {
  12. var line = _lines[index];
  13. <div @key="index" class="whitespace-pre">
  14. @line
  15. </div>
  16. }
  17. </div>
  18. <!-- Input line -->
  19. <div class="flex border-t border-green-900/40 pt-2 mt-2 overflow-x-auto whitespace-pre">
  20. <span class="text-green-500 mr-2 flex-shrink-0">@Prompt</span>
  21. <span>@_currentInput[.._cursorIndex]</span>
  22. @if (_showCursor)
  23. {
  24. <span class="w-2 bg-green-400 animate-pulse">&nbsp;</span>
  25. }
  26. <span>@_currentInput[_cursorIndex..]</span>
  27. </div>
  28. <!-- Hidden input to capture keys -->
  29. <input @ref="_inputRef"
  30. class="absolute opacity-0"
  31. @onkeydown="HandleKeyDown"
  32. @oninput="OnInputChanged"
  33. disabled="@_busy" />
  34. </div>
  35. @code {
  36. private List<string> _lines = new();
  37. private List<string> _history = new();
  38. private int _historyIndex = -1;
  39. private string _currentInput = "";
  40. private int _cursorIndex = 0;
  41. private bool _busy;
  42. private bool _showCursor = true;
  43. private ElementReference _inputRef;
  44. private ElementReference _outputDiv;
  45. [Parameter]
  46. public string Prompt { get; set; } = "rpk>";
  47. protected override void OnInitialized()
  48. {
  49. WriteLine("RackPeek Console Emulator");
  50. WriteLine("Type '--help' to begin.");
  51. _ = CursorBlinkLoop();
  52. }
  53. protected override async Task OnAfterRenderAsync(bool firstRender)
  54. {
  55. if (firstRender)
  56. await _inputRef.FocusAsync();
  57. }
  58. // required but unused — we handle keys manually
  59. private void OnInputChanged(ChangeEventArgs _) { }
  60. private async Task HandleKeyDown(KeyboardEventArgs e)
  61. {
  62. if (_busy)
  63. return;
  64. switch (e.Key)
  65. {
  66. case "Enter":
  67. await ExecuteCommand();
  68. return;
  69. case "ArrowLeft":
  70. if (_cursorIndex > 0)
  71. _cursorIndex--;
  72. break;
  73. case "ArrowRight":
  74. if (_cursorIndex < _currentInput.Length)
  75. _cursorIndex++;
  76. break;
  77. case "ArrowUp":
  78. NavigateHistory(-1);
  79. _cursorIndex = _currentInput.Length;
  80. break;
  81. case "ArrowDown":
  82. NavigateHistory(1);
  83. _cursorIndex = _currentInput.Length;
  84. break;
  85. case "Backspace":
  86. if (_cursorIndex > 0)
  87. {
  88. _currentInput =
  89. _currentInput.Remove(_cursorIndex - 1, 1);
  90. _cursorIndex--;
  91. }
  92. break;
  93. case "Delete":
  94. if (_cursorIndex < _currentInput.Length)
  95. {
  96. _currentInput =
  97. _currentInput.Remove(_cursorIndex, 1);
  98. }
  99. break;
  100. default:
  101. // printable character
  102. if (e.Key.Length == 1 && !e.CtrlKey && !e.MetaKey)
  103. {
  104. _currentInput =
  105. _currentInput.Insert(_cursorIndex, e.Key);
  106. _cursorIndex++;
  107. }
  108. break;
  109. }
  110. StateHasChanged();
  111. }
  112. private async Task ExecuteCommand()
  113. {
  114. var cmd = _currentInput.Trim();
  115. if (string.IsNullOrWhiteSpace(cmd))
  116. return;
  117. if (cmd.Equals("clear", StringComparison.OrdinalIgnoreCase))
  118. {
  119. _currentInput = "";
  120. _cursorIndex = 0;
  121. _lines.Clear();
  122. StateHasChanged();
  123. return;
  124. }
  125. if (cmd.Equals("help", StringComparison.OrdinalIgnoreCase))
  126. cmd = "--help";
  127. WriteLine($"{Prompt} {cmd}");
  128. _history.Add(cmd);
  129. _historyIndex = _history.Count;
  130. _currentInput = "";
  131. _cursorIndex = 0;
  132. _busy = true;
  133. StateHasChanged();
  134. try
  135. {
  136. var result = await Console.Execute(cmd);
  137. if (!string.IsNullOrWhiteSpace(result))
  138. {
  139. foreach (var line in result.Split('\n'))
  140. WriteLine(AnsiStripper.Strip(line));
  141. }
  142. }
  143. catch
  144. {
  145. WriteLine("Oops, Something went wrong");
  146. }
  147. finally
  148. {
  149. _busy = false;
  150. }
  151. await ScrollToBottom();
  152. await _inputRef.FocusAsync();
  153. StateHasChanged();
  154. }
  155. private void NavigateHistory(int direction)
  156. {
  157. if (_history.Count == 0)
  158. return;
  159. _historyIndex += direction;
  160. _historyIndex = Math.Clamp(_historyIndex, 0, _history.Count);
  161. _currentInput = _historyIndex < _history.Count
  162. ? _history[_historyIndex]
  163. : "";
  164. _cursorIndex = _currentInput.Length;
  165. }
  166. private void WriteLine(string text)
  167. => _lines.Add(text);
  168. private async Task HandleContainerClick()
  169. => await _inputRef.FocusAsync();
  170. private async Task ScrollToBottom()
  171. {
  172. await Task.Delay(10);
  173. await JS.InvokeVoidAsync("consoleEmulatorScroll", _outputDiv);
  174. }
  175. private async Task CursorBlinkLoop()
  176. {
  177. while (true)
  178. {
  179. _showCursor = !_showCursor;
  180. await InvokeAsync(StateHasChanged);
  181. await Task.Delay(500);
  182. }
  183. }
  184. }