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) { 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 (cell.attrs.reverse) 139 font.SetFace(B_NEGATIVE_FACE); 140 if (cell.attrs.strike) 141 font.SetFace(B_STRIKEOUT_FACE); 142 143 // TODO handle "font" (alternate fonts), dwl and dhl (double size) 144 145 SetFont(&font); 146 DrawString(string); 147 x += width * fFontWidth; 148 149 // Prepare for next cell 150 cell = newCell; 151 string = ""; 152 width = 0; 153 } 154 155 if (pos.col == cursorPos.col && pos.row == cursorPos.row) 156 isCursor = true; 157 else 158 isCursor = false; 159 160 if (newCell.chars[0] == 0) { 161 string += " "; 162 pos.col ++; 163 width += 1; 164 } else { 165 char buffer[VTERM_MAX_CHARS_PER_CELL]; 166 wcstombs(buffer, (wchar_t*)newCell.chars, 167 VTERM_MAX_CHARS_PER_CELL); 168 string += buffer; 169 width += newCell.width; 170 pos.col += newCell.width; 171 } 172 } 173 } 174 } 175 176 177 void 178 TermView::FrameResized(float width, float height) 179 { 180 VTermRect newSize = _PixelsToGlyphs(BRect(0, 0, width - 2 * kBorderSpacing, 181 height - 2 * kBorderSpacing)); 182 vterm_set_size(fTerm, newSize.end_row, newSize.end_col); 183 } 184 185 186 void 187 TermView::GetPreferredSize(float* width, float* height) 188 { 189 if (width != NULL) 190 *width = kDefaultWidth * fFontWidth + 2 * kBorderSpacing - 1; 191 if (height != NULL) 192 *height = kDefaultHeight * fFontHeight + 2 * kBorderSpacing - 1; 193 } 194 195 196 void 197 TermView::KeyDown(const char* bytes, int32 numBytes) 198 { 199 // Translate some keys to more usual VT100 escape codes 200 switch (bytes[0]) { 201 case B_UP_ARROW: 202 numBytes = 3; 203 bytes = "\x1B[A"; 204 break; 205 case B_DOWN_ARROW: 206 numBytes = 3; 207 bytes = "\x1B[B"; 208 break; 209 case B_RIGHT_ARROW: 210 numBytes = 3; 211 bytes = "\x1B[C"; 212 break; 213 case B_LEFT_ARROW: 214 numBytes = 3; 215 bytes = "\x1B[D"; 216 break; 217 case B_BACKSPACE: 218 numBytes = 1; 219 bytes = "\x7F"; 220 break; 221 case '\n': 222 numBytes = fLineTerminator.Length(); 223 bytes = fLineTerminator.String(); 224 break; 225 } 226 227 // Send the bytes to the serial port 228 BMessage* keyEvent = new BMessage(kMsgDataWrite); 229 keyEvent->AddData("data", B_RAW_TYPE, bytes, numBytes); 230 be_app_messenger.SendMessage(keyEvent); 231 } 232 233 234 void 235 TermView::MessageReceived(BMessage* message) 236 { 237 switch (message->what) 238 { 239 case 'DATA': 240 { 241 entry_ref ref; 242 if (message->FindRef("refs", &ref) == B_OK) 243 { 244 // The user just dropped a file on us 245 // TODO send it by XMODEM or so 246 } 247 break; 248 } 249 default: 250 BView::MessageReceived(message); 251 } 252 } 253 254 255 void 256 TermView::SetLineTerminator(BString terminator) 257 { 258 fLineTerminator = terminator; 259 } 260 261 262 void 263 TermView::PushBytes(const char* bytes, size_t length) 264 { 265 vterm_push_bytes(fTerm, bytes, length); 266 } 267 268 269 void 270 TermView::Clear() 271 { 272 while (fScrollBuffer.ItemAt(0)) { 273 free(fScrollBuffer.RemoveItem((int32)0)); 274 } 275 276 vterm_state_reset(vterm_obtain_state(fTerm), 1); 277 vterm_screen_reset(fTermScreen, 1); 278 279 _UpdateScrollbar(); 280 } 281 282 283 // #pragma mark - 284 285 286 void 287 TermView::_Init() 288 { 289 SetFont(be_fixed_font); 290 291 font_height height; 292 GetFontHeight(&height); 293 fFontHeight = (int)(ceilf(height.ascent) + ceilf(height.descent) 294 + ceilf(height.leading)); 295 fFontWidth = (int)be_fixed_font->StringWidth("X"); 296 fTerm = vterm_new(kDefaultHeight, kDefaultWidth); 297 298 fTermScreen = vterm_obtain_screen(fTerm); 299 vterm_screen_set_callbacks(fTermScreen, &sScreenCallbacks, this); 300 vterm_screen_reset(fTermScreen, 1); 301 302 vterm_parser_set_utf8(fTerm, 1); 303 304 VTermScreenCell cell; 305 VTermPos firstPos; 306 firstPos.row = 0; 307 firstPos.col = 0; 308 _GetCell(firstPos, cell); 309 310 rgb_color background; 311 background.red = cell.bg.red; 312 background.green = cell.bg.green; 313 background.blue = cell.bg.blue; 314 background.alpha = 255; 315 316 SetViewColor(background); 317 SetLineTerminator("\n"); 318 } 319 320 321 VTermRect 322 TermView::_PixelsToGlyphs(BRect pixels) const 323 { 324 pixels.OffsetBy(-kBorderSpacing, -kBorderSpacing); 325 326 VTermRect rect; 327 rect.start_col = (int)floor(pixels.left / fFontWidth); 328 rect.end_col = (int)ceil(pixels.right / fFontWidth); 329 rect.start_row = (int)floor(pixels.top / fFontHeight); 330 rect.end_row = (int)ceil(pixels.bottom / fFontHeight); 331 #if 0 332 printf( 333 "TOP %d ch < %f px\n" 334 "BTM %d ch < %f px\n" 335 "LFT %d ch < %f px\n" 336 "RGH %d ch < %f px\n", 337 rect.start_row, pixels.top, 338 rect.end_row, pixels.bottom, 339 rect.start_col, pixels.left, 340 rect.end_col, pixels.right 341 ); 342 #endif 343 return rect; 344 } 345 346 347 BRect TermView::_GlyphsToPixels(const VTermRect& glyphs) const 348 { 349 BRect rect; 350 rect.top = glyphs.start_row * fFontHeight; 351 rect.bottom = glyphs.end_row * fFontHeight; 352 rect.left = glyphs.start_col * fFontWidth; 353 rect.right = glyphs.end_col * fFontWidth; 354 355 rect.OffsetBy(kBorderSpacing, kBorderSpacing); 356 #if 0 357 printf( 358 "TOP %d ch > %f px (%f)\n" 359 "BTM %d ch > %f px\n" 360 "LFT %d ch > %f px (%f)\n" 361 "RGH %d ch > %f px\n", 362 glyphs.start_row, rect.top, fFontHeight, 363 glyphs.end_row, rect.bottom, 364 glyphs.start_col, rect.left, fFontWidth, 365 glyphs.end_col, rect.right 366 ); 367 #endif 368 return rect; 369 } 370 371 372 BRect 373 TermView::_GlyphsToPixels(const int width, const int height) const 374 { 375 VTermRect rect; 376 rect.start_row = 0; 377 rect.start_col = 0; 378 rect.end_row = height; 379 rect.end_col = width; 380 return _GlyphsToPixels(rect); 381 } 382 383 384 void 385 TermView::_GetCell(VTermPos pos, VTermScreenCell& cell) 386 { 387 // First handle cells from the normal screen 388 if (vterm_screen_get_cell(fTermScreen, pos, &cell) != 0) 389 return; 390 391 // Try the scroll-back buffer 392 if (pos.row < 0 && pos.col >= 0) { 393 int offset = - pos.row - 1; 394 ScrollBufferItem* line 395 = (ScrollBufferItem*)fScrollBuffer.ItemAt(offset); 396 if (line != NULL && pos.col < line->cols) { 397 cell = line->cells[pos.col]; 398 return; 399 } 400 } 401 402 // All cells outside the used terminal area are drawn with the same 403 // background color as the top-left one. 404 // TODO should they use the attributes of the closest neighbor instead? 405 VTermPos firstPos; 406 firstPos.row = 0; 407 firstPos.col = 0; 408 vterm_screen_get_cell(fTermScreen, firstPos, &cell); 409 cell.chars[0] = 0; 410 cell.width = 1; 411 } 412 413 414 void 415 TermView::_Damage(VTermRect rect) 416 { 417 Invalidate(_GlyphsToPixels(rect)); 418 } 419 420 421 void 422 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible) 423 { 424 VTermRect r; 425 426 // We need to erase the cursor from its old position 427 r.start_row = oldPos.row; 428 r.start_col = oldPos.col; 429 r.end_col = oldPos.col + 1; 430 r.end_row = oldPos.row + 1; 431 Invalidate(_GlyphsToPixels(r)); 432 433 // And we need to draw it at the new one 434 r.start_row = pos.row; 435 r.start_col = pos.col; 436 r.end_col = pos.col + 1; 437 r.end_row = pos.row + 1; 438 Invalidate(_GlyphsToPixels(r)); 439 } 440 441 442 void 443 TermView::_PushLine(int cols, const VTermScreenCell* cells) 444 { 445 ScrollBufferItem* item = (ScrollBufferItem*)malloc(sizeof(int) 446 + cols * sizeof(VTermScreenCell)); 447 item->cols = cols; 448 memcpy(item->cells, cells, cols * sizeof(VTermScreenCell)); 449 450 fScrollBuffer.AddItem(item, 0); 451 452 // Remove extra items if the scrollback gets too long 453 free(fScrollBuffer.RemoveItem(kScrollBackSize)); 454 455 _UpdateScrollbar(); 456 } 457 458 459 void 460 TermView::_UpdateScrollbar() 461 { 462 int availableRows, availableCols; 463 vterm_get_size(fTerm, &availableRows, &availableCols); 464 465 VTermRect dirty; 466 dirty.start_col = 0; 467 dirty.end_col = availableCols; 468 dirty.end_row = 0; 469 dirty.start_row = -fScrollBuffer.CountItems(); 470 // FIXME we should rather use CopyRect if possible, and only invalidate the 471 // newly exposed area here. 472 Invalidate(_GlyphsToPixels(dirty)); 473 474 BScrollBar* scrollBar = ScrollBar(B_VERTICAL); 475 if (scrollBar != NULL) { 476 float range = (fScrollBuffer.CountItems() + availableRows) 477 * fFontHeight; 478 scrollBar->SetRange(availableRows * fFontHeight - range, 0.0f); 479 // TODO we need to adjust this in FrameResized, as availableRows can 480 // change 481 scrollBar->SetProportion(availableRows * fFontHeight / range); 482 scrollBar->SetSteps(fFontHeight, fFontHeight * 3); 483 } 484 } 485 486 487 int 488 TermView::_PopLine(int cols, VTermScreenCell* cells) 489 { 490 ScrollBufferItem* item = 491 (ScrollBufferItem*)fScrollBuffer.RemoveItem((int32)0); 492 if (item == NULL) 493 return 0; 494 495 _UpdateScrollbar(); 496 if (item->cols >= cols) { 497 memcpy(cells, item->cells, cols * sizeof(VTermScreenCell)); 498 } else { 499 memcpy(cells, item->cells, item->cols * sizeof(VTermScreenCell)); 500 for (int i = item->cols; i < cols; i++) 501 cells[i] = cells[i - 1]; 502 } 503 free(item); 504 return 1; 505 } 506 507 508 /* static */ int 509 TermView::_Damage(VTermRect rect, void* user) 510 { 511 TermView* view = (TermView*)user; 512 view->_Damage(rect); 513 514 return 0; 515 } 516 517 518 /* static */ int 519 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible, void* user) 520 { 521 TermView* view = (TermView*)user; 522 view->_MoveCursor(pos, oldPos, visible); 523 524 return 0; 525 } 526 527 528 /* static */ int 529 TermView::_PushLine(int cols, const VTermScreenCell* cells, void* user) 530 { 531 TermView* view = (TermView*)user; 532 view->_PushLine(cols, cells); 533 534 return 0; 535 } 536 537 538 /* static */ int 539 TermView::_PopLine(int cols, VTermScreenCell* cells, void* user) 540 { 541 TermView* view = (TermView*)user; 542 return view->_PopLine(cols, cells); 543 } 544 545 546 const 547 VTermScreenCallbacks TermView::sScreenCallbacks = { 548 &TermView::_Damage, 549 /*.moverect =*/ NULL, 550 &TermView::_MoveCursor, 551 /*.settermprop =*/ NULL, 552 /*.setmousefunc =*/ NULL, 553 /*.bell =*/ NULL, 554 /*.resize =*/ NULL, 555 &TermView::_PushLine, 556 &TermView::_PopLine, 557 }; 558