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