mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-18 11:25:07 +00:00
**Fonts** Dynamically load text and icon fonts when the device boots. Font bitdepth depends on whether the device is an ESP32 one and whether it has external RAM available. When no external RAM is available on ESP32, only small and medium font variants are generated; large font variants refer to the medium size ones. Implemented Appearance settings app for changing font and font size. **Other changes** - Added keyboard shortcuts to delete or rename files in directory views. - Improved app-grid tile sizing to provide more room for labels. - Reduced the default LED strip brightness.
273 lines
8.8 KiB
C++
273 lines
8.8 KiB
C++
#include <Tactility/app/terminal/KeyboardInput.h>
|
|
#include <Tactility/app/terminal/Scrollback.h>
|
|
#include <Tactility/app/terminal/Shell.h>
|
|
#include <Tactility/app/terminal/Terminal.h>
|
|
#include <Tactility/app/terminal/TerminalRenderer.h>
|
|
#include <Tactility/app/terminal/TouchInput.h>
|
|
#include <Tactility/lvgl/Fonts.h>
|
|
|
|
#include <binfont/binfont.h>
|
|
|
|
#include <tactility/device.h>
|
|
#include <tactility/drivers/keyboard.h>
|
|
|
|
#include <tactility/log.h>
|
|
|
|
#include <tactility/freertos/freertos.h>
|
|
#include <tactility/freertos/semphr.h>
|
|
#include <tactility/freertos/task.h>
|
|
|
|
#include <atomic>
|
|
#include <memory>
|
|
|
|
#ifdef ESP_PLATFORM
|
|
#include <esp_log.h>
|
|
#endif
|
|
|
|
extern "C" {
|
|
#include <Tactility/app/terminal/vterm/vterm.h>
|
|
}
|
|
|
|
constexpr auto* TAG = "terminal";
|
|
|
|
// Keyboard polling and redraw cadence. Shell output wakes the I/O task early, so this mostly bounds
|
|
// how long a keypress waits to be picked up.
|
|
constexpr uint32_t RENDER_INTERVAL_MS = 16;
|
|
|
|
// The I/O task polls the keyboards, paints the screen, and watches for the touch-to-exit gesture.
|
|
// It runs a step above this app's own task: input, drawing and the exit gesture must keep working
|
|
// while that task is blocked draining a shell that is itself blocked running something, which is
|
|
// the entire reason this is a separate task.
|
|
constexpr uint32_t IO_TASK_STACK = 8 * 1024;
|
|
constexpr UBaseType_t IO_TASK_PRIORITY = 6;
|
|
|
|
// Lines of scrollback kept, and how far one Ctrl+Up/Down moves the view. 500 lines of an 80-column
|
|
// terminal is about 80KB, which is nothing in PSRAM and covers any realistic burst of output.
|
|
constexpr int SCROLLBACK_LINES = 500;
|
|
constexpr int SCROLL_STEP_LINES = 5;
|
|
|
|
namespace {
|
|
|
|
/** Set once the shell should wind down (touch-to-exit in ioTask(), or APP_EVENT_CLOSE in runShell()). Read by runTerminal()'s own
|
|
* pump loop, which closes the shell app's stdin to unstick it (see that loop's own comment). */
|
|
volatile bool stopRequested = false;
|
|
|
|
/** Set once runShell() has returned. The I/O task keeps running until then, because runShell()
|
|
* notifies it about output until the very end. */
|
|
std::atomic<bool> shellFinished = false;
|
|
|
|
/**
|
|
* Translates a kernel keyboard event into the byte a terminal expects.
|
|
* Returns 0 for keys with no terminal representation.
|
|
*/
|
|
char translateKey(const KeyboardKeyData& data) {
|
|
// Ctrl chords first, so they win over the plain-letter reading of the same key code.
|
|
if (data.ctrl && data.key >= 'a' && data.key <= 'z') {
|
|
return static_cast<char>(data.key & 0x1F);
|
|
}
|
|
if (data.ctrl && data.key >= 'A' && data.key <= 'Z') {
|
|
return static_cast<char>(data.key & 0x1F);
|
|
}
|
|
|
|
switch (data.key) {
|
|
case CODEPOINT_ENTER: return '\r';
|
|
case CODEPOINT_BACKSPACE: return 0x7F; // DEL, which is what linenoise expects for backspace
|
|
case CODEPOINT_ESCAPE: return 0x1B;
|
|
case CODEPOINT_DELETE: return 0x7F;
|
|
default: break;
|
|
}
|
|
|
|
if (data.key == CODEPOINT_TAB && !data.ctrl) {
|
|
return '\t';
|
|
}
|
|
|
|
if (data.key >= 0x20 && data.key < 0x7F) {
|
|
return static_cast<char>(data.key);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Feeds arrow keys to the terminal as ANSI escape sequences,
|
|
* which is how linenoise recognises history navigation and cursor movement.
|
|
*/
|
|
void feedArrowKey(uint32_t key) {
|
|
const char* sequence = nullptr;
|
|
switch (key) {
|
|
case CODEPOINT_ARROW_UP: sequence = "\x1B[A"; break;
|
|
case CODEPOINT_ARROW_DOWN: sequence = "\x1B[B"; break;
|
|
case CODEPOINT_ARROW_RIGHT: sequence = "\x1B[C"; break;
|
|
case CODEPOINT_ARROW_LEFT: sequence = "\x1B[D"; break;
|
|
default: return;
|
|
}
|
|
for (const char* c = sequence; *c != '\0'; c++) {
|
|
vterm_send_input(vterm_get_active(), *c);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles one key from a keyboard.
|
|
* @return true if the scrollback view moved
|
|
*/
|
|
bool handleKey(unsigned int key, bool ctrl, bool alt) {
|
|
(void)alt;
|
|
|
|
if (ctrl) {
|
|
if (key == CODEPOINT_ARROW_UP) {
|
|
return Scrollback::scroll(SCROLL_STEP_LINES);
|
|
}
|
|
if (key == CODEPOINT_ARROW_DOWN) {
|
|
return Scrollback::scroll(-SCROLL_STEP_LINES);
|
|
}
|
|
}
|
|
|
|
// Any other key returns to the live screen: someone typing wants to see the prompt they are
|
|
// typing at rather than whatever history was being reviewed.
|
|
const bool viewChanged = Scrollback::reset();
|
|
|
|
// Plain arrows become escape sequences; everything else maps to a single byte.
|
|
if (!ctrl && (key == CODEPOINT_ARROW_UP || key == CODEPOINT_ARROW_DOWN ||
|
|
key == CODEPOINT_ARROW_LEFT || key == CODEPOINT_ARROW_RIGHT)) {
|
|
feedArrowKey(key);
|
|
return viewChanged;
|
|
}
|
|
|
|
KeyboardKeyData data {};
|
|
data.key = key;
|
|
data.pressed = true;
|
|
data.ctrl = ctrl;
|
|
data.alt = alt;
|
|
|
|
const char c = translateKey(data);
|
|
if (c != 0) {
|
|
vterm_send_input(vterm_get_active(), c);
|
|
}
|
|
return viewChanged;
|
|
}
|
|
|
|
/**
|
|
* Parameters for the I/O task. Lives in runTerminal()'s frame, which outlives the task itself:
|
|
* runTerminal() does not return until the I/O task has acknowledged the stop request.
|
|
*/
|
|
struct IoTaskParams {
|
|
KeyboardInput* keyboards;
|
|
TouchInput* touch;
|
|
TerminalRenderer* renderer;
|
|
SemaphoreHandle_t doneSem;
|
|
};
|
|
|
|
/** Keyboard/touch input and display rendering. */
|
|
void ioTask(void* arg) {
|
|
auto* params = static_cast<IoTaskParams*>(arg);
|
|
|
|
while (!shellFinished) {
|
|
// Keys belong to whichever window is on top, so they are left alone while that is not this one.
|
|
const bool shown = params->renderer->isShown();
|
|
params->keyboards->setExclusive(shown);
|
|
|
|
// A scroll replaces every row at once, so the renderer is told to repaint rather than rely
|
|
// on its per-cell comparison.
|
|
const bool viewMoved = shown && params->keyboards->pump(handleKey);
|
|
params->renderer->render(viewMoved);
|
|
|
|
if (!stopRequested && params->touch != nullptr && params->touch->touched(!params->keyboards->empty())) {
|
|
LOG_I(TAG, "Touch detected - stopping");
|
|
stopRequested = true;
|
|
}
|
|
|
|
// runShell() notifies this task when output arrives, so it is drawn without waiting out the interval
|
|
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(RENDER_INTERVAL_MS));
|
|
}
|
|
|
|
// The shell's last output can arrive after the final pass above, so draw once more
|
|
params->renderer->render(false);
|
|
|
|
xSemaphoreGive(params->doneSem);
|
|
vTaskDelete(nullptr);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void runTerminal(Device* display, TerminalRenderer& renderer, bool touchToExit) {
|
|
stopRequested = false;
|
|
shellFinished = false;
|
|
|
|
KeyboardInput keyboards;
|
|
TouchInput touch;
|
|
|
|
std::unique_ptr<BinFont, decltype(&binfont_close)> font(tt::lvgl::loadMonoFont(), binfont_close);
|
|
if (font == nullptr) {
|
|
LOG_E(TAG, "Failed to load font");
|
|
return;
|
|
}
|
|
renderer.setFont(font.get());
|
|
|
|
if (vterm_init() != ERROR_NONE) {
|
|
LOG_E(TAG, "vterm_init failed");
|
|
return;
|
|
}
|
|
|
|
if (!renderer.begin(display)) {
|
|
LOG_E(TAG, "Renderer failed to start");
|
|
vterm_deinit();
|
|
return;
|
|
}
|
|
|
|
// Tell vterm the real drawable size, which may be smaller than its compiled-in grid.
|
|
vterm_set_size_override(renderer.rows(), renderer.columns());
|
|
|
|
// History of lines that scroll off the top. vterm discards them, so the callback below catches
|
|
// each one while it is still readable. Failure is not fatal; the terminal simply has no scrollback.
|
|
if (Scrollback::begin(renderer.columns(), SCROLLBACK_LINES)) {
|
|
vterm_set_scroll_callback(scrollback_capture_top_line);
|
|
}
|
|
|
|
renderer.render(true);
|
|
|
|
// Input, drawing and the touch-to-exit gesture run on their own task, so that all three keep
|
|
// working while this one is blocked draining the shell app. See ioTask().
|
|
IoTaskParams ioParams {
|
|
.keyboards = &keyboards,
|
|
.touch = touchToExit ? &touch : nullptr,
|
|
.renderer = &renderer,
|
|
.doneSem = xSemaphoreCreateBinary(),
|
|
};
|
|
TaskHandle_t ioHandle = nullptr;
|
|
if (ioParams.doneSem != nullptr) {
|
|
if (xTaskCreate(ioTask, "terminal-io", IO_TASK_STACK, &ioParams,
|
|
IO_TASK_PRIORITY, &ioHandle) != pdPASS) {
|
|
ioHandle = nullptr;
|
|
}
|
|
}
|
|
if (ioHandle == nullptr) {
|
|
LOG_E(TAG, "I/O task failed to start - the terminal cannot run");
|
|
} else {
|
|
#ifdef ESP_PLATFORM
|
|
esp_log_level_set("ELF", ESP_LOG_WARN);
|
|
#endif
|
|
runShell(renderer.columns(), renderer.rows(), &stopRequested, ioHandle);
|
|
shellFinished = true;
|
|
xTaskNotifyGive(ioHandle);
|
|
#ifdef ESP_PLATFORM
|
|
esp_log_level_set("ELF", ESP_LOG_INFO);
|
|
#endif
|
|
}
|
|
|
|
if (ioHandle != nullptr) {
|
|
xSemaphoreTake(ioParams.doneSem, portMAX_DELAY);
|
|
}
|
|
if (ioParams.doneSem != nullptr) {
|
|
vSemaphoreDelete(ioParams.doneSem);
|
|
}
|
|
|
|
vterm_set_scroll_callback(nullptr);
|
|
Scrollback::end();
|
|
|
|
renderer.end();
|
|
|
|
vterm_deinit();
|
|
|
|
LOG_I(TAG, "Terminal stopped");
|
|
}
|