[bt][cli] Tabulate output of the list-devices command

When using `list-devices` from bt-cli, tabulate the output of all
devices for easier scanning by human-eye.

test: manual - run `bt-cli` then `list-devices` and check the output visually

Change-Id: I1e5ed52ed26ef529f4a8434fbf1396cabc9c3d02
diff --git a/bin/bluetooth/tools/bt-cli/BUILD.gn b/bin/bluetooth/tools/bt-cli/BUILD.gn
index 91fe60b..b9edfab 100644
--- a/bin/bluetooth/tools/bt-cli/BUILD.gn
+++ b/bin/bluetooth/tools/bt-cli/BUILD.gn
@@ -2,8 +2,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import("//build/rust/rustc_binary.gni")
 import("//build/package.gni")
+import("//build/rust/rustc_binary.gni")
 
 rustc_binary("bin") {
   name = "bt_cli"
@@ -16,9 +16,11 @@
     "//garnet/public/fidl/fuchsia.bluetooth.gatt:fuchsia.bluetooth.gatt-rustc",
     "//garnet/public/fidl/fuchsia.bluetooth.le:fuchsia.bluetooth.le-rustc",
     "//garnet/public/lib/fidl/rust/fidl",
+    "//garnet/public/rust/algebra",
     "//garnet/public/rust/fuchsia-app",
     "//garnet/public/rust/fuchsia-async",
     "//garnet/public/rust/fuchsia-zircon",
+    "//garnet/public/rust/text-format",
     "//third_party/rust-crates/rustc_deps:failure",
     "//third_party/rust-crates/rustc_deps:futures-preview",
     "//third_party/rust-crates/rustc_deps:log",
diff --git a/bin/bluetooth/tools/bt-cli/src/main.rs b/bin/bluetooth/tools/bt-cli/src/main.rs
index 3bdf07b..7903dbd 100644
--- a/bin/bluetooth/tools/bt-cli/src/main.rs
+++ b/bin/bluetooth/tools/bt-cli/src/main.rs
@@ -6,9 +6,10 @@
 
 use {
     failure::{bail, Error, ResultExt},
-    fidl_fuchsia_bluetooth_control::{ControlEvent, ControlEventStream, ControlMarker, ControlProxy},
+    fidl_fuchsia_bluetooth_control::{ControlEvent, ControlEventStream, ControlMarker, ControlProxy, TechnologyType},
     fuchsia_app::client::connect_to_service,
     fuchsia_async::{self as fasync, futures::select},
+    fuchsia_bluetooth::assigned_numbers::find_service_uuid,
     fuchsia_bluetooth::types::Status,
     futures::{
         channel::mpsc::{channel, SendError},
@@ -24,7 +25,8 @@
     parking_lot::Mutex,
     regex::Regex,
     rustyline::{error::ReadlineError, CompletionType, Config, EditMode, Editor},
-    std::{collections::HashMap, fmt::Write, iter::FromIterator, sync::Arc, thread},
+    std::{collections::HashMap, fmt::Write, sync::Arc, thread},
+    text_format::tabulate,
 };
 
 use crate::{
@@ -35,7 +37,6 @@
 mod commands;
 mod types;
 
-
 static PROMPT: &str = "\x1b[34mbt>\x1b[0m ";
 /// Escape code to clear the pty line on which the cursor is located.
 /// Used when evented output is intermingled with the REPL prompt.
@@ -78,9 +79,7 @@
     if state.devices.is_empty() {
         String::from("No known remote devices")
     } else {
-        String::from_iter(
-            state.devices.values().map(|device| device.to_string())
-        )
+        format_devices(state.devices.iter().map(|(_, d)| d).collect())
     }
 }
 
@@ -105,6 +104,61 @@
     }
 }
 
