1 /*
2 * Copyright 2012-2019, Adrien Destugues, pulkomandy@pulkomandy.tk
3 * Distributed under the terms of the MIT licence.
4 */
5
6
7 #include "TermView.h"
8
9 #include <stdio.h>
10
11 #include <Clipboard.h>
12 #include <Entry.h>
13 #include <File.h>
14 #include <Font.h>
15 #include <Layout.h>
16 #include <ScrollBar.h>
17
18 #include "SerialApp.h"
19 #include "libvterm/src/vterm_internal.h"
20
21
22 struct ScrollBufferItem {
23 int cols;
24 VTermScreenCell cells[];
25 };
26
27
TermView()28 TermView::TermView()
29 :
30 BView("TermView", B_WILL_DRAW | B_FRAME_EVENTS)
31 {
32 _Init();
33 }
34
35
TermView(BRect r)36 TermView::TermView(BRect r)
37 :
38 BView(r, "TermView", B_FOLLOW_ALL_SIDES, B_WILL_DRAW | B_FRAME_EVENTS)
39 {
40 _Init();
41 }
42
43
~TermView()44 TermView::~TermView()
45 {
46 vterm_free(fTerm);
47 }
48
49
50 void
AttachedToWindow()51 TermView::AttachedToWindow()
52 {
53 BView::AttachedToWindow();
54 MakeFocus();
55 }
56
57
58 void
Draw(BRect updateRect)59 TermView::Draw(BRect updateRect)
60 {
61 VTermRect updatedChars = _PixelsToGlyphs(updateRect);
62
63 VTermPos pos;
64 font_height height;
65 GetFontHeight(&height);
66
67 VTermPos cursorPos;
68 vterm_state_get_cursorpos(vterm_obtain_state(fTerm), &cursorPos);
69
70 for (pos.row = updatedChars.start_row; pos.row <= updatedChars.end_row;
71 pos.row++) {
72 int x = updatedChars.start_col * fFontWidth + kBorderSpacing;
73 int y = pos.row * fFontHeight + (int)ceil(height.ascent)
74 + kBorderSpacing;
75 MovePenTo(x, y);
76
77 BString string;
78 VTermScreenCell cell;
79 int width = 0;
80 bool isCursor = false;
81
82 pos.col = updatedChars.start_col;
83 _GetCell(pos, cell);
84
85 for (pos.col = updatedChars.start_col;
86 pos.col <= updatedChars.end_col;) {
87
88 VTermScreenCell newCell;
89 _GetCell(pos, newCell);
90
91 // We need to start a new extent if:
92 // - The attributes change
93 // - The colors change
94 // - The end of line is reached
95 // - The current cell is under the cursor
96 // - The current cell is right of the cursor
97 if (*(uint32_t*)&cell.attrs != *(uint32_t*)&newCell.attrs
98 || !vterm_color_equal(cell.fg, newCell.fg)
99 || !vterm_color_equal(cell.bg, newCell.bg)
100 || pos.col >= updatedChars.end_col
101 || (pos.col == cursorPos.col && pos.row == cursorPos.row)
102 || (pos.col == cursorPos.col + 1 && pos.row == cursorPos.row)) {
103
104 rgb_color foreground, background;
105 foreground.red = cell.fg.red;
106 foreground.green = cell.fg.green;
107 foreground.blue = cell.fg.blue;
108 foreground.alpha = 255;
109 background.red = cell.bg.red;
110 background.green = cell.bg.green;
111 background.blue = cell.bg.blue;
112 background.alpha = 255;
113
114 // Draw the cursor by swapping foreground and background colors
115 if (isCursor ^ cell.attrs.reverse) {
116 SetLowColor(foreground);
117 SetViewColor(foreground);
118 SetHighColor(background);
119 } else {
120 SetLowColor(background);
121 SetViewColor(background);
122 SetHighColor(foreground);
123 }
124
125 FillRect(BRect(x, y - ceil(height.ascent) + 1,
126 x + width * fFontWidth - 1,
127 y + ceil(height.descent) + ceil(height.leading)),
128 B_SOLID_LOW);
129
130 BFont font = be_fixed_font;
131 if (cell.attrs.bold)
132 font.SetFace(B_BOLD_FACE);
133 if (cell.attrs.underline)
134 font.SetFace(B_UNDERSCORE_FACE);
135 if (cell.attrs.italic)
136 font.SetFace(B_ITALIC_FACE);
137 if (cell.attrs.blink) // FIXME make it actually blink
138 font.SetFace(B_OUTLINED_FACE);
139 #if 0
140 // FIXME B_NEGATIVE_FACE isn't actually implemented so we
141 // instead swap the colors above
142 if (cell.attrs.reverse)
143 font.SetFace(B_NEGATIVE_FACE);
144 #endif
145 if (cell.attrs.strike)
146 font.SetFace(B_STRIKEOUT_FACE);
147
148 // TODO handle "font" (alternate fonts), dwl and dhl (double size)
149
150 SetFont(&font);
151 DrawString(string);
152 x += width * fFontWidth;
153
154 // Prepare for next cell
155 cell = newCell;
156 string = "";
157 width = 0;
158 }
159
160 if (pos.col == cursorPos.col && pos.row == cursorPos.row)
161 isCursor = true;
162 else
163 isCursor = false;
164
165 if (newCell.chars[0] == 0) {
166 string += " ";
167 pos.col ++;
168 width += 1;
169 } else {
170 char buffer[VTERM_MAX_CHARS_PER_CELL];
171 wcstombs(buffer, (wchar_t*)newCell.chars,
172 VTERM_MAX_CHARS_PER_CELL);
173 string += buffer;
174 width += newCell.width;
175 pos.col += newCell.width;
176 }
177 }
178 }
179 }
180
181
182 void
FrameResized(float width,float height)183 TermView::FrameResized(float width, float height)
184 {
185 VTermRect newSize = _PixelsToGlyphs(BRect(0, 0, width - 2 * kBorderSpacing,
186 height - 2 * kBorderSpacing));
187 vterm_set_size(fTerm, newSize.end_row, newSize.end_col);
188 }
189
190
191 void
GetPreferredSize(float * width,float * height)192 TermView::GetPreferredSize(float* width, float* height)
193 {
194 if (width != NULL)
195 *width = kDefaultWidth * fFontWidth + 2 * kBorderSpacing - 1;
196 if (height != NULL)
197 *height = kDefaultHeight * fFontHeight + 2 * kBorderSpacing - 1;
198 }
199
200
201 void
KeyDown(const char * bytes,int32 numBytes)202 TermView::KeyDown(const char* bytes, int32 numBytes)
203 {
204 // Translate some keys to more usual VT100 escape codes
205 switch (bytes[0]) {
206 case B_UP_ARROW:
207 numBytes = 3;
208 bytes = "\x1B[A";
209 break;
210 case B_DOWN_ARROW:
211 numBytes = 3;
212 bytes = "\x1B[B";
213 break;
214 case B_RIGHT_ARROW:
215 numBytes = 3;
216 bytes = "\x1B[C";
217 break;
218 case B_LEFT_ARROW:
219 numBytes = 3;
220 bytes = "\x1B[D";
221 break;
222 case B_BACKSPACE:
223 numBytes = 1;
224 bytes = "\x7F";
225 break;
226 case '\n':
227 numBytes = fLineTerminator.Length();
228 bytes = fLineTerminator.String();
229 break;
230 }
231
232 // Send the bytes to the serial port
233 BMessage* keyEvent = new BMessage(kMsgDataWrite);
234 keyEvent->AddData("data", B_RAW_TYPE, bytes, numBytes);
235 be_app_messenger.SendMessage(keyEvent);
236 }
237
238
239 void
MouseDown(BPoint where)240 TermView::MouseDown(BPoint where)
241 {
242 int32 buttons = B_PRIMARY_MOUSE_BUTTON;
243 int32 clicks = 1;
244 if (Looper() != NULL && Looper()->CurrentMessage() != NULL) {
245 Looper()->CurrentMessage()->FindInt32("buttons", &buttons);
246 Looper()->CurrentMessage()->FindInt32("clicks", &clicks);
247 }
248
249 if (buttons == B_TERTIARY_MOUSE_BUTTON && clicks == 1)
250 PasteFromClipboard();
251 }
252
253
254 void
MessageReceived(BMessage * message)255 TermView::MessageReceived(BMessage* message)
256 {
257 switch (message->what)
258 {
259 case 'DATA':
260 {
261 entry_ref ref;
262 if (message->FindRef("refs", &ref) == B_OK)
263 {
264 // The user just dropped a file on us
265 // TODO send it by XMODEM or so
266 }
267 break;
268 }
269 default:
270 BView::MessageReceived(message);
271 }
272 }
273
274
275 void
SetLineTerminator(BString terminator)276 TermView::SetLineTerminator(BString terminator)
277 {
278 fLineTerminator = terminator;
279 }
280
281
282 void
PushBytes(const char * bytes,size_t length)283 TermView::PushBytes(const char* bytes, size_t length)
284 {
285 vterm_push_bytes(fTerm, bytes, length);
286 }
287
288
289 void
Clear()290 TermView::Clear()
291 {
292 while (fScrollBuffer.ItemAt(0)) {
293 free(fScrollBuffer.RemoveItem((int32)0));
294 }
295
296 vterm_state_reset(vterm_obtain_state(fTerm), 1);
297 vterm_screen_reset(fTermScreen, 1);
298
299 _UpdateScrollbar();
300 }
301
302
303 void
PasteFromClipboard()304 TermView::PasteFromClipboard()
305 {
306 if (!be_clipboard->Lock())
307 return;
308
309 BMessage* message = be_clipboard->Data();
310
311 const void *data;
312 ssize_t size;
313 if (message->FindData("text/plain", B_MIME_TYPE, &data, &size) == B_OK) {
314 BMessage* keyEvent = new BMessage(kMsgDataWrite);
315 keyEvent->AddData("data", B_RAW_TYPE, data, size);
316 be_app_messenger.SendMessage(keyEvent);
317 }
318
319 be_clipboard->Unlock();
320 }
321
322
323 // #pragma mark -
324
325
326 void
_Init()327 TermView::_Init()
328 {
329 SetFont(be_fixed_font);
330
331 font_height height;
332 GetFontHeight(&height);
333 fFontHeight = (int)(ceilf(height.ascent) + ceilf(height.descent)
334 + ceilf(height.leading));
335 fFontWidth = (int)be_fixed_font->StringWidth("X");
336 fTerm = vterm_new(kDefaultHeight, kDefaultWidth);
337
338 fTermScreen = vterm_obtain_screen(fTerm);
339 vterm_screen_set_callbacks(fTermScreen, &sScreenCallbacks, this);
340 vterm_screen_reset(fTermScreen, 1);
341
342 vterm_parser_set_utf8(fTerm, 1);
343
344 VTermScreenCell cell;
345 VTermPos firstPos;
346 firstPos.row = 0;
347 firstPos.col = 0;
348 _GetCell(firstPos, cell);
349
350 rgb_color background;
351 background.red = cell.bg.red;
352 background.green = cell.bg.green;
353 background.blue = cell.bg.blue;
354 background.alpha = 255;
355
356 SetViewColor(background);
357 SetLineTerminator("\n");
358 }
359
360
361 VTermRect
_PixelsToGlyphs(BRect pixels) const362 TermView::_PixelsToGlyphs(BRect pixels) const
363 {
364 pixels.OffsetBy(-kBorderSpacing, -kBorderSpacing);
365
366 VTermRect rect;
367 rect.start_col = (int)floor(pixels.left / fFontWidth);
368 rect.end_col = (int)ceil(pixels.right / fFontWidth);
369 rect.start_row = (int)floor(pixels.top / fFontHeight);
370 rect.end_row = (int)ceil(pixels.bottom / fFontHeight);
371 #if 0
372 printf(
373 "TOP %d ch < %f px\n"
374 "BTM %d ch < %f px\n"
375 "LFT %d ch < %f px\n"
376 "RGH %d ch < %f px\n",
377 rect.start_row, pixels.top,
378 rect.end_row, pixels.bottom,
379 rect.start_col, pixels.left,
380 rect.end_col, pixels.right
381 );
382 #endif
383 return rect;
384 }
385
386
_GlyphsToPixels(const VTermRect & glyphs) const387 BRect TermView::_GlyphsToPixels(const VTermRect& glyphs) const
388 {
389 BRect rect;
390 rect.top = glyphs.start_row * fFontHeight;
391 rect.bottom = glyphs.end_row * fFontHeight;
392 rect.left = glyphs.start_col * fFontWidth;
393 rect.right = glyphs.end_col * fFontWidth;
394
395 rect.OffsetBy(kBorderSpacing, kBorderSpacing);
396 #if 0
397 printf(
398 "TOP %d ch > %f px (%f)\n"
399 "BTM %d ch > %f px\n"
400 "LFT %d ch > %f px (%f)\n"
401 "RGH %d ch > %f px\n",
402 glyphs.start_row, rect.top, fFontHeight,
403 glyphs.end_row, rect.bottom,
404 glyphs.start_col, rect.left, fFontWidth,
405 glyphs.end_col, rect.right
406 );
407 #endif
408 return rect;
409 }
410
411
412 BRect
_GlyphsToPixels(const int width,const int height) const413 TermView::_GlyphsToPixels(const int width, const int height) const
414 {
415 VTermRect rect;
416 rect.start_row = 0;
417 rect.start_col = 0;
418 rect.end_row = height;
419 rect.end_col = width;
420 return _GlyphsToPixels(rect);
421 }
422
423
424 void
_GetCell(VTermPos pos,VTermScreenCell & cell)425 TermView::_GetCell(VTermPos pos, VTermScreenCell& cell)
426 {
427 // First handle cells from the normal screen
428 if (vterm_screen_get_cell(fTermScreen, pos, &cell) != 0)
429 return;
430
431 // Try the scroll-back buffer
432 if (pos.row < 0 && pos.col >= 0) {
433 int offset = - pos.row - 1;
434 ScrollBufferItem* line
435 = (ScrollBufferItem*)fScrollBuffer.ItemAt(offset);
436 if (line != NULL && pos.col < line->cols) {
437 cell = line->cells[pos.col];
438 return;
439 }
440 }
441
442 // All cells outside the used terminal area are drawn with the same
443 // background color as the top-left one.
444 // TODO should they use the attributes of the closest neighbor instead?
445 VTermPos firstPos;
446 firstPos.row = 0;
447 firstPos.col = 0;
448 vterm_screen_get_cell(fTermScreen, firstPos, &cell);
449 cell.chars[0] = 0;
450 cell.width = 1;
451 }
452
453
454 void
_Damage(VTermRect rect)455 TermView::_Damage(VTermRect rect)
456 {
457 Invalidate(_GlyphsToPixels(rect));
458 }
459
460
461 void
_MoveCursor(VTermPos pos,VTermPos oldPos,int visible)462 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible)
463 {
464 VTermRect r;
465
466 // We need to erase the cursor from its old position
467 r.start_row = oldPos.row;
468 r.start_col = oldPos.col;
469 r.end_col = oldPos.col + 1;
470 r.end_row = oldPos.row + 1;
471 Invalidate(_GlyphsToPixels(r));
472
473 // And we need to draw it at the new one
474 r.start_row = pos.row;
475 r.start_col = pos.col;
476 r.end_col = pos.col + 1;
477 r.end_row = pos.row + 1;
478 Invalidate(_GlyphsToPixels(r));
479 }
480
481
482 void
_PushLine(int cols,const VTermScreenCell * cells)483 TermView::_PushLine(int cols, const VTermScreenCell* cells)
484 {
485 ScrollBufferItem* item = (ScrollBufferItem*)malloc(sizeof(int)
486 + cols * sizeof(VTermScreenCell));
487 item->cols = cols;
488 memcpy(item->cells, cells, cols * sizeof(VTermScreenCell));
489
490 fScrollBuffer.AddItem(item, 0);
491
492 // Remove extra items if the scrollback gets too long
493 free(fScrollBuffer.RemoveItem(kScrollBackSize));
494
495 _UpdateScrollbar();
496 }
497
498
499 void
_UpdateScrollbar()500 TermView::_UpdateScrollbar()
501 {
502 int availableRows, availableCols;
503 vterm_get_size(fTerm, &availableRows, &availableCols);
504
505 VTermRect dirty;
506 dirty.start_col = 0;
507 dirty.end_col = availableCols;
508 dirty.end_row = 0;
509 dirty.start_row = -fScrollBuffer.CountItems();
510 // FIXME we should rather use CopyRect if possible, and only invalidate the
511 // newly exposed area here.
512 Invalidate(_GlyphsToPixels(dirty));
513
514 BScrollBar* scrollBar = ScrollBar(B_VERTICAL);
515 if (scrollBar != NULL) {
516 float range = (fScrollBuffer.CountItems() + availableRows)
517 * fFontHeight;
518 scrollBar->SetRange(availableRows * fFontHeight - range, 0.0f);
519 // TODO we need to adjust this in FrameResized, as availableRows can
520 // change
521 scrollBar->SetProportion(availableRows * fFontHeight / range);
522 scrollBar->SetSteps(fFontHeight, fFontHeight * 3);
523 }
524 }
525
526
527 int
_PopLine(int cols,VTermScreenCell * cells)528 TermView::_PopLine(int cols, VTermScreenCell* cells)
529 {
530 ScrollBufferItem* item =
531 (ScrollBufferItem*)fScrollBuffer.RemoveItem((int32)0);
532 if (item == NULL)
533 return 0;
534
535 _UpdateScrollbar();
536 if (item->cols >= cols) {
537 memcpy(cells, item->cells, cols * sizeof(VTermScreenCell));
538 } else {
539 memcpy(cells, item->cells, item->cols * sizeof(VTermScreenCell));
540 for (int i = item->cols; i < cols; i++)
541 cells[i] = cells[i - 1];
542 }
543 free(item);
544 return 1;
545 }
546
547
548 /* static */ int
_Damage(VTermRect rect,void * user)549 TermView::_Damage(VTermRect rect, void* user)
550 {
551 TermView* view = (TermView*)user;
552 view->_Damage(rect);
553
554 return 0;
555 }
556
557
558 /* static */ int
_MoveCursor(VTermPos pos,VTermPos oldPos,int visible,void * user)559 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible, void* user)
560 {
561 TermView* view = (TermView*)user;
562 view->_MoveCursor(pos, oldPos, visible);
563
564 return 0;
565 }
566
567
568 /* static */ int
_PushLine(int cols,const VTermScreenCell * cells,void * user)569 TermView::_PushLine(int cols, const VTermScreenCell* cells, void* user)
570 {
571 TermView* view = (TermView*)user;
572 view->_PushLine(cols, cells);
573
574 return 0;
575 }
576
577
578 /* static */ int
_PopLine(int cols,VTermScreenCell * cells,void * user)579 TermView::_PopLine(int cols, VTermScreenCell* cells, void* user)
580 {
581 TermView* view = (TermView*)user;
582 return view->_PopLine(cols, cells);
583 }
584
585
586 const
587 VTermScreenCallbacks TermView::sScreenCallbacks = {
588 &TermView::_Damage,
589 /*.moverect =*/ NULL,
590 &TermView::_MoveCursor,
591 /*.settermprop =*/ NULL,
592 /*.setmousefunc =*/ NULL,
593 /*.bell =*/ NULL,
594 /*.resize =*/ NULL,
595 &TermView::_PushLine,
596 &TermView::_PopLine,
597 };
598