console mode: three colours per line, trackball scroll, shell prompt, visible banner
Colour, properly this time. A line can carry THREE coloured segments, because the three parts of a message answer different questions: WHERE it arrived (channel, cyan), WHO said it (sender, yellow) and WHAT they said (grey). A DM has no channel, so its sender takes the magenta slot and stands out in a busy feed. Same treatment for stored history, contacts and unread. The palette is explicit RGB565, deliberately NOT the UIColor theme names: several of those resolve to the same value on a given board, so 'channel' and 'sender' would have come out identical. e-ink falls back to one ink colour, where anything else would be invisible. Banner: it WAS printing, just scrolling off. Boot output was seventeen lines on a panel that fits fewer. The art is now three lines, identity is one, and the menu is three-per-row instead of two, so the whole thing fits. Prompt: 'node:recipient$' instead of a bare caret. Where you are and who you are talking to is what a shell prompt is for, and 'to <name>' now visibly changes it. Trackball scrolls the scrollback, accumulated so one flick does not race past everything, and it wakes a dark screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a1916cfcc7
commit
4b5b51ef01
3 files changed
+207
-55
No files matched your search
+163
-49
@@ -64,6 +64,9 @@ constexpr int kInputCap = 128;
|
||||
|
||||
char* s_ring = nullptr; // kMaxLines * (kLineCap + 1)
|
||||
uint8_t* s_ring_col = nullptr; // one ConsoleColor per stored line
|
||||
uint8_t* s_ring_split = nullptr; // length of segment 1
|
||||
uint8_t* s_ring_col2 = nullptr; // colour of segment 2
|
||||
uint8_t* s_ring_spl2 = nullptr; // length of segment 2 (the rest is CC_TEXT)
|
||||
int s_head = 0; // next write slot
|
||||
int s_count = 0;
|
||||
int s_scroll = 0; // lines scrolled back from the newest
|
||||
@@ -80,6 +83,11 @@ int s_cur_x = 0, s_cur_y = 0; // cursor cell, set by the last input-line dr
|
||||
DisplayDriver* s_disp = nullptr;
|
||||
int s_char_w = 6, s_line_h = 8, s_cols = 40, s_rows = 10;
|
||||
|
||||
// Current recipient (set by `to`) and node name: both appear in the shell
|
||||
// prompt, so they have to be declared before the input line is drawn.
|
||||
char s_to[40] = {0};
|
||||
char s_node[24] = {0};
|
||||
|
||||
char s_input[kInputCap] = {0};
|
||||
int s_input_len = 0;
|
||||
uint32_t s_blink_ms = 0;
|
||||
@@ -87,7 +95,10 @@ bool s_blink_on = true;
|
||||
|
||||
inline char* lineAt(int i) { return s_ring + (size_t)i * (kLineCap + 1); }
|
||||
|
||||
uint8_t s_push_col = CC_TEXT; // colour applied by the next ringPush
|
||||
uint8_t s_push_col = CC_TEXT; // colour of segment 1 for the next ringPush
|
||||
uint8_t s_push_split = 0; // length of segment 1 (0 = whole line)
|
||||
uint8_t s_push_col2 = CC_TEXT; // colour of segment 2
|
||||
uint8_t s_push_spl2 = 0; // length of segment 2
|
||||
|
||||
void ringPush(const char* s, int len) {
|
||||
if (!s_ring) return;
|
||||
@@ -95,7 +106,10 @@ void ringPush(const char* s, int len) {
|
||||
char* dst = lineAt(s_head);
|
||||
memcpy(dst, s, len);
|
||||
dst[len] = '\0';
|
||||
if (s_ring_col) s_ring_col[s_head] = s_push_col;
|
||||
if (s_ring_col) s_ring_col[s_head] = s_push_col;
|
||||
if (s_ring_split) s_ring_split[s_head] = s_push_split;
|
||||
if (s_ring_col2) s_ring_col2[s_head] = s_push_col2;
|
||||
if (s_ring_spl2) s_ring_spl2[s_head] = s_push_spl2;
|
||||
s_head = (s_head + 1) % kMaxLines;
|
||||
if (s_count < kMaxLines) s_count++;
|
||||
s_scroll = 0; // any new output jumps back to the live tail
|
||||
@@ -117,21 +131,41 @@ uint8_t ringGetCol(int i) {
|
||||
const int start = (s_head - s_count + kMaxLines * 2) % kMaxLines;
|
||||
return s_ring_col[(start + i) % kMaxLines];
|
||||
}
|
||||
uint8_t ringGetSplit(int i) {
|
||||
if (!s_ring_split || i < 0 || i >= s_count) return 0;
|
||||
const int start = (s_head - s_count + kMaxLines * 2) % kMaxLines;
|
||||
return s_ring_split[(start + i) % kMaxLines];
|
||||
}
|
||||
uint8_t ringGetCol2(int i) {
|
||||
if (!s_ring_col2 || i < 0 || i >= s_count) return CC_TEXT;
|
||||
const int start = (s_head - s_count + kMaxLines * 2) % kMaxLines;
|
||||
return s_ring_col2[(start + i) % kMaxLines];
|
||||
}
|
||||
uint8_t ringGetSpl2(int i) {
|
||||
if (!s_ring_spl2 || i < 0 || i >= s_count) return 0;
|
||||
const int start = (s_head - s_count + kMaxLines * 2) % kMaxLines;
|
||||
return s_ring_spl2[(start + i) % kMaxLines];
|
||||
}
|
||||
|
||||
// One place that decides what a colour means on this panel. UIColor is the
|
||||
// core's named set, so the console tracks the rest of the firmware rather than
|
||||
// inventing its own values.
|
||||
// The console palette, as explicit RGB565. Deliberately NOT the UIColor theme
|
||||
// names: several of those resolve to the same value on a given board, so
|
||||
// "channel" and "sender" would have come out identical, and on e-ink they are
|
||||
// all black. Explicit values guarantee the three parts of a message actually
|
||||
// look different.
|
||||
uint16_t colourFor(uint8_t c) {
|
||||
// e-ink has one ink colour; anything else would be invisible or dithered.
|
||||
if (s_disp && s_disp->isEink()) return UIColor::primary_txt;
|
||||
switch (c) {
|
||||
case CC_DIM: return UIColor::secondary_txt;
|
||||
case CC_ECHO: return UIColor::title_txt;
|
||||
case CC_OK: return UIColor::corp_blue;
|
||||
case CC_WARN: return UIColor::warning_txt;
|
||||
case CC_ERR: return UIColor::warning_txt;
|
||||
case CC_CHAN: return UIColor::corp_blue;
|
||||
case CC_DM: return UIColor::title_txt;
|
||||
case CC_HEAD: return UIColor::title_txt;
|
||||
default: return UIColor::primary_txt;
|
||||
case CC_DIM: return 0x7BEF; // grey
|
||||
case CC_ECHO: return 0x07E0; // green, the shell-echo convention
|
||||
case CC_OK: return 0x07E0; // green
|
||||
case CC_WARN: return 0xFD20; // orange
|
||||
case CC_ERR: return 0xF800; // red
|
||||
case CC_CHAN: return 0x07FF; // cyan - where it came from
|
||||
case CC_SENDER: return 0xFFE0; // yellow - who said it
|
||||
case CC_DM: return 0xF81F; // magenta - a DM stands out in a busy feed
|
||||
case CC_HEAD: return 0x15B4; // brand teal
|
||||
default: return 0xC618; // light grey, the message itself
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,9 +317,34 @@ void render() {
|
||||
const int idx = first + r;
|
||||
const char* l = (idx >= 0) ? ringGet(idx) : nullptr;
|
||||
if (l && *l) {
|
||||
s_disp->setColor(colourFor(ringGetCol(idx)));
|
||||
s_disp->setCursor(0, y);
|
||||
s_disp->print(l);
|
||||
// Up to three coloured segments on one line: where it came from, who
|
||||
// said it, and what they said. Drawn left to right, each starting at the
|
||||
// measured width of everything before it.
|
||||
const int len = (int)strlen(l);
|
||||
int n1 = ringGetSplit(idx);
|
||||
int n2 = ringGetSpl2(idx);
|
||||
if (n1 > len) n1 = len;
|
||||
if (n1 + n2 > len) n2 = len - n1;
|
||||
int x = 0;
|
||||
char seg[kLineCap + 1];
|
||||
if (n1 > 0) {
|
||||
memcpy(seg, l, n1); seg[n1] = '\0';
|
||||
s_disp->setColor(colourFor(ringGetCol(idx)));
|
||||
s_disp->setCursor(x, y); s_disp->print(seg);
|
||||
x += s_disp->getTextWidth(seg);
|
||||
}
|
||||
if (n2 > 0) {
|
||||
memcpy(seg, l + n1, n2); seg[n2] = '\0';
|
||||
s_disp->setColor(colourFor(ringGetCol2(idx)));
|
||||
s_disp->setCursor(x, y); s_disp->print(seg);
|
||||
x += s_disp->getTextWidth(seg);
|
||||
}
|
||||
if (n1 + n2 < len) {
|
||||
// The remainder. When there were no segments this is the whole line, so
|
||||
// it takes the line's own colour rather than always being plain text.
|
||||
s_disp->setColor(colourFor((n1 + n2) == 0 ? ringGetCol(idx) : CC_TEXT));
|
||||
s_disp->setCursor(x, y); s_disp->print(l + n1 + n2);
|
||||
}
|
||||
}
|
||||
y += s_line_h;
|
||||
}
|
||||
@@ -299,10 +358,10 @@ void render() {
|
||||
s_dirty = false;
|
||||
}
|
||||
|
||||
// Current recipient, set by `to`. A name rather than an index or a slot, for the
|
||||
// same reason the send path matches by name: an index goes stale the moment the
|
||||
// contact list changes underneath it.
|
||||
char s_to[40] = {0};
|
||||
// (s_to is declared with the console state above, because the shell prompt
|
||||
// draws it. It holds a NAME rather than an index for the same reason the send
|
||||
// path matches by name: an index goes stale the moment the contact list
|
||||
// changes underneath it.)
|
||||
|
||||
void cmdContacts() {
|
||||
char name[36], pk[12]; int type; uint32_t ago; double lat, lon; int32_t la6, lo6;
|
||||
@@ -311,9 +370,12 @@ void cmdContacts() {
|
||||
if (!luaHostContactAt(i, name, sizeof name, &type, &ago, &lat, &lon, pk, sizeof pk, &la6, &lo6)) break;
|
||||
static const char* kType[5] = { "?", "chat", "repeater", "room", "sensor" };
|
||||
char line[kLineCap];
|
||||
snprintf(line, sizeof line, "%-16s %-8s %s", name,
|
||||
// Name in the accent, the type and key dim: the name is what you are
|
||||
// looking for, the rest is reference.
|
||||
const int split = snprintf(line, sizeof line, "%-16s ", name);
|
||||
snprintf(line + split, sizeof line - split, "%-8s %s",
|
||||
(type >= 1 && type <= 4) ? kType[type] : "?", pk);
|
||||
consoleWriteLine(line);
|
||||
consoleWriteLineSplit(CC_DM, split, line);
|
||||
shown++;
|
||||
}
|
||||
if (!shown) consoleWriteLine("(no contacts yet)");
|
||||
@@ -338,18 +400,26 @@ void drawInputLine(bool cursor_on) {
|
||||
s_disp->fillRect(0, iy, s_disp->width(), s_line_h);
|
||||
|
||||
s_disp->setTextSize(1);
|
||||
s_disp->setColor(UIColor::title_txt);
|
||||
// A shell prompt rather than a bare caret: node name, the current recipient
|
||||
// as the working "directory", then $. Where you are and who you are talking
|
||||
// to is exactly what a shell prompt is for.
|
||||
char prompt[48];
|
||||
snprintf(prompt, sizeof prompt, "%s:%s$ ",
|
||||
s_node[0] ? s_node : "wadamesh", s_to[0] ? s_to : "~");
|
||||
s_disp->setColor(colourFor(CC_OK));
|
||||
s_disp->setCursor(0, iy);
|
||||
s_disp->print(">");
|
||||
s_disp->print(prompt);
|
||||
const int px = s_disp->getTextWidth(prompt);
|
||||
s_disp->setColor(UIColor::primary_txt);
|
||||
s_disp->setCursor(s_char_w * 2, iy);
|
||||
s_disp->setCursor(px, iy);
|
||||
// Show the tail of a long line so the caret stays visible while typing.
|
||||
const int room = s_cols - 3;
|
||||
const int room = s_cols - (int)strlen(prompt) - 1;
|
||||
if (room < 4) { /* tiny panel: still show something */ }
|
||||
const char* shown = s_input;
|
||||
if (s_input_len > room) shown = s_input + (s_input_len - room);
|
||||
s_disp->print(shown);
|
||||
|
||||
s_cur_x = s_char_w * 2 + s_disp->getTextWidth(shown);
|
||||
s_cur_x = px + s_disp->getTextWidth(shown);
|
||||
s_cur_y = iy;
|
||||
if (cursor_on) {
|
||||
s_disp->setColor(UIColor::primary_txt);
|
||||
@@ -378,10 +448,9 @@ void drawCursorOnly(bool on) {
|
||||
// Deliberately narrow: 26 columns fits the smallest panel we ship (the V4 at
|
||||
// 240 px), so nobody sees a broken box.
|
||||
const char* const kBanner[] = {
|
||||
" _ _ _ ___ ___ ___ ",
|
||||
"| | | | .'| | _|",
|
||||
"|_____|__,|_|_|_| ",
|
||||
" m e s h",
|
||||
" _ _ _ _ ___ _ _ _ ___ ___ _ _ ",
|
||||
"| | | |/ \\| \\ / \\ | \\/ |/ __/ __| |_|",
|
||||
"|_____|\\_/|_/ \\_/ |_/\\_|\\___/___|_|_|",
|
||||
};
|
||||
|
||||
// The quick menu. Numbers because typing 1 is faster than typing 'contacts',
|
||||
@@ -401,17 +470,14 @@ const QuickItem kQuick[] = {
|
||||
const int kQuickN = (int)(sizeof(kQuick) / sizeof(kQuick[0]));
|
||||
|
||||
void cmdMenu() {
|
||||
consoleWriteLineC(CC_HEAD, "quick launch");
|
||||
for (int i = 0; i < kQuickN; i += 2) {
|
||||
char line[kLineCap];
|
||||
if (i + 1 < kQuickN)
|
||||
snprintf(line, sizeof line, " %s %-10s %s %-10s",
|
||||
kQuick[i].key, kQuick[i].label, kQuick[i + 1].key, kQuick[i + 1].label);
|
||||
else
|
||||
snprintf(line, sizeof line, " %s %-10s", kQuick[i].key, kQuick[i].label);
|
||||
// Three per row: the menu costs three lines instead of five, which is what
|
||||
// lets the banner above it stay on screen.
|
||||
for (int i = 0; i < kQuickN; i += 3) {
|
||||
char line[kLineCap]; int o = 0;
|
||||
for (int k = i; k < i + 3 && k < kQuickN; k++)
|
||||
o += snprintf(line + o, sizeof line - o, "%s %-9s", kQuick[k].key, kQuick[k].label);
|
||||
consoleWriteLine(line);
|
||||
}
|
||||
consoleWriteLineC(CC_DIM, "type a number, or 'help'");
|
||||
}
|
||||
|
||||
// ---- command dispatch -------------------------------------------------------
|
||||
@@ -458,7 +524,7 @@ void submit() {
|
||||
}
|
||||
#endif
|
||||
if (!strcasecmp(cmd, "help")) {
|
||||
consoleWriteLine("console clear, help, mem, ui");
|
||||
consoleWriteLineC(CC_HEAD, "console clear, help, menu, mem, ui");
|
||||
consoleWriteLine("mesh contacts, chans, unread");
|
||||
consoleWriteLine(" chat <name> show a thread");
|
||||
consoleWriteLine(" discover / discovered");
|
||||
@@ -507,7 +573,10 @@ void submit() {
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (!consoleHostThreadAt(i, name, sizeof name, &un, &chan)) break;
|
||||
if (un <= 0) continue;
|
||||
consolePrintf("%s%-20s %d", chan ? "#" : " ", name, un);
|
||||
char row[kLineCap];
|
||||
const int sp = snprintf(row, sizeof row, "%s%-18s ", chan ? "#" : " ", name);
|
||||
snprintf(row + sp, sizeof row - sp, "%d unread", un);
|
||||
consoleWriteLineSplit(chan ? CC_CHAN : CC_DM, sp, row);
|
||||
shown++;
|
||||
}
|
||||
consolePrintf("%d unread in %d thread%s", consoleHostUnreadTotal(), shown, shown == 1 ? "" : "s");
|
||||
@@ -529,7 +598,10 @@ void submit() {
|
||||
// Collect newest-first, print oldest-first, so it reads like a conversation.
|
||||
for (int i = 11; i >= 0; i--) {
|
||||
if (!consoleHostHistoryAt(who, i, sender, sizeof sender, text, sizeof text, &ts, &out)) continue;
|
||||
consolePrintfC(out ? CC_DIM : CC_TEXT, "%s%s: %s", out ? "> " : "", sender, text);
|
||||
char row[220];
|
||||
const int sp = snprintf(row, sizeof row, "%s%s: ", out ? "> " : "", sender);
|
||||
snprintf(row + sp, sizeof row - sp, "%s", text);
|
||||
consoleWriteLineSplit(out ? CC_DIM : CC_DM, sp, row);
|
||||
shown++;
|
||||
}
|
||||
if (!shown) consolePrintfC(CC_WARN, "nothing stored for '%s'", who);
|
||||
@@ -625,6 +697,19 @@ void consoleBegin(DisplayDriver* d) {
|
||||
#endif
|
||||
if (!s_ring_col) s_ring_col = (uint8_t*)malloc(kMaxLines);
|
||||
if (s_ring_col) memset(s_ring_col, CC_TEXT, kMaxLines); // null is tolerated: all text
|
||||
#if defined(ESP32)
|
||||
s_ring_split = (uint8_t*)heap_caps_malloc(kMaxLines, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
#endif
|
||||
if (!s_ring_split) s_ring_split = (uint8_t*)malloc(kMaxLines);
|
||||
if (s_ring_split) memset(s_ring_split, 0, kMaxLines);
|
||||
#if defined(ESP32)
|
||||
s_ring_col2 = (uint8_t*)heap_caps_malloc(kMaxLines, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
s_ring_spl2 = (uint8_t*)heap_caps_malloc(kMaxLines, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
#endif
|
||||
if (!s_ring_col2) s_ring_col2 = (uint8_t*)malloc(kMaxLines);
|
||||
if (!s_ring_spl2) s_ring_spl2 = (uint8_t*)malloc(kMaxLines);
|
||||
if (s_ring_col2) memset(s_ring_col2, CC_TEXT, kMaxLines);
|
||||
if (s_ring_spl2) memset(s_ring_spl2, 0, kMaxLines);
|
||||
}
|
||||
s_active = true;
|
||||
measure();
|
||||
@@ -634,15 +719,30 @@ void consoleBegin(DisplayDriver* d) {
|
||||
|
||||
// Printed once at boot by UITask::begin. Not inside consoleBegin, because the
|
||||
// node name and version are not knowable until the mesh is up.
|
||||
void consoleSetNodeName(const char* n) { if (n) snprintf(s_node, sizeof s_node, "%s", n); }
|
||||
|
||||
void consoleBanner(const char* node_name, const char* version) {
|
||||
consoleSetNodeName(node_name);
|
||||
// Kept SHORT on purpose. The first version was seventeen lines of boot text
|
||||
// on a panel that fits fewer, so the art scrolled off before anyone saw it,
|
||||
// which is exactly how it was reported. Banner + identity + menu now fits.
|
||||
for (unsigned i = 0; i < sizeof(kBanner) / sizeof(kBanner[0]); i++)
|
||||
consoleWriteLineC(CC_HEAD, kBanner[i]);
|
||||
consoleWriteLine("");
|
||||
if (node_name && *node_name) consolePrintfC(CC_OK, "node %s", node_name);
|
||||
if (version && *version) consolePrintfC(CC_DIM, "build %s", version);
|
||||
consoleWriteLine("");
|
||||
consolePrintfC(CC_OK, "%s %s", node_name && *node_name ? node_name : "node",
|
||||
version && *version ? version : "");
|
||||
cmdMenu();
|
||||
consoleWriteLine("");
|
||||
}
|
||||
|
||||
// Scroll the view. +1 goes one line back into history, -1 one line towards live.
|
||||
// Clamped so it cannot run past either end.
|
||||
void consoleScroll(int delta) {
|
||||
if (!s_active) return;
|
||||
int limit = s_count - s_rows;
|
||||
if (limit < 0) limit = 0;
|
||||
int next = s_scroll + delta;
|
||||
if (next < 0) next = 0;
|
||||
if (next > limit) next = limit;
|
||||
if (next != s_scroll) { s_scroll = next; s_dirty = true; }
|
||||
}
|
||||
|
||||
void consoleEnd() { s_active = false; }
|
||||
@@ -691,6 +791,20 @@ void consoleWriteLineC(uint8_t colour, const char* line) {
|
||||
s_push_col = CC_TEXT;
|
||||
}
|
||||
|
||||
static inline uint8_t clamp255(int v) { return (uint8_t)(v < 0 ? 0 : (v > 255 ? 255 : v)); }
|
||||
|
||||
void consoleWriteLineSeg(uint8_t c1, int len1, uint8_t c2, int len2, const char* line) {
|
||||
s_push_col = c1; s_push_split = clamp255(len1);
|
||||
s_push_col2 = c2; s_push_spl2 = clamp255(len2);
|
||||
consoleWriteLine(line);
|
||||
s_push_col = CC_TEXT; s_push_split = 0;
|
||||
s_push_col2 = CC_TEXT; s_push_spl2 = 0;
|
||||
}
|
||||
|
||||
void consoleWriteLineSplit(uint8_t colour, int split, const char* line) {
|
||||
consoleWriteLineSeg(colour, split, CC_TEXT, 0, line);
|
||||
}
|
||||
|
||||
void consolePrintfC(uint8_t colour, const char* fmt, ...) {
|
||||
char buf[256];
|
||||
va_list ap;
|
||||
|
||||
@@ -37,8 +37,9 @@ enum ConsoleColor : uint8_t {
|
||||
CC_OK, // success
|
||||
CC_WARN, // warnings, refusals
|
||||
CC_ERR, // failures
|
||||
CC_CHAN, // an incoming channel message
|
||||
CC_DM, // an incoming direct message
|
||||
CC_CHAN, // the channel a message arrived on
|
||||
CC_SENDER, // who sent it
|
||||
CC_DM, // a direct message (its sender)
|
||||
CC_HEAD, // section headings, the banner
|
||||
};
|
||||
|
||||
@@ -48,9 +49,17 @@ void consoleWriteLine(const char* line);
|
||||
void consoleWriteLineC(uint8_t colour, const char* line);
|
||||
void consolePrintf(const char* fmt, ...);
|
||||
void consolePrintfC(uint8_t colour, const char* fmt, ...);
|
||||
// A line in up to THREE colours: [0,len1) in c1, [len1,len1+len2) in c2, the
|
||||
// remainder as normal text. That is exactly the shape of an incoming message:
|
||||
// the channel, then who said it, then what they said, each its own colour.
|
||||
void consoleWriteLineSeg(uint8_t c1, int len1, uint8_t c2, int len2, const char* line);
|
||||
void consoleWriteLineSplit(uint8_t colour, int split, const char* line); // two-tone shorthand
|
||||
|
||||
// The login banner: ASCII mark, who this node is, and the quick-launch menu.
|
||||
void consoleBanner(const char* node_name, const char* version);
|
||||
void consoleSetNodeName(const char* n);
|
||||
// Scroll the view. +1 = one line back into history, -1 = one line towards live.
|
||||
void consoleScroll(int delta);
|
||||
|
||||
// Feed one character from a hardware keyboard. '\n' submits, '\b' deletes.
|
||||
// Returns true if the console consumed it.
|
||||
|
||||
+33
-4
@@ -51652,10 +51652,23 @@ void UITask::newMsgImpl(uint8_t path_len, const char* from_name, const char* tex
|
||||
// message is stored and still counts as unread. Watching it scroll past is not
|
||||
// reading it; opening the thread is.
|
||||
if (s_console_mode && touchPrefsGetConsoleMonitor()) {
|
||||
char line[200];
|
||||
if (channel) snprintf(line, sizeof line, "#%s %s: %s", thread, sender, body);
|
||||
else snprintf(line, sizeof line, "%s: %s", sender, body);
|
||||
consoleWriteLine(line);
|
||||
char line[220];
|
||||
// Three colours, because the three parts answer different questions:
|
||||
// WHERE it arrived (channel, cyan), WHO said it (sender, yellow) and WHAT
|
||||
// they said (grey). A DM has no channel, so its sender takes the magenta
|
||||
// slot instead and stands out in a busy feed.
|
||||
int n1 = 0, n2 = 0;
|
||||
if (channel) {
|
||||
n1 = snprintf(line, sizeof line, "#%s ", thread);
|
||||
if (n1 < 0) n1 = 0;
|
||||
n2 = snprintf(line + n1, sizeof line - n1, "%s: ", sender);
|
||||
if (n2 < 0) n2 = 0;
|
||||
} else {
|
||||
n2 = snprintf(line, sizeof line, "%s: ", sender);
|
||||
if (n2 < 0) n2 = 0;
|
||||
}
|
||||
snprintf(line + n1 + n2, sizeof line - n1 - n2, "%s", body ? body : "");
|
||||
consoleWriteLineSeg(CC_CHAN, n1, channel ? CC_SENDER : CC_DM, n2, line);
|
||||
}
|
||||
#endif
|
||||
#if defined(HAS_TOUCH_UI)
|
||||
@@ -52151,6 +52164,22 @@ void UITask::loop() {
|
||||
#endif
|
||||
#if CAP_TOUCH
|
||||
{ uint16_t _tx, _ty; if (heltecV4CapTouchGetLive(&_tx, &_ty)) { con_activity = true; } }
|
||||
#endif
|
||||
#if defined(HAS_TDECK_TRACKBALL)
|
||||
// Trackball scrolls the scrollback. Roll up for history, down for live.
|
||||
// Accumulated because the ball reports many small deltas per physical flick;
|
||||
// one line per detent would race past everything.
|
||||
{
|
||||
int tdx = 0, tdy = 0;
|
||||
if (tdeckTrackballReadMotion(&tdx, &tdy) && tdy != 0) {
|
||||
static int tb_accum = 0;
|
||||
tb_accum += tdy;
|
||||
while (tb_accum <= -2) { consoleScroll(+1); tb_accum += 2; } // up = back
|
||||
while (tb_accum >= 2) { consoleScroll(-1); tb_accum -= 2; } // down = live
|
||||
con_activity = true;
|
||||
if (_screen_off) wakeScreen();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (con_activity) _last_input_ms = now;
|
||||
|
||||
|
||||
Reference in new issue
Block a user