+fn format_devices(devices: Vec<&RemoteDevice>) -> String {
+    let headers = vec![
+        "Identifier",
+        "Address",
+        "Name",
+        "Technology",
+        "RSSI",
+        "Connected",
+        "Bonded",
+        "Appearance",
+        "TX Power",
+        "Services",
+    ];
+    let rows: Vec<Vec<String>> = devices.into_iter().map(format_device).collect();
+    tabulate(rows, Some(headers))
+}
+
+fn format_device(d: &RemoteDevice) -> Vec<String> {
+    fn show_option_int(i: &Option<Box<fidl_fuchsia_bluetooth::Int8>>) -> String {
+        match i {
+            Some(i) => i.value.to_string(),
+            None => "".to_string(),
+        }
+    }
+
+    let d = &d.0;
+    vec![
+        d.identifier.clone(),
+        d.address.clone(),
+        match &d.name {
+            Some(n) => n.clone(),
+            _ => "".to_string(),
+        },
+        format_technology(d.technology),
+        show_option_int(&d.rssi),
+        d.connected.to_string(),
+        d.bonded.to_string(),
+        format!("{:?}", d.appearance),
+        show_option_int(&d.tx_power),
+        d.service_uuids
+            .iter()
+            .map(|uuid| find_service_uuid(uuid).map(|an| an.name).unwrap_or(uuid))
+            .collect::<Vec<_>>()
+            .join(", "),
+    ]
+}
+
+fn format_technology(tech: TechnologyType) -> String {
+    match tech {
+        TechnologyType::LowEnergy => "LE".to_string(),
+        TechnologyType::Classic => "Classic".to_string(),
+        TechnologyType::DualMode => "DualMode".to_string(),
+    }
+}
+
 // Find the identifier for a `RemoteDevice` based on a `key` that is either an identifier or an
 // address.
 // Returns `None` if the given address does not belong to a known device.
diff --git a/public/rust/BUILD.gn b/public/rust/BUILD.gn
index 325a327..c1554ae 100644
--- a/public/rust/BUILD.gn
+++ b/public/rust/BUILD.gn
@@ -8,6 +8,7 @@
   testonly = true
 
   deps = [
+    "algebra",
     "fdio",
     "fuchsia-async",
     "fuchsia-cobalt",
@@ -24,6 +25,7 @@
     "mundane",
     "packet",
     "shared-buffer",
+    "text-format",
     "zerocopy",
   ]
 
