mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 10:14:35 +00:00
feat(cli): add column truncation controls (#1838)
- drop low-priority columns when tables exceed terminal width - truncate optional columns to fit remaining width - add --no-trunc flag to disable truncation - compute column widths using unicode display width Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Generated
+2
@@ -2240,6 +2240,7 @@ dependencies = [
|
|||||||
"sys-locale",
|
"sys-locale",
|
||||||
"tabled",
|
"tabled",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"terminal_size",
|
||||||
"thiserror 1.0.63",
|
"thiserror 1.0.63",
|
||||||
"thunk-rs",
|
"thunk-rs",
|
||||||
"tikv-jemalloc-ctl",
|
"tikv-jemalloc-ctl",
|
||||||
@@ -2258,6 +2259,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tun-easytier",
|
"tun-easytier",
|
||||||
|
"unicode-width 0.1.11",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"version-compare",
|
"version-compare",
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ x25519-dalek = "2.0"
|
|||||||
# for cli
|
# for cli
|
||||||
tabled = "0.16"
|
tabled = "0.16"
|
||||||
humansize = "2.1.3"
|
humansize = "2.1.3"
|
||||||
|
terminal_size = "0.4"
|
||||||
|
unicode-width = "0.1"
|
||||||
|
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ use easytier::ShellType;
|
|||||||
use humansize::format_size;
|
use humansize::format_size;
|
||||||
use rust_i18n::t;
|
use rust_i18n::t;
|
||||||
use service_manager::*;
|
use service_manager::*;
|
||||||
use tabled::settings::Style;
|
use tabled::settings::{location::ByColumnName, object::Columns, Disable, Modify, Style, Width};
|
||||||
|
use terminal_size::{terminal_size, Width as TerminalWidth};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use easytier::service_manager::{Service, ServiceInstallOptions};
|
use easytier::service_manager::{Service, ServiceInstallOptions};
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
@@ -86,6 +88,13 @@ struct Cli {
|
|||||||
)]
|
)]
|
||||||
output_format: OutputFormat,
|
output_format: OutputFormat,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long = "no-trunc",
|
||||||
|
default_value = "false",
|
||||||
|
help = "disable column truncation"
|
||||||
|
)]
|
||||||
|
no_trunc: bool,
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
instance_select: InstanceSelectArgs,
|
instance_select: InstanceSelectArgs,
|
||||||
|
|
||||||
@@ -388,6 +397,7 @@ struct CommandHandler<'a> {
|
|||||||
client: tokio::sync::Mutex<RpcClient>,
|
client: tokio::sync::Mutex<RpcClient>,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
output_format: &'a OutputFormat,
|
output_format: &'a OutputFormat,
|
||||||
|
no_trunc: bool,
|
||||||
instance_selector: InstanceIdentifier,
|
instance_selector: InstanceIdentifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,7 +717,13 @@ impl CommandHandler<'_> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
print_output(&items, self.output_format)?;
|
print_output(
|
||||||
|
&items,
|
||||||
|
self.output_format,
|
||||||
|
&["tunnel", "version"],
|
||||||
|
&["version", "tunnel", "nat", "tx", "rx", "loss", "lat(ms)"],
|
||||||
|
self.no_trunc,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -942,7 +958,13 @@ impl CommandHandler<'_> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
print_output(&items, self.output_format)?;
|
print_output(
|
||||||
|
&items,
|
||||||
|
self.output_format,
|
||||||
|
&["proxy_cidrs", "version"],
|
||||||
|
&["proxy_cidrs", "version"],
|
||||||
|
self.no_trunc,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1124,7 +1146,7 @@ impl CommandHandler<'_> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
print_output(&items, self.output_format)?;
|
print_output(&items, self.output_format, &[], &[], self.no_trunc)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1373,13 +1395,45 @@ impl CommandHandler<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_output<T>(items: &[T], format: &OutputFormat) -> Result<(), Error>
|
fn print_output<T>(
|
||||||
|
items: &[T],
|
||||||
|
format: &OutputFormat,
|
||||||
|
optional_columns: &[&str],
|
||||||
|
drop_columns: &[&str],
|
||||||
|
no_trunc: bool,
|
||||||
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
T: tabled::Tabled + serde::Serialize,
|
T: tabled::Tabled + serde::Serialize,
|
||||||
{
|
{
|
||||||
match format {
|
match format {
|
||||||
OutputFormat::Table => {
|
OutputFormat::Table => {
|
||||||
println!("{}", tabled::Table::new(items).with(Style::markdown()));
|
let mut table = tabled::Table::new(items);
|
||||||
|
table.with(Style::markdown());
|
||||||
|
if no_trunc {
|
||||||
|
println!("{}", table);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let headers = T::headers()
|
||||||
|
.iter()
|
||||||
|
.map(|header| header.as_ref().to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let col_widths = compute_column_widths(items);
|
||||||
|
let terminal_width = terminal_table_width();
|
||||||
|
let drop_indices = header_indices(&headers, drop_columns);
|
||||||
|
let optional_indices = header_indices(&headers, optional_columns);
|
||||||
|
let (active, drop_indices, total_width) =
|
||||||
|
select_columns_to_drop(terminal_width, &drop_indices, &col_widths);
|
||||||
|
apply_column_drops(&mut table, &drop_indices);
|
||||||
|
apply_optional_column_truncation(
|
||||||
|
&mut table,
|
||||||
|
terminal_width,
|
||||||
|
&headers,
|
||||||
|
&optional_indices,
|
||||||
|
&col_widths,
|
||||||
|
&active,
|
||||||
|
total_width,
|
||||||
|
);
|
||||||
|
println!("{}", table);
|
||||||
}
|
}
|
||||||
OutputFormat::Json => {
|
OutputFormat::Json => {
|
||||||
println!("{}", serde_json::to_string_pretty(items)?);
|
println!("{}", serde_json::to_string_pretty(items)?);
|
||||||
@@ -1388,6 +1442,178 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn terminal_table_width() -> Option<usize> {
|
||||||
|
let (TerminalWidth(width), _) = terminal_size()?;
|
||||||
|
let width = width as usize;
|
||||||
|
// Avoid wrapping at the last column which can still trigger a hard line break.
|
||||||
|
width.checked_sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_optional_column_truncation(
|
||||||
|
table: &mut tabled::Table,
|
||||||
|
terminal_width: Option<usize>,
|
||||||
|
headers: &[String],
|
||||||
|
optional_indices: &[usize],
|
||||||
|
col_widths: &[usize],
|
||||||
|
active: &[bool],
|
||||||
|
total_width: usize,
|
||||||
|
) {
|
||||||
|
let Some(terminal_width) = terminal_width else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if optional_indices.is_empty() || total_width <= terminal_width {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targets = optional_column_targets(terminal_width, optional_indices, col_widths, active);
|
||||||
|
for (index, width) in targets {
|
||||||
|
if let Some(name) = headers.get(index) {
|
||||||
|
table.with(
|
||||||
|
Modify::new(ByColumnName::new(name)).with(Width::truncate(width).suffix("...")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_column_drops(table: &mut tabled::Table, drop_indices: &[usize]) {
|
||||||
|
let mut indices = drop_indices.to_vec();
|
||||||
|
indices.sort_unstable_by(|a, b| b.cmp(a));
|
||||||
|
for index in indices {
|
||||||
|
table.with(Disable::column(Columns::single(index)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_column_widths<T>(items: &[T]) -> Vec<usize>
|
||||||
|
where
|
||||||
|
T: tabled::Tabled,
|
||||||
|
{
|
||||||
|
let mut widths = vec![0usize; T::LENGTH];
|
||||||
|
for (idx, header) in T::headers().iter().enumerate() {
|
||||||
|
widths[idx] = widths[idx].max(text_width(header.as_ref()));
|
||||||
|
}
|
||||||
|
for item in items {
|
||||||
|
for (idx, field) in item.fields().iter().enumerate() {
|
||||||
|
widths[idx] = widths[idx].max(text_width(field.as_ref()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
widths
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_width(text: &str) -> usize {
|
||||||
|
text.split('\n')
|
||||||
|
.map(UnicodeWidthStr::width)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header_indices(headers: &[String], names: &[&str]) -> Vec<usize> {
|
||||||
|
let mut indices = Vec::new();
|
||||||
|
for name in names {
|
||||||
|
if let Some(index) = headers
|
||||||
|
.iter()
|
||||||
|
.position(|header| header.eq_ignore_ascii_case(name))
|
||||||
|
{
|
||||||
|
if !indices.contains(&index) {
|
||||||
|
indices.push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indices
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_columns_to_drop(
|
||||||
|
terminal_width: Option<usize>,
|
||||||
|
drop_indices: &[usize],
|
||||||
|
col_widths: &[usize],
|
||||||
|
) -> (Vec<bool>, Vec<usize>, usize) {
|
||||||
|
let mut active = vec![true; col_widths.len()];
|
||||||
|
let Some(terminal_width) = terminal_width else {
|
||||||
|
let total = table_total_width(col_widths, &active);
|
||||||
|
return (active, vec![], total);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut total = table_total_width(col_widths, &active);
|
||||||
|
if total <= terminal_width {
|
||||||
|
return (active, vec![], total);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dropped = vec![];
|
||||||
|
for &index in drop_indices {
|
||||||
|
if total <= terminal_width {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if active[index] {
|
||||||
|
active[index] = false;
|
||||||
|
dropped.push(index);
|
||||||
|
total = table_total_width(col_widths, &active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(active, dropped, total)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_total_width(col_widths: &[usize], active: &[bool]) -> usize {
|
||||||
|
let col_count = active.iter().filter(|value| **value).count();
|
||||||
|
if col_count == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let content_width = col_widths
|
||||||
|
.iter()
|
||||||
|
.zip(active.iter())
|
||||||
|
.filter_map(|(width, keep)| keep.then_some(*width))
|
||||||
|
.sum::<usize>();
|
||||||
|
content_width + 3 * col_count + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn optional_column_targets(
|
||||||
|
terminal_width: usize,
|
||||||
|
optional_indices: &[usize],
|
||||||
|
col_widths: &[usize],
|
||||||
|
active: &[bool],
|
||||||
|
) -> Vec<(usize, usize)> {
|
||||||
|
if optional_indices.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut is_optional = vec![false; col_widths.len()];
|
||||||
|
for &index in optional_indices {
|
||||||
|
if let Some(flag) = is_optional.get_mut(index) {
|
||||||
|
*flag = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let optional_indices = optional_indices
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|idx| active.get(*idx).copied().unwrap_or(false))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if optional_indices.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let col_count = active.iter().filter(|value| **value).count();
|
||||||
|
let overhead = 3 * col_count + 1;
|
||||||
|
let mut required_width = overhead;
|
||||||
|
for (idx, width) in col_widths.iter().enumerate() {
|
||||||
|
if active.get(idx).copied().unwrap_or(false) && !is_optional[idx] {
|
||||||
|
required_width += *width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining = terminal_width.saturating_sub(required_width);
|
||||||
|
let min_width = 6usize;
|
||||||
|
let per_column = if remaining == 0 {
|
||||||
|
min_width
|
||||||
|
} else {
|
||||||
|
(remaining / optional_indices.len()).clamp(min_width, 24)
|
||||||
|
};
|
||||||
|
|
||||||
|
optional_indices
|
||||||
|
.into_iter()
|
||||||
|
.map(|idx| (idx, col_widths[idx].min(per_column)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
async fn main() -> Result<(), Error> {
|
async fn main() -> Result<(), Error> {
|
||||||
@@ -1404,6 +1630,7 @@ async fn main() -> Result<(), Error> {
|
|||||||
client: tokio::sync::Mutex::new(client),
|
client: tokio::sync::Mutex::new(client),
|
||||||
verbose: cli.verbose,
|
verbose: cli.verbose,
|
||||||
output_format: &cli.output_format,
|
output_format: &cli.output_format,
|
||||||
|
no_trunc: cli.no_trunc,
|
||||||
instance_selector: (&cli.instance_select).into(),
|
instance_selector: (&cli.instance_select).into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1608,7 +1835,13 @@ async fn main() -> Result<(), Error> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
print_output(&table_rows, &cli.output_format)?;
|
print_output(
|
||||||
|
&table_rows,
|
||||||
|
&cli.output_format,
|
||||||
|
&["direct_peers"],
|
||||||
|
&["direct_peers"],
|
||||||
|
cli.no_trunc,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
SubCommand::VpnPortal => {
|
SubCommand::VpnPortal => {
|
||||||
let vpn_portal_client = handler.get_vpn_portal_client().await?;
|
let vpn_portal_client = handler.get_vpn_portal_client().await?;
|
||||||
@@ -1816,7 +2049,13 @@ async fn main() -> Result<(), Error> {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
print_output(&table_rows, &cli.output_format)?;
|
print_output(
|
||||||
|
&table_rows,
|
||||||
|
&cli.output_format,
|
||||||
|
&["start_time", "state", "transport_type"],
|
||||||
|
&["start_time", "state", "transport_type"],
|
||||||
|
cli.no_trunc,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
SubCommand::Acl(acl_args) => match &acl_args.sub_command {
|
SubCommand::Acl(acl_args) => match &acl_args.sub_command {
|
||||||
Some(AclSubCommand::Stats) | None => {
|
Some(AclSubCommand::Stats) | None => {
|
||||||
@@ -1925,7 +2164,13 @@ async fn main() -> Result<(), Error> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
print_output(&table_rows, &cli.output_format)?
|
print_output(
|
||||||
|
&table_rows,
|
||||||
|
&cli.output_format,
|
||||||
|
&["labels"],
|
||||||
|
&["labels"],
|
||||||
|
cli.no_trunc,
|
||||||
|
)?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(StatsSubCommand::Prometheus) => {
|
Some(StatsSubCommand::Prometheus) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user