root/src/apps/serialconnect/TermView.cpp
/*
 * Copyright 2012-2019, Adrien Destugues, pulkomandy@pulkomandy.tk
 * Distributed under the terms of the MIT licence.
 */


#include "TermView.h"

#include <stdio.h>

#include <Clipboard.h>
#include <Entry.h>
#include <File.h>
#include <Font.h>
#include <Layout.h>
#include <ScrollBar.h>

#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,
};