use anyhow::Result;
use clap::Parser;
use crate::cli::drive::format::{output_as, OutputFormat};
use crate::cli::drive::helpers;
use crate::cli::format::sanitize_for_terminal;
use crate::drive::client::DriveClient;
use crate::drive::sheets::client::SheetsClient;
use crate::drive::sheets::format::{
describe_lines, format, BorderSides, CellFormatFlags, FormatOptions, FormatVerb,
TextRotationFlag,
};
use crate::drive::sheets::types::Dimension;
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum HorizontalAlign {
Left,
Center,
Right,
}
impl HorizontalAlign {
const fn wire(self) -> &'static str {
match self {
Self::Left => "LEFT",
Self::Center => "CENTER",
Self::Right => "RIGHT",
}
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum VerticalAlign {
Top,
Middle,
Bottom,
}
impl VerticalAlign {
const fn wire(self) -> &'static str {
match self {
Self::Top => "TOP",
Self::Middle => "MIDDLE",
Self::Bottom => "BOTTOM",
}
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum WrapStrategy {
Overflow,
Clip,
Wrap,
}
impl WrapStrategy {
const fn wire(self) -> &'static str {
match self {
Self::Overflow => "OVERFLOW_CELL",
Self::Clip => "CLIP",
Self::Wrap => "WRAP",
}
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum NumberFormatType {
Text,
Number,
Percent,
Currency,
Date,
Time,
DateTime,
Scientific,
}
impl NumberFormatType {
const fn wire(self) -> &'static str {
match self {
Self::Text => "TEXT",
Self::Number => "NUMBER",
Self::Percent => "PERCENT",
Self::Currency => "CURRENCY",
Self::Date => "DATE",
Self::Time => "TIME",
Self::DateTime => "DATE_TIME",
Self::Scientific => "SCIENTIFIC",
}
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum HyperlinkDisplayType {
Linked,
PlainText,
}
impl HyperlinkDisplayType {
const fn wire(self) -> &'static str {
match self {
Self::Linked => "LINKED",
Self::PlainText => "PLAIN_TEXT",
}
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum TextDirection {
LeftToRight,
RightToLeft,
}
impl TextDirection {
const fn wire(self) -> &'static str {
match self {
Self::LeftToRight => "LEFT_TO_RIGHT",
Self::RightToLeft => "RIGHT_TO_LEFT",
}
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum BorderStyle {
Solid,
SolidMedium,
SolidThick,
Dashed,
Dotted,
Double,
}
impl BorderStyle {
const fn wire(self) -> &'static str {
match self {
Self::Solid => "SOLID",
Self::SolidMedium => "SOLID_MEDIUM",
Self::SolidThick => "SOLID_THICK",
Self::Dashed => "DASHED",
Self::Dotted => "DOTTED",
Self::Double => "DOUBLE",
}
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum MergeType {
All,
Rows,
Columns,
}
impl MergeType {
const fn wire(self) -> &'static str {
match self {
Self::All => "MERGE_ALL",
Self::Rows => "MERGE_ROWS",
Self::Columns => "MERGE_COLUMNS",
}
}
}
impl std::fmt::Display for MergeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.wire().to_ascii_lowercase().replace('_', "-"))
}
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum DimensionArg {
Rows,
Columns,
}
impl DimensionArg {
pub(crate) const fn engine(self) -> Dimension {
match self {
Self::Rows => Dimension::Rows,
Self::Columns => Dimension::Columns,
}
}
}
#[derive(Parser)]
pub struct FormatCellsCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "A1")]
pub range: Option<String>,
#[arg(long, value_name = "NAME")]
pub sheet: Option<String>,
#[arg(long, value_name = "BOOL")]
pub bold: Option<bool>,
#[arg(long, value_name = "BOOL")]
pub italic: Option<bool>,
#[arg(long, value_name = "BOOL")]
pub strikethrough: Option<bool>,
#[arg(long, value_name = "BOOL")]
pub underline: Option<bool>,
#[arg(long, value_name = "N")]
pub font_size: Option<i64>,
#[arg(long, value_name = "HEX")]
pub text_color: Option<String>,
#[arg(long, value_name = "HEX")]
pub background: Option<String>,
#[arg(long, value_enum, value_name = "ALIGN")]
pub horizontal_align: Option<HorizontalAlign>,
#[arg(long, value_enum, value_name = "ALIGN")]
pub vertical_align: Option<VerticalAlign>,
#[arg(long, value_name = "PATTERN")]
pub number_format: Option<String>,
#[arg(long, value_enum, value_name = "TYPE")]
pub number_format_type: Option<NumberFormatType>,
#[arg(long, value_enum, value_name = "WRAP")]
pub wrap: Option<WrapStrategy>,
#[arg(long, value_name = "NAME")]
pub font_family: Option<String>,
#[arg(
long,
value_name = "DEGREES",
allow_hyphen_values = true,
conflicts_with = "text_rotation_vertical"
)]
pub text_rotation_angle: Option<i64>,
#[arg(long, conflicts_with = "text_rotation_angle")]
pub text_rotation_vertical: bool,
#[arg(long, value_enum, value_name = "TYPE")]
pub hyperlink_display_type: Option<HyperlinkDisplayType>,
#[arg(long, value_name = "N")]
pub padding_top: Option<i64>,
#[arg(long, value_name = "N")]
pub padding_right: Option<i64>,
#[arg(long, value_name = "N")]
pub padding_bottom: Option<i64>,
#[arg(long, value_name = "N")]
pub padding_left: Option<i64>,
#[arg(long, value_enum, value_name = "DIR")]
pub text_direction: Option<TextDirection>,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl FormatCellsCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let format = CellFormatFlags {
bold: self.bold,
italic: self.italic,
strikethrough: self.strikethrough,
underline: self.underline,
font_size: self.font_size,
text_color: self.text_color,
background: self.background,
horizontal_align: self
.horizontal_align
.map(HorizontalAlign::wire)
.map(str::to_string),
vertical_align: self
.vertical_align
.map(VerticalAlign::wire)
.map(str::to_string),
number_format_pattern: self.number_format,
number_format_type: self
.number_format_type
.map(NumberFormatType::wire)
.map(str::to_string),
wrap: self.wrap.map(WrapStrategy::wire).map(str::to_string),
font_family: self.font_family,
text_rotation: match (self.text_rotation_angle, self.text_rotation_vertical) {
(Some(angle), _) => Some(TextRotationFlag::Angle(angle)),
(None, true) => Some(TextRotationFlag::Vertical),
(None, false) => None,
},
hyperlink_display_type: self
.hyperlink_display_type
.map(HyperlinkDisplayType::wire)
.map(str::to_string),
padding_top: self.padding_top,
padding_right: self.padding_right,
padding_bottom: self.padding_bottom,
padding_left: self.padding_left,
text_direction: self
.text_direction
.map(TextDirection::wire)
.map(str::to_string),
};
let opts = FormatOptions {
spreadsheet_id: self.spreadsheet_id,
verb: FormatVerb::FormatCells {
sheet: self.sheet,
range: self.range,
format: Box::new(format),
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_format(client, &opts, &self.write.output).await
}
}
#[derive(Parser)]
pub struct UpdateBordersCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "A1")]
pub range: Option<String>,
#[arg(long, value_name = "NAME")]
pub sheet: Option<String>,
#[arg(long)]
pub top: bool,
#[arg(long)]
pub bottom: bool,
#[arg(long)]
pub left: bool,
#[arg(long)]
pub right: bool,
#[arg(long)]
pub all: bool,
#[arg(long)]
pub inner_horizontal: bool,
#[arg(long)]
pub inner_vertical: bool,
#[arg(long, value_enum, default_value_t = BorderStyle::Solid)]
pub style: BorderStyle,
#[arg(long, value_name = "HEX")]
pub color: Option<String>,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl std::fmt::Display for BorderStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.wire().to_ascii_lowercase().replace('_', "-"))
}
}
impl UpdateBordersCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let sides = BorderSides {
top: self.top || self.all,
bottom: self.bottom || self.all,
left: self.left || self.all,
right: self.right || self.all,
inner_horizontal: self.inner_horizontal,
inner_vertical: self.inner_vertical,
};
let opts = FormatOptions {
spreadsheet_id: self.spreadsheet_id,
verb: FormatVerb::UpdateBorders {
sheet: self.sheet,
range: self.range,
sides,
style: self.style.wire().to_string(),
color: self.color,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_format(client, &opts, &self.write.output).await
}
}
#[derive(Parser)]
pub struct MergeCellsCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "A1")]
pub range: Option<String>,
#[arg(long, value_name = "NAME")]
pub sheet: Option<String>,
#[arg(long, value_enum, default_value_t = MergeType::All)]
pub r#type: MergeType,
#[arg(long)]
pub dry_run: bool,
#[command(flatten)]
pub lease: crate::cli::drive::helpers::LeaseTokenArg,
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl MergeCellsCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = FormatOptions {
spreadsheet_id: self.spreadsheet_id,
verb: FormatVerb::MergeCells {
sheet: self.sheet,
range: self.range,
merge_type: self.r#type.wire().to_string(),
},
dry_run: self.dry_run,
lease_token: self.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.dry_run)?,
};
run_format(client, &opts, &self.output).await
}
}
#[derive(Parser)]
pub struct UnmergeCellsCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "A1")]
pub range: Option<String>,
#[arg(long, value_name = "NAME")]
pub sheet: Option<String>,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl UnmergeCellsCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = FormatOptions {
spreadsheet_id: self.spreadsheet_id,
verb: FormatVerb::UnmergeCells {
sheet: self.sheet,
range: self.range,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_format(client, &opts, &self.write.output).await
}
}
#[derive(Parser)]
pub struct AutoResizeDimensionCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "NAME")]
pub sheet: String,
#[arg(long, value_enum)]
pub dimension: DimensionArg,
#[arg(long, value_name = "N")]
pub start: i64,
#[arg(long, value_name = "N")]
pub end: i64,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl AutoResizeDimensionCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = FormatOptions {
spreadsheet_id: self.spreadsheet_id,
verb: FormatVerb::AutoResizeDimension {
sheet: self.sheet,
dimension: self.dimension.engine(),
start: self.start,
end: self.end,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_format(client, &opts, &self.write.output).await
}
}
#[derive(Parser)]
pub struct UpdateDimensionPropertiesCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "NAME")]
pub sheet: String,
#[arg(long, value_enum)]
pub dimension: DimensionArg,
#[arg(long, value_name = "N")]
pub start: i64,
#[arg(long, value_name = "N")]
pub end: i64,
#[arg(long, value_name = "N")]
pub pixel_size: i64,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl UpdateDimensionPropertiesCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = FormatOptions {
spreadsheet_id: self.spreadsheet_id,
verb: FormatVerb::UpdateDimensionProperties {
sheet: self.sheet,
dimension: self.dimension.engine(),
start: self.start,
end: self.end,
pixel_size: self.pixel_size,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_format(client, &opts, &self.write.output).await
}
}
async fn run_format(
client: &DriveClient,
opts: &FormatOptions,
output: &OutputFormat,
) -> Result<()> {
let sheets = SheetsClient::from_drive_client(client)?;
let rules = helpers::active_account_rules()?;
let outcome = format(client, &sheets, opts, &rules).await;
if output_as(&outcome, output)? {
return Ok(());
}
let lines: Vec<String> = describe_lines(&outcome)
.into_iter()
.map(|line| sanitize_for_terminal(&line))
.collect();
println!("{}", lines.join("\n"));
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn horizontal_align_wire_maps_every_variant() {
assert_eq!(HorizontalAlign::Left.wire(), "LEFT");
assert_eq!(HorizontalAlign::Center.wire(), "CENTER");
assert_eq!(HorizontalAlign::Right.wire(), "RIGHT");
}
#[test]
fn vertical_align_wire_maps_every_variant() {
assert_eq!(VerticalAlign::Top.wire(), "TOP");
assert_eq!(VerticalAlign::Middle.wire(), "MIDDLE");
assert_eq!(VerticalAlign::Bottom.wire(), "BOTTOM");
}
#[test]
fn hyperlink_display_type_wire_maps_every_variant() {
assert_eq!(HyperlinkDisplayType::Linked.wire(), "LINKED");
assert_eq!(HyperlinkDisplayType::PlainText.wire(), "PLAIN_TEXT");
}
#[test]
fn text_direction_wire_maps_every_variant() {
assert_eq!(TextDirection::LeftToRight.wire(), "LEFT_TO_RIGHT");
assert_eq!(TextDirection::RightToLeft.wire(), "RIGHT_TO_LEFT");
}
#[test]
fn wrap_strategy_wire_maps_every_variant() {
assert_eq!(WrapStrategy::Overflow.wire(), "OVERFLOW_CELL");
assert_eq!(WrapStrategy::Clip.wire(), "CLIP");
assert_eq!(WrapStrategy::Wrap.wire(), "WRAP");
}
#[test]
fn number_format_type_wire_maps_every_variant() {
assert_eq!(NumberFormatType::Text.wire(), "TEXT");
assert_eq!(NumberFormatType::Number.wire(), "NUMBER");
assert_eq!(NumberFormatType::Percent.wire(), "PERCENT");
assert_eq!(NumberFormatType::Currency.wire(), "CURRENCY");
assert_eq!(NumberFormatType::Date.wire(), "DATE");
assert_eq!(NumberFormatType::Time.wire(), "TIME");
assert_eq!(NumberFormatType::DateTime.wire(), "DATE_TIME");
assert_eq!(NumberFormatType::Scientific.wire(), "SCIENTIFIC");
}
#[test]
fn border_style_wire_maps_every_variant() {
assert_eq!(BorderStyle::Solid.wire(), "SOLID");
assert_eq!(BorderStyle::SolidMedium.wire(), "SOLID_MEDIUM");
assert_eq!(BorderStyle::SolidThick.wire(), "SOLID_THICK");
assert_eq!(BorderStyle::Dashed.wire(), "DASHED");
assert_eq!(BorderStyle::Dotted.wire(), "DOTTED");
assert_eq!(BorderStyle::Double.wire(), "DOUBLE");
}
#[test]
fn border_style_display_is_lowercase_hyphenated() {
assert_eq!(BorderStyle::Solid.to_string(), "solid");
assert_eq!(BorderStyle::SolidMedium.to_string(), "solid-medium");
assert_eq!(BorderStyle::SolidThick.to_string(), "solid-thick");
assert_eq!(BorderStyle::Dashed.to_string(), "dashed");
assert_eq!(BorderStyle::Dotted.to_string(), "dotted");
assert_eq!(BorderStyle::Double.to_string(), "double");
}
#[test]
fn merge_type_wire_maps_every_variant() {
assert_eq!(MergeType::All.wire(), "MERGE_ALL");
assert_eq!(MergeType::Rows.wire(), "MERGE_ROWS");
assert_eq!(MergeType::Columns.wire(), "MERGE_COLUMNS");
}
#[test]
fn merge_type_display_is_lowercase_hyphenated() {
assert_eq!(MergeType::All.to_string(), "merge-all");
assert_eq!(MergeType::Rows.to_string(), "merge-rows");
assert_eq!(MergeType::Columns.to_string(), "merge-columns");
}
#[test]
fn dimension_arg_engine_maps_every_variant() {
assert_eq!(DimensionArg::Rows.engine(), Dimension::Rows);
assert_eq!(DimensionArg::Columns.engine(), Dimension::Columns);
}
}