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 28 TermView::TermView() 29 : 30 BView("TermView", B_WILL_DRAW | B_FRAME_EVENTS) 31 { 32 _Init(); 33 } 34 35 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 44 TermView::~TermView() 45 { 46 vterm_free(fTerm); 47 } 48 49 50 void 51 TermView::AttachedToWindow() 52 { 53 BView::AttachedToWindow(); 54 MakeFocus(); 55 } 56 57 58 void 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 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 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 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 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 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 276 TermView::SetLineTerminator(BString terminator) 277 { 278 fLineTerminator = terminator; 279 } 280 281 282 void 283 TermView::PushBytes(const char* bytes, size_t length) 284 { 285 vterm_push_bytes(fTerm, bytes, length); 286 } 287 288 289 void 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 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 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 362 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 387 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 413 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 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 455 TermView::_Damage(VTermRect rect) 456 { 457 Invalidate(_GlyphsToPixels(rect)); 458 } 459 460 461 void 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 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 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 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 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 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 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 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