[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();
+}