dbus-top: initial commits

This commit covers the basic functionalities of the dbus-top tool.

The UI is divided into 3 windows as follows:

  +--------------------------+  Window list
  |         Window A         |  A: Summary statistics
  +------------+-------------+  B: Sensor list or detail
  |  Window B  |   Window C  |  C: Detailed statistics
  +------------+-------------+

To navigate the UI:
* Use tab to navigate each window

When a window is highlighted:
  In Window B:
  * Press esc key 3 times to leave the current sensor selection

  In Window C:
  * Press [Enter] to show/hide pop-up menu for column selectio
  * Press [Left]  to move highlight cursor to the left
  * Press [Right] to move highlight cursor to the right
  * Press [A] to sort by the highlighted column in ascending order
  * Press [D] to sort by the highlighted column in descending order

To add recipe to Yocto and build the recipe:
1) Copy and paste the content of the .bb file into a folder that can be
   detected by bitbake, such as meta-phosphor/recipes-phosphor/ipmi.
2) run "devtool modify -n dbus-top (path_to_openbmc_tools)/dbus-top/".

Signed-off-by: Adedeji Adebisi <adedejiadebisi01@gmail.com>
Change-Id: Id58ba30b815cfd9d18f54cf477d749dbdbc4545b
(cherry picked from commit d83c1aa45aa8cc9b61530b4f0fe1d04aa64d2c41)
diff --git a/views.hpp b/views.hpp
new file mode 100644
index 0000000..d4f3779
--- /dev/null
+++ b/views.hpp
@@ -0,0 +1,539 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include "analyzer.hpp"
+#include "bargraph.hpp"
+#include "main.hpp"
+#include "menu.hpp"
+#include "sensorhelper.hpp"
+
+#include <ncurses.h>
+#include <string>
+#include <vector>
+constexpr int MARGIN_BOTTOM = 1;
+class DBusTopWindow
+{
+  public:
+    DBusTopWindow()
+    {
+        win = newwin(25, 80, 0, 0); // Default to 80x25, will be updated
+        has_border_ = true;
+        focused_ = false;
+        selectable_ = true;
+        visible_ = true;
+        maximize_ = false;
+    }
+
+    virtual ~DBusTopWindow()
+    {}
+    virtual void OnKeyDown(const std::string& key) = 0;
+    virtual void Render() = 0;
+    virtual void OnResize(int win_w, int win_h) = 0;
+    void UpdateWindowSizeAndPosition()
+    {
+        mvwin(win, rect.y, rect.x);
+        wresize(win, rect.h, rect.w);
+    }
+
+    void DrawBorderIfNeeded()
+    {
+        if (focused_)
+        {
+            wborder(win, '*', '*', '*', '*', '*', '*', '*', '*');
+        }
+        else
+        {
+            wborder(win, '|', '|', '-', '-', '+', '+', '+', '+');
+        }
+        wrefresh(win);
+    }
+
+    virtual void RecreateWindow()
+    {
+        delwin(win);
+        win = newwin(25, 80, 0, 0);
+        UpdateWindowSizeAndPosition();
+    }
+
+    virtual std::string GetStatusString() = 0;
+    WINDOW* win;
+    Rect rect;
+    bool has_border_;
+    bool focused_;
+    bool selectable_;
+    bool maximize_;
+    bool visible_;
+};
+
+class SummaryView : public DBusTopWindow
+{
+  public:
+    SummaryView() : DBusTopWindow()
+    {}
+    void Render() override;
+    void OnResize(int win_w, int win_h) override
+    {
+        rect.h = 8;
+        rect.w = win_w;
+        rect.x = 0;
+        rect.y = 0;
+        UpdateWindowSizeAndPosition();
+    }
+
+    void UpdateDBusTopStatistics(DBusTopStatistics* stat);
+    void OnKeyDown(const std::string& key) override
+    {}
+    std::string GetStatusString() override
+    {
+        return "Summary View";
+    }
+
+  private:
+    float method_call_, method_return_, signal_, error_, total_;
+};
+
+class SensorDetailView : public DBusTopWindow
+{
+  public:
+    SensorDetailView() : DBusTopWindow()
+    {
+        choice_ = -999; // -999 means invalid
+        h_padding = 2;
+        h_spacing = 3;
+        col_width = 15;
+        idx0 = idx1 = -999;
+        state = SensorList;
+    }
+
+    void Render() override;
+    int DispSensorsPerColumn()
+    {
+        return rect.h - 3;
+    }
+
+    int DispSensorsPerRow()
+    {
+        int ncols = 0;
+        while (true)
+        {
+            int next = ncols + 1;
+            int w = 2 * h_padding + col_width * next;
+            if (next > 1)
+                w += (next - 1) * h_spacing;
+            if (w <= rect.w - 2)
+            {
+                ncols = next;
+            }
+            else
+            {
+                break;
+            }
+        }
+        return ncols;
+    }
+
+    void OnKeyDown(const std::string& key) override
+    {
+        if (state == SensorList)
+        { // Currently in sensor list
+            if (key == "right")
+            {
+                MoveChoiceCursorHorizontally(1);
+            }
+            else if (key == "left")
+            {
+                MoveChoiceCursorHorizontally(-1);
+            }
+            else if (key == "up")
+            {
+                MoveChoiceCursor(-1, true);
+            }
+            else if (key == "down")
+            {
+                MoveChoiceCursor(1, true);
+            }
+            else if (key == "enter")
+            {
+                if (choice_ != -999)
+                {
+                    state = SensorDetail;
+                }
+            }
+            else if (key == "escape")
+            {
+                choice_ = -999;
+            }
+        }
+        else if (state == SensorDetail)
+        { // Currently focusing on a sensor
+            if (key == "right" || key == "down")
+            {
+                MoveChoiceCursor(1, true);
+            }
+            else if (key == "left" || key == "up")
+            {
+                MoveChoiceCursor(-1, true);
+            }
+            else if (key == "escape")
+            {
+                state = SensorList;
+            }
+        }
+
+        Render(); // This window is already on top, redrawing won't corrupt
+    }
+
+    void MoveChoiceCursor(int delta, bool wrap_around = true)
+    {
+        const int ns = sensor_ids_.size();
+        if (ns < 1)
+            return;
+        // First of all, if cursor is inactive, activate it
+        if (choice_ == -999)
+        {
+            if (delta > 0)
+            {
+                choice_ = 0;
+                curr_sensor_id_ = sensor_ids_[0];
+                return;
+            }
+            else
+            {
+                choice_ = ns - 1;
+                curr_sensor_id_ = sensor_ids_.back();
+                return;
+            }
+        }
+        int choice_next = choice_ + delta;
+        while (choice_next >= ns)
+        {
+            if (wrap_around)
+            {
+                choice_next -= ns;
+            }
+            else
+            {
+                choice_next = ns - 1;
+            }
+        }
+        while (choice_next < 0)
+        {
+            if (wrap_around)
+            {
+                choice_next += ns;
+            }
+            else
+            {
+                choice_next = 0;
+            }
+        }
+        choice_ = choice_next;
+        curr_sensor_id_ = sensor_ids_[choice_];
+    }
+
+    void MoveChoiceCursorHorizontally(int delta)
+    {
+        if (delta != 0 && delta != -1 && delta != 1)
+            return;
+        const int ns = sensor_ids_.size();
+        if (ns < 1)
+            return;
+        if (choice_ == -999)
+        {
+            if (delta > 0)
+            {
+                choice_ = 0;
+                curr_sensor_id_ = sensor_ids_[0];
+                return;
+            }
+            else
+            {
+                curr_sensor_id_ = sensor_ids_.back();
+                choice_ = ns - 1;
+                return;
+            }
+        }
+        const int nrows = DispSensorsPerColumn();
+        int tot_columns = (ns - 1) / nrows + 1;
+        int num_rows_last_column = ns - nrows * (tot_columns - 1);
+        int y = choice_ % nrows, x = choice_ / nrows;
+        if (delta == 1)
+        {
+            x++;
+        }
+        else
+        {
+            x--;
+        }
+        bool overflow_to_right = false;
+        if (y < num_rows_last_column)
+        {
+            if (x >= tot_columns)
+            {
+                overflow_to_right = true;
+            }
+        }
+        else
+        {
+            if (x >= tot_columns - 1)
+            {
+                overflow_to_right = true;
+            }
+        }
+        bool overflow_to_left = false;
+        if (x < 0)
+        {
+            overflow_to_left = true;
+        }
+        {
+            if (overflow_to_right)
+            {
+                y++;
+                // overflow past the right of window
+                // Start probing next line
+                if (y >= nrows)
+                {
+                    choice_ = 0;
+                    return;
+                }
+                else
+                {
+                    choice_ = y;
+                    return;
+                }
+            }
+            else if (overflow_to_left)
+            { // overflow past the left of window
+                y--;
+                if (y < 0)
+                { // overflow past the top of window
+                    // Focus on the visually bottom-right entry
+                    if (num_rows_last_column == nrows)
+                    { // last col fully populated
+                        choice_ = ns - 1;
+                    }
+                    else
+                    { // last column is not fully populated
+                        choice_ = ns - num_rows_last_column - 1;
+                    }
+                    return;
+                }
+                else
+                {
+                    if (y < num_rows_last_column)
+                    {
+                        choice_ = nrows * (tot_columns - 1) + y;
+                    }
+                    else
+                    {
+                        choice_ = nrows * (tot_columns - 2) + y;
+                    }
+                }
+            }
+            else
+            {
+                choice_ = y + x * nrows;
+            }
+        }
+        curr_sensor_id_ = sensor_ids_[choice_];
+    }
+
+    // Cache the sensor list in the sensor snapshot
+    void UpdateSensorSnapshot(SensorSnapshot* snapshot)
+    {
+        std::string old_sensor_id = "";
+        if (choice_ != -999)
+        {
+            old_sensor_id = sensor_ids_[choice_];
+        }
+        std::vector<std::string> new_sensors =
+            snapshot->GetDistinctSensorNames();
+        if (new_sensors == sensor_ids_)
+        {
+            return; // Nothing is changed
+        }
+        // Assume changed
+        sensor_ids_ = new_sensors;
+        choice_ = -999;
+        for (int i = 0; i < static_cast<int>(new_sensors.size()); i++)
+        {
+            if (new_sensors[i] == old_sensor_id)
+            {
+                choice_ = i;
+                break;
+                curr_sensor_id_ = sensor_ids_[choice_];
+            }
+        }
+    }
+
+    void OnResize(int win_w, int win_h) override
+    {
+        rect.x = 0;
+        rect.y = 8 - MARGIN_BOTTOM;
+        rect.w = win_w / 2;
+        rect.h = win_h - rect.y - MARGIN_BOTTOM;
+        UpdateWindowSizeAndPosition();
+    }
+
+    std::vector<std::string> sensor_ids_;
+    // We need to keep track of the currently-selected sensor ID because
+    // the sensor ID might theoretically become invalidated at any moment, and
+    // we should allow the UI to show an error gracefully in that case.
+    std::string curr_sensor_id_;
+    int choice_;
+    int h_padding;
+    int h_spacing;
+    int col_width;
+    int idx0, idx1; // Range of sensors on display
+    enum State
+    {
+        SensorList,
+        SensorDetail,
+    };
+
+    State state;
+    std::string GetStatusString() override;
+};
+
+class DBusStatListView : public DBusTopWindow
+{
+  public:
+    DBusStatListView();
+    void Render() override;
+    void OnResize(int win_w, int win_h) override;
+    void OnKeyDown(const std::string& key) override;
+    int horizontal_pan_;
+    int menu_h_, menu_w_, menu_margin_;
+    ArrowKeyNavigationMenu* menu1_;
+    ArrowKeyNavigationMenu* menu2_;
+    int highlight_col_idx_; // Currently highlighted column
+    int row_idx_;           // Currently highlighted row
+
+    int sort_col_idx_; // Column used for sorting
+    enum SortOrder
+    {
+        Ascending,
+        Descending,
+    };
+    SortOrder sort_order_;
+
+    int disp_row_idx_; // From which row to start displaying? (essentially a
+                       // vertical scroll bar)
+    int last_choices_[2]; // Last choice index on either side
+    enum MenuState
+    {
+        Hidden,
+        LeftSide,  // When the user is choosing an entry on the left side
+        RightSide, // When the user is choosing an entry on the right side
+    };
+
+    std::vector<DBusTopSortField> GetSortFields();
+    MenuState curr_menu_state_, last_menu_state_;
+    std::string GetStatusString() override;
+    void RecreateWindow()
+    {
+        delwin(win);
+        win = newwin(25, 80, 0, 0);
+        menu1_->win_ = win;
+        menu2_->win_ = win;
+        UpdateWindowSizeAndPosition();
+    }
+    
+  private:
+    void SetMenuState(MenuState s)
+    {
+        last_menu_state_ = curr_menu_state_;
+        // Moving out from a certain side: save the last choice of that side
+        switch (curr_menu_state_)
+        {
+            case LeftSide:
+                if (s == RightSide)
+                {
+                    last_choices_[0] = menu1_->Choice();
+                    menu1_->Deselect();
+                }
+                break;
+            case RightSide:
+                if (s == LeftSide)
+                {
+                    last_choices_[1] = menu2_->Choice();
+                    menu2_->Deselect();
+                }
+                break;
+            default:
+                break;
+        }
+        // Moving into a certain side: save the cursor
+        switch (s)
+        {
+            case LeftSide:
+                if (!menu1_->Empty())
+                {
+                    menu1_->SetChoiceAndConstrain(last_choices_[0]);
+                }
+                break;
+            case RightSide:
+                if (!menu2_->Empty())
+                {
+                    menu2_->SetChoiceAndConstrain(last_choices_[1]);
+                }
+                break;
+            default:
+                break;
+        }
+        curr_menu_state_ = s;
+    }
+    void PanViewportOrMoveHighlightedColumn(const int delta_x);
+    // ColumnHeaders and ColumnWidths are the actual column widths used for
+    // display. They are "msg/s" or "I2c/s" prepended to the chosen set of
+    // fields.
+    std::vector<std::string> ColumnHeaders();
+    std::vector<int> ColumnWidths();
+    // X span, for checking visibility
+    std::pair<int, int> GetXSpanForColumn(const int col_idx);
+    bool IsXSpanVisible(const std::pair<int, int>& xs,
+                        const int tolerance); // uses horizontal_pan_
+    std::vector<std::string> visible_columns_;
+    std::unordered_map<std::string, int> column_widths_;
+    std::map<std::vector<std::string>, DBusTopComputedMetrics> stats_snapshot_;
+};
+
+class FooterView : public DBusTopWindow
+{
+  public:
+    FooterView() : DBusTopWindow()
+    {
+        selectable_ = false; // Cannot be selected by the tab key
+    }
+
+    void OnKeyDown(const std::string& key) override
+    {}
+    void OnResize(int win_w, int win_h) override
+    {
+        rect.h = 1;
+        rect.w = win_w;
+        rect.x = 0;
+        rect.y = win_h - 1;
+        UpdateWindowSizeAndPosition();
+    }
+
+    void Render() override;
+    std::string GetStatusString() override
+    {
+        return "";
+    }
+
+};