diff --git a/public/rust/algebra/BUILD.gn b/public/rust/algebra/BUILD.gn
new file mode 100644
index 0000000..93f0c95
--- /dev/null
+++ b/public/rust/algebra/BUILD.gn
@@ -0,0 +1,11 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/rust/rustc_library.gni")
+
+rustc_library("algebra") {
+  name = "algebra"
+  version = "0.1.0"
+  edition = "2018"
+}
diff --git a/public/rust/algebra/MAINTAINERS b/public/rust/algebra/MAINTAINERS
new file mode 100644
index 0000000..8ba11db
--- /dev/null
+++ b/public/rust/algebra/MAINTAINERS
@@ -0,0 +1 @@
+nickpollard@google.com
diff --git a/public/rust/algebra/src/lib.rs b/public/rust/algebra/src/lib.rs
new file mode 100644
index 0000000..31f1d96
--- /dev/null
+++ b/public/rust/algebra/src/lib.rs
@@ -0,0 +1,6 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+pub mod monoid;
+pub mod zip;
diff --git a/public/rust/algebra/src/monoid.rs b/public/rust/algebra/src/monoid.rs
new file mode 100644
index 0000000..1276532
--- /dev/null
+++ b/public/rust/algebra/src/monoid.rs
@@ -0,0 +1,99 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! # monoid
+//!
+//! This module introduces the notion of a _Monoid_. For a full description,
+//! see [Monoid on Wikipedia](https://en.wikipedia.org/wiki/Monoid)
+//!
+//! Briefly, a Monoid generalizes over the notion of summing. We're comfortable
+//! with summing integers - adding all the integers in a list to produce a
+//! total. We're probably also comfortable adding real numbers too. What about
+//! other types - Vectors? Matrices? _Strings_? It turns out that other things
+//! can be summed as well, and in defining the commonalities between those types
+//! of 'sum', we end up with only a few requirements:
+//!
+//! * A binary operation that combines two values into a resulting value of the
+//!   same type
+//! * That operation must be *associative*
+//!   ([Associativity on Wikipedia](https://en.wikipedia.org/wiki/Associative_property))
+//! * A *zero* or *identity* value, which when summed with another value leaves
+//!   the other value unchanged
+//!   ([Identity Element on Wikipedia](https://en.wikipedia.org/wiki/Identity_element))
+//!
+//! These few rules define a generalized notion of a 'sum' which covers addition
+//! over a variety of types, but also certain types of operation - taking the
+//! maximum or minimum, for example, or combining associative arrays - which we
+//! don't traditionally think of as sums.
+//!
+//! This abstraction is useful because it allows us to write code that is
+//! generic over any type of sum. Many algorithms can be thought of in terms of
+//! Monoids. Famously, the 'reduce' in map-reduce can be thought of as a Monoid.
+//!
+//! We also include the notion of a semigroup, a more general structure which
+//! lacks the identity element. There are times this is useful, but Monoids are
+//! the more often used structure. As a generalization of Monoids, all Monoids
+//! are Semigroups; not all Semigroups are Monoids.
+
+/// Trait representing the abstract algebra concept of a Semigroup - an
+/// associative binary operation `mappend`. Semigroup's do not have an identity
+/// element - they are `Monoid`s minus identity. Common examples include integer
+/// addition, multiplication, maximum, minimum.
+pub trait Semigroup {
+    /// Binary operation - this *must* be associative, i.e.
+    ///
+    ///   `(a op b) op c === a op (b op c)`
+    fn mappend(&self, b: &Self) -> Self;
+}
+
+/// Trait representing the abstract algebra concept of a Monoid - an associative
+/// binary operation `mappend` with an identity element `mzero`. This abstracts
+/// over 'summing', including addition, multiplication, concatenation, minimum
+/// and maximum.
+///
+/// Monoids are useful in defining standard ways to 'combine' certain types and
+/// then easily combining collections of these, and also deriving ways to define
+/// compound types.
+pub trait Monoid: Semigroup {
+    /// Identity element - an element that has no affect when combined via
+    /// `mappend`. This *must* obey the following laws:
+    ///
+    /// `mzero().mappend(a) === a`
+    ///   and
+    /// `a.mappend(mzero()) === a`
+    fn mzero() -> Self;
+}
+
+/// Newtype wrapper invoking the `Max` Monoid.
+/// Any numeric with a MIN_VALUE forms a Monoid with:
+///   `mappend() = max()`
+///     and
+///   `mzero() == MIN_VALUE`
+/// In the case of usize below, 0 is the MIN_VALUE
+#[repr(transparent)]
+pub struct Max(pub usize);
+
+impl Semigroup for Max {
+    fn mappend(&self, b: &Max) -> Max {
+        Max(self.0.max(b.0))
+    }
+}
+
+impl Monoid for Max {
+    fn mzero() -> Max {
+        Max(0)
+    }
+}
+
+/// Sum all items in an iterator, using the provided Monoid
+///
+/// We can always sum an iterator if we have a Monoid - we can recursively
+/// combine elements by `mappend`, and if the iterator is empty the result is
+/// just `mzero`.
+///
+/// Fold can be viewed as a Monoid Homomorphism - it maps from one Monoid (list)
+/// to another
+pub fn msum<I: Iterator<Item = T>, T: Monoid>(iter: I) -> T {
+    iter.fold(T::mzero(), |a, b| a.mappend(&b))
+}
diff --git a/public/rust/algebra/src/zip.rs b/public/rust/algebra/src/zip.rs
new file mode 100644
index 0000000..5fdba9d
--- /dev/null
+++ b/public/rust/algebra/src/zip.rs
@@ -0,0 +1,47 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use crate::monoid::*;
+
+/// Newtype wrapper for a ZipVec, a Vec whose monoid zips items together rather
+/// than concatenating If we know how to combine elements of a Vec, then we can
+/// combine two ZipVecs element-wise, padding the shortest with mzero.
+///
+/// e.g. using the addition monoid on ints:
+///
+///   > Zip(vec![0,1]).mappend( Zip(vec![1,2,4]) )
+///   Zip( vec![1,3,4] )
+pub struct Zip<T>(pub Vec<T>);
+
+#[rustfmt::skip]
+impl<T: Monoid> Semigroup for Zip<T> {
+    fn mappend(&self, b: &Zip<T>) -> Zip<T> {
+        let mut v = Zip(vec![]);
+        let mut a_ = self.0.iter();
+        let mut b_ = b.0.iter();
+        loop {
+            match (a_.next(), b_.next()) {
+                (Some(a), Some(b)) => v.0.push(a.mappend(b)),
+                (Some(a), None   ) => v.0.push(a.mappend(&T::mzero())),
+                (None,    Some(b)) => v.0.push(b.mappend(&T::mzero())),
+                (None,    None   ) => return v,
+            }
+        }
+    }
+}
+
+impl<T: Monoid> Monoid for Zip<T> {
+    fn mzero() -> Zip<T> {
+        Zip(vec![])
+    }
+}
+
+impl<T> Zip<T> {
+    pub fn map<U, F>(&self, f: F) -> Zip<U>
+    where
+        F: Fn(&T) -> U,
+    {
+        Zip(self.0.iter().map(f).collect())
+    }
+}
diff --git a/public/rust/text-format/BUILD.gn b/public/rust/text-format/BUILD.gn
new file mode 100644
index 0000000..fa660cd
--- /dev/null
+++ b/public/rust/text-format/BUILD.gn
@@ -0,0 +1,15 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/rust/rustc_library.gni")
+
+rustc_library("text-format") {
+  name = "text_format"
+  version = "0.1.0"
+  edition = "2018"
+
+  deps = [
+    "//garnet/public/rust/algebra",
+  ]
+}
diff --git a/public/rust/text-format/MAINTAINERS b/public/rust/text-format/MAINTAINERS
new file mode 100644
index 0000000..8ba11db
--- /dev/null
+++ b/public/rust/text-format/MAINTAINERS
@@ -0,0 +1 @@
+nickpollard@google.com
diff --git a/public/rust/text-format/src/lib.rs b/public/rust/text-format/src/lib.rs
new file mode 100644
index 0000000..4977e41
--- /dev/null
+++ b/public/rust/text-format/src/lib.rs
@@ -0,0 +1,52 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use algebra::monoid::*;
+use algebra::zip::*;
+use std::fmt::Write;
+
+/// Format a matrix of strings (plus an optional row of headers) into an aligned table
+/// e.g.
+///
+///   > tabulate(vec![vec![("yellow", "green", "blue")],
+///                   vec![("red", "orange", "fuchsia")]],
+///              Some(vec!["First", "Second", "Third"]))
+///
+///   First  Second Third
+///   ======================
+///   yellow green  blue
+///   red    orange fuchsia
+pub fn tabulate<S: AsRef<str>, T: AsRef<str>>(rows: Vec<Vec<S>>, headers: Option<Vec<T>>) -> String {
+
+    fn row_widths<S: AsRef<str>>(row: &Vec<S>) -> Zip<Max> {
+        Zip(row.iter().map(|s| Max(s.as_ref().len())).collect())
+    };
+
+    let header_widths = msum(headers.iter().map(row_widths));
+    let widths = msum(rows.iter().map(row_widths));
+    let widths = header_widths.mappend(&widths).map(|n| n.0 + 1).0;
+
+    let mut string = String::new();
+    for hs in headers {
+        write_row_padded(&mut string, &widths, &hs);
+        write_underline(&mut string, widths.iter().sum());
+    }
+    for row in rows {
+        write_row_padded(&mut string, &widths, &row);
+    }
+    string
+}
+
+/// Write an underline of a given length of '=' characters to a target string
+fn write_underline(out: &mut String, length: usize) {
+    writeln!(out, "{:=<width$}", "", width = length).unwrap();
+}
+
+/// Write a row of fields padded to given widths to a target string
+fn write_row_padded<S: AsRef<str>>(out: &mut String, widths: &Vec<usize>, row: &Vec<S>) {
+    for (text, cols) in row.iter().zip(widths.iter()) {
+        write!(out, "{:width$}", text.as_ref(), width = cols).unwrap();
+    }
+    writeln!(out, "").unwrap();
+}