gigatron/rom/Contrib/at67/terminal.cpp
2025-01-28 19:17:01 +03:00

684 lines
22 KiB
C++

#include <stdio.h>
#include <climits>
#include <vector>
#include <algorithm>
#include <sys/stat.h>
#include <SDL.h>
#include "memory.h"
#include "cpu.h"
#include "audio.h"
#include "editor.h"
#include "loader.h"
#include "timing.h"
#include "image.h"
#include "graphics.h"
#include "menu.h"
#define MAX_TERM_COLS 80
#define MAX_TERM_ROWS 47
#define CMD_LINE_ROW 48
#define MAX_HISTORY_CMD 50
#define MAX_COMMAND_CHARS 79
namespace Terminal
{
const int _screenMaxIndex = (SCREEN_HEIGHT - (FONT_HEIGHT+2)*1)/(FONT_HEIGHT+2);
const std::string _eraseLine = std::string(MAX_COMMAND_CHARS+1, 32);
enum MenuItem {MenuCopy=0, MenuAll, MenuCut, MenuDel};
bool _terminalModeGiga = false;
bool _waitForPromptGiga = false;
int _scrollOffset = 0;
int _scrollDelta = 0;
int _scrollIndex = 0;
int _commandHistoryIndex = 0;
int _commandCharIndex = 0;
std::string _commandLine;
std::vector<std::string> _commandLineHistory;
std::vector<int> _selectedText;
std::vector<std::string> _terminalText;
void initialise(void)
{
std::vector<std::string> items = {"TERM ", "Copy ", "All ", "Cut ", "Delete"};
Menu::createMenu("Terminal", items, 6, 5);
}
void sendCommandToGiga(const std::string& cmd)
{
if(_waitForPromptGiga) return;
for(int i=0; i<int(cmd.size()); i++)
{
if(!Loader::sendCharGiga(cmd.c_str()[i])) return;
}
_waitForPromptGiga = true;
}
void scrollToEnd(void)
{
_scrollOffset = _scrollIndex;
}
void clearCommandLine(void)
{
_commandCharIndex = 0;
_commandLine.clear();
}
void clearHistoryCommandLine(void)
{
_commandHistoryIndex = 0;
_commandLineHistory.clear();
}
void exitTerminalModeGiga(void)
{
clearCommandLine();
clearHistoryCommandLine();
if(_terminalModeGiga)
{
Loader::sendCharGiga(4);
_terminalModeGiga = false;
}
}
void switchToTerminal(void)
{
Editor::setEditorMode(Editor::Term);
scrollToEnd();
}
void prevCommandLineHistory(void)
{
if(_commandLineHistory.size() == 0) return;
if(--_commandHistoryIndex < 0) _commandHistoryIndex = 0;
_commandLine = _commandLineHistory[_commandHistoryIndex];
_commandCharIndex = int(_commandLine.size());
}
void nextCommandLineHistory(void)
{
if(_commandLineHistory.size() == 0) return;
if(++_commandHistoryIndex >= int(_commandLineHistory.size())) _commandHistoryIndex = int(_commandLineHistory.size()) - 1;
_commandLine = _commandLineHistory[_commandHistoryIndex];
_commandCharIndex = int(_commandLine.size());
}
void prevCommandLineChar(void)
{
if(--_commandCharIndex < 0) _commandCharIndex = 0;
}
void nextCommandLineChar(void)
{
if(++_commandCharIndex > int(_commandLine.size())) _commandCharIndex = int(_commandLine.size());
}
void backspaceCommandLineChar(void)
{
if(_commandLine.size() && _commandCharIndex > 0 && _commandCharIndex <= int(_commandLine.size()))
{
_commandLine.erase(--_commandCharIndex, 1);
}
}
void deleteCommandLineChar(void)
{
if(_commandLine.size() && _commandCharIndex >= 0 && _commandCharIndex < int(_commandLine.size()))
{
_commandLine.erase(_commandCharIndex, 1);
if(_commandCharIndex > int(_commandLine.size())) _commandCharIndex = int(_commandLine.size());
}
}
void copyCommandLineToHistory(void)
{
if(_commandLineHistory.size() < MAX_HISTORY_CMD)
{
_commandLineHistory.push_back(_commandLine);
_commandHistoryIndex = int(_commandLineHistory.size());
}
else
{
_commandLineHistory.push_back(_commandLine);
_commandLineHistory.erase(_commandLineHistory.begin());
_commandHistoryIndex = int(_commandLineHistory.size()) - 1;
}
clearCommandLine();
}
void copyTextToClipboard(void)
{
int clipboardTextSize = 0;
for(int i=0; i<int(_terminalText.size()); i++) clipboardTextSize += int(_terminalText[i].size());
if(clipboardTextSize == 0) return;
char* clipboardText = new (std::nothrow) char[clipboardTextSize];
if(!clipboardText) return;
// Copy text line by line, char by char, replace trailing zero's with newlines
int clipboardTextIndex = 0;
for(int i=0; i<int(_selectedText.size()); i++)
{
int index = _selectedText[i];
for(int j=0; j<int(_terminalText[index].size())-1; j++)
{
clipboardText[clipboardTextIndex++] = _terminalText[index][j];
}
// trailing zero's get replaced with newlines, except for last one
clipboardText[clipboardTextIndex++] = (i < int(_selectedText.size()) - 1) ? '\n' : 0;
}
// Save to system clipboard
SDL_SetClipboardText(clipboardText);
delete [] clipboardText;
}
void printTerminal(void)
{
_scrollIndex = std::max(0, int(_terminalText.size()) - _screenMaxIndex);
if(_scrollOffset >= _scrollIndex) _scrollOffset = _scrollIndex;
if(_scrollOffset < 0) _scrollOffset = 0;
Graphics::clearScreen(0x22222222);
Graphics::drawText(_eraseLine, 0, _screenMaxIndex*(FONT_HEIGHT+2)+1, 0x55555555, true, MAX_COMMAND_CHARS+1);
// Terminal text
for(int i=_scrollOffset; i<int(_terminalText.size()); i++)
{
if(i - _scrollOffset >= _screenMaxIndex) break;
bool invert = false;
for(int j=0; j<int(_selectedText.size()); j++)
{
if(i == _selectedText[j])
{
invert = true;
break;
}
}
Graphics::drawText(_terminalText[i], FONT_WIDTH, (i-_scrollOffset)*(FONT_HEIGHT+2), 0xAAAAAAAA, invert, MAX_TERM_COLS);
}
// Command line
std::string commandLine = _commandLine;
if(_commandCharIndex >= int(commandLine.size()))
{
_commandCharIndex = int(commandLine.size());
commandLine += char(32);
}
// Flash cursor
static uint8_t toggle = 0;
bool invert = ((toggle++) >>4) & 1;
Graphics::drawText(commandLine, FONT_WIDTH, _screenMaxIndex*(FONT_HEIGHT+2)+1, 0xFFFFFFFF, invert, 1, _commandCharIndex);
}
void handleGuiEvents(SDL_Event& event)
{
switch(event.type)
{
case SDL_WINDOWEVENT:
{
switch(event.window.event)
{
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
{
Graphics::setWidthHeight(event.window.data1, event.window.data2);
}
break;
default: break;
}
}
break;
case SDL_QUIT:
{
exitTerminalModeGiga();
Cpu::shutdown();
exit(0);
}
default: break;
}
}
void handleMouseLeftButtonDown(int mouseX, int mouseY)
{
// Normalised mouse position
float mx = float(mouseX) / float(Graphics::getWidth());
float my = float(mouseY) / float(Graphics::getHeight());
mouseX = int(mx * float(SCREEN_WIDTH));
mouseY = int(my * float(SCREEN_HEIGHT));
//fprintf(stderr, "%d %d %f %f\n", mouseX, mouseY, mx, my);
int terminalTextSelected = (mouseY + 2) / (FONT_HEIGHT+2) + _scrollOffset;
// Save selected text line as long as it doesn't already exist
bool saveText = true;
for(int i=0; i<int(_selectedText.size()); i++)
{
if(_selectedText[i] == terminalTextSelected)
{
saveText = false;
break;
}
}
if(saveText && terminalTextSelected < int(_terminalText.size())) _selectedText.push_back(terminalTextSelected);
// Select everything between min and max as long as it doesn't already exist
int selectedMin = INT_MAX;
int selectedMax = INT_MIN;
for(int i=0; i<int(_selectedText.size()); i++)
{
if(_selectedText[i] > selectedMax) selectedMax = _selectedText[i];
if(_selectedText[i] < selectedMin) selectedMin = _selectedText[i];
}
for(int i=selectedMin+1; i<selectedMax; i++)
{
saveText = true;
for(int j=0; j<int(_selectedText.size()); j++)
{
if(_selectedText[j] == i)
{
saveText = false;
break;
}
}
if(saveText && i < int(_terminalText.size())) _selectedText.push_back(i);
}
if(mouseY == 0) _scrollOffset--;
}
void handleMouseRightButtonDown(int mouseX, int mouseY)
{
Menu::captureItem("Terminal", mouseX, mouseY);
Menu::renderMenu("Terminal");
}
void handleMouseButtonDown(const SDL_Event& event, const Editor::MouseState& mouseState)
{
UNREFERENCED_PARAM(event);
if(mouseState._state == SDL_BUTTON_LEFT) _selectedText.clear();
if(mouseState._state == SDL_BUTTON_X1)
{
Menu::captureMenu("Terminal", mouseState._x, mouseState._y);
}
}
void handleMouseButtonUp(const SDL_Event& event, const Editor::MouseState& mouseState)
{
UNREFERENCED_PARAM(event);
UNREFERENCED_PARAM(mouseState);
int menuItemIndex;
Menu::getMenuItemIndex("Terminal", menuItemIndex);
switch(menuItemIndex)
{
// Copy selected text
case MenuCopy:
{
if(_terminalText.size() && _selectedText.size())
{
// Sort selected text
std::sort(_selectedText.begin(), _selectedText.end());
copyTextToClipboard();
}
}
break;
// Toggle between select all and select none
case MenuAll:
{
if(_selectedText.size() == _terminalText.size())
{
_selectedText.clear();
}
else
{
_selectedText.clear();
for(int i=0; i<int(_terminalText.size()); i++)
{
_selectedText.push_back(i);
}
}
}
break;
case MenuCut:
{
if(_terminalText.size() && _selectedText.size())
{
// Sort selected text
std::sort(_selectedText.begin(), _selectedText.end());
copyTextToClipboard();
// Delete selected
int selectedMin = _selectedText[0];
int selectedMax = _selectedText.back();
if(int(_terminalText.size()) > selectedMax)
{
_terminalText.erase(_terminalText.begin() + selectedMin, _terminalText.begin() + selectedMax + 1);
}
}
_selectedText.clear();
}
break;
case MenuDel:
{
if(_terminalText.size() && _selectedText.size())
{
// Sort selected text
std::sort(_selectedText.begin(), _selectedText.end());
// Delete selected
int selectedMin = _selectedText[0];
int selectedMax = _selectedText.back();
if(int(_terminalText.size()) > selectedMax)
{
_terminalText.erase(_terminalText.begin() + selectedMin, _terminalText.begin() + selectedMax + 1);
}
}
_selectedText.clear();
}
break;
default: break;
}
}
void handleMouseWheel(const SDL_Event& event)
{
if(event.wheel.y > 0) _scrollOffset -= 1;
if(event.wheel.y < 0) _scrollOffset += 1;
}
void handleKey(const SDL_Event& event)
{
char keyCode = event.text.text[0];
if(_terminalModeGiga) Loader::sendCharGiga(keyCode);
if(keyCode >= 32 && keyCode <= 126)
{
_commandLine.insert(_commandLine.begin() + _commandCharIndex, char(keyCode));
_commandCharIndex++;
if(_commandLine.size() >= MAX_COMMAND_CHARS) copyCommandLineToHistory();
}
}
void handleKeyDown(SDL_Keycode keyCode, Uint16 keyMod)
{
// Leave terminal mode
if(keyCode == Editor::getEmulatorScanCode("Terminal") && keyMod == Editor::getEmulatorKeyMod("Terminal"))
{
exitTerminalModeGiga();
Editor::setEditorToPrevMode();
}
// Quit
else if(keyCode == Editor::getEmulatorScanCode("Quit") && keyMod == Editor::getEmulatorKeyMod("Quit"))
{
exitTerminalModeGiga();
Cpu::shutdown();
exit(0);
}
// Image editor
else if(keyCode == Editor::getEmulatorScanCode("ImageEditor") && keyMod == Editor::getEmulatorKeyMod("ImageEditor"))
{
Editor::setEditorMode(Editor::Image);
}
// Audio editor
else if(keyCode == Editor::getEmulatorScanCode("AudioEditor") && keyMod == Editor::getEmulatorKeyMod("AudioEditor"))
{
Editor::setEditorMode(Editor::Audio);
}
// No modifier keys
static bool useTerminalHistory = false;
if(keyMod == 0x0000)
{
// Both modes
switch(keyCode)
{
case SDLK_PAGEUP: _scrollOffset -= _screenMaxIndex; break;
case SDLK_PAGEDOWN: _scrollOffset += _screenMaxIndex; break;
case SDLK_HOME: _scrollOffset = 0; break;
case SDLK_END: _scrollOffset = _scrollIndex; break;
default: break;
}
// Normal mode
if(!_terminalModeGiga)
{
switch(keyCode)
{
case SDLK_UP: prevCommandLineHistory(); break;
case SDLK_DOWN: nextCommandLineHistory(); break;
case SDLK_LEFT: prevCommandLineChar(); break;
case SDLK_RIGHT: nextCommandLineChar(); break;
case SDLK_DELETE: deleteCommandLineChar(); break;
case SDLK_BACKSPACE: backspaceCommandLineChar(); break;
case '\r':
case '\n':
{
if(!_terminalModeGiga && _commandLine.size() == 1 && (_commandLine[0] == 't' || _commandLine[0] == 'T'))
{
_terminalModeGiga = true;
sendCommandToGiga(_commandLine + "\n");
clearCommandLine();
clearHistoryCommandLine();
}
else
{
sendCommandToGiga(_commandLine + "\n");
copyCommandLineToHistory();
}
}
break;
default: break;
}
}
// Terminal mode
else
{
switch(keyCode)
{
case SDLK_UP: Loader::sendCharGiga(27); Loader::sendCharGiga('['); Loader::sendCharGiga('A'); break;
case SDLK_DOWN: Loader::sendCharGiga(27); Loader::sendCharGiga('['); Loader::sendCharGiga('B'); break;
case SDLK_RIGHT: Loader::sendCharGiga(27); Loader::sendCharGiga('['); Loader::sendCharGiga('C'); break;
case SDLK_LEFT: Loader::sendCharGiga(27); Loader::sendCharGiga('['); Loader::sendCharGiga('D'); break;
case SDLK_TAB: Loader::sendCharGiga(char(keyCode)); break;
case SDLK_DELETE: Loader::sendCharGiga(char(keyCode)); backspaceCommandLineChar(); break;
case '\r':
case '\n':
{
if(useTerminalHistory)
{
useTerminalHistory = false;
for(int i=0; i<int(_commandLine.size()); i++) Loader::sendCharGiga(_commandLine[i]);
Loader::sendCharGiga('\n');
clearCommandLine();
}
else
{
Loader::sendCharGiga(char(keyCode));
copyCommandLineToHistory();
clearCommandLine();
}
}
break;
default: break;
}
}
}
// Terminal mode modifier keys
else if(_terminalModeGiga)
{
if(keyMod & KMOD_LCTRL)
{
switch(keyCode)
{
case SDLK_UP: prevCommandLineHistory(); useTerminalHistory = true; break;
case SDLK_DOWN: nextCommandLineHistory(); useTerminalHistory = true; break;
case SDLK_c: Loader::sendCharGiga(3); break; // CTRL C to break BASIC
case SDLK_d: exitTerminalModeGiga(); break; // CTRL D to exit terminal mode
default: break;
}
}
}
}
void handleKeyUp(SDL_Keycode keyCode, Uint16 keyMod)
{
// Help screen
if(keyCode == Editor::getEmulatorScanCode("Help") && keyMod == Editor::getEmulatorKeyMod("Help"))
{
static bool helpScreen = false;
helpScreen = !helpScreen;
Graphics::setDisplayHelpScreen(helpScreen);
}
// TODO: conflicts with CTRL-D in pluggy terminal mode, Disassembler
//else if(keyCode == Editor::getEmulatorScanCode("Disassembler") && keyMod == Editor::getEmulatorKeyMod("Disassembler"))
//{
// Editor::setEditorMode(Editor::Dasm);
//}
// Browser
else if(keyCode == Editor::getEmulatorScanCode("Browse") && keyMod == Editor::getEmulatorKeyMod("Browse"))
{
Editor::browseDirectory();
Editor::setEditorMode(Editor::Load);
}
// Hex monitor
else if(keyCode == Editor::getEmulatorScanCode("HexMonitor") && keyMod == Editor::getEmulatorKeyMod("HexMonitor"))
{
Editor::setEditorMode(Editor::Hex);
}
}
void handleInput(void)
{
// Mouse button state
Editor::MouseState mouseState;
mouseState._state = SDL_GetMouseState(&mouseState._x, &mouseState._y);
SDL_Event event;
while(SDL_PollEvent(&event))
{
SDL_Keycode keyCode = event.key.keysym.sym;
Uint16 keyMod = event.key.keysym.mod & (KMOD_LCTRL | KMOD_LALT | KMOD_LSHIFT);
mouseState._state = SDL_GetMouseState(&mouseState._x, &mouseState._y);
handleGuiEvents(event);
switch(event.type)
{
case SDL_MOUSEBUTTONDOWN: handleMouseButtonDown(event, mouseState); break;
case SDL_MOUSEBUTTONUP: handleMouseButtonUp(event, mouseState); break;
case SDL_MOUSEWHEEL: handleMouseWheel(event); break;
case SDL_TEXTINPUT: handleKey(event); break;
case SDL_KEYDOWN: handleKeyDown(keyCode, keyMod); break;
case SDL_KEYUP: handleKeyUp(keyCode, keyMod); break;
default: break;
}
}
if(_waitForPromptGiga)
{
std::string line;
if(!Loader::readLineGiga(line))
{
_waitForPromptGiga = false;
fprintf(stderr, "Terminal::handleInput() : timed out on COM port '%s'\n", Loader::getConfigComPort().c_str());
}
else
{
_terminalText.push_back(line);
_scrollDelta = 1;
if(line.find("?") != std::string::npos || line.find("Ctrl-D") != std::string::npos)
{
_waitForPromptGiga = false;
_scrollDelta = 2;
}
}
}
// Read chars back from Gigatron in terminal mode
if(_terminalModeGiga)
{
char chr = 0;
static std::string line;
if(Loader::readCharGiga(&chr))
{
if(chr >= 32 && chr <= 126) line.push_back(chr);
if(chr == '\r' || chr == '\n')
{
if(line.size())
{
_terminalText.push_back(line);
_scrollDelta = 1;
}
line.clear();
}
}
}
// Scroll text one line at a time until end is reached, (jump to end - delta if scroll index is not at end)
if(_scrollDelta)
{
_scrollOffset = _scrollIndex - _scrollDelta + 2;
_scrollDelta--;
}
printTerminal();
switch(mouseState._state)
{
case SDL_BUTTON_LEFT: handleMouseLeftButtonDown(mouseState._x, mouseState._y); break;
case SDL_BUTTON_X1: handleMouseRightButtonDown(mouseState._x, mouseState._y); break;
default: break;
}
}
void process(void)
{
handleInput();
Graphics::render(true);
}
}