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/.clang-format b/.clang-format
new file mode 100644
index 0000000..3652c56
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,113 @@
+---
+Language:        Cpp
+# BasedOnStyle:  LLVM
+AccessModifierOffset: -2
+AlignAfterOpenBracket: Align
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignEscapedNewlines: Right
+AlignOperands:   true
+AlignTrailingComments: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+BinPackArguments: true
+BinPackParameters: true
+BraceWrapping:
+  AfterCaseLabel:  true
+  AfterClass:      true
+  AfterControlStatement: true
+  AfterEnum:       true
+  AfterFunction:   true
+  AfterNamespace:  true
+  AfterObjCDeclaration: true
+  AfterStruct:     true
+  AfterUnion:      true
+  AfterExternBlock: true
+  BeforeCatch:     true
+  BeforeElse:      true
+  IndentBraces:    false
+  SplitEmptyFunction:   false
+  SplitEmptyRecord:     false
+  SplitEmptyNamespace:  false
+BreakBeforeBinaryOperators: None
+BreakBeforeBraces: Custom
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializers: AfterColon
+BreakInheritanceList: AfterColon
+BreakStringLiterals: true
+ColumnLimit:     80
+CommentPragmas:  '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+PointerAlignment: Left
+DisableFormat:   false
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:   [ foreach, Q_FOREACH, BOOST_FOREACH ]
+IncludeBlocks: Regroup
+IncludeCategories:
+  - Regex:           '^[<"](gtest|gmock)'
+    Priority:        7
+  - Regex:           '^"config.h"'
+    Priority:        -1
+  - Regex:           '^".*\.h"'
+    Priority:        1
+  - Regex:           '^".*\.hpp"'
+    Priority:        2
+  - Regex:           '^<.*\.h>'
+    Priority:        3
+  - Regex:           '^<.*\.hpp>'
+    Priority:        4
+  - Regex:           '^<.*'
+    Priority:        5
+  - Regex:           '.*'
+    Priority:        6
+IndentCaseLabels: true
+IndentWidth:     4
+IndentWrappedFunctionNames: true
+KeepEmptyLinesAtTheStartOfBlocks: true
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBlockIndentWidth: 2
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakBeforeFirstCallParameter: 19
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakString: 1000
+PenaltyExcessCharacter: 1000000
+PenaltyReturnTypeOnItsOwnLine: 60
+ReflowComments:  true
+SortIncludes:    true
+SortUsingDeclarations: true
+SpaceAfterCStyleCast: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: ControlStatements
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  false
+SpacesInContainerLiterals: true
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+Standard:        Latest
+TabWidth:        4
+UseTab:          Never
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a2ae220
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+oe-logs
+oe-workdir
+.vscode
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..dc4386f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,13 @@
+{
+    "C_Cpp.errorSquiggles": "Disabled",
+    "files.associations": {
+        "filesystem": "cpp",
+        "*.ipp": "cpp",
+<<<<<<< HEAD
+        "iosfwd": "cpp",
+        "thread": "cpp"
+=======
+        "iosfwd": "cpp"
+>>>>>>> cf7590f (Revert "dbus-top: WIP of all currently-implemented features")
+    }
+}
\ No newline at end of file
diff --git a/analyzer.cpp b/analyzer.cpp
new file mode 100644
index 0000000..f31868c
--- /dev/null
+++ b/analyzer.cpp
@@ -0,0 +1,624 @@
+// 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.
+
+#include "analyzer.hpp"
+#include "histogram.hpp"
+#include "main.hpp"
+#include "sensorhelper.hpp"
+#include "views.hpp"
+#include "xmlparse.hpp"
+
+#include <unistd.h>
+#include <atomic>
+#include <filesystem>
+#include <fstream>
+#include <functional>
+#include <iostream>
+#include <sstream>
+#include <string>
+
+extern SensorSnapshot* g_sensor_snapshot;
+extern DBusConnectionSnapshot* g_connection_snapshot;
+extern sd_bus* g_bus;
+extern SensorDetailView* g_sensor_detail_view;
+
+static std::unordered_map<uint64_t, uint64_t>
+    in_flight_methodcalls; // serial => microseconds
+uint64_t Microseconds()
+{
+    long us;  // usec
+    time_t s; // Seconds
+    struct timespec spec;
+    clock_gettime(CLOCK_REALTIME, &spec);
+    s = spec.tv_sec;
+    us = round(spec.tv_nsec / 1000); // Convert nanoseconds to milliseconds
+    if (us > 999999)
+    {
+        s++;
+        us = 0;
+    }
+    return s * 1000000 + us;
+}
+
+int g_update_interval_millises = 2000;
+int GetSummaryIntervalInMillises()
+{
+    return g_update_interval_millises;
+}
+
+bool DBusTopSortFieldIsNumeric(DBusTopSortField field)
+{
+    switch (field)
+    {
+        case kSender:
+        case kDestination:
+        case kInterface:
+        case kPath:
+        case kMember:
+        case kSenderCMD:
+            return false;
+        case kSenderPID:
+        case kMsgPerSec:
+        case kAverageLatency:
+            return true;
+    }
+    return false;
+}
+
+namespace dbus_top_analyzer
+{
+    DBusTopStatistics g_dbus_statistics;
+    Histogram<float> g_mc_time_histogram;
+    std::unordered_map<uint32_t, uint64_t> in_flight_methodcalls;
+    std::atomic<bool> g_program_done = false;
+    std::chrono::time_point<std::chrono::steady_clock> g_last_update;
+    DBusTopStatisticsCallback g_callback;
+    void SetDBusTopStatisticsCallback(DBusTopStatisticsCallback cb)
+    {
+        g_callback = cb;
+    }
+
+    int UserInputThread()
+    {
+        return 0;
+    }
+
+    std::string g_dbus_top_conn = " ";
+    void SetDBusTopConnectionForMonitoring(const std::string& conn)
+    {
+        g_dbus_top_conn = conn;
+    }
+
+    // Performs one step of analysis
+    void Process()
+    {
+        std::chrono::time_point<std::chrono::steady_clock> t =
+            std::chrono::steady_clock::now();
+        std::chrono::time_point<std::chrono::steady_clock> next_update =
+            g_last_update + std::chrono::milliseconds(g_update_interval_millises);
+        if (t >= next_update)
+        {
+            float seconds_since_last_sample =
+                std::chrono::duration_cast<std::chrono::microseconds>(t -
+                                                                    g_last_update)
+                    .count() /
+                1000000.0f;
+            g_dbus_statistics.seconds_since_last_sample_ =
+                seconds_since_last_sample;
+            // Update snapshot
+            if (g_callback)
+            {
+                g_callback(&g_dbus_statistics, &g_mc_time_histogram);
+            }
+            g_dbus_statistics.Reset();
+            g_last_update = t;
+        }
+    }
+
+    void Finish()
+    {
+        g_program_done = true;
+    }
+
+    std::vector<std::string> FindAllObjectPathsForService(
+        const std::string& service,
+        std::function<void(const std::string&, const std::vector<std::string>&)>
+            on_interface_cb)
+    {
+        sd_bus_error err = SD_BUS_ERROR_NULL;
+        sd_bus_message *m, *reply;
+        std::vector<std::string> paths; // Current iteration
+        std::vector<std::string>
+            all_obj_paths; // All object paths under the supervision of ObjectMapper
+        paths.push_back("/");
+        // busctl call xyz.openbmc_project.ObjectMapper        /
+        // org.freedesktop.DBus.Introspectable        Introspect
+        while (!paths.empty())
+        {
+            // printf("%d paths to explore, total %d paths so far\n",
+            // int(paths.size()), int(all_obj_paths.size()));
+            std::vector<std::string> new_paths;
+            for (const std::string& obj_path : paths)
+            {
+                all_obj_paths.push_back(obj_path);
+                int r = sd_bus_message_new_method_call(
+                    g_bus, &m, service.c_str(), obj_path.c_str(),
+                    "org.freedesktop.DBus.Introspectable", "Introspect");
+                if (r < 0)
+                {
+                    printf("Oh! Cannot create new method call. r=%d, strerror=%s\n",
+                        r, strerror(-r));
+                    continue;
+                }
+                r = sd_bus_call(g_bus, m, 0, &err, &reply);
+                if (r < 0)
+                {
+                    printf("Could not execute method call, r=%d, strerror=%s\n", r,
+                        strerror(-r));
+                }
+                const char* sig = sd_bus_message_get_signature(reply, 0);
+                if (!strcmp(sig, "s"))
+                {
+                    const char* s;
+                    int r = sd_bus_message_read(reply, "s", &s);
+                    std::string s1(s);
+                    if (r < 0)
+                    {
+                        printf("Could not read string payload, r=%d, strerror=%s\n",
+                            r, strerror(-r));
+                    }
+                    else
+                    {
+                        XMLNode* t = ParseXML(s1);
+                        std::vector<std::string> ch = t->GetChildNodeNames();
+                        if (on_interface_cb != nullptr)
+                        {
+                            on_interface_cb(obj_path, t->GetInterfaceNames());
+                        }
+                        DeleteTree(t);
+                        for (const std::string& cn : ch)
+                        {
+                            std::string ch_path = obj_path;
+                            if (obj_path.back() == '/')
+                            {}
+                            else
+                                ch_path.push_back('/');
+                            ch_path += cn;
+                            new_paths.push_back(ch_path);
+                        }
+                    }
+                }
+            }
+            paths = new_paths;
+        }
+        return all_obj_paths;
+    }
+
+    void ListAllSensors()
+    {
+        g_connection_snapshot = new DBusConnectionSnapshot();
+        printf("1. Getting names\n");
+        char** names;
+        int r = sd_bus_list_names(g_bus, &names, nullptr);
+        std::vector<std::string> services;
+        std::vector<int> pids;
+        std::vector<std::string> comms;
+        for (char** ptr = names; ptr && *ptr; ++ptr)
+        {
+            services.push_back(*ptr);
+            free(*ptr);
+        }
+        free(names);
+        printf("2. Getting creds of each name\n");
+        for (int i = 0; i < static_cast<int>(services.size()); i++)
+        {
+            const std::string& service = services[i];
+            sd_bus_creds* creds = nullptr;
+            r = sd_bus_get_name_creds(g_bus, services[i].c_str(),
+                                    SD_BUS_CREDS_AUGMENT | SD_BUS_CREDS_EUID |
+                                        SD_BUS_CREDS_PID | SD_BUS_CREDS_COMM |
+                                        SD_BUS_CREDS_UNIQUE_NAME |
+                                        SD_BUS_CREDS_UNIT | SD_BUS_CREDS_SESSION |
+                                        SD_BUS_CREDS_DESCRIPTION,
+                                    &creds);
+            // PID
+            int pid = INVALID;
+            if (r < 0)
+            {
+                printf("Oh! Cannot get creds for %s\n", services[i].c_str());
+            }
+            else
+            {
+                r = sd_bus_creds_get_pid(creds, &pid);
+            }
+            pids.push_back(pid);
+            // comm
+            std::string comm;
+            if (pid != INVALID)
+            {
+                std::ifstream ifs("/proc/" + std::to_string(pid) + "/cmdline");
+                std::string line;
+                std::getline(ifs, line);
+                for (char c : line)
+                {
+                    if (c < 32 || c >= 127)
+                        c = ' ';
+                    comm.push_back(c);
+                }
+            }
+            comms.push_back(comm);
+            // unique name, also known as "Connection"
+            std::string connection;
+            const char* u;
+            r = sd_bus_creds_get_unique_name(creds, &u);
+            if (r >= 0)
+            {
+                connection = u;
+            }
+            else
+            {
+                printf("Oh! Could not get unique name for %s\n", service.c_str());
+            }
+            std::string unit;
+            r = sd_bus_creds_get_unit(creds, &u);
+            if (r >= 0)
+            {
+                unit = u;
+            }
+            else
+            {
+                printf("Oh! Could not get unit name for %s\n", unit.c_str());
+            }
+            printf("AddConnection    %s    %s    %s    %s    %d\n", service.c_str(),
+                connection.c_str(), comm.c_str(), unit.c_str(), pid);
+            g_connection_snapshot->AddConnection(service, connection, comm, unit,
+                                                pid);
+        }
+        printf("There are %d DBus names.\n", int(services.size()));
+        for (int i = 0; i < int(services.size()); i++)
+        {
+            printf("    %d: %s [%s]\n", i, services[i].c_str(), comms[i].c_str());
+        }
+        g_sensor_snapshot = new SensorSnapshot(g_connection_snapshot);
+        // busctl call xyz.openbmc_project.ObjectMapper /
+        // org.freedesktop.DBus.Introspectable Introspect
+        printf("3. See which sensors are visible from Object Mapper\n");
+        printf("3.1. Introspect Object Mapper for object paths\n");
+        std::vector<std::string> all_obj_paths = FindAllObjectPathsForService(
+            "xyz.openbmc_project.ObjectMapper", nullptr);
+        sd_bus_error err = SD_BUS_ERROR_NULL;
+        sd_bus_message *m, *reply;
+        printf("%d paths found while introspecting ObjectMapper.\n",
+            int(all_obj_paths.size()));
+        printf("3.2. Call ObjectMapper's GetObject method against the sensor "
+            "object paths that represent sensors\n");
+        for (const std::string& p : all_obj_paths)
+        {
+            if (IsSensorObjectPath(p))
+            {
+                err = SD_BUS_ERROR_NULL;
+                r = sd_bus_message_new_method_call(
+                    g_bus, &m, "xyz.openbmc_project.ObjectMapper",
+                    "/xyz/openbmc_project/object_mapper",
+                    "xyz.openbmc_project.ObjectMapper", "GetObject");
+                if (r < 0)
+                {
+                    printf("Cannot create new method call. r=%d, strerror=%s\n", r,
+                        strerror(-r));
+                    continue;
+                }
+                r = sd_bus_message_append_basic(m, 's', p.c_str());
+                if (r < 0)
+                {
+                    printf("Could not append a string parameter to m\n");
+                    continue;
+                }
+                // empty array
+                r = sd_bus_message_open_container(m, 'a', "s");
+                if (r < 0)
+                {
+                    printf("Could not open a container for m\n");
+                    continue;
+                }
+                r = sd_bus_message_close_container(m);
+                if (r < 0)
+                {
+                    printf("Could not close container for m\n");
+                    continue;
+                }
+                r = sd_bus_call(g_bus, m, 0, &err, &reply);
+                if (r < 0)
+                {
+                    printf("Error performing dbus method call\n");
+                }
+                const char* sig = sd_bus_message_get_signature(reply, 0);
+                if (!strcmp(sig, "a{sas}"))
+                {
+                    r = sd_bus_message_enter_container(reply, 'a', "{sas}");
+                    if (r < 0)
+                    {
+                        printf("Could not enter the level 0 array container\n");
+                        continue;
+                    }
+                    while (true)
+                    {
+                        r = sd_bus_message_enter_container(
+                            reply, SD_BUS_TYPE_DICT_ENTRY, "sas");
+                        if (r < 0)
+                        {
+                            // printf("Could not enter the level 1 dict
+                            // container\n");
+                            goto DONE;
+                        }
+                        else if (r == 0)
+                        {}
+                        else
+                        {
+                            // The following 2 correspond to `interface_map` in
+                            // phosphor-mapper
+                            const char* interface_map_first;
+                            r = sd_bus_message_read_basic(reply, 's',
+                                                        &interface_map_first);
+                            if (r < 0)
+                            {
+                                printf("Could not read interface_map_first\n");
+                                goto DONE;
+                            }
+                            r = sd_bus_message_enter_container(reply, 'a', "s");
+                            if (r < 0)
+                            {
+                                printf("Could not enter the level 2 array "
+                                    "container\n");
+                                goto DONE;
+                            }
+                            bool has_value_interface = false;
+                            while (true)
+                            {
+                                const char* interface_map_second;
+                                r = sd_bus_message_read_basic(
+                                    reply, 's', &interface_map_second);
+                                if (r < 0)
+                                {
+                                    printf("Could not read interface_map_second\n");
+                                }
+                                else if (r == 0)
+                                    break;
+                                else
+                                {
+                                    // printf("    %s\n", interface_map_second);
+                                    if (!strcmp(interface_map_second,
+                                                "xyz.openbmc_project.Sensor.Value"))
+                                    {
+                                        has_value_interface = true;
+                                    }
+                                }
+                            }
+                            if (has_value_interface)
+                            {
+                                g_sensor_snapshot->SerSensorVisibleFromObjectMapper(
+                                    std::string(interface_map_first), p);
+                            }
+                            r = sd_bus_message_exit_container(reply);
+                        }
+                        r = sd_bus_message_exit_container(reply);
+                    }
+                    r = sd_bus_message_exit_container(reply);
+                }
+            DONE:
+            {}
+            }
+        }
+        printf("4. Check Hwmon's DBus objects\n");
+        for (int i = 0; i < int(comms.size()); i++)
+        {
+            const std::string& comm = comms[i];
+            const std::string& service = services[i];
+            if (comm.find("phosphor-hwmon-readd") != std::string::npos &&
+                !IsUniqueName(service))
+            {
+                // printf("Should introspect %s\n", service.c_str());
+                std::vector<std::string> objpaths =
+                    FindAllObjectPathsForService(service, nullptr);
+                for (const std::string& op : objpaths)
+                {
+                    if (IsSensorObjectPath(op))
+                    {
+                        g_sensor_snapshot->SetSensorVisibleFromHwmon(service, op);
+                    }
+                }
+            }
+        }
+        // Call `ipmitool sdr list` and see which sensors exist.
+        printf("5. Checking ipmitool SDR List\n");
+        std::string out;
+        bool skip_sdr_list = false;
+        if (getenv("SKIP"))
+        {
+            skip_sdr_list = true;
+        }
+        if (!skip_sdr_list)
+        {
+            constexpr int MAX_BUFFER = 255;
+            char buffer[MAX_BUFFER];
+            FILE* stream = popen("ipmitool sdr list", "r");
+            while (fgets(buffer, MAX_BUFFER, stream) != NULL)
+            {
+                out.append(buffer);
+            }
+            pclose(stream);
+        }
+        std::stringstream ss(out);
+        while (true)
+        {
+            std::string sensor_id, reading, status;
+            std::getline(ss, sensor_id, '|');
+            std::getline(ss, reading, '|');
+            std::getline(ss, status);
+            // printf("%s %s %s\n", sensor_id.c_str(), reading.c_str(),
+            // status.c_str());
+            if (sensor_id.size() > 0 && reading.size() > 0 && status.size() > 0)
+            {
+                g_sensor_snapshot->SetSensorVisibleFromIpmitoolSdr(Trim(sensor_id));
+            }
+            else
+                break;
+        }
+        printf("=== Sensors snapshot summary: ===\n");
+        g_sensor_snapshot->PrintSummary();
+    }
+} // namespace dbus_top_analyzer
+
+void DBusTopStatistics::OnNewDBusMessage(const char* sender,
+                                         const char* destination,
+                                         const char* interface,
+                                         const char* path, const char* member,
+                                         const char type, sd_bus_message* m)
+{
+    num_messages_++;
+    std::vector<std::string> keys;
+
+    std::string sender_orig = CheckAndFixNullString(sender);
+    std::string dest_orig = CheckAndFixNullString(destination);
+    // For method return messages, we actually want to show the sender
+    // and destination of the original method call, so we swap the
+    // sender and destination
+    if (type == 2)
+    { // DBUS_MESSAGE_TYPE_METHOD_METHOD_RETURN
+        std::swap(sender_orig, dest_orig);
+    }
+
+    // Special case: when PID == 1 (init), the DBus unit would be systemd.
+    // It seems it was not possible to obtain the connection name of systemd
+    // so we manually set it here.
+    const int sender_orig_pid =
+        g_connection_snapshot->GetConnectionPIDFromNameOrUniqueName(
+            sender_orig);
+
+    if (sender_orig_pid == 1)
+    {
+        sender_orig = "systemd";
+    }
+    const int dest_orig_pid =
+        g_connection_snapshot->GetConnectionPIDFromNameOrUniqueName(dest_orig);
+    if (dest_orig_pid == 1)
+    {
+        dest_orig = "systemd";
+    }
+
+    for (DBusTopSortField field : fields_)
+    {
+        switch (field)
+        {
+            case kSender:
+                keys.push_back(sender_orig);
+                break;
+            case kDestination:
+                keys.push_back(dest_orig);
+                break;
+            case kInterface:
+                keys.push_back(CheckAndFixNullString(interface));
+                break;
+            case kPath:
+                keys.push_back(CheckAndFixNullString(path));
+                break;
+            case kMember:
+                keys.push_back(CheckAndFixNullString(member));
+                break;
+            case kSenderPID:
+            {
+                if (sender_orig_pid != INVALID)
+                {
+                    keys.push_back(std::to_string(sender_orig_pid));
+                }
+                else
+                {
+                    keys.push_back("(unknown)");
+                }
+                break;
+            }
+            case kSenderCMD:
+            {
+                keys.push_back(
+                    g_connection_snapshot->GetConnectionCMDFromNameOrUniqueName(
+                        sender_orig));
+                break;
+            }
+            case kMsgPerSec:
+            case kAverageLatency:
+                break; // Don't populate "keys" using these 2 fields
+        }
+    }
+    // keys = combination of fields of user's choice
+
+    if (stats_.count(keys) == 0)
+    {
+        stats_[keys] = DBusTopComputedMetrics();
+    }
+    // Need to update msg/s regardless
+    switch (type)
+    {
+        case 1: // DBUS_MESSAGE_TYPE_METHOD_CALL
+            stats_[keys].num_method_calls++;
+            break;
+        case 2: // DBUS_MESSAGE_TYPE_METHOD_METHOD_RETURN
+            stats_[keys].num_method_returns++;
+            break;
+        case 3: // DBUS_MESSAGE_TYPE_ERROR
+            stats_[keys].num_errors++;
+            break;
+        case 4: // DBUS_MESSAGE_TYPE_SIGNAL
+            stats_[keys].num_signals++;
+            break;
+    }
+    // Update global latency histogram
+    // For method call latency
+    if (type == 1) // DBUS_MESSAGE_TYPE_METHOD_CALL
+    {
+        uint64_t serial; // serial == cookie
+        sd_bus_message_get_cookie(m, &serial);
+        in_flight_methodcalls[serial] = Microseconds();
+    }
+    else if (type == 2) // DBUS_MESSAGE_TYPE_MEHOTD_RETURN
+    {
+        uint64_t reply_serial = 0; // serial == cookie
+        sd_bus_message_get_reply_cookie(m, &reply_serial);
+        if (in_flight_methodcalls.count(reply_serial) > 0)
+        {
+            float dt_usec =
+                Microseconds() - in_flight_methodcalls[reply_serial];
+            in_flight_methodcalls.erase(reply_serial);
+            dbus_top_analyzer::g_mc_time_histogram.AddSample(dt_usec);
+
+            // Add method call count and total latency to the corresponding key
+            stats_[keys].total_latency_usec += dt_usec;
+        }
+    }
+    // For meaning of type see here
+    // https://dbus.freedesktop.org/doc/api/html/group__DBusProtocol.html#ga4a9012edd7f22342f845e98150aeb858
+    switch (type)
+    {
+        case 1:
+            num_mc_++;
+            break;
+        case 2:
+            num_mr_++;
+            break;
+        case 3:
+            num_error_++;
+            break;
+        case 4:
+            num_sig_++;
+            break;
+    }
+}
diff --git a/analyzer.hpp b/analyzer.hpp
new file mode 100644
index 0000000..2284c7f
--- /dev/null
+++ b/analyzer.hpp
@@ -0,0 +1,177 @@
+// 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 "histogram.hpp"
+
+#include <systemd/sd-bus.h>
+
+#include <atomic>
+#include <map>
+#include <mutex>
+#include <string>
+#include <vector>
+
+enum DBusTopSortField
+{
+    // DBus Message properties
+    kSender,
+    kDestination,
+    kInterface,
+    kPath,
+    kMember,
+    kSenderPID,
+    kSenderCMD,
+    // Computed metrics
+    kMsgPerSec,
+    kAverageLatency,
+};
+
+const std::string FieldNames[] = {"Sender",     "Destination", "Interface",
+                                  "Path",       "Member",      "Sender PID",
+                                  "Sender CMD", "Msg/s",       "Avg Latency"};
+const int FieldPreferredWidths[] = {18, 20, 12, 10, 10, 10, 25, 8, 12};
+bool DBusTopSortFieldIsNumeric(DBusTopSortField field);
+
+struct DBusTopComputedMetrics
+{
+    DBusTopComputedMetrics()
+    {
+        num_signals = 0;
+        num_errors = 0;
+        num_method_returns = 0;
+        num_method_calls = 0;
+        total_latency_usec = 0;
+    }
+    int num_method_returns = 0;
+    int num_errors = 0;
+    int num_signals = 0;
+    int num_method_calls;
+    uint64_t total_latency_usec;
+};
+
+class DBusTopStatistics
+{
+  public:
+    int num_messages_;
+    int num_mc_, num_mr_, num_sig_, num_error_;
+    float seconds_since_last_sample_;
+    std::vector<DBusTopSortField> fields_;
+    std::map<std::vector<std::string>, DBusTopComputedMetrics> stats_;
+    DBusTopStatistics() :
+        num_messages_(0), num_mc_(0), num_mr_(0), num_sig_(0), num_error_(0),
+        seconds_since_last_sample_(0)
+    {
+        fields_ = {kSender, kDestination, kSenderPID, kSenderCMD};
+        stats_.clear();
+    }
+
+    std::vector<DBusTopSortField> GetFields()
+    {
+        return fields_;
+    }
+
+    std::vector<std::string> GetFieldNames()
+    {
+        const int N = fields_.size();
+        std::vector<std::string> ret(N);
+        for (int i = 0; i < static_cast<int>(fields_.size()); i++)
+        {
+            ret[i] = FieldNames[static_cast<int>(fields_[i])];
+        }
+        return ret;
+    }
+
+    std::vector<int> GetFieldPreferredWidths()
+    {
+        const int N = fields_.size();
+        std::vector<int> ret(N);
+        for (int i = 0; i < static_cast<int>(fields_.size()); i++)
+        {
+            ret[i] = FieldPreferredWidths[static_cast<int>(fields_[i])];
+        }
+        return ret;
+    }
+
+    void Reset()
+    {
+        num_messages_ = 0;
+        num_mc_ = 0;
+        num_mr_ = 0;
+        num_sig_ = 0;
+        num_error_ = 0;
+        stats_.clear();
+    }
+    
+    void SetSortFieldsAndReset(const std::vector<DBusTopSortField>& f)
+    {
+        num_messages_ = 0;
+        num_mc_ = 0;
+        num_mr_ = 0;
+        num_sig_ = 0;
+        num_error_ = 0;
+        stats_.clear();
+        fields_ = f;
+    }
+
+    void Assign(DBusTopStatistics* out)
+    {
+        out->num_messages_ = this->num_messages_;
+        out->num_mc_ = this->num_mc_;
+        out->num_mr_ = this->num_mr_;
+        out->num_sig_ = this->num_sig_;
+        out->num_error_ = this->num_error_;
+        out->seconds_since_last_sample_ = this->seconds_since_last_sample_;
+        out->fields_ = this->fields_;
+        out->stats_ = this->stats_;
+    }
+
+    void OnNewDBusMessage(const char* sender, const char* destination,
+                          const char* interface, const char* path,
+                          const char* message, const char type,
+                          sd_bus_message* m);
+    std::string CheckAndFixNullString(const char* x)
+    {
+        if (x == nullptr)
+            return "(null)";
+        else
+            return std::string(x);
+    }
+
+    std::map<std::vector<std::string>, DBusTopComputedMetrics> StatsSnapshot()
+    {
+        std::map<std::vector<std::string>, DBusTopComputedMetrics> ret;
+        ret = stats_;
+        return ret;
+    }
+
+  private:
+    std::mutex mtx_;
+};
+
+int GetSummaryIntervalInMillises();
+// Monitor sensor details-related DBus method calls/replies
+// typedef void (*SetDBusTopConnection)(const char* conn);
+namespace dbus_top_analyzer
+{
+    void Process();
+    void Finish();
+    typedef void (*DBusTopStatisticsCallback)(DBusTopStatistics*,
+                                            Histogram<float>*);
+    void SetDBusTopStatisticsCallback(DBusTopStatisticsCallback cb);
+    void AnalyzerThread();
+    // Methods for sending Object Mapper queries
+    void ListAllSensors();
+} // namespace dbus_top_analyzer
diff --git a/bargraph.hpp b/bargraph.hpp
new file mode 100644
index 0000000..36f34a1
--- /dev/null
+++ b/bargraph.hpp
@@ -0,0 +1,59 @@
+// 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 <stdio.h>
+
+#include <vector>
+
+template <typename ValueType>
+class BarGraph
+{
+  public:
+    std::vector<ValueType> history_; // is actually a ring buffer.
+    int next_;   // index to the slot where the next insertion will go
+    int length_; // total # of data points that have ever been added
+    explicit BarGraph(int capacity) : next_(0), length_(0)
+    {
+        history_.resize(capacity);
+        std::fill(history_.begin(), history_.end(), 0);
+    }
+
+    // The last value is in [0], so on and so forth
+    // Note: if there are not enough data, this function will return as much
+    // as is available
+    std::vector<float> GetLastNValues(int x)
+    {
+        std::vector<float> ret;
+        const int N = static_cast<int>(history_.size());
+        int imax = x;
+        imax = std::min(imax, length_);
+        imax = std::min(imax, N);
+        int idx = (next_ - 1 + N) % N;
+        for (int i = 0; i < imax; i++)
+        {
+            ret.push_back(history_[idx]);
+            idx = (idx - 1 + N) % N;
+        }
+        return ret;
+    }
+
+    void AddValue(ValueType x)
+    {
+        history_[next_] = x;
+        next_ = (next_ + 1) % history_.size();
+        length_++;
+    }
+};
diff --git a/dbus_capture.cpp b/dbus_capture.cpp
new file mode 100644
index 0000000..715fd41
--- /dev/null
+++ b/dbus_capture.cpp
@@ -0,0 +1,205 @@
+// 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.
+
+#include "analyzer.hpp"
+#include "histogram.hpp"
+#include "sensorhelper.hpp"
+#include "main.hpp"
+
+#include <ncurses.h>
+#include <stdlib.h>
+#include <systemd/sd-bus.h>
+
+#include <unordered_map>
+
+bool IS_USER_BUS = false; // User bus or System bus?
+extern sd_bus* g_bus;
+extern DBusConnectionSnapshot* g_connection_snapshot;
+static std::unordered_map<uint64_t, uint64_t> in_flight_methodcalls;
+
+namespace dbus_top_analyzer
+{
+    extern DBusTopStatistics g_dbus_statistics;
+    extern Histogram<float> g_mc_time_histogram;
+} // namespace dbus_top_analyzer
+
+static void TrackMessage(sd_bus_message* m)
+{}
+// Obtain a Monitoring DBus connection
+int AcquireBus(sd_bus** ret)
+{
+    int r;
+    r = sd_bus_new(ret);
+    if (r < 0)
+    {
+        printf("Could not allocate bus: %s\n", strerror(-r));
+        return 0;
+    }
+    r = sd_bus_set_monitor(*ret, true);
+    if (r < 0)
+    {
+        printf("Could not set monitor mode: %s\n", strerror(-r));
+        return 0;
+    }
+    r = sd_bus_negotiate_creds(*ret, true, _SD_BUS_CREDS_ALL);
+    if (r < 0)
+    {
+        printf("Could not enable credentials: %s\n", strerror(-r));
+        return 0;
+    }
+    r = sd_bus_negotiate_timestamp(*ret, true);
+    if (r < 0)
+    {
+        printf("Could not enable timestamps: %s\n", strerror(-r));
+        return 0;
+    }
+    r = sd_bus_negotiate_fds(*ret, true);
+    if (r < 0)
+    {
+        printf("Could not enable fds: %s\n", strerror(-r));
+        return 0;
+    }
+    r = sd_bus_set_bus_client(*ret, true);
+    if (r < 0)
+    {
+        printf("Could not enable bus client: %s\n", strerror(-r));
+        return 0;
+    }
+    if (IS_USER_BUS)
+    {
+        r = sd_bus_set_address(*ret, "haha");
+        if (r < 0)
+        {
+            printf("Could not set user bus: %s\n", strerror(-r));
+            return 0;
+        }
+    }
+    else
+    {
+        r = sd_bus_set_address(*ret, "unix:path=/run/dbus/system_bus_socket");
+        if (r < 0)
+        {
+            printf("Could not set system bus: %s\n", strerror(-r));
+            return 0;
+        }
+    }
+    r = sd_bus_start(*ret);
+    if (r < 0)
+    {
+        printf("Could not connect to bus: %s\n", strerror(-r));
+        return 0;
+    }
+    return r;
+}
+
+void DbusCaptureThread()
+{
+    int r;
+    // Become Monitor
+    uint32_t flags = 0;
+    sd_bus_message* message;
+    sd_bus_error error = SD_BUS_ERROR_NULL;
+    r = sd_bus_message_new_method_call(
+        g_bus, &message, "org.freedesktop.DBus", "/org/freedesktop/DBus",
+        "org.freedesktop.DBus.Monitoring", "BecomeMonitor");
+    if (r < 0)
+    {
+        printf("Could not create the BecomeMonitor function call\n");
+        exit(0);
+    }
+    // Match conditions
+    r = sd_bus_message_open_container(message, 'a', "s");
+    if (r < 0)
+    {
+        printf("Could not open container\n");
+        exit(0);
+    }
+    r = sd_bus_message_close_container(message);
+    if (r < 0)
+    {
+        printf("Could not close container\n");
+        exit(0);
+    }
+    r = sd_bus_message_append_basic(message, 'u', &flags);
+    if (r < 0)
+    {
+        printf("Could not append flags\n");
+        exit(0);
+    }
+    r = sd_bus_call(g_bus, message, 0, &error, nullptr);
+    if (r < 0)
+    {
+        printf("Could not call org.freedesktop.DBus.Monitoring.BecomeMonitor "
+               "%d, %s\n",
+               r, strerror(-r));
+        exit(0);
+    }
+    const char* unique_name;
+    r = sd_bus_get_unique_name(g_bus, &unique_name);
+    if (r < 0)
+    {
+        printf("Could not get unique name: %s\n", strerror(-r));
+        exit(0);
+    }
+    // Enter packet processing loop
+    while (true)
+    {
+        struct sd_bus_message* m = nullptr;
+        r = sd_bus_process(g_bus, &m);
+        if (m != nullptr)
+        {
+            if (r < 0)
+            {
+                printf("Failed to call sd_bus_process: %s\n", strerror(-r));
+            }
+            uint8_t type;
+            r = sd_bus_message_get_type(m, &type);
+            const char* path = sd_bus_message_get_path(m);
+            // const char* iface = sd_bus_message_get_interface(m);
+            const char* sender = sd_bus_message_get_sender(m);
+            const char* destination = sd_bus_message_get_destination(m);
+            const char* interface = sd_bus_message_get_interface(m);
+            const char* member = sd_bus_message_get_member(m);
+            // TODO: This is for the bottom-left window
+            TrackMessage(m);
+
+            // Look up the unique connection name for sender and destination
+            std::string sender_uniq, dest_uniq;
+            if (sender != nullptr)
+            {
+                sender_uniq =
+                    g_connection_snapshot->GetUniqueNameIfExists(sender);
+            }
+            if (destination != nullptr)
+            {
+                dest_uniq =
+                    g_connection_snapshot->GetUniqueNameIfExists(destination);
+            }
+            // This is for the bottom-right window
+            dbus_top_analyzer::g_dbus_statistics.OnNewDBusMessage(
+            sender_uniq.c_str(), dest_uniq.c_str(), interface, path, member,
+                type, m);
+            sd_bus_message_unref(m);
+        }
+        r = sd_bus_wait(g_bus,
+                        (uint64_t)(GetSummaryIntervalInMillises() * 1000));
+        if (r < 0)
+        {
+            printf("Failed to wait on bus: %s\n", strerror(-r));
+            abort();
+        }
+        // Perform one analysis step
+        dbus_top_analyzer::Process();
+    }
+}
\ No newline at end of file
diff --git a/dbus_capture.hpp b/dbus_capture.hpp
new file mode 100644
index 0000000..825a519
--- /dev/null
+++ b/dbus_capture.hpp
@@ -0,0 +1,20 @@
+// 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 <systemd/sd-bus.h>
+
+int AcquireBus(sd_bus** ret);
+void DbusCaptureThread();
diff --git a/dbus_top_recipe.bb b/dbus_top_recipe.bb
new file mode 100644
index 0000000..4de4f0e
--- /dev/null
+++ b/dbus_top_recipe.bb
@@ -0,0 +1,23 @@
+HOMEPAGE = "http://github.com/openbmc/openbmc-tools"
+PR = "r1"
+PV = "1.0+git${SRCPV}"
+
+LICENSE = "Apache-2.0"
+LIC_FILES_CHKSUM = "file://../LICENSE;md5=e3fc50a88d0a364313df4b21ef20c29e"
+
+SRC_URI += "git://github.com/openbmc-tools"
+SRCREV = "4a0e2e3c10327dac1c923d263929be9a20478b24"
+
+S = "${WORKDIR}/git/"
+inherit meson
+
+SUMMARY = "DBus-Top"
+DESCRIPTION = "DBUs-Top."
+GOOGLE_MISC_PROJ = "dbus-top"
+
+DEPENDS += "systemd"
+DEPENDS += "sdbusplus"
+DEPENDS += "phosphor-mapper"
+DEPENDS += "ncurses"
+
+inherit systemd
diff --git a/histogram.hpp b/histogram.hpp
new file mode 100644
index 0000000..750a9f4
--- /dev/null
+++ b/histogram.hpp
@@ -0,0 +1,184 @@
+// 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 <math.h>
+#include <stdio.h>
+
+#include <vector>
+template <typename ValueType>
+class Histogram
+{
+  public:
+    Histogram()
+    {
+        num_entries_ = 0;
+        num_low_outliers_ = 0;
+        num_high_outliers_ = 0;
+        low_percentile_ = 0;
+        high_percentile_ = 0;
+        SetWindowSize(10000);
+        SetCumulativeDensityThresholds(0.01f, 0.99f);
+    }
+
+    void AddSample(ValueType s)
+    {
+        int N = static_cast<int>(samples_.size());
+        samples_[num_entries_ % N] = s;
+        num_entries_++;
+    }
+
+    void SetBucketCount(int bc)
+    {
+        buckets_.resize(bc);
+    }
+
+    void SetWindowSize(int s)
+    {
+        samples_.resize(s);
+    }
+
+    void SetCumulativeDensityThresholds(float low_cd, float high_cd)
+    {
+        low_cum_density_ = low_cd;
+        high_cum_density_ = high_cd;
+    }
+
+    void ComputeHistogram()
+    {
+        const int NS = static_cast<int>(samples_.size());
+        int N = NS;
+        if (num_entries_ < N)
+        {
+            N = num_entries_;
+        }
+        if (N <= 0)
+        {
+            return;
+        }
+        std::vector<ValueType> sorted;
+        if (N == NS)
+            sorted = samples_;
+        else
+            sorted.insert(sorted.end(), samples_.begin(), samples_.begin() + N);
+        sort(sorted.begin(), sorted.end());
+        int idx_low = static_cast<int>(N * low_cum_density_);
+        int idx_high = static_cast<int>(N * high_cum_density_);
+        if (idx_high - idx_low + 1 < 1)
+        {
+            return; // No entries can be shown, quit
+        }
+        max_bucket_height_ = 0;
+        ValueType value_low = sorted[idx_low];
+        ValueType value_high = sorted[idx_high];
+        low_percentile_ = value_low;
+        high_percentile_ = value_high;
+        const int NB = static_cast<int>(buckets_.size()); // Number of Bins
+        float bucket_width = (value_high - value_low) / NB;
+        // If all values are the same, manually extend the range to
+        // (value-1, value+1)
+        const float EPS = 1e-4;
+        if (fabs(value_high - value_low) <= EPS)
+        {
+            value_low = value_low - 1;
+            value_high = value_high + 1;
+            bucket_width = (value_high - value_low) / NB;
+        }
+        else
+        {}
+        buckets_.assign(NB, 0);
+        num_low_outliers_ = 0;
+        num_high_outliers_ = 0;
+        for (int i = idx_low; i <= idx_high; i++)
+        {
+            ValueType v = sorted[i];
+            ValueType dist = (v - value_low);
+            int bucket_idx = dist / bucket_width;
+            if (bucket_idx < 0)
+            {
+                num_low_outliers_++;
+            }
+            else if (bucket_idx >= NB)
+            {
+                num_high_outliers_++;
+            }
+            else
+            {
+                buckets_[bucket_idx]++;
+                max_bucket_height_ =
+                    std::max(max_bucket_height_, buckets_[bucket_idx]);
+            }
+        }
+    }
+
+    int BucketHeight(int idx)
+    {
+        if (idx < 0 || idx >= static_cast<int>(buckets_.size()))
+            return 0;
+        return buckets_[idx];
+    }
+    
+    void Assign(Histogram<ValueType>* out)
+    {
+        out->num_entries_ = num_entries_;
+        out->samples_ = samples_;
+        out->num_low_outliers_ = num_low_outliers_;
+        out->num_high_outliers_ = num_high_outliers_;
+        out->buckets_ = buckets_;
+        out->low_cum_density_ = low_cum_density_;
+        out->high_cum_density_ = high_cum_density_;
+        out->low_percentile_ = low_percentile_;
+        out->high_percentile_ = high_percentile_;
+        out->max_bucket_height_ = max_bucket_height_;
+    }
+
+    int MaxBucketHeight()
+    {
+        return max_bucket_height_;
+    }
+
+    ValueType LowPercentile()
+    {
+        return low_percentile_;
+    }
+
+    ValueType HighPercentile()
+    {
+        return high_percentile_;
+    }
+
+    float LowCumDensity()
+    {
+        return low_cum_density_;
+    }
+
+    float HighCumDensity()
+    {
+        return high_cum_density_;
+    }
+
+    bool Empty()
+    {
+        return num_entries_ == 0;
+    }
+    
+    int num_entries_;
+    std::vector<ValueType> samples_;
+    int num_low_outliers_, num_high_outliers_;
+    std::vector<int> buckets_;
+    float low_cum_density_, high_cum_density_;
+    // "1% percentile" means "1% of the samples are below this value"
+    ValueType low_percentile_, high_percentile_;
+    int max_bucket_height_;
+};
\ No newline at end of file
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 0000000..d56b72e
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,392 @@
+// 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.
+
+#include "main.hpp"
+
+#include "analyzer.hpp"
+#include "bargraph.hpp"
+#include "dbus_capture.hpp"
+#include "histogram.hpp"
+#include "menu.hpp"
+#include "sensorhelper.hpp"
+#include "views.hpp"
+
+#include <fmt/printf.h>
+#include <ncurses.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <iomanip>
+#include <sstream>
+#include <thread>
+
+DBusTopWindow* g_current_active_view;
+SummaryView* g_summary_window;
+SensorDetailView* g_sensor_detail_view;
+DBusStatListView* g_dbus_stat_list_view;
+FooterView* g_footer_view;
+BarGraph<float>* g_bargraph = nullptr;
+Histogram<float>* g_histogram;
+std::vector<DBusTopWindow*> g_views;
+int g_highlighted_view_index = INVALID;
+sd_bus* g_bus = nullptr;
+SensorSnapshot* g_sensor_snapshot;
+DBusConnectionSnapshot* g_connection_snapshot;
+DBusTopStatistics* g_dbus_statistics; // At every update interval,
+                                      // dbus_top_analyzer::g_dbus_statistics's
+                                      // value is copied to this one for display
+void ReinitializeUI();
+int maxx, maxy, halfx, halfy;
+
+void ActivateWindowA()
+{
+    g_views[0]->visible_=true;
+    g_views[0]->maximize_=true;
+    g_views[1]->visible_=false;
+    g_views[2]->visible_=false;
+}
+
+void ActivateWindowB()
+{
+    g_views[1]->visible_=true;
+    g_views[1]->maximize_=true;
+    g_views[0]->visible_=false;
+    g_views[2]->visible_=false;
+}
+void ActivateWindowC()
+{
+    g_views[2]->visible_=true;
+    g_views[2]->maximize_=true;
+    g_views[0]->visible_=false;
+    g_views[1]->visible_=false;
+}
+void ActivateAllWindows()
+{
+    g_views[0]->maximize_ = false;
+    g_views[1]->maximize_=false;
+    g_views[2]->maximize_=false;
+    g_views[0]->visible_=true;
+    g_views[1]->visible_=true;
+    g_views[2]->visible_= true;
+}
+
+void InitColorPairs()
+{
+    init_pair(1, COLOR_WHITE, COLOR_BLACK); // Does not work on actual machine
+    init_pair(2, COLOR_BLACK, COLOR_WHITE);
+}
+
+// Returns number of lines drawn
+int DrawTextWithWidthLimit(WINDOW* win, std::string txt, int y, int x,
+                           int width, const std::string& delimiters)
+{
+    int ret = 0;
+    std::string curr_word, curr_line;
+    while (txt.empty() == false)
+    {
+        ret++;
+        if (static_cast<int>(txt.size()) > width)
+        {
+            mvwprintw(win, y, x, txt.substr(0, width).c_str());
+            txt = txt.substr(width);
+        }
+        else
+        {
+            mvwprintw(win, y, x, txt.c_str());
+            break;
+        }
+        y++;
+    }
+    return ret;
+}
+
+void UpdateWindowSizes()
+{
+    /* calculate window sizes and locations */
+    if (getenv("FIXED_TERMINAL_SIZE"))
+    {
+        maxx = 100;
+        maxy = 30;
+    }
+    else
+    {
+        getmaxyx(stdscr, maxy, maxx);
+        halfx = maxx >> 1;
+        halfy = maxy >> 1;
+    }
+    for (DBusTopWindow* v : g_views)
+    {
+        v->OnResize(maxx, maxy);
+        if(v->maximize_){
+            v->rect={0,0,maxx,maxy-MARGIN_BOTTOM};
+            v->UpdateWindowSizeAndPosition();
+        }
+    }
+}
+
+std::string FloatToString(float value)
+{
+    return fmt::sprintf("%.2f", value);
+}
+
+void DBusTopRefresh()
+{
+    UpdateWindowSizes();
+    for (DBusTopWindow* v : g_views)
+    {
+        v->Render();
+    }
+    DBusTopWindow* focused_view = g_current_active_view;
+    if (focused_view)
+    {
+        focused_view->DrawBorderIfNeeded(); // focused view border: on top
+    }
+    refresh();
+}
+
+// This function is called by the Capture thread
+void DBusTopStatisticsCallback(DBusTopStatistics* stat, Histogram<float>* hist)
+{
+    if (stat == nullptr)
+        return;
+    // Makes a copy for display
+    // TODO: Add a mutex here for safety
+    stat->Assign(g_dbus_statistics);
+    hist->Assign(g_histogram);
+    float interval_secs = stat->seconds_since_last_sample_;
+    if (interval_secs == 0)
+    {
+        interval_secs = GetSummaryIntervalInMillises() / 1000.0f;
+    }
+    g_summary_window->UpdateDBusTopStatistics(stat);
+    stat->SetSortFieldsAndReset(g_dbus_stat_list_view->GetSortFields());
+    // ReinitializeUI(); // Don't do it here, only when user presses [R]
+    DBusTopRefresh();
+}
+
+void CycleHighlightedView()
+{
+    int new_index = 0;
+    if (g_highlighted_view_index == INVALID)
+    {
+        new_index = 0;
+    }
+    else
+    {
+        new_index = g_highlighted_view_index + 1;
+    }
+    while (new_index < static_cast<int>(g_views.size()) &&
+           g_views[new_index]->selectable_ == false)
+    {
+        new_index++;
+    }
+    if (new_index >= static_cast<int>(g_views.size()))
+    {
+        new_index = INVALID;
+    }
+    // Un-highlight all
+    for (DBusTopWindow* v : g_views)
+    {
+        v->focused_ = false;
+    }
+    if (new_index != INVALID)
+    {
+        g_views[new_index]->focused_ = true;
+        g_current_active_view = g_views[new_index];
+    }
+    else
+    {
+        g_current_active_view = nullptr;
+    }
+    g_highlighted_view_index = new_index;
+    DBusTopRefresh();
+}
+
+int UserInputThread()
+{
+    while (true)
+    {
+        int c = getch();
+        DBusTopWindow* curr_view = g_current_active_view;
+        // If a view is currently focused on
+        if (curr_view)
+        {
+            switch (c)
+            {
+                case '\e': // 27 in dec, 0x1B in hex, escape key
+                {
+                    getch();
+                    c = getch();
+                    switch (c)
+                    {
+                        case 'A':
+                            curr_view->OnKeyDown("up");
+                            break;
+                        case 'B':
+                            curr_view->OnKeyDown("down");
+                            break;
+                        case 'C':
+                            curr_view->OnKeyDown("right");
+                            break;
+                        case 'D':
+                            curr_view->OnKeyDown("left");
+                            break;
+                        case '\e':
+                            curr_view->OnKeyDown("escape");
+                            break;
+                    }
+                    break;
+                }
+                case '\n': // 10 in dec, 0x0A in hex, line feed
+                {
+                    curr_view->OnKeyDown("enter");
+                    break;
+                }
+                case 'q':
+                case 'Q': // Q key
+                {
+                    curr_view->OnKeyDown("escape");
+                    break;
+                }
+                case 'a':
+                case 'A': // A key
+                {
+                    curr_view->OnKeyDown("a");
+                    break;
+                }
+                case 'd':
+                case 'D': // D key
+                {
+                    curr_view->OnKeyDown("d");
+                    break;
+                }
+                case 33: // Page up
+                {
+                    curr_view->OnKeyDown("pageup");
+                    break;
+                }
+                case 34: // Page down
+                {
+                    curr_view->OnKeyDown("pagedown");
+                    break;
+                }
+                case ' ': // Spacebar
+                {
+                    curr_view->OnKeyDown("space");
+                    break;
+                }
+            }
+        }
+        // The following keys are registered both when a view is selected and
+        // when it is not
+        switch (c)
+        {
+            case '\t': // 9 in dec, 0x09 in hex, tab
+            {
+                CycleHighlightedView();
+                break;
+            }
+            case 'r':
+            case 'R':
+            {
+                ReinitializeUI();
+                DBusTopRefresh();
+                break;
+            }
+            case 'x':
+            case 'X':
+            {
+                clear();
+                ActivateWindowA();
+                break;
+            }
+            case 'y':
+            case 'Y':
+            {
+                clear();
+                ActivateWindowB();
+                break;
+            }
+            case 'z':
+            case 'Z':
+            {
+                clear();
+                ActivateWindowC();
+                break;
+            }
+            case 'h':
+            case 'H':
+            {
+                ActivateAllWindows();
+                DBusTopRefresh();
+            }
+            default:
+                break;
+        }
+    }
+    exit(0);
+}
+
+void ReinitializeUI()
+{
+    endwin();
+    initscr();
+    use_default_colors();
+    noecho();
+    for (int i = 0; i < static_cast<int>(g_views.size()); i++)
+    {
+        g_views[i]->RecreateWindow();
+    }
+}
+
+int main(int argc, char** argv)
+{
+    int r = AcquireBus(&g_bus);
+    if (r <= 0)
+    {
+        printf("Error acquiring bus for monitoring\n");
+        exit(0);
+    }
+
+    printf("Listing all sensors for display\n");
+    // ListAllSensors creates connection snapshot and sensor snapshot
+    dbus_top_analyzer::ListAllSensors();
+    g_bargraph = new BarGraph<float>(300);
+    g_histogram = new Histogram<float>();
+
+    initscr();
+    use_default_colors();
+    start_color();
+    noecho();
+
+    clear();
+    g_dbus_statistics = new DBusTopStatistics();
+    g_summary_window = new SummaryView();
+    g_sensor_detail_view = new SensorDetailView();
+    g_dbus_stat_list_view = new DBusStatListView();
+    g_footer_view = new FooterView();
+    g_views.push_back(g_summary_window);
+    g_views.push_back(g_sensor_detail_view);
+    g_views.push_back(g_dbus_stat_list_view);
+    g_views.push_back(g_footer_view);
+
+    g_sensor_detail_view->UpdateSensorSnapshot(g_sensor_snapshot);
+    UpdateWindowSizes();
+    dbus_top_analyzer::SetDBusTopStatisticsCallback(&DBusTopStatisticsCallback);
+    std::thread capture_thread(DbusCaptureThread);
+    std::thread user_input_thread(UserInputThread);
+    capture_thread.join();
+
+    return 0;
+}
diff --git a/main.hpp b/main.hpp
new file mode 100644
index 0000000..91c07da
--- /dev/null
+++ b/main.hpp
@@ -0,0 +1,37 @@
+// 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 "bargraph.hpp"
+
+#include <ncurses.h>
+
+#include <string>
+
+const int INVALID = -999; // Constant indicating invalid indices
+struct Rect
+{
+    int x, y, w, h; // X, Y, Width, Height
+    Rect(int _x, int _y, int _w, int _h) : x(_x), y(_y), w(_w), h(_h)
+    {}
+    Rect() : x(0), y(0), w(1), h(1)
+    {}
+};
+
+int DrawTextWithWidthLimit(WINDOW* win, std::string txt, int y, int x,
+                           int width, const std::string& delimiters);
+std::string FloatToString(float value);
+template <typename T>
+void HistoryBarGraph(WINDOW* win, const Rect& rect, BarGraph<T>* bargraph);
diff --git a/menu.cpp b/menu.cpp
new file mode 100644
index 0000000..af59f14
--- /dev/null
+++ b/menu.cpp
@@ -0,0 +1,322 @@
+// 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.
+
+#include "menu.hpp"
+
+#include "views.hpp"
+
+ArrowKeyNavigationMenu::ArrowKeyNavigationMenu(DBusTopWindow* view) :
+    win_(view->win), parent_(view), idx0_(INVALID), idx1_(INVALID),
+    h_padding_(2), col_width_(15), h_spacing_(2), choice_(INVALID)
+{}
+
+void ArrowKeyNavigationMenu::do_Render(bool is_column_major)
+{
+    const int nrows = DispEntriesPerColumn();
+    const int ncols = DispEntriesPerRow();
+    const int items_per_page = nrows * ncols;
+    if (items_per_page < 1)
+        return;
+    int tot_num_items = items_.size();
+    // int tot_num_columns = (tot_num_items - 1) / nrows + 1;
+    // Determine whether cursor is outside the current rectangular viewport
+    bool is_cursor_out_of_view = false;
+    if (idx0_ > choice_ || idx1_ <= choice_)
+    {
+        is_cursor_out_of_view = true;
+    }
+    if (idx0_ == INVALID || idx1_ == INVALID)
+    {
+        is_cursor_out_of_view = true;
+    }
+    // Scroll the viewport such that it contains the cursor
+    if (is_cursor_out_of_view)
+    {
+        idx0_ = 0;
+        idx1_ = items_per_page;
+    }
+    while (idx1_ <= choice_)
+    {
+        if (is_column_major)
+        {
+            idx0_ += nrows;
+            idx1_ += nrows;
+        }
+        else
+        {
+            idx0_ += ncols;
+            idx1_ += ncols;
+        }
+    }
+    int y0 = rect_.y, x0 = rect_.x;
+    int y = y0, x = x0;
+    for (int i = 0; i < items_per_page; i++)
+    {
+        int idx = idx0_ + i;
+        if (idx < tot_num_items)
+        {
+            if (idx == choice_)
+            {
+                wattrset(win_, A_REVERSE);
+            }
+            std::string s = items_[idx];
+            while (s.size() < col_width_)
+            {
+                s.push_back(' ');
+            }
+            mvwprintw(win_, y, x, s.c_str());
+            wattrset(win_, 0);
+        }
+        else
+        {
+            break;
+        }
+        if (is_column_major)
+        {
+            y++;
+            if (i % nrows == nrows - 1)
+            {
+                y = y0;
+                x += col_width_ + h_spacing_;
+            }
+        }
+        else
+        {
+            x += col_width_ + h_spacing_;
+            if (i % ncols == ncols - 1)
+            {
+                x = x0;
+                y++;
+            }
+        }
+    }
+}
+
+void ArrowKeyNavigationMenu::Render()
+{
+    do_Render(order == ColumnMajor);
+}
+
+void ArrowKeyNavigationMenu::OnKeyDown(const std::string& key)
+{
+    switch (order)
+    {
+        case ColumnMajor:
+            if (key == "up")
+            {
+                MoveCursorAlongPrimaryAxis(-1);
+            }
+            else if (key == "down")
+            {
+                MoveCursorAlongPrimaryAxis(1);
+            }
+            else if (key == "left")
+            {
+                MoveCursorAlongSecondaryAxis(-1);
+            }
+            else if (key == "right")
+            {
+                MoveCursorAlongSecondaryAxis(1);
+            }
+            break;
+        case RowMajor:
+            if (key == "up")
+            {
+                MoveCursorAlongSecondaryAxis(-1);
+            }
+            else if (key == "down")
+            {
+                MoveCursorAlongSecondaryAxis(1);
+            }
+            else if (key == "left")
+            {
+                MoveCursorAlongPrimaryAxis(-1);
+            }
+            else if (key == "right")
+            {
+                MoveCursorAlongPrimaryAxis(1);
+            }
+            break;
+            break;
+    }
+}
+
+void ArrowKeyNavigationMenu::MoveCursorAlongPrimaryAxis(int delta)
+{
+    const int N = items_.size();
+    if (N < 1)
+        return;
+    // If the cursor is inactive, activate it
+    if (choice_ == INVALID)
+    {
+        if (delta > 0)
+        {
+            choice_ = 0;
+        }
+        else
+        {
+            choice_ = N - 1;
+        }
+        return;
+    }
+    int choice_next = choice_ + delta;
+    while (choice_next >= N)
+    {
+        choice_next -= N;
+    }
+    while (choice_next < 0)
+    {
+        choice_next += N;
+    }
+    choice_ = choice_next;
+}
+
+void ArrowKeyNavigationMenu::MoveCursorAlongSecondaryAxis(int delta)
+{
+    if (delta != 0 && delta != 1 && delta != -1)
+        return;
+    const int N = items_.size();
+    if (N < 1)
+        return;
+    // If the cursor is inactive, activate it
+    if (choice_ == INVALID)
+    {
+        if (delta > 0)
+        {
+            choice_ = 0;
+        }
+        else
+        {
+            choice_ = N - 1;
+        }
+        return;
+    }
+    const int nrows =
+        (order == ColumnMajor) ? DispEntriesPerColumn() : DispEntriesPerRow();
+    const int tot_columns = (N - 1) / nrows + 1;
+    const int num_rows_last_column = N - 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 && x >= tot_columns)
+    {
+        overflow_to_right = true;
+    }
+    if (y >= num_rows_last_column && 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++;
+        if (y >= nrows)
+        {
+            choice_ = 0;
+            return;
+        }
+        else
+        {
+            choice_ = y;
+            return;
+        }
+    }
+    else if (overflow_to_left)
+    {
+        y--;
+        if (y < 0)
+        {
+            if (num_rows_last_column == nrows)
+            {
+                choice_ = N - 1;
+            }
+            else
+            {
+                choice_ = N - 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;
+    }
+}
+
+void ArrowKeyNavigationMenu::SetChoiceAndConstrain(int c)
+{
+    if (Empty())
+    {
+        choice_ = INVALID;
+        return;
+    }
+    if (c < 0)
+        c = 0;
+    if (c >= static_cast<int>(items_.size()))
+    {
+        c = static_cast<int>(items_.size() - 1);
+    }
+    choice_ = c;
+}
+
+void ArrowKeyNavigationMenu::AddItem(const std::string& s)
+{
+    items_.push_back(s);
+}
+
+bool ArrowKeyNavigationMenu::RemoveHighlightedItem(std::string* ret)
+{
+    if (choice_ < 0 || choice_ >= items_.size())
+        return false;
+    std::string r = items_[choice_];
+    items_.erase(items_.begin() + choice_);
+    if (items_.empty())
+    {
+        Deselect();
+    }
+    else
+    {
+        if (choice_ >= items_.size())
+        {
+            choice_ = items_.size() - 1;
+        }
+    }
+    if (ret)
+    {
+        *ret = r;
+    }
+    return true;
+}
\ No newline at end of file
diff --git a/menu.hpp b/menu.hpp
new file mode 100644
index 0000000..afb4110
--- /dev/null
+++ b/menu.hpp
@@ -0,0 +1,122 @@
+// 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 "main.hpp"
+
+#include <ncurses.h>
+
+#include <string>
+#include <vector>
+class DBusTopWindow;
+class ArrowKeyNavigationMenu
+{
+  public:
+    explicit ArrowKeyNavigationMenu(WINDOW* win) :
+        win_(win), h_padding_(2), col_width_(15), h_spacing_(2), idx0_(INVALID),
+        idx1_(INVALID), choice_(INVALID), parent_(nullptr)
+    {}
+    explicit ArrowKeyNavigationMenu(DBusTopWindow* view);
+    void LoadDummyValues()
+    {
+        items_.clear();
+        items_.push_back("Sender");
+        items_.push_back("Destination");
+        items_.push_back("Interface");
+        items_.push_back("Path");
+        items_.push_back("Member");
+    }
+
+    void OnKeyDown(const std::string& key);
+    void Render();
+    void MoveCursorAlongSecondaryAxis(int delta);
+    void MoveCursorAlongPrimaryAxis(int delta);
+    int DispEntriesPerRow()
+    {
+        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;
+    }
+
+    int DispEntriesPerColumn()
+    {
+        return rect_.h;
+    }
+
+    void SetRect(const Rect& rect)
+    {
+        rect_ = rect;
+    }
+
+    enum Order
+    {
+        ColumnMajor,
+        RowMajor,
+    };
+
+    void SetOrder(Order o)
+    {
+        order = o;
+    }
+
+    int Choice()
+    {
+        return choice_;
+    }
+
+    void Deselect()
+    {
+        choice_ = INVALID;
+    }
+
+    bool Empty()
+    {
+        return items_.empty();
+    }
+
+    void SetChoiceAndConstrain(int c);
+    Rect rect_;
+    void AddItem(const std::string& s);
+    bool RemoveHighlightedItem(std::string* ret); // returns true if successful
+    std::vector<std::string> Items()
+    {
+        return items_;
+    }
+
+    void do_Render(bool is_column_major);
+    std::vector<std::string> items_;
+    WINDOW* win_;
+    int h_padding_;
+    int col_width_;
+    int h_spacing_;
+    int idx0_, idx1_;
+    int choice_;
+    DBusTopWindow* parent_;
+    Order order;
+};
+
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..60d4641
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,48 @@
+# 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.
+
+project('dbus-top', 'cpp',
+    version: '0.1',
+    default_options: 
+    [
+        'cpp_std=c++17',
+    ],
+)
+
+cmake = import('cmake')
+fmt_proj = cmake.subproject('fmt')
+fmt_dep = fmt_proj.dependency('fmt')
+
+bindir = get_option('prefix') / get_option('bindir')
+executable('dbus-top',
+    [
+        'menu.cpp',
+        'analyzer.cpp',
+        'dbus_capture.cpp',
+        'sensorhelper.cpp',
+        'xmlparse.cpp',
+        'views.cpp',
+        'main.cpp',
+    ],
+    dependencies:
+    [
+        dependency('systemd'),
+        dependency('sdbusplus'),
+        dependency('ncurses'),
+        dependency('threads'),
+        fmt_dep
+    ],
+    install: true,
+    install_dir: bindir
+)
diff --git a/rect.hpp b/rect.hpp
new file mode 100644
index 0000000..aa7bf89
--- /dev/null
+++ b/rect.hpp
@@ -0,0 +1,26 @@
+/*
+ * 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
+
+struct Rect
+{
+    int x, y, w, h; // X, Y, Width, Height
+    Rect(int _x, int _y, int _w, int _h) : x(_x), y(_y), w(_w), h(_h)
+    {}
+    Rect() : x(0), y(0), w(1), h(1)
+    {}
+};
\ No newline at end of file
diff --git a/sensorhelper.cpp b/sensorhelper.cpp
new file mode 100644
index 0000000..3f8bba6
--- /dev/null
+++ b/sensorhelper.cpp
@@ -0,0 +1,130 @@
+// 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.
+
+#include "sensorhelper.hpp"
+#include "main.hpp"
+
+#include <unistd.h>
+
+#include <cassert>
+#include <fstream>
+#include <functional>
+#include <sstream>
+#include <string>
+#include <vector>
+
+extern SensorSnapshot* g_sensor_snapshot;
+extern DBusConnectionSnapshot* g_connection_snapshot;
+
+std::vector<std::string> MySplit(const std::string& s)
+{
+    int idx = 0, prev_idx = 0;
+    std::vector<std::string> ret;
+    while (idx <= static_cast<int>(s.size()))
+    {
+        if (idx == static_cast<int>(s.size()) || s[idx] == '/')
+        {
+            if (idx > prev_idx)
+            {
+                ret.push_back(s.substr(prev_idx, idx - prev_idx));
+            }
+            prev_idx = idx + 1;
+        }
+        idx++;
+    }
+    return ret;
+}
+
+// Example: /xyz/openbmc_project/sensors/temperature/powerseq_temp
+bool IsSensorObjectPath(const std::string& s)
+{
+    std::vector<std::string> sections = MySplit(s);
+    if (sections.size() == 5 && sections[0] == "xyz" &&
+        sections[1] == "openbmc_project" && sections[2] == "sensors")
+    {
+        return true;
+    }
+    else
+    {
+        return false;
+    }
+}
+
+// Example: /xyz/openbmc_project/sensors/temperature/powerseq_temp/chassis
+bool IsSensorObjectPathWithAssociation(const std::string& s,
+                                       std::string* sensor_obj_path)
+{
+    std::vector<std::string> sections = MySplit(s);
+    if (sections.size() == 6)
+    {
+        size_t idx = s.rfind('/');
+        return IsSensorObjectPath(s.substr(0, idx));
+    }
+    else
+    {
+        return false;
+    }
+}
+
+bool IsUniqueName(const std::string& x)
+{
+    if (x.empty())
+        return false;
+    if (x[0] != ':')
+        return false;
+    if (x[0] == ':')
+    {
+        for (int i = 1; i < int(x.size()); i++)
+        {
+            const char ch = x[i];
+            if (ch >= '0' || ch <= '9')
+                continue;
+            else if (ch == '.')
+                continue;
+            else
+                return false;
+        }
+    }
+    return true;
+}
+
+std::vector<std::string> FindAllObjectPathsForService(
+    const std::string& service,
+    std::function<void(const std::string&, const std::vector<std::string>&)>
+        on_interface_cb)
+{
+    // Not available for PCAP replay, only valid with actual DBus capture
+    assert(false);
+}
+
+bool IsWhitespace(const char c)
+{
+    if (c == ' ' || c == '\r' || c == '\n')
+        return true;
+    else
+        return false;
+}
+
+std::string Trim(const std::string& s)
+{
+    const int N = int(s.size());
+    int idx0 = 0, idx1 = int(N - 1);
+    while (idx0 < N && IsWhitespace(s[idx0]))
+        idx0++;
+    while (idx1 >= 0 && IsWhitespace(s[idx1]))
+        idx1--;
+    if (idx0 >= N || idx1 < 0)
+        return "";
+    return s.substr(idx0, idx1 - idx0 + 1);
+}
diff --git a/sensorhelper.hpp b/sensorhelper.hpp
new file mode 100644
index 0000000..a619c4a
--- /dev/null
+++ b/sensorhelper.hpp
@@ -0,0 +1,342 @@
+// 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 "main.hpp"
+// This is the form a sensor assumes on DBus.
+// Aggregates their view from all other daemons.
+#include <bitset>
+#include <optional>
+#include <set>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+// Where is this sensor seen?
+constexpr int VISIBILITY_OBJECT_MAPPER = 0;
+constexpr int VISIBILITY_HWMON = 1;
+constexpr int VISIBILITY_IPMITOOL_SDR = 2;
+class DBusConnection
+{
+  public:
+    std::string service;    // example: "systemd-resolved"
+    std::string connection; // example: ":1.1"
+    std::string cmd;        // the comm line
+    std::string unit;       // example: "systemd-resolved.service"
+    int pid;
+    // For actual DBus capture: service name, connection name, command line,
+    //                          systmed unit and PID are all known
+    DBusConnection(const std::string& _s, const std::string& _c,
+                   const std::string& _cmd, const std::string& _u, int _pid)
+    {
+        service = _s;
+        connection = _c;
+        cmd = _cmd;
+        unit = _u;
+        pid = _pid;
+    }
+
+    // During PCap replay: only service name, connection name, and PID are known
+    //                     cmd and unit are not known since they are not
+    //                     stored in the PCap file
+    DBusConnection(const std::string& _s, const std::string& _c, int _pid)
+    {
+        service = _s;
+        connection = _c;
+        pid = _pid;
+    }
+};
+
+class DBusConnectionSnapshot
+{
+  public:
+    std::vector<DBusConnection*> connections_;
+    std::unordered_map<std::string, DBusConnection*> unique_name_to_cxn;
+    DBusConnection* FindDBusConnectionByService(const std::string& service)
+    {
+        for (DBusConnection* cxn : connections_)
+        {
+            if (cxn->service == service)
+                return cxn;
+        }
+        return nullptr;
+    }
+
+    void SetConnectionPID(const std::string& connection, int pid)
+    {
+        DBusConnection* cxn = FindDBusConnectionByService(connection);
+        if (cxn != nullptr)
+        {
+            cxn->pid = pid;
+            unique_name_to_cxn[connection] = cxn; // Just to make sure
+        }
+    }
+
+    void SetConnectionUniqueName(const std::string& service,
+                                 const std::string& unique_name)
+    {
+        DBusConnection* cxn = FindDBusConnectionByService(service);
+        if (cxn != nullptr)
+        {
+            cxn->connection = unique_name;
+            unique_name_to_cxn[unique_name] = cxn;
+        }
+    }
+
+    DBusConnection* FindDBusConnectionByConnection(const std::string& conn)
+    {
+        for (DBusConnection* cxn : connections_)
+        {
+            if (cxn->connection == conn)
+                return cxn;
+        }
+        return nullptr;
+    }
+
+
+    // Only when service is known (during playback)
+    void AddConnection(const std::string& _s)
+    {
+        connections_.push_back(new DBusConnection(_s, "", "", "", INVALID));
+    }
+
+    // When all 5 pieces of details are known (during actual capture)
+    void AddConnection(const std::string& _s, const std::string& _connection,
+                       const std::string& _cmd, const std::string& _unit,
+                       int _pid)
+    {
+        DBusConnection* cxn =
+            new DBusConnection(_s, _connection, _cmd, _unit, _pid);
+        connections_.push_back(cxn);
+        unique_name_to_cxn[_connection] = cxn;
+    }
+
+    int GetConnectionPIDFromNameOrUniqueName(const std::string& key)
+    {
+        if (unique_name_to_cxn.find(key) == unique_name_to_cxn.end())
+        {
+            return INVALID;
+        }
+        else
+        {
+            return unique_name_to_cxn[key]->pid;
+        }
+    }
+
+    std::string GetConnectionCMDFromNameOrUniqueName(const std::string& key)
+    {
+        if (unique_name_to_cxn.find(key) == unique_name_to_cxn.end())
+        {
+            return "(unknown)";
+        }
+        else
+        {
+            return unique_name_to_cxn[key]->cmd;
+        }
+    }
+
+    std::string GetUniqueNameIfExists(const std::string service)
+    {
+        for (DBusConnection* cxn : connections_)
+        {
+            if (cxn->service == service)
+                return cxn->connection;
+        }
+        return service;
+    }
+
+};
+
+// Each sensor might have different units, for example current and voltage
+class Sensor
+{
+  public:
+    DBusConnection* connection_;
+    // Example: "/xyz/openbmc_project/sensors/temperature/powerseq_temp"
+    std::string object_path_;
+    std::string SensorID()
+    {
+        const size_t idx = object_path_.rfind('/');
+        if (idx != std::string::npos)
+        {
+            return object_path_.substr(idx + 1);
+        }
+        else
+            return ("unknown sensor");
+    }
+
+    std::string ServiceName()
+    {
+        if (connection_ == nullptr)
+            return "";
+        else
+            return connection_->service;
+    }
+
+    std::string ConnectionName()
+    {
+        if (connection_ == nullptr)
+            return "";
+        else
+            return connection_->connection;
+    }
+
+    std::string ObjectPath()
+    {
+        return object_path_;
+    }
+
+    // Should contain the following:
+    // 1. "org.freedesktop.DBus.Introspectable"
+    // 2. "org.freedesktop.DBus.Peer"
+    // 3. "org.freedesktop.DBus.Properties"
+    // 4. "xyz.openbmc_project.Sensor.Value"
+    // 5. "xyz.openbmc_project.State.Decorator.OperationalStatus"
+    std::set<std::string> interfaces_;
+    std::bitset<4> visibility_flags_;
+    std::set<std::string> associations_;
+};
+
+class SensorSnapshot
+{
+  public:
+    std::vector<std::string> GetDistinctSensorNames()
+    {
+        std::unordered_set<std::string> seen;
+        std::vector<std::string> ret;
+        for (Sensor* s : sensors_)
+        {
+            std::string sn = s->SensorID();
+            if (seen.find(sn) == seen.end())
+            {
+                ret.push_back(sn);
+                seen.insert(sn);
+            }
+        }
+        return ret;
+    }
+
+    explicit SensorSnapshot(DBusConnectionSnapshot* cs)
+    {
+        connection_snapshot_ = cs;
+    }
+
+    ~SensorSnapshot()
+    {
+        for (Sensor* s : sensors_)
+        {
+            delete s;
+        }
+    }
+
+    int SensorCount()
+    {
+        return int(sensors_.size());
+    }
+
+    Sensor* FindOrCreateSensorByServiceAndObject(const std::string& service,
+                                                 const std::string& object)
+    {
+        Sensor* ret = nullptr;
+        for (Sensor* s : sensors_)
+        {
+            if (s->ServiceName() == service && s->object_path_ == object)
+            {
+                ret = s;
+                break;
+            }
+        }
+        if (ret == nullptr)
+        {
+            DBusConnection* cxn =
+                connection_snapshot_->FindDBusConnectionByService(service);
+            ret = new Sensor();
+            ret->connection_ = cxn;
+            ret->object_path_ = object;
+            sensors_.push_back(ret);
+        }
+        return ret;
+    }
+
+    // Note: one sensor_id might correspond to multiple sensors.
+    // Example: "VDD" can have all 3 of power, current and voltage.
+    std::vector<Sensor*> FindSensorsBySensorID(const std::string& sensor_id)
+    {
+        std::vector<Sensor*> ret;
+        for (Sensor* s : sensors_)
+        {
+            const std::string& p = s->object_path_;
+            if (p.find(sensor_id) == p.size() - sensor_id.size())
+            {
+                ret.push_back(s);
+            }
+        }
+        return ret;
+    }
+
+    // This sensor is visible from Object Mapper
+    void SerSensorVisibleFromObjectMapper(const std::string& service,
+                                          const std::string& object)
+    {
+        Sensor* s = FindOrCreateSensorByServiceAndObject(service, object);
+        s->visibility_flags_.set(VISIBILITY_OBJECT_MAPPER);
+    }
+    
+    // This sensor is visible from Hwmon
+    void SetSensorVisibleFromHwmon(const std::string& service,
+                                   const std::string& object)
+    {
+        Sensor* s = FindOrCreateSensorByServiceAndObject(service, object);
+        s->visibility_flags_.set(VISIBILITY_HWMON);
+    }
+
+    // This sensor is visible from `ipmitool sdr`
+    // The first column is referred to as "sensorid".
+    void SetSensorVisibleFromIpmitoolSdr(const std::string& sensor_id)
+    {
+        std::vector<Sensor*> sensors = FindSensorsBySensorID(sensor_id);
+        for (Sensor* s : sensors)
+            s->visibility_flags_.set(VISIBILITY_IPMITOOL_SDR);
+    }
+
+    void PrintSummary()
+    {
+        for (Sensor* s : sensors_)
+        {
+            printf("%50s %50s %9s\n", s->ServiceName().c_str(),
+                   s->object_path_.c_str(),
+                   s->visibility_flags_.to_string().c_str());
+        }
+    }
+
+    Sensor* FindSensorByDBusUniqueNameOrServiceName(const std::string& key)
+    {
+        for (Sensor* s : sensors_)
+        {
+            if (s->ConnectionName() == key || s->ServiceName() == key)
+                return s;
+        }
+        return nullptr;
+    }
+
+  private:
+    std::vector<Sensor*> sensors_;
+    std::unordered_map<std::string, int> conn2pid_;
+    DBusConnectionSnapshot* connection_snapshot_;
+};
+
+bool IsSensorObjectPath(const std::string& s);
+bool IsUniqueName(const std::string& x);
+std::string Trim(const std::string& s);
diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap
new file mode 100644
index 0000000..6847ae5
--- /dev/null
+++ b/subprojects/fmt.wrap
@@ -0,0 +1,3 @@
+[wrap-git]
+url = https://github.com/fmtlib/fmt
+revision = HEAD
diff --git a/subprojects/sdbusplus.wrap b/subprojects/sdbusplus.wrap
new file mode 100644
index 0000000..7b076d0
--- /dev/null
+++ b/subprojects/sdbusplus.wrap
@@ -0,0 +1,6 @@
+[wrap-git]
+url = https://github.com/openbmc/sdbusplus.git
+revision = HEAD
+
+[provide]
+sdbusplus = sdbusplus_dep
diff --git a/views.cpp b/views.cpp
new file mode 100644
index 0000000..0e6f8b0
--- /dev/null
+++ b/views.cpp
@@ -0,0 +1,1236 @@
+// 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.
+
+#include "views.hpp"
+#include "bargraph.hpp"
+#include "histogram.hpp"
+#include "menu.hpp"
+#include <string.h>
+#include <algorithm>
+
+extern SensorSnapshot* g_sensor_snapshot;
+extern BarGraph<float>* g_bargraph;
+extern DBusTopStatistics* g_dbus_statistics;
+extern Histogram<float>* g_histogram;
+extern DBusTopWindow* g_current_active_view;
+extern const std::string FieldNames[];
+extern const int FieldPreferredWidths[];
+
+namespace dbus_top_analyzer
+{
+    extern DBusTopStatistics g_dbus_statistics;
+}
+
+// Linear interpolation
+float Lerp(float a, float b, float t)
+{
+    return a + t * (b - a);
+}
+
+// Linear map
+float Map(float value, float start1, float stop1, float start2, float stop2,
+          bool within_bounds)
+{
+    float t = (value - start1) / (stop1 - start1);
+    float ret = Lerp(start2, stop2, t);
+    if (within_bounds)
+    {
+        if (ret < start2)
+            ret = start2;
+        if (ret > stop2)
+            ret = stop2;
+    }
+    return ret;
+}
+
+template <typename T>
+void HistoryBarGraph(WINDOW* win, const Rect& rect, BarGraph<T>* bargraph)
+{
+    const int RIGHT_MARGIN = 5;
+    const int x0 = rect.x, y0 = 2;
+    const int w = rect.w - 2 - RIGHT_MARGIN;
+    const int h = rect.h - 3; // height of content
+    wattrset(win, 0);
+    wattron(win, A_BOLD | A_UNDERLINE);
+    mvwaddstr(win, 1, x0, "History     (Total msg/s)");
+    wattrset(win, 0);
+    // 1. Obtain data, determine Y range
+    std::vector<float> data = bargraph->GetLastNValues(w - RIGHT_MARGIN - 1);
+    float ymax = -1e20, ymin = 1e20;
+    if (data.empty())
+    {
+        data.push_back(0);
+        ymin = 0;
+        ymax = 10;
+    }
+    else
+    {
+        for (const float x : data)
+        {
+            ymax = std::max(ymax, x);
+            ymin = std::min(ymin, x);
+        }
+    }
+    // Fix edge case for both == 0
+    float diff = ymax - ymin;
+    if (diff < 0)
+    {
+        diff = -diff;
+    }
+    const float EPS = 1e-4;
+    if (diff < EPS)
+    {
+        ymax += 10;
+        ymin -= 10;
+    }
+    // Choose a suitable round-up unit to snap the grid labels to
+    int snap = 1;
+    if (ymax < 100)
+    {
+        snap = 10;
+    }
+    else if (ymax < 10000)
+    {
+        snap = 100;
+    }
+    else
+    {
+        snap = 1000;
+    }
+    const float eps = snap / 100.0f;
+    int label_ymax =
+        (static_cast<int>((ymax - eps) / snap) + 1) * snap; // round up
+    int label_ymin = static_cast<int>(ymin / snap) * snap;  // round down
+    float y_per_row = (label_ymax - label_ymin) * 1.0f / (h - 1);
+    int actual_ymax = label_ymax + static_cast<int>(y_per_row / 2);
+    int actual_ymin = label_ymin - static_cast<int>(y_per_row / 2);
+    // 2. Print Y axis ticks
+    for (int i = 0; i < h; i++)
+    {
+        char buf[10];
+        snprintf(
+            buf, sizeof(buf), "%-6d",
+            static_cast<int>(Lerp(label_ymax, label_ymin, i * 1.0f / (h - 1))));
+        mvwaddstr(win, i + y0, x0 + w - RIGHT_MARGIN + 1, buf);
+        mvwaddch(win, i + y0, x0, '-');
+        mvwaddch(win, i + y0, x0 + w - RIGHT_MARGIN, '-');
+    }
+    // 3. Go through the historical data and draw on the canvas
+    for (int i = 0;
+         i < std::min(static_cast<int>(data.size()), w - RIGHT_MARGIN - 1); i++)
+    {
+        float value = data[i];
+        // antialiasing: todo for now
+        // float value1 = value; // value1 is 1 column to the right
+        // if (i > 0) value1 = data[i-1];
+        int x = x0 + w - i - RIGHT_MARGIN - 1;
+        float t = Map(value, actual_ymin, actual_ymax, 0, h, true);
+        int row = static_cast<int>(t);
+        float remaining = t - row;
+        char ch; // Last filling character
+        if (remaining >= 0.66f)
+        {
+            ch = ':';
+        }
+        else if (remaining >= 0.33f)
+        {
+            ch = '.';
+        }
+        else
+        {
+            ch = ' ';
+        }
+        int y = y0 + h - row - 1;
+        mvwaddch(win, y, x, ch);
+        for (int j = 0; j < row; j++)
+        {
+            mvwaddch(win, y + j + 1, x, ':');
+        }
+    }
+}
+
+template <typename T>
+void DrawHistogram(WINDOW* win, const Rect& rect, Histogram<T>* histogram)
+{
+    // const int MARGIN = 7;  // 5 digits margin
+    const int LEFT_MARGIN = 7;
+    // const int max_bucket_h = histogram->MaxBucketHeight();
+    const int H_PAD = 0, V_PAD = 1;
+    // x0, x1, y0 and y1 are the bounding box of the contents to be printed
+    const int x0 = rect.x + H_PAD;
+    const int x1 = rect.x + rect.w - H_PAD;
+    const int y0 = rect.y + V_PAD;
+    const int y1 = rect.y + rect.h - 1 - V_PAD;
+    // Title
+    wattron(win, A_BOLD | A_UNDERLINE);
+    mvwaddstr(win, y0, x0, "Method Call Time (us) Histogram");
+    wattrset(win, 0);
+    // x2 is the beginning X of the histogram itself (not containing the margin)
+    const int x2 = x0 + LEFT_MARGIN;
+    if (histogram->Empty())
+    {
+        mvwaddstr(win, (y1 + y0) / 2, (x0 + x1) / 2, "(Empty)");
+        return;
+    }
+    histogram->SetBucketCount(x1 - x2 + 1);
+    histogram->ComputeHistogram();
+    // Draw X axis labels
+    char buf[22];
+    snprintf(buf, sizeof(buf), "%.2f",
+             static_cast<float>(histogram->LowPercentile()));
+    mvwaddstr(win, y1, x0 + LEFT_MARGIN, buf);
+    snprintf(buf, sizeof(buf), "%.2f",
+             static_cast<float>(histogram->HighPercentile()));
+    mvwaddstr(win, y1, x1 + 1 - strlen(buf), buf);
+    snprintf(buf, sizeof(buf), "%d%%-%d%%",
+             static_cast<int>(histogram->LowCumDensity() * 100),
+             static_cast<int>(histogram->HighCumDensity() * 100));
+    mvwaddstr(win, y1, x0, buf);
+    // Draw Y axis labels
+    const float hist_ymax = y1 - 1;
+    const float hist_ymin = y0 + 1;
+    const int max_histogram_h = histogram->MaxBucketHeight();
+    if (hist_ymax <= hist_ymin)
+        return; // Not enough space for rendering
+    if (max_histogram_h <= 0)
+        return;
+    bool LOG_TRANSFORM = true;
+    float lg_maxh = 0;
+    if (LOG_TRANSFORM)
+    {
+        lg_maxh = log(max_histogram_h);
+    }
+    for (int y = hist_ymin; y <= hist_ymax; y++)
+    {
+        // There are (hist_ymax - hist_ymin + 1) divisions
+        float fullness;
+        fullness = (hist_ymax - y + 1) * 1.0f / (hist_ymax - hist_ymin + 1);
+        int h;
+        if (!LOG_TRANSFORM)
+        {
+            h = static_cast<int>(max_histogram_h * fullness);
+        }
+        else
+        {
+            h = static_cast<int>(exp(fullness * lg_maxh));
+        }
+        snprintf(buf, sizeof(buf), "%6d-", h);
+        mvwaddstr(win, y, x0 + LEFT_MARGIN - strlen(buf), buf);
+    }
+    const int bar_height = hist_ymax - hist_ymin + 1; // Height of a full bar
+    for (int x = x2, bidx = 0; x <= x1; x++, bidx++)
+    {
+        int h = histogram->BucketHeight(bidx);
+        float lines_visible;
+        if (!LOG_TRANSFORM)
+        {
+            lines_visible = h * 1.0f / max_histogram_h * bar_height;
+        }
+        else
+        {
+            if (h <= 0)
+                lines_visible = 0;
+            else
+                lines_visible = log(h) * 1.0f / lg_maxh * bar_height;
+        }
+        // The histogram's top shall start from this line
+        int y = hist_ymax - static_cast<int>(lines_visible);
+        float y_frac = lines_visible - static_cast<int>(lines_visible);
+        char ch; // Last filling character
+        if (y >= hist_ymin)
+        { // At the maximum bucket the Y overflows, so skip
+            if (y_frac >= 0.66f)
+            {
+                ch = ':';
+            }
+            else if (y_frac >= 0.33f)
+            {
+                ch = '.';
+            }
+            else
+            {
+                if (y < hist_ymax)
+                {
+                    ch = ' ';
+                }
+                else
+                {
+                    if (y_frac > 0)
+                    {
+                        ch =
+                            '.'; // Makes long-tailed distribution easier to see
+                    }
+                }
+            }
+            mvwaddch(win, y, x, ch);
+        }
+        y++;
+        for (; y <= hist_ymax; y++)
+        {
+            mvwaddch(win, y, x, ':');
+        }
+    }
+}
+
+void SummaryView::UpdateDBusTopStatistics(DBusTopStatistics* stat)
+{
+    if (!stat)
+        return;
+    float interval_secs = stat->seconds_since_last_sample_;
+    if (interval_secs == 0)
+    {
+        interval_secs = GetSummaryIntervalInMillises() / 1000.0f;
+    }
+    // Per-second
+    method_call_ = stat->num_mc_ / interval_secs;
+    method_return_ = stat->num_mr_ / interval_secs;
+    signal_ = stat->num_sig_ / interval_secs;
+    error_ = stat->num_error_ / interval_secs;
+    total_ = stat->num_messages_ / interval_secs;
+    g_bargraph->AddValue(total_);
+}
+
+std::string Ellipsize(const std::string& s, int len_limit)
+{
+    if (len_limit <= 3)
+        return s.substr(0, len_limit);
+    if (static_cast<int>(s.size()) < len_limit)
+    {
+        return s;
+    }
+    else
+    {
+        return s.substr(0, len_limit - 3) + "...";
+    }
+}
+
+void SummaryView::Render()
+{
+    // Draw text
+    werase(win);
+    if (!visible_)
+        return;
+    wattron(win, A_BOLD | A_UNDERLINE);
+    mvwaddstr(win, 1, 1, "Message Type          | msg/s");
+    wattrset(win, 0);
+    const int xend = 30;
+    std::string s;
+    s = FloatToString(method_call_);
+    mvwaddstr(win, 2, 1, "Method Call");
+    mvwaddstr(win, 2, xend - s.size(), s.c_str());
+    s = FloatToString(method_return_);
+    mvwaddstr(win, 3, 1, "Method Return ");
+    mvwaddstr(win, 3, xend - s.size(), s.c_str());
+    s = FloatToString(signal_);
+    mvwaddstr(win, 4, 1, "Signal");
+    mvwaddstr(win, 4, xend - s.size(), s.c_str());
+    s = FloatToString(error_);
+    mvwaddstr(win, 5, 1, "Error ");
+    mvwaddstr(win, 5, xend - s.size(), s.c_str());
+    wattron(win, A_UNDERLINE);
+    s = FloatToString(total_);
+    mvwaddstr(win, 6, 1, "Total");
+    mvwaddstr(win, 6, xend - s.size(), s.c_str());
+    wattroff(win, A_UNDERLINE);
+    wattrset(win, 0);
+    // Draw history bar graph
+    Rect bargraph_rect = rect;
+    const int bargraph_x = 64;
+    bargraph_rect.x += bargraph_x;
+    bargraph_rect.w -= bargraph_x;
+    HistoryBarGraph(win, bargraph_rect, g_bargraph);
+    // Draw histogram
+    Rect histogram_rect = rect;
+    histogram_rect.x += 32;
+    histogram_rect.w = bargraph_rect.x - histogram_rect.x - 3;
+    DrawHistogram(win, histogram_rect, g_histogram);
+    // Draw border between summary and histogram
+    for (int y = bargraph_rect.y; y <= bargraph_rect.y + bargraph_rect.h; y++)
+    {
+        mvwaddch(win, y, histogram_rect.x - 1, '|');
+        mvwaddch(win, y, bargraph_rect.x - 1, '|');
+    }
+    DrawBorderIfNeeded();
+    wrefresh(win);
+}
+
+void SensorDetailView::Render()
+{
+    werase(win);
+    if (!visible_)
+        return;
+    // If some sensor is focused, show details regarding that sensor
+    if (state == SensorList)
+    { // Otherwise show the complete list
+        const int ncols = DispSensorsPerRow(); // Number of columns in viewport
+        const int nrows = DispSensorsPerColumn(); // # rows in viewport
+        int sensors_per_page = nrows * ncols;
+        // Just in case the window gets invisibly small
+        if (sensors_per_page < 1)
+            return;
+        int num_sensors = sensor_ids_.size();
+        int total_num_columns = (num_sensors - 1) / nrows + 1;
+        bool is_cursor_out_of_view = false;
+        if (idx0 > choice_ || idx1 <= choice_)
+        {
+            is_cursor_out_of_view = true;
+        }
+        if (idx0 == INVALID || idx1 == INVALID)
+        {
+            is_cursor_out_of_view = true;
+        }
+        if (is_cursor_out_of_view)
+        {
+            idx0 = 0, idx1 = sensors_per_page;
+        }
+        while (idx1 <= choice_)
+        {
+            idx0 += nrows;
+            idx1 += nrows;
+        }
+        const int y0 = 2; // to account for the border and info line
+        const int x0 = 4; // to account for the left overflow marks
+        int y = y0, x = x0;
+        for (int i = 0; i < sensors_per_page; i++)
+        {
+            int idx = idx0 + i;
+            if (idx < static_cast<int>(sensor_ids_.size()))
+            {
+                if (idx == choice_)
+                {
+                    wattrset(win, A_REVERSE);
+                }
+                std::string s = sensor_ids_[idx];
+                if (static_cast<int>(s.size()) > col_width) {
+                    s = s.substr(0, col_width - 2) + "..";
+                } else {
+                    while (static_cast<int>(s.size()) < col_width)
+                    {
+                        s.push_back(' ');
+                    }
+                }
+                mvwprintw(win, y, x, s.c_str());
+                wattrset(win, 0);
+            }
+            else
+                break;
+            y++;
+            if (i % nrows == nrows - 1)
+            {
+                y = y0;
+                x += col_width + h_spacing;
+            }
+        }
+        // Print overflow marks to the right of the screen
+        for (int i = 0; i < nrows; i++)
+        {
+            int idx = idx0 + sensors_per_page + i;
+            if (idx < num_sensors)
+            {
+                mvwaddch(win, y0 + i, x, '>');
+            }
+        }
+        // Print overflow marks to the left of the screen
+        for (int i = 0; i < nrows; i++)
+        {
+            int idx = idx0 - nrows + i;
+            if (idx >= 0)
+            {
+                mvwaddch(win, y0 + i, 2, '<');
+            }
+        }
+        // idx1 is one past the visible range, so no need to +1
+        const int col0 = idx0 / nrows + 1, col1 = idx1 / nrows;
+        mvwprintw(win, 1, 2, "Columns %d-%d of %d", col0, col1,
+                  total_num_columns);
+        mvwprintw(win, 1, rect.w - 15, "%d sensors", sensor_ids_.size());
+    }
+    else if (state == SensorDetail)
+    {
+        // sensor_ids_ is the cached list of sensors, it should be the same size
+        // as the actual number of sensors in the snapshot
+        mvwprintw(win, 1, 2, "Details of sensor %s", curr_sensor_id_.c_str());
+        mvwprintw(win, 1, rect.w - 15, "Sensor %d/%u", choice_ + 1,
+                  sensor_ids_.size()); // 1-based
+        std::vector<Sensor*> sensors =
+            g_sensor_snapshot->FindSensorsBySensorID(curr_sensor_id_);
+        const int N = static_cast<int>(sensors.size());
+        const int w = rect.w - 5;
+        mvwprintw(win, 3, 2, "There are %d sensors with the name %s", N,
+                  curr_sensor_id_.c_str());
+        int y = 5;
+        int x = 2;
+        if (N > 0)
+        {
+            for (int j = 0; j < N; j++)
+            {
+                Sensor* sensor = sensors[j];
+                mvwprintw(win, y, x, "%d/%d", j + 1, N);
+                char buf[200];
+                snprintf(buf, sizeof(buf), "DBus Service    : %s",
+                         sensor->ServiceName().c_str());
+                y += DrawTextWithWidthLimit(win, buf, y, x, w, "/");
+                snprintf(buf, sizeof(buf), "DBus Connection : %s",
+                         sensor->ConnectionName().c_str());
+                y += DrawTextWithWidthLimit(win, buf, y, x, w, "/");
+                snprintf(buf, sizeof(buf), "DBus Object Path: %s",
+                         sensor->ObjectPath().c_str());
+                y += DrawTextWithWidthLimit(win, buf, y, x, w, "/");
+                y++;
+            }
+        }
+        else
+        {
+            mvwprintw(win, y, x, "Sensor details not found");
+        }
+    }
+    DrawBorderIfNeeded();
+    wrefresh(win);
+}
+
+std::string SensorDetailView::GetStatusString()
+{
+    if (state == SensorList)
+    {
+        return "[Arrow Keys]=Move Cursor [Q]=Deselect [Enter]=Show Sensor "
+               "Detail";
+    }
+    else
+    {
+        return "[Arrow Keys]=Cycle Through Sensors [Esc/Q]=Exit";
+    }
+}
+
+DBusStatListView::DBusStatListView() : DBusTopWindow()
+{
+    highlight_col_idx_ = 0;
+    sort_col_idx_ = 0;
+    sort_order_ = SortOrder::Ascending;
+    horizontal_pan_ = 0;
+    row_idx_ = INVALID;
+    disp_row_idx_ = 0;
+    horizontal_pan_ = 0;
+    menu1_ = new ArrowKeyNavigationMenu(this);
+    menu2_ = new ArrowKeyNavigationMenu(this);
+    // Load all available field names
+    std::set<std::string> inactive_fields;
+    std::set<std::string> active_fields;
+
+    // Default choice of field names
+    const int N = static_cast<int>(sizeof(FieldNames) / sizeof(FieldNames[0]));
+    for (int i = 0; i < N; i++)
+    {
+        inactive_fields.insert(FieldNames[i]);
+    }
+    for (const std::string& s :
+         dbus_top_analyzer::g_dbus_statistics.GetFieldNames())
+    {
+        inactive_fields.erase(s);
+        active_fields.insert(s);
+    }
+    for (int i = 0; i < N; i++)
+    {
+    const std::string s = FieldNames[i];
+        if (inactive_fields.count(s) > 0)
+        {
+            menu1_->AddItem(s);
+        }
+        else
+        {
+            menu2_->AddItem(s);
+        }
+    }
+    
+    curr_menu_state_ = LeftSide;
+    menu_h_ = 5;
+    menu_w_ = 24; // Need at least 2*padding + 15 for enough space, see menu.hpp
+    menu_margin_ = 6;
+    // Populate preferred column widths
+    for (int i = 0; i < N; i++)
+    {
+        column_widths_[FieldNames[i]] = FieldPreferredWidths[i];
+    }
+}
+
+std::pair<int, int> DBusStatListView::GetXSpanForColumn(const int col_idx)
+{
+    std::vector<int> cw = ColumnWidths();
+    if (col_idx < 0 || col_idx >= static_cast<int>(cw.size()))
+    {
+        return std::make_pair(INVALID, INVALID);
+    }
+    int x0 = 0, x1 = 0;
+    for (int i = 0; i < col_idx; i++)
+    {
+        if (i > 0)
+        {
+            x0 += cw[i];
+        }
+    }
+    x1 = x0 + cw[col_idx] - 1;
+    return std::make_pair(x0, x1);
+}
+
+// If tolerance > 0, consider overlap before 2 intervals intersect
+// If tolerance ==0, consider overlap if 2 intervals exactly intersect
+// If tolerance < 0, consider overlap if Minimal Translate Distance is >=
+// -threshold
+bool IsSpansOverlap(const std::pair<int, int>& s0,
+                    const std::pair<int, int>& s1, int tolerance)
+{
+    if (tolerance >= 0)
+    {
+        if (s0.second < s1.first - tolerance)
+            return false;
+        else if (s1.second < s0.first - tolerance)
+            return false;
+        else
+            return true;
+    }
+    else
+    {
+        // Compute overlapping distance
+        std::vector<std::pair<int, int>> tmp(
+            4); // [x, 1] means the start of interval
+                // [x,-1] means the end of interval
+        tmp[0] = std::make_pair(s0.first, 1);
+        tmp[1] = std::make_pair(s0.second, -1);
+        tmp[2] = std::make_pair(s1.first, 1);
+        tmp[3] = std::make_pair(s1.second, -1);
+        std::sort(tmp.begin(), tmp.end());
+        int overlap_x0 = -INVALID, overlap_x1 = -INVALID;
+        int idx = 0;
+        const int N = static_cast<int>(tmp.size());
+        int level = 0;
+        while (idx < N)
+        {
+            const int x = tmp[idx].first;
+            while (idx < N && x == tmp[idx].first)
+            {
+                level += tmp[idx].second;
+                idx++;
+            }
+            // The starting position of the overlap
+            if (level == 2)
+            {
+                overlap_x0 = idx - 1;
+            }
+            // The ending position of the overlap
+            if (overlap_x0 != -INVALID && level < 2 && overlap_x1 == -INVALID)
+            {
+                overlap_x1 = idx - 1;
+            }
+        }
+        const int overlap_length = overlap_x1 - overlap_x0 + 1;
+        if (overlap_length >= -tolerance)
+            return true;
+        else
+            return false;
+    }
+}
+
+bool DBusStatListView::IsXSpanVisible(const std::pair<int, int>& xs,
+                                      int tolerance)
+{
+    const std::pair<int, int> vxs = {horizontal_pan_, horizontal_pan_ + rect.w};
+    return IsSpansOverlap(xs, vxs, tolerance);
+}
+std::vector<std::string> DBusStatListView::ColumnHeaders()
+{
+    return visible_columns_;
+}
+
+std::vector<int> DBusStatListView::ColumnWidths()
+{
+    std::vector<int> widths = {8}; // for "Msg/s"
+    std::vector<std::string> agg_headers = visible_columns_;
+    std::vector<int> agg_widths(agg_headers.size(), 0);
+    for (int i = 0; i < static_cast<int>(agg_headers.size()); i++)
+    {
+        agg_widths[i] = column_widths_[agg_headers[i]];
+    }
+    widths.insert(widths.end(), agg_widths.begin(), agg_widths.end());
+    return widths;
+}
+
+// Coordinate systems are in world space, +x faces to the right
+// Viewport:            [horizontal_pan_,   horizontal_pan_ + rect.w]
+// Contents:  [  column_width[0]  ][  column_width[1] ][  column_width[2]  ]
+void DBusStatListView::PanViewportOrMoveHighlightedColumn(const int delta_x)
+{
+    // If the column to the left is visible, highlight it
+    const int N = static_cast<int>(ColumnHeaders().size());
+    bool col_idx_changed = false;
+    if (delta_x < 0)
+    { // Pan left
+        if (highlight_col_idx_ > 0)
+        {
+            std::pair<int, int> xs_left =
+                GetXSpanForColumn(highlight_col_idx_ - 1);
+            if (IsXSpanVisible(xs_left, -1))
+            {
+                highlight_col_idx_--;
+                col_idx_changed = true;
+            }
+        }
+        if (!col_idx_changed)
+        {
+            horizontal_pan_ += delta_x;
+        }
+    }
+    else if (delta_x > 0)
+    { // Pan right
+        if (highlight_col_idx_ < N - 1)
+        {
+            std::pair<int, int> xs_right =
+                GetXSpanForColumn(highlight_col_idx_ + 1);
+            if (IsXSpanVisible(xs_right, -1))
+            {
+                highlight_col_idx_++;
+                col_idx_changed = true;
+            }
+        }
+        if (!col_idx_changed)
+        {
+            horizontal_pan_ += delta_x;
+        }
+    }
+}
+
+void DBusStatListView::OnKeyDown(const std::string& key)
+{
+    {
+        switch (curr_menu_state_)
+        {
+            case LeftSide:
+            {
+                if (key == "up")
+                {
+                    menu1_->OnKeyDown("up");
+                }
+                else if (key == "down")
+                {
+                    menu1_->OnKeyDown("down");
+                }
+                else if (key == "right")
+                {
+                    SetMenuState(RightSide);
+                }
+                else if (key == "enter")
+                {
+                    SetMenuState(Hidden);
+                }
+                else if (key == "space")
+                {
+                    std::string ch;
+                    if (menu1_->RemoveHighlightedItem(&ch))
+                    {
+                        menu2_->AddItem(ch);
+                    }
+                }
+                break;
+            }
+            case RightSide:
+            {
+                if (key == "up")
+                {
+                    menu2_->OnKeyDown("up");
+                }
+                else if (key == "down")
+                {
+                    menu2_->OnKeyDown("down");
+                }
+                else if (key == "left")
+                {
+                    SetMenuState(LeftSide);
+                }
+                else if (key == "enter")
+                {
+                    SetMenuState(Hidden);
+                }
+                else if (key == "space")
+                {
+                    std::string ch;
+                    if (menu2_->RemoveHighlightedItem(&ch))
+                    {
+                        menu1_->AddItem(ch);
+                    }
+                }
+                break;
+            }
+            case Hidden:
+            {
+                if (key == "enter")
+                {
+                    switch (last_menu_state_)
+                    {
+                        case LeftSide:
+                        case RightSide:
+                            SetMenuState(last_menu_state_);
+                            break;
+                        default:
+                            SetMenuState(LeftSide);
+                    }
+                }
+                else if (key == "left")
+                {
+                    PanViewportOrMoveHighlightedColumn(-2);
+                }
+                else if (key == "right")
+                {
+                    PanViewportOrMoveHighlightedColumn(2);
+                }
+                else if (key == "up")
+                {
+                    disp_row_idx_--;
+                    if (disp_row_idx_ < 0)
+                    {
+                        disp_row_idx_ = 0;
+                    }
+                }
+                else if (key == "down")
+                {
+                    disp_row_idx_++;
+                    const int N = static_cast<int>(stats_snapshot_.size());
+                    if (disp_row_idx_ >= N)
+                    {
+                        disp_row_idx_ = N - 1;
+                    }
+                }
+                else if (key == "a")
+                {
+                    sort_order_ = SortOrder::Ascending;
+                    sort_col_idx_ = highlight_col_idx_;
+                    break;
+                }
+                else if (key == "d")
+                {
+                    sort_order_ = SortOrder::Descending;
+                    sort_col_idx_ = highlight_col_idx_;
+                    break;
+                }
+                break;
+            }
+        }
+    }
+    Render();
+}
+
+// Window C
+void DBusStatListView::Render()
+{
+    werase(win);
+    if (!visible_)
+        return;
+    int num_lines_shown = rect.h - 3;
+    if (curr_menu_state_ == LeftSide || curr_menu_state_ == RightSide)
+    {
+        menu1_->Render();
+        menu2_->Render();
+        num_lines_shown -= (menu_h_ + 3);
+        // Draw the arrow
+        const int x1 = menu1_->rect_.x;
+        const int h1 = menu1_->rect_.h;
+        const int x2 = menu2_->rect_.x;
+        const int w2 = menu2_->rect_.w;
+        const int y1 = menu1_->rect_.y;
+        const int arrow_x = (x1 + x2 + w2) / 2 - 2;
+        const int arrow_y = y1 + 2;
+        const int caption_x = x1;
+        const int caption_y = y1 + h1;
+        for (int x = 1; x < rect.w - 1; x++)
+        {
+            mvwaddch(win, y1 - 3, x, '-');
+        }
+        mvwprintw(win, y1 - 3, arrow_x - 8, "Press [Enter] to show/hide");
+        mvwprintw(win, y1 - 2, caption_x - 5,
+                  "DBus fields for aggregating and sorting results:");
+        if (curr_menu_state_ == LeftSide)
+        {
+            mvwprintw(win, y1 - 1, x1 - 4, "--[ Available Fields ]--");
+            mvwprintw(win, y1 - 1, x2 - 4, "--- Active Fields ---");
+        }
+        else
+        {
+            mvwprintw(win, y1 - 1, x1 - 4, "--- Available Fields ---");
+            mvwprintw(win, y1 - 1, x2 - 4, "--[ Active Fields ]--");
+        }
+        if (curr_menu_state_ == LeftSide)
+        {
+            mvwprintw(win, arrow_y, arrow_x, "-->");
+            mvwprintw(win, caption_y, caption_x,
+                      "Press [Space] to move to the right");
+        }
+        else
+        {
+            mvwprintw(win, arrow_y, arrow_x, "<--");
+            mvwprintw(win, caption_y, caption_x,
+                      "Press [Space] to move to the left");
+        }
+    }
+    std::vector<std::string> headers;
+    std::vector<int> widths;
+    visible_columns_ = g_dbus_statistics->GetFieldNames();
+    std::vector<std::string> agg_headers = visible_columns_;
+    std::vector<int> agg_widths(agg_headers.size(), 0);
+    for (int i = 0; i < static_cast<int>(agg_headers.size()); i++)
+    {
+        agg_widths[i] = column_widths_[agg_headers[i]];
+    }
+    headers.insert(headers.end(), agg_headers.begin(), agg_headers.end());
+    widths.insert(widths.end(), agg_widths.begin(), agg_widths.end());
+    std::vector<int> xs;
+    int curr_x = 2 - horizontal_pan_;
+    for (const int w : widths)
+    {
+        xs.push_back(curr_x);
+        curr_x += w;
+    }
+    const int N = headers.size();
+    // Bound col_idx_
+    if (highlight_col_idx_ >= N)
+    {
+        highlight_col_idx_ = N - 1;
+    }
+    // Render column headers
+    for (int i = 0; i < N; i++)
+    {
+        std::string s = headers[i];
+        // 1 char outside boundary = start printing from the second character,
+        // etc
+
+        // Print "<" for Ascending order (meaning: row 0 < row 1 < row 2 ... )
+        // Print ">" for Descending order (meaning: row 0 > row 1 > row 2 ... )
+        if (sort_col_idx_ == i)
+        {
+            if (sort_order_ == SortOrder::Ascending)
+            {
+                s.push_back('<');
+            }
+            else
+            {
+                s.push_back('>');
+            }
+        }
+
+        // Highlight the "currently-selected column"
+        if (highlight_col_idx_ == i)
+        {
+            wattrset(win, 0);
+            wattron(win, A_REVERSE);
+        }
+        else
+        {
+            wattrset(win, 0);
+            wattron(win, A_UNDERLINE);
+        }
+        int x = xs[i];
+        if (x < 0)
+        {
+            if (-x < static_cast<int>(s.size()))
+            {
+                s = s.substr(-x);
+            }
+            else
+                s = "";
+            x = 0;
+        }
+        mvwaddstr(win, 1, x, s.c_str());
+    }
+    wattrset(win, 0);
+    // Time since the last update of Window C
+    float interval_secs = g_dbus_statistics->seconds_since_last_sample_;
+    if (interval_secs == 0)
+    {
+        interval_secs = GetSummaryIntervalInMillises() / 1000.0f;
+    }
+
+    stats_snapshot_ = g_dbus_statistics->StatsSnapshot();
+    const int nrows = static_cast<int>(stats_snapshot_.size());
+    const std::vector<DBusTopSortField> fields = g_dbus_statistics->GetFields();
+    const int ncols = static_cast<int>(fields.size());
+    // Merge the list of DBus Message properties & computed metrics together
+    std::map<std::vector<std::string>, DBusTopComputedMetrics>::iterator itr =
+        stats_snapshot_.begin();
+    struct StringOrFloat
+    { // Cannot use union so using struct
+        std::string s;
+        float f;
+    };
+
+    // "Stage" the snapshot for displaying in the form of a spreadsheet
+    std::vector<std::pair<StringOrFloat, std::vector<std::string>>>
+        stats_snapshot_staged;
+    const DBusTopSortField sort_field = fields[sort_col_idx_];
+    const bool is_sort_key_numeric = DBusTopSortFieldIsNumeric(sort_field);
+
+    for (int i = 0; i < nrows; i++) // One row of cells
+    {
+        int idx0 = 0; // indexing into the std::vector<string> of each row
+        std::vector<std::string> row;
+
+        StringOrFloat sort_key; // The key used for sorting
+        for (int j = 0; j < ncols; j++) // one column in the row
+        {
+            DBusTopSortField field = fields[j];
+            // Populate the content of stats_snapshot_staged
+
+            StringOrFloat sof; // Represents this column
+            
+            // When we haven't used up all
+            if (idx0 < static_cast<int>(itr->first.size()))
+            {
+                sof.s = itr->first[idx0];
+            }
+            switch (field)
+            {
+                case kSender:      // string
+                case kDestination: // string
+                case kInterface:   // string
+                case kPath:        // string
+                case kMember:      // string
+                case kSenderPID:   // numeric
+                case kSenderCMD:   // string
+                    row.push_back(itr->first[idx0]);
+                    idx0++;
+                    if (field == kSenderPID)
+                    {
+                        // Note: attempting to std::atof("(unknown)") on the BMC
+                        // will cause hang. And GDB won't show backtrace.
+                        if (sof.s == "(unknown)")
+                        {
+                            if (sort_order_ == Ascending)
+                            {
+                                sof.f = -1;
+                            }
+                            else
+                            {
+                                sof.f = 1e20;
+                            }
+                        }
+                        else
+                        {
+                            sof.f = std::atof(sof.s.c_str());
+                        }
+                    }
+                    break;
+                case kMsgPerSec: // Compute "messages per second"
+                {
+                    int numbers[] = {
+                        itr->second.num_method_calls,
+                        itr->second.num_method_returns,
+                        itr->second.num_signals,
+                        itr->second.num_errors,
+                    };
+                    int the_sum = 0; // For sorting
+
+                    std::string s; // String representation in the form or
+                                   // "1.00/2.00/3.00/4.00"
+                    for (int i = 0; i < 4; i++)
+                    {
+                        the_sum += numbers[i];
+                        if (i > 0)
+                            s += "/";
+                        float per_sec = numbers[i] / interval_secs;
+                        s += FloatToString(per_sec);
+                    }
+
+                    row.push_back(s);
+                    sof.f = the_sum;
+                    break;
+                }
+                case kAverageLatency: // Compute "average Method Call latency"
+                    const DBusTopComputedMetrics& m = itr->second;
+                    if (m.num_method_calls == 0)
+                    {
+                        row.push_back("n/a");
+                        if (sort_order_ == Ascending)
+                        {
+                            sof.f = -1; // Put to the top
+                        }
+                        else
+                        {
+                            sof.f = 1e20; // Put to the top
+                        }
+                    }
+                    else
+                    {
+                        float avg_latency_usec =
+                            m.total_latency_usec / m.num_method_calls;
+                        row.push_back(FloatToString(avg_latency_usec));
+                        sof.f = avg_latency_usec;
+                    }
+                    break;
+            }
+            if (j == sort_col_idx_)
+            {
+                sort_key = sof;
+            }
+        }
+        stats_snapshot_staged.push_back(std::make_pair(sort_key, row));
+        itr++;
+    }
+    
+    // Sort the "staged snapshot" using the sort_key, using different functions
+    // depending on whether sort key is numeric or string
+    if (is_sort_key_numeric)
+    {
+        std::sort(
+            stats_snapshot_staged.begin(), stats_snapshot_staged.end(),
+            [](const std::pair<StringOrFloat, std::vector<std::string>>& a,
+               const std::pair<StringOrFloat, std::vector<std::string>>& b) {
+                return a.first.f < b.first.f;
+            });
+    }
+    else
+    {
+        std::sort(
+            stats_snapshot_staged.begin(), stats_snapshot_staged.end(),
+            [](const std::pair<StringOrFloat, std::vector<std::string>>& a,
+               const std::pair<StringOrFloat, std::vector<std::string>>& b) {
+                return a.first.s < b.first.s;
+            });
+    }
+    
+    if (sort_order_ == Descending)
+    {
+        std::reverse(stats_snapshot_staged.begin(),
+                     stats_snapshot_staged.end());
+    }
+    // Minus 2 because of "msgs/s" and "+"
+    const int num_fields = N;
+    // The Y span of the area for rendering the "spreadsheet"
+    const int y0 = 2, y1 = y0 + num_lines_shown - 1;
+    // Key is sender, destination, interface, path, etc
+    for (int i = 0, shown = 0;
+        i + disp_row_idx_ < static_cast<int>(stats_snapshot_staged.size()) &&
+        shown < num_lines_shown;
+        i++, shown++)
+    {
+        std::string s;
+        int x = 0;
+        const std::vector<std::string> key =
+            stats_snapshot_staged[i + disp_row_idx_].second;
+        for (int j = 0; j < num_fields; j++)
+        {
+            x = xs[j];
+            s = key[j];
+            // Determine column width limit for this particular column
+            int col_width = 100;
+            if (j < num_fields - 1)
+            {
+                col_width = xs[j + 1] - xs[j] - 1;
+            }
+            s = Ellipsize(s, col_width);
+            if (x < 0)
+            {
+                if (-x < static_cast<int>(s.size()))
+                    s = s.substr(-x);
+                else
+                    s = "";
+                x = 0;
+            }
+            // Trim if string overflows to the right
+            if (x + static_cast<int>(s.size()) > rect.w)
+            {
+                s = s.substr(0, rect.w - x);
+            }
+            mvwaddstr(win, 2 + i, x, s.c_str());
+        }
+    }
+    // Overflows past the top ...
+    if (disp_row_idx_ > 0)
+    {
+        std::string x = " [+" + std::to_string(disp_row_idx_) + " rows above]";
+        mvwaddstr(win, y0, rect.w - static_cast<int>(x.size()) - 1, x.c_str());
+    }
+    // Overflows past the bottom ...
+    const int last_disp_row_idx = disp_row_idx_ + num_lines_shown - 1;
+    if (last_disp_row_idx < nrows - 1)
+    {
+        std::string x = " [+" +
+                        std::to_string((nrows - 1) - last_disp_row_idx) +
+                        " rows below]";
+        mvwaddstr(win, y1, rect.w - static_cast<int>(x.size()) - 1, x.c_str());
+    }
+    DrawBorderIfNeeded();
+    wrefresh(win);
+}
+
+void DBusStatListView::OnResize(int win_w, int win_h)
+{
+    rect.y = 8 - MARGIN_BOTTOM;
+    rect.w = win_w - (win_w / 2) + 1; // Perfectly overlap on the vertical edge
+    rect.x = win_w - rect.w;
+    rect.h = win_h - rect.y - MARGIN_BOTTOM;
+    const int x0 = rect.w / 2 - menu_w_ - menu_margin_ / 2;
+    const int x1 = x0 + menu_margin_ + menu_w_;
+    const int menu_y = rect.h - menu_h_;
+    menu1_->SetRect(Rect(x0, menu_y, menu_w_, menu_h_)); // Local coordinates
+    menu1_->SetOrder(ArrowKeyNavigationMenu::Order::ColumnMajor);
+    menu2_->SetRect(Rect(x1, menu_y, menu_w_, menu_h_));
+    menu2_->SetOrder(ArrowKeyNavigationMenu::Order::ColumnMajor);
+    UpdateWindowSizeAndPosition();
+}
+
+std::vector<DBusTopSortField> DBusStatListView::GetSortFields()
+{
+    std::vector<DBusTopSortField> ret;
+    const int N = sizeof(FieldNames) / sizeof(FieldNames[0]);
+    for (const std::string& s : menu2_->Items())
+    {
+        for (int i = 0; i < N; i++)
+        {
+            if (FieldNames[i] == s)
+            {
+                ret.push_back(static_cast<DBusTopSortField>(i));
+                break;
+            }
+        }
+    }
+    return ret;
+}
+
+std::string DBusStatListView::GetStatusString()
+{
+    if (curr_menu_state_ == LeftSide || curr_menu_state_ == RightSide)
+    {
+        return "[Enter]=Hide Panel [Space]=Choose Entry [Arrow Keys]=Move "
+               "Cursor";
+    }
+    else
+    {
+        return "[Enter]=Show Column Select Panel [Arrow Keys]=Pan View";
+    }
+}
+
+void FooterView::Render()
+{
+    werase(win);
+    const time_t now = time(nullptr);
+    const char* date_time = ctime(&now);
+    wattrset(win, 0);
+    std::string help_info;
+    if (g_current_active_view == nullptr)
+    {
+        help_info = "Press [Tab] to cycle through views";
+    }
+    else
+    {
+        help_info = g_current_active_view->GetStatusString();
+    }
+    mvwaddstr(win, 0, 1, date_time);
+    mvwaddstr(win, 0, rect.w - int(help_info.size()) - 1, help_info.c_str());
+    wrefresh(win);
+}
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 "";
+    }
+
+};
diff --git a/xmlparse.cpp b/xmlparse.cpp
new file mode 100644
index 0000000..2e4b1a3
--- /dev/null
+++ b/xmlparse.cpp
@@ -0,0 +1,193 @@
+// 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.
+
+#include "xmlparse.hpp"
+#include "main.hpp"
+
+int Munch(const std::string& sv, int* idx, std::string* out)
+{
+    if (*idx >= static_cast<int>(sv.size()))
+        return -INVALID;
+    while (::isspace(sv[*idx]))
+    {
+        (*idx)++;
+    }
+    int ret = 0;
+    *out = "";
+    int quote_state = 0; // 0: not seen, 1: seen opening quotation, 2: ended
+    while (*idx < static_cast<int>(sv.size()))
+    {
+        const char ch = sv[*idx];
+        if (::isspace(ch) && quote_state != 1)
+        {
+            break;
+        }
+        (*idx)++;
+        if (ch == '<')
+        {
+            if (*idx < static_cast<int>(sv.size()) && sv[*idx] == '!')
+            {
+                ret = 10; // Comment
+            }
+            else if (*idx < static_cast<int>(sv.size()) && sv[*idx] == '/')
+            {
+                ret = 22; // Closing tag
+                (*idx)++; // Skip the '/'
+            }
+            else
+            {
+                ret = 1; // <
+            }
+        }
+        else if (ch == '>')
+        {
+            if (ret == 1)
+            {
+                ret = 12;
+            } // < >
+            else if (ret == 22)
+            {}
+            else
+                ret = 2; //   >
+            if (out->size() == 0)
+            {
+                (*idx)++;
+            }
+            break; // Do not consume
+        }
+        else if (ch == '\"')
+        {
+            ret = 3; //
+            switch (quote_state)
+            {
+                case 0:
+                {
+                    quote_state = 1;
+                    continue;
+                }
+                case 1:
+                {
+                    quote_state = 2;
+                    break;
+                }
+            }
+        }
+        else if (ch == '/' && *idx < static_cast<int>(sv.size()) &&
+                 sv[*idx] == '>')
+        {
+            ret = 22; // Closing tag
+            (*idx)++;
+            break;
+        }
+        else
+        {
+            out->push_back(ch);
+        }
+    }
+    return ret;
+}
+
+XMLNode* ParseXML(const std::string& sv)
+{
+    int verbose = 0;
+    char* v = getenv("VERBOSE");
+    if (v)
+    {
+        verbose = std::atoi(v);
+    }
+    int idx = 0;
+    std::string out;
+    int res;
+    std::vector<std::string> tags;
+    std::vector<XMLNode*> nodestack;
+    XMLNode* root = nullptr;
+    if (verbose > 0)
+    {
+        printf("%s\n", sv.c_str());
+    }
+    while ((res = Munch(sv, &idx, &out)) != -INVALID)
+    {
+        if (res == 1 || res == 12)
+        {
+            XMLNode* newnode = new XMLNode(out);
+            if (tags.empty())
+            {
+                root = newnode;
+            }
+            else
+            {
+                nodestack.back()->AddChild(newnode);
+            }
+            tags.push_back(out);
+            nodestack.push_back(newnode);
+        }
+
+        // Add name (has to be before pop_back)
+        if (out.find("name=") == 0)
+        {
+            nodestack.back()->SetName(out.substr(5));
+        }
+
+        if (res == 22 && tags.size() > 0)
+        {
+            tags.pop_back();
+            nodestack.pop_back();
+        }
+        if (verbose >= 2)
+        {
+            printf("Munch %d %s, tags:", res, out.c_str());
+            for (const std::string& x : tags)
+            {
+                printf(" %s", x.c_str());
+            }
+            printf("\n");
+        }
+    }
+    return root;
+}
+
+void DeleteTree(XMLNode* x)
+{
+    for (XMLNode* ch : x->children)
+    {
+        DeleteTree(ch);
+    }
+    delete x;
+}
+
+std::vector<std::string> XMLNode::GetChildNodeNames()
+{
+    std::vector<std::string> ret;
+    for (XMLNode* n : children)
+    {
+        if (n->tag == "node")
+        {
+            ret.push_back(n->fields["name"]);
+        }
+    }
+    return ret;
+}
+
+std::vector<std::string> XMLNode::GetInterfaceNames()
+{
+    std::vector<std::string> ret;
+    for (XMLNode* n : children)
+    {
+        if (n->tag == "interface")
+        {
+            ret.push_back(n->fields["name"]);
+        }
+    }
+    return ret;
+}
\ No newline at end of file
diff --git a/xmlparse.hpp b/xmlparse.hpp
new file mode 100644
index 0000000..3a76e9a
--- /dev/null
+++ b/xmlparse.hpp
@@ -0,0 +1,67 @@
+// 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 <map>
+#include <string>
+#include <vector>
+
+class XMLNode
+{
+  public:
+    std::string tag;
+    std::map<std::string, std::string> fields;
+    std::vector<XMLNode*> children;
+    std::vector<XMLNode*> interfaces;
+    XMLNode(const std::string& t) : tag(t)
+    {}
+
+    void AddChild(XMLNode* x)
+    {
+        children.push_back(x);
+    }
+
+    void do_Print(int indent)
+    {
+        for (int i = 0; i < indent; i++)
+            printf("  ");
+        printf("%s", tag.c_str());
+        if (fields["name"] != "")
+        {
+            printf(" name=[%s]", fields["name"].c_str());
+        }
+        printf("\n");
+        for (XMLNode* ch : children)
+        {
+            ch->do_Print(indent + 1);
+        }
+    }
+
+    void Print()
+    {
+        do_Print(0);
+    }
+
+    void SetName(const std::string& n)
+    {
+        fields["name"] = n;
+    }
+    
+    std::vector<std::string> GetChildNodeNames();
+    std::vector<std::string> GetInterfaceNames();
+};
+
+XMLNode* ParseXML(const std::string& sv);
+void DeleteTree(XMLNode* x);