/* * Copyright 2012-2019, Adrien Destugues, pulkomandy@pulkomandy.tk * Distributed under the terms of the MIT licence. */ #include "TermView.h" #include #include #include #include #include #include #include #include "SerialApp.h" #include "libvterm/src/vterm_internal.h" struct ScrollBufferItem { int cols; VTermScreenCell cells[]; }; TermView::TermView() : BView("TermView", B_WILL_DRAW | B_FRAME_EVENTS) { _Init(); } TermView::TermView(BRect r) : BView(r, "TermView", B_FOLLOW_ALL_SIDES, B_WILL_DRAW | B_FRAME_EVENTS) { _Init(); } TermView::~TermView() { vterm_free(fTerm); } void TermView::AttachedToWindow() { BView::AttachedToWindow(); MakeFocus(); } void TermView::Draw(BRect updateRect) { VTermRect updatedChars = _PixelsToGlyphs(updateRect); VTermPos pos; font_height height; GetFontHeight(&height); VTermPos cursorPos; vterm_state_get_cursorpos(vterm_obtain_state(fTerm), &cursorPos); for (pos.row = updatedChars.start_row; pos.row <= updatedChars.end_row; pos.row++) { int x = updatedChars.start_col * fFontWidth + kBorderSpacing; int y = pos.row * fFontHeight + (int)ceil(height.ascent) + kBorderSpacing; MovePenTo(x, y); BString string; VTermScreenCell cell; int width = 0; bool isCursor = false; pos.col = updatedChars.start_col; _GetCell(pos, cell); for (pos.col = updatedChars.start_col; pos.col <= updatedChars.end_col;) { VTermScreenCell newCell; _GetCell(pos, newCell); // We need to start a new extent if: // - The attributes change // - The colors change // - The end of line is reached // - The current cell is under the cursor // - The current cell is right of the cursor if (*(uint32_t*)&cell.attrs != *(uint32_t*)&newCell.attrs || !vterm_color_equal(cell.fg, newCell.fg) || !vterm_color_equal(cell.bg, newCell.bg) || pos.col >= updatedChars.end_col || (pos.col == cursorPos.col && pos.row == cursorPos.row) || (pos.col == cursorPos.col + 1 && pos.row == cursorPos.row)) { rgb_color foreground, background; foreground.red = cell.fg.red; foreground.green = cell.fg.green; foreground.blue = cell.fg.blue; foreground.alpha = 255; background.red = cell.bg.red; background.green = cell.bg.green; background.blue = cell.bg.blue; background.alpha = 255; // Draw the cursor by swapping foreground and background colors if (isCursor ^ cell.attrs.reverse) { SetLowColor(foreground); SetViewColor(foreground); SetHighColor(background); } else { SetLowColor(background); SetViewColor(background); SetHighColor(foreground); } FillRect(BRect(x, y - ceil(height.ascent) + 1, x + width * fFontWidth - 1, y + ceil(height.descent) + ceil(height.leading)), B_SOLID_LOW); BFont font = be_fixed_font; if (cell.attrs.bold) font.SetFace(B_BOLD_FACE); if (cell.attrs.underline) font.SetFace(B_UNDERSCORE_FACE); if (cell.attrs.italic) font.SetFace(B_ITALIC_FACE); if (cell.attrs.blink) // FIXME make it actually blink font.SetFace(B_OUTLINED_FACE); #if 0 // FIXME B_NEGATIVE_FACE isn't actually implemented so we // instead swap the colors above if (cell.attrs.reverse) font.SetFace(B_NEGATIVE_FACE); #endif if (cell.attrs.strike) font.SetFace(B_STRIKEOUT_FACE); // TODO handle "font" (alternate fonts), dwl and dhl (double size) SetFont(&font); DrawString(string); x += width * fFontWidth; // Prepare for next cell cell = newCell; string = ""; width = 0; } if (pos.col == cursorPos.col && pos.row == cursorPos.row) isCursor = true; else isCursor = false; if (newCell.chars[0] == 0) { string += " "; pos.col ++; width += 1; } else { char buffer[VTERM_MAX_CHARS_PER_CELL]; wcstombs(buffer, (wchar_t*)newCell.chars, VTERM_MAX_CHARS_PER_CELL); string += buffer; width += newCell.width; pos.col += newCell.width; } } } } void TermView::FrameResized(float width, float height) { VTermRect newSize = _PixelsToGlyphs(BRect(0, 0, width - 2 * kBorderSpacing, height - 2 * kBorderSpacing)); vterm_set_size(fTerm, newSize.end_row, newSize.end_col); } void TermView::GetPreferredSize(float* width, float* height) { if (width != NULL) *width = kDefaultWidth * fFontWidth + 2 * kBorderSpacing - 1; if (height != NULL) *height = kDefaultHeight * fFontHeight + 2 * kBorderSpacing - 1; } void TermView::KeyDown(const char* bytes, int32 numBytes) { // Translate some keys to more usual VT100 escape codes switch (bytes[0]) { case B_UP_ARROW: numBytes = 3; bytes = "\x1B[A"; break; case B_DOWN_ARROW: numBytes = 3; bytes = "\x1B[B"; break; case B_RIGHT_ARROW: numBytes = 3; bytes = "\x1B[C"; break; case B_LEFT_ARROW: numBytes = 3; bytes = "\x1B[D"; break; case B_BACKSPACE: numBytes = 1; bytes = "\x7F"; break; case '\n': numBytes = fLineTerminator.Length(); bytes = fLineTerminator.String(); break; } // Send the bytes to the serial port BMessage* keyEvent = new BMessage(kMsgDataWrite); keyEvent->AddData("data", B_RAW_TYPE, bytes, numBytes); be_app_messenger.SendMessage(keyEvent); } void TermView::MouseDown(BPoint where) { int32 buttons = B_PRIMARY_MOUSE_BUTTON; int32 clicks = 1; if (Looper() != NULL && Looper()->CurrentMessage() != NULL) { Looper()->CurrentMessage()->FindInt32("buttons", &buttons); Looper()->CurrentMessage()->FindInt32("clicks", &clicks); } if (buttons == B_TERTIARY_MOUSE_BUTTON && clicks == 1) PasteFromClipboard(); } void TermView::MessageReceived(BMessage* message) { switch (message->what) { case 'DATA': { entry_ref ref; if (message->FindRef("refs", &ref) == B_OK) { // The user just dropped a file on us // TODO send it by XMODEM or so } break; } default: BView::MessageReceived(message); } } void TermView::SetLineTerminator(BString terminator) { fLineTerminator = terminator; } void TermView::PushBytes(const char* bytes, size_t length) { vterm_push_bytes(fTerm, bytes, length); } void TermView::Clear() { while (fScrollBuffer.ItemAt(0)) { free(fScrollBuffer.RemoveItem((int32)0)); } vterm_state_reset(vterm_obtain_state(fTerm), 1); vterm_screen_reset(fTermScreen, 1); _UpdateScrollbar(); } void TermView::PasteFromClipboard() { if (!be_clipboard->Lock()) return; BMessage* message = be_clipboard->Data(); const void *data; ssize_t size; if (message->FindData("text/plain", B_MIME_TYPE, &data, &size) == B_OK) { BMessage* keyEvent = new BMessage(kMsgDataWrite); keyEvent->AddData("data", B_RAW_TYPE, data, size); be_app_messenger.SendMessage(keyEvent); } be_clipboard->Unlock(); } // #pragma mark - void TermView::_Init() { SetFont(be_fixed_font); font_height height; GetFontHeight(&height); fFontHeight = (int)(ceilf(height.ascent) + ceilf(height.descent) + ceilf(height.leading)); fFontWidth = (int)be_fixed_font->StringWidth("X"); fTerm = vterm_new(kDefaultHeight, kDefaultWidth); fTermScreen = vterm_obtain_screen(fTerm); vterm_screen_set_callbacks(fTermScreen, &sScreenCallbacks, this); vterm_screen_reset(fTermScreen, 1); vterm_parser_set_utf8(fTerm, 1); VTermScreenCell cell; VTermPos firstPos; firstPos.row = 0; firstPos.col = 0; _GetCell(firstPos, cell); rgb_color background; background.red = cell.bg.red; background.green = cell.bg.green; background.blue = cell.bg.blue; background.alpha = 255; SetViewColor(background); SetLineTerminator("\n"); } VTermRect TermView::_PixelsToGlyphs(BRect pixels) const { pixels.OffsetBy(-kBorderSpacing, -kBorderSpacing); VTermRect rect; rect.start_col = (int)floor(pixels.left / fFontWidth); rect.end_col = (int)ceil(pixels.right / fFontWidth); rect.start_row = (int)floor(pixels.top / fFontHeight); rect.end_row = (int)ceil(pixels.bottom / fFontHeight); #if 0 printf( "TOP %d ch < %f px\n" "BTM %d ch < %f px\n" "LFT %d ch < %f px\n" "RGH %d ch < %f px\n", rect.start_row, pixels.top, rect.end_row, pixels.bottom, rect.start_col, pixels.left, rect.end_col, pixels.right ); #endif return rect; } BRect TermView::_GlyphsToPixels(const VTermRect& glyphs) const { BRect rect; rect.top = glyphs.start_row * fFontHeight; rect.bottom = glyphs.end_row * fFontHeight; rect.left = glyphs.start_col * fFontWidth; rect.right = glyphs.end_col * fFontWidth; rect.OffsetBy(kBorderSpacing, kBorderSpacing); #if 0 printf( "TOP %d ch > %f px (%f)\n" "BTM %d ch > %f px\n" "LFT %d ch > %f px (%f)\n" "RGH %d ch > %f px\n", glyphs.start_row, rect.top, fFontHeight, glyphs.end_row, rect.bottom, glyphs.start_col, rect.left, fFontWidth, glyphs.end_col, rect.right ); #endif return rect; } BRect TermView::_GlyphsToPixels(const int width, const int height) const { VTermRect rect; rect.start_row = 0; rect.start_col = 0; rect.end_row = height; rect.end_col = width; return _GlyphsToPixels(rect); } void TermView::_GetCell(VTermPos pos, VTermScreenCell& cell) { // First handle cells from the normal screen if (vterm_screen_get_cell(fTermScreen, pos, &cell) != 0) return; // Try the scroll-back buffer if (pos.row < 0 && pos.col >= 0) { int offset = - pos.row - 1; ScrollBufferItem* line = (ScrollBufferItem*)fScrollBuffer.ItemAt(offset); if (line != NULL && pos.col < line->cols) { cell = line->cells[pos.col]; return; } } // All cells outside the used terminal area are drawn with the same // background color as the top-left one. // TODO should they use the attributes of the closest neighbor instead? VTermPos firstPos; firstPos.row = 0; firstPos.col = 0; vterm_screen_get_cell(fTermScreen, firstPos, &cell); cell.chars[0] = 0; cell.width = 1; } void TermView::_Damage(VTermRect rect) { Invalidate(_GlyphsToPixels(rect)); } void TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible) { VTermRect r; // We need to erase the cursor from its old position r.start_row = oldPos.row; r.start_col = oldPos.col; r.end_col = oldPos.col + 1; r.end_row = oldPos.row + 1; Invalidate(_GlyphsToPixels(r)); // And we need to draw it at the new one r.start_row = pos.row; r.start_col = pos.col; r.end_col = pos.col + 1; r.end_row = pos.row + 1; Invalidate(_GlyphsToPixels(r)); } void TermView::_PushLine(int cols, const VTermScreenCell* cells) { ScrollBufferItem* item = (ScrollBufferItem*)malloc(sizeof(int) + cols * sizeof(VTermScreenCell)); item->cols = cols; memcpy(item->cells, cells, cols * sizeof(VTermScreenCell)); fScrollBuffer.AddItem(item, 0); // Remove extra items if the scrollback gets too long free(fScrollBuffer.RemoveItem(kScrollBackSize)); _UpdateScrollbar(); } void TermView::_UpdateScrollbar() { int availableRows, availableCols; vterm_get_size(fTerm, &availableRows, &availableCols); VTermRect dirty; dirty.start_col = 0; dirty.end_col = availableCols; dirty.end_row = 0; dirty.start_row = -fScrollBuffer.CountItems(); // FIXME we should rather use CopyRect if possible, and only invalidate the // newly exposed area here. Invalidate(_GlyphsToPixels(dirty)); BScrollBar* scrollBar = ScrollBar(B_VERTICAL); if (scrollBar != NULL) { float range = (fScrollBuffer.CountItems() + availableRows) * fFontHeight; scrollBar->SetRange(availableRows * fFontHeight - range, 0.0f); // TODO we need to adjust this in FrameResized, as availableRows can // change scrollBar->SetProportion(availableRows * fFontHeight / range); scrollBar->SetSteps(fFontHeight, fFontHeight * 3); } } int TermView::_PopLine(int cols, VTermScreenCell* cells) { ScrollBufferItem* item = (ScrollBufferItem*)fScrollBuffer.RemoveItem((int32)0); if (item == NULL) return 0; _UpdateScrollbar(); if (item->cols >= cols) { memcpy(cells, item->cells, cols * sizeof(VTermScreenCell)); } else { memcpy(cells, item->cells, item->cols * sizeof(VTermScreenCell)); for (int i = item->cols; i < cols; i++) cells[i] = cells[i - 1]; } free(item); return 1; } /* static */ int TermView::_Damage(VTermRect rect, void* user) { TermView* view = (TermView*)user; view->_Damage(rect); return 0; } /* static */ int TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible, void* user) { TermView* view = (TermView*)user; view->_MoveCursor(pos, oldPos, visible); return 0; } /* static */ int TermView::_PushLine(int cols, const VTermScreenCell* cells, void* user) { TermView* view = (TermView*)user; view->_PushLine(cols, cells); return 0; } /* static */ int TermView::_PopLine(int cols, VTermScreenCell* cells, void* user) { TermView* view = (TermView*)user; return view->_PopLine(cols, cells); } const VTermScreenCallbacks TermView::sScreenCallbacks = { &TermView::_Damage, /*.moverect =*/ NULL, &TermView::_MoveCursor, /*.settermprop =*/ NULL, /*.setmousefunc =*/ NULL, /*.bell =*/ NULL, /*.resize =*/ NULL, &TermView::_PushLine, &TermView::_PopLine, };