4
0

ConsoleEmulatorComponent.razor 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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 readonly List<string> _lines = new();
  37. private readonly List<string> _history = new();
  38. private int _historyIndex = -1;
  39. private string _currentInput = "";
  40. private int _cursorIndex;
  41. private bool _busy;
  42. private bool _showCursor = true;
  43. private ElementReference _inputRef;
  44. private ElementReference _outputDiv;
  45. [Parameter] public string Prompt { get; set; } = "rpk>";
  46. protected override void OnInitialized()
  47. {
  48. WriteLine("RackPeek Console Emulator");
  49. WriteLine("Type '--help' to begin.");
  50. _ = CursorBlinkLoop();
  51. }
  52. protected override async Task OnAfterRenderAsync(bool firstRender)
  53. {
  54. if (firstRender)
  55. await _inputRef.FocusAsync();
  56. }
  57. // required but unused — we handle keys manually
  58. private void OnInputChanged(ChangeEventArgs _)
  59. {
  60. }
  61. private async Task HandleKeyDown(KeyboardEventArgs e)
  62. {
  63. if (_busy)
  64. return;
  65. switch (e.Key)
  66. {
  67. case "Enter":
  68. await ExecuteCommand();
  69. return;
  70. case "ArrowLeft":
  71. if (_cursorIndex > 0)
  72. _cursorIndex--;
  73. break;
  74. case "ArrowRight":
  75. if (_cursorIndex < _currentInput.Length)
  76. _cursorIndex++;
  77. break;
  78. case "ArrowUp":
  79. NavigateHistory(-1);
  80. _cursorIndex = _currentInput.Length;
  81. break;
  82. case "ArrowDown":
  83. NavigateHistory(1);
  84. _cursorIndex = _currentInput.Length;
  85. break;
  86. case "Backspace":
  87. if (_cursorIndex > 0)
  88. {
  89. _currentInput =
  90. _currentInput.Remove(_cursorIndex - 1, 1);
  91. _cursorIndex--;
  92. }
  93. break;
  94. case "Delete":
  95. if (_cursorIndex < _currentInput.Length)
  96. {
  97. _currentInput =
  98. _currentInput.Remove(_cursorIndex, 1);
  99. }
  100. break;
  101. default:
  102. // printable character
  103. if (e.Key.Length == 1 && !e.CtrlKey && !e.MetaKey)
  104. {
  105. _currentInput =
  106. _currentInput.Insert(_cursorIndex, e.Key);
  107. _cursorIndex++;
  108. }
  109. break;
  110. }
  111. StateHasChanged();
  112. }
  113. private async Task ExecuteCommand()
  114. {
  115. var cmd = _currentInput.Trim();
  116. if (string.IsNullOrWhiteSpace(cmd))
  117. return;
  118. if (cmd.Equals("clear", StringComparison.OrdinalIgnoreCase))
  119. {
  120. _currentInput = "";
  121. _cursorIndex = 0;
  122. _lines.Clear();
  123. StateHasChanged();
  124. return;
  125. }
  126. if (cmd.Equals("help", StringComparison.OrdinalIgnoreCase))
  127. cmd = "--help";
  128. WriteLine($"{Prompt} {cmd}");
  129. _history.Add(cmd);
  130. _historyIndex = _history.Count;
  131. _currentInput = "";
  132. _cursorIndex = 0;
  133. _busy = true;
  134. StateHasChanged();
  135. try
  136. {
  137. var result = await Console.Execute(cmd);
  138. if (!string.IsNullOrWhiteSpace(result))
  139. {
  140. foreach (var line in result.Split('\n'))
  141. WriteLine(AnsiStripper.Strip(line));
  142. }
  143. }
  144. catch
  145. {
  146. WriteLine("Oops, Something went wrong");
  147. }
  148. finally
  149. {
  150. _busy = false;
  151. }
  152. await ScrollToBottom();
  153. await _inputRef.FocusAsync();
  154. StateHasChanged();
  155. }
  156. private void NavigateHistory(int direction)
  157. {
  158. if (_history.Count == 0)
  159. return;
  160. _historyIndex += direction;
  161. _historyIndex = Math.Clamp(_historyIndex, 0, _history.Count);
  162. _currentInput = _historyIndex < _history.Count
  163. ? _history[_historyIndex]
  164. : "";
  165. _cursorIndex = _currentInput.Length;
  166. }
  167. private void WriteLine(string text)
  168. {
  169. _lines.Add(text);
  170. }
  171. private async Task HandleContainerClick()
  172. {
  173. await _inputRef.FocusAsync();
  174. }
  175. private async Task ScrollToBottom()
  176. {
  177. await Task.Delay(10);
  178. await JS.InvokeVoidAsync("consoleEmulatorScroll", _outputDiv);
  179. }
  180. private async Task CursorBlinkLoop()
  181. {
  182. while (true)
  183. {
  184. _showCursor = !_showCursor;
  185. await InvokeAsync(StateHasChanged);
  186. await Task.Delay(500);
  187. }
  188. }
  189. }