1 /* 2 * Copyright 2012-2015, Adrien Destugues, pulkomandy@gmail.com 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 // #pragma mark - 270 271 272 void 273 TermView::_Init() 274 { 275 SetFont(be_fixed_font); 276 277 font_height height; 278 GetFontHeight(&height); 279 fFontHeight = (int)(ceilf(height.ascent) + ceilf(height.descent) 280 + ceilf(height.leading)); 281 fFontWidth = (int)be_fixed_font->StringWidth("X"); 282 fTerm = vterm_new(kDefaultHeight, kDefaultWidth); 283 284 fTermScreen = vterm_obtain_screen(fTerm); 285 vterm_screen_set_callbacks(fTermScreen, &sScreenCallbacks, this); 286 vterm_screen_reset(fTermScreen, 1); 287 288 vterm_parser_set_utf8(fTerm, 1); 289 290 VTermScreenCell cell; 291 VTermPos firstPos; 292 firstPos.row = 0; 293 firstPos.col = 0; 294 _GetCell(firstPos, cell); 295 296 rgb_color background; 297 background.red = cell.bg.red; 298 background.green = cell.bg.green; 299 background.blue = cell.bg.blue; 300 background.alpha = 255; 301 302 SetViewColor(background); 303 SetLineTerminator("\n"); 304 } 305 306 307 VTermRect 308 TermView::_PixelsToGlyphs(BRect pixels) const 309 { 310 pixels.OffsetBy(-kBorderSpacing, -kBorderSpacing); 311 312 VTermRect rect; 313 rect.start_col = (int)floor(pixels.left / fFontWidth); 314 rect.end_col = (int)ceil(pixels.right / fFontWidth); 315 rect.start_row = (int)floor(pixels.top / fFontHeight); 316 rect.end_row = (int)ceil(pixels.bottom / fFontHeight); 317 #if 0 318 printf( 319 "TOP %d ch < %f px\n" 320 "BTM %d ch < %f px\n" 321 "LFT %d ch < %f px\n" 322 "RGH %d ch < %f px\n", 323 rect.start_row, pixels.top, 324 rect.end_row, pixels.bottom, 325 rect.start_col, pixels.left, 326 rect.end_col, pixels.right 327 ); 328 #endif 329 return rect; 330 } 331 332 333 BRect TermView::_GlyphsToPixels(const VTermRect& glyphs) const 334 { 335 BRect rect; 336 rect.top = glyphs.start_row * fFontHeight; 337 rect.bottom = glyphs.end_row * fFontHeight; 338 rect.left = glyphs.start_col * fFontWidth; 339 rect.right = glyphs.end_col * fFontWidth; 340 341 rect.OffsetBy(kBorderSpacing, kBorderSpacing); 342 #if 0 343 printf( 344 "TOP %d ch > %f px (%f)\n" 345 "BTM %d ch > %f px\n" 346 "LFT %d ch > %f px (%f)\n" 347 "RGH %d ch > %f px\n", 348 glyphs.start_row, rect.top, fFontHeight, 349 glyphs.end_row, rect.bottom, 350 glyphs.start_col, rect.left, fFontWidth, 351 glyphs.end_col, rect.right 352 ); 353 #endif 354 return rect; 355 } 356 357 358 BRect 359 TermView::_GlyphsToPixels(const int width, const int height) const 360 { 361 VTermRect rect; 362 rect.start_row = 0; 363 rect.start_col = 0; 364 rect.end_row = height; 365 rect.end_col = width; 366 return _GlyphsToPixels(rect); 367 } 368 369 370 void 371 TermView::_GetCell(VTermPos pos, VTermScreenCell& cell) 372 { 373 // First handle cells from the normal screen 374 if (vterm_screen_get_cell(fTermScreen, pos, &cell) != 0) 375 return; 376 377 // Try the scroll-back buffer 378 if (pos.row < 0 && pos.col >= 0) { 379 int offset = - pos.row - 1; 380 ScrollBufferItem* line 381 = (ScrollBufferItem*)fScrollBuffer.ItemAt(offset); 382 if (line != NULL && pos.col < line->cols) { 383 cell = line->cells[pos.col]; 384 return; 385 } 386 } 387 388 // All cells outside the used terminal area are drawn with the same 389 // background color as the top-left one. 390 // TODO should they use the attributes of the closest neighbor instead? 391 VTermPos firstPos; 392 firstPos.row = 0; 393 firstPos.col = 0; 394 vterm_screen_get_cell(fTermScreen, firstPos, &cell); 395 cell.chars[0] = 0; 396 cell.width = 1; 397 } 398 399 400 void 401 TermView::_Damage(VTermRect rect) 402 { 403 Invalidate(_GlyphsToPixels(rect)); 404 } 405 406 407 void 408 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible) 409 { 410 VTermRect r; 411 412 // We need to erase the cursor from its old position 413 r.start_row = oldPos.row; 414 r.start_col = oldPos.col; 415 r.end_col = oldPos.col + 1; 416 r.end_row = oldPos.row + 1; 417 Invalidate(_GlyphsToPixels(r)); 418 419 // And we need to draw it at the new one 420 r.start_row = pos.row; 421 r.start_col = pos.col; 422 r.end_col = pos.col + 1; 423 r.end_row = pos.row + 1; 424 Invalidate(_GlyphsToPixels(r)); 425 } 426 427 428 void 429 TermView::_PushLine(int cols, const VTermScreenCell* cells) 430 { 431 ScrollBufferItem* item = (ScrollBufferItem*)malloc(sizeof(int) 432 + cols * sizeof(VTermScreenCell)); 433 item->cols = cols; 434 memcpy(item->cells, cells, cols * sizeof(VTermScreenCell)); 435 436 fScrollBuffer.AddItem(item, 0); 437 438 free(fScrollBuffer.RemoveItem(kScrollBackSize)); 439 440 int availableRows, availableCols; 441 vterm_get_size(fTerm, &availableRows, &availableCols); 442 443 VTermRect dirty; 444 dirty.start_col = 0; 445 dirty.end_col = availableCols; 446 dirty.end_row = 0; 447 dirty.start_row = -fScrollBuffer.CountItems(); 448 // FIXME we should rather use CopyRect if possible, and only invalidate the 449 // newly exposed area here. 450 Invalidate(_GlyphsToPixels(dirty)); 451 452 BScrollBar* scrollBar = ScrollBar(B_VERTICAL); 453 if (scrollBar != NULL) { 454 float range = (fScrollBuffer.CountItems() + availableRows) 455 * fFontHeight; 456 scrollBar->SetRange(availableRows * fFontHeight - range, 0.0f); 457 // TODO we need to adjust this in FrameResized, as availableRows can 458 // change 459 scrollBar->SetProportion(availableRows * fFontHeight / range); 460 scrollBar->SetSteps(fFontHeight, fFontHeight * 3); 461 } 462 } 463 464 465 /* static */ int 466 TermView::_Damage(VTermRect rect, void* user) 467 { 468 TermView* view = (TermView*)user; 469 view->_Damage(rect); 470 471 return 0; 472 } 473 474 475 /* static */ int 476 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible, void* user) 477 { 478 TermView* view = (TermView*)user; 479 view->_MoveCursor(pos, oldPos, visible); 480 481 return 0; 482 } 483 484 485 /* static */ int 486 TermView::_PushLine(int cols, const VTermScreenCell* cells, void* user) 487 { 488 TermView* view = (TermView*)user; 489 view->_PushLine(cols, cells); 490 491 return 0; 492 } 493 494 495 const 496 VTermScreenCallbacks TermView::sScreenCallbacks = { 497 &TermView::_Damage, 498 /*.moverect =*/ NULL, 499 &TermView::_MoveCursor, 500 /*.settermprop =*/ NULL, 501 /*.setmousefunc =*/ NULL, 502 /*.bell =*/ NULL, 503 /*.resize =*/ NULL, 504 &TermView::_PushLine, 505 /*.sb_popline =*/ NULL, 506 }; 507