feat: cli colors

This commit is contained in:
Lucas Colombo 2024-03-30 14:23:36 -03:00
parent 58ec1de9e1
commit 41944ba7ee
Signed by: lucas
GPG Key ID: EF34786CFEFFAE35
15 changed files with 585 additions and 17 deletions

15
.github/img/logo.svg vendored Normal file
View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 142 64">
<style>
.a { fill: #000000; }
@media (prefers-color-scheme: dark) {
.a { fill: #ffffff; }
}
</style>
<g>
<path class="a" d="M0,0H20.42877V61.64158h4.66951v1.51648H.33347V61.64158H4.91962V1.516H0Z" />
<path class="a" d="M32.68564,58.6096Q27.34846,53.22312,27.34919,43.157q0-10.06074,5.7116-15.49373,5.71016-5.432,16.051-5.432,10.33773,0,15.42578,5.01055Q69.6225,32.25308,69.624,42.52651,69.624,63.999,48.44489,64,38.02064,64,32.68564,58.6096Zm20.929-12.04176V39.41033a57.79486,57.79486,0,0,0-.87584-11.916q-.87535-3.74542-4.04378-3.74714a4.16126,4.16126,0,0,0-2.96,1.05243,6.48776,6.48776,0,0,0-1.62614,3.495,64.70357,64.70357,0,0,0-.7503,11.5366v6.56814q0,9.18111.54189,11.03143a18.941,18.941,0,0,0,1.12545,3.032,3.54892,3.54892,0,0,0,3.58575,2.02164q3.252,0,4.25219-3.79071Q53.61486,56.00056,53.61461,46.56784Z" />
<path class="a" d="M77.96259,58.6096q-5.3379-5.38648-5.33742-15.45262,0-10.06074,5.7116-15.49373,5.71161-5.432,16.051-5.432,10.33846,0,15.42675,5.01055,5.08344,5.01129,5.08538,15.28472Q114.89994,63.999,93.72087,64,83.29808,64,77.96259,58.6096Zm20.929-12.04176V39.41033a57.82133,57.82133,0,0,0-.87632-11.916q-.87536-3.74542-4.04427-3.74714a4.16,4.16,0,0,0-2.95952,1.05243,6.48414,6.48414,0,0,0-1.62566,3.495,64.70357,64.70357,0,0,0-.7503,11.5366v6.56814q0,9.18111.54092,11.03143a18.934,18.934,0,0,0,1.12642,3.032,3.54712,3.54712,0,0,0,3.58478,2.02164q3.25275,0,4.25268-3.79071Q98.892,56.00056,98.89156,46.56784Z" />
<path class="a" d="M116.90172,0h20.42876V61.64158H142v1.51648H117.23518V61.64158h4.58615V1.516h-4.91961Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -4,7 +4,7 @@
"Taskfile.yaml": true,
".cocorc": true,
".task": true,
".cargo": true,
// ".cargo": true,
// "Cargo.toml": true,
// 📦
@ -12,7 +12,7 @@
"target": true,
// 📝 readmes
"**/**/README.md": true,
// "**/**/README.md": true,
"LICENSE": true,
// 🗑

39
Cargo.lock generated Normal file
View File

@ -0,0 +1,39 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bitflags"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "eyre"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "lool"
version = "0.0.0-alpha.0"
dependencies = [
"bitflags",
"eyre",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"

View File

@ -16,10 +16,15 @@ overflow-checks = false
debug = 0
debug-assertions = false
[dependencies]
eyre = { version = "0.6.12", default-features = false, features = ["auto-install"] }
indoc = "2.0.4"
[registry]
default = "gitea"
[[bin]]
name = "lool"
path = "src/main.rs"
[registries.gitea]
index = "sparse+http://lugit.local/api/packages/lucodear/cargo/"
[lib]
path = "lib/lib.rs"
[dependencies]
bitflags = "2.5.0"
eyre = { version = "0.6.12", default-features = false }

20
README.md Normal file
View File

@ -0,0 +1,20 @@
<p align="center"><img src=".github/img/logo.svg" height="128"></p>
<br>
<br>
<br>
<p align="center"><b>𝐥𝐨𝐨𝐥</b> is a tool-box library with common utilities for <b>𝚕𝚞𝚌𝚘𝚍𝚎.𝚊𝚛</b> projects.
</p>
<br>
<br>
<br>
# Features
- [ ] **logging**: A simple logging utility.
- [ ] **cli**: Command-line interface utilities:
- [ ] **argparse**: A simple wrapper around `argparse`.
- [ ] **color**: `foo.<color>()` like extension methods for strings.
- [ ] **case**: Case conversion utilities.

View File

@ -20,6 +20,16 @@ tasks:
- python check_size.py
fmt:
desc: 🚀 format lool
desc: 🎨 format lool
cmds:
- cargo +nightly fmt --all
- cargo +nightly fmt --all
lint:
desc: 🧶 lint lool
cmds:
- cargo clippy --fix
test:
desc: 🧪 test lool
cmds:
- cargo nextest run

38
examples/stylizer.rs Normal file
View File

@ -0,0 +1,38 @@
use eyre::{set_hook, DefaultHandler, Result};
use lool::cli::colors::{stylize, Stylize};
fn setup_eyre() {
let _ = set_hook(Box::new(DefaultHandler::default_with));
}
fn main() -> Result<()> {
setup_eyre();
let red_bold = stylize("[red+bold]", "red+bold");
let alt_red_bold = stylize(stylize("alt [red+bold]", "red"), "+bold");
let red_bold_italic = stylize("[red+bold|italic]", "red+bold|italic");
let alt_red_bold_italic = stylize(stylize(stylize("alt [red+bold|italic]", "red"), "+bold"), "+italic");
let red_on_blue = stylize("[white on blue]", "white on blue");
let rgb = stylize("[#3a95ef]", "#3a95ef");
let rgb_on_rgb = stylize("[#3a95ef on #c174dd]", "#3a95ef on #c174dd");
println!("pre {} post", red_bold);
println!("pre {} post", alt_red_bold);
println!("pre {} post", red_bold_italic);
println!("pre {} post", alt_red_bold_italic);
println!("pre {} post", red_on_blue);
println!("pre {} post", rgb);
println!("pre {} post", rgb_on_rgb);
println!("pre {} post", "[green]".stl("green").stl("+bold"));
println!("pre {} post", "[green+bold]".stl("green+bold"));
println!("pre {} post", "[.blue()]".blue());
println!("pre {} post", "[.blue().bold()]".blue().bold());
println!("pre {} post", "[.blue().on_red().bold()]".blue().on_red().bold());
Ok(())
}

7
lib/cli/colors.rs Normal file
View File

@ -0,0 +1,7 @@
mod style;
mod stylizer;
pub use stylizer::{stylize, Stylize};
#[cfg(test)]
mod tests;

148
lib/cli/colors/style.rs Normal file
View File

@ -0,0 +1,148 @@
use bitflags::bitflags;
use eyre::Context;
use std::borrow::Cow;
/// The 8 standard colors.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
TrueColor { r: u8, g: u8, b: u8 },
}
impl Color {
pub fn to_fg_str(&self) -> Cow<'static, str> {
match *self {
Color::Black => "30".into(),
Color::Red => "31".into(),
Color::Green => "32".into(),
Color::Yellow => "33".into(),
Color::Blue => "34".into(),
Color::Magenta => "35".into(),
Color::Cyan => "36".into(),
Color::White => "37".into(),
Color::BrightBlack => "90".into(),
Color::BrightRed => "91".into(),
Color::BrightGreen => "92".into(),
Color::BrightYellow => "93".into(),
Color::BrightBlue => "94".into(),
Color::BrightMagenta => "95".into(),
Color::BrightCyan => "96".into(),
Color::BrightWhite => "97".into(),
Color::TrueColor { r, g, b } => format!("38;2;{};{};{}", r, g, b).into(),
}
}
pub fn to_bg_str(&self) -> Cow<'static, str> {
match *self {
Color::Black => "40".into(),
Color::Red => "41".into(),
Color::Green => "42".into(),
Color::Yellow => "43".into(),
Color::Blue => "44".into(),
Color::Magenta => "45".into(),
Color::Cyan => "46".into(),
Color::White => "47".into(),
Color::BrightBlack => "100".into(),
Color::BrightRed => "101".into(),
Color::BrightGreen => "102".into(),
Color::BrightYellow => "103".into(),
Color::BrightBlue => "104".into(),
Color::BrightMagenta => "105".into(),
Color::BrightCyan => "106".into(),
Color::BrightWhite => "107".into(),
Color::TrueColor { r, g, b } => format!("48;2;{};{};{}", r, g, b).into(),
}
}
pub fn from_str(s: &str) -> eyre::Result<Color> {
let color = match s {
"" => None,
"black" => Some(Color::Black),
"red" => Some(Color::Red),
"green" => Some(Color::Green),
"yellow" => Some(Color::Yellow),
"blue" => Some(Color::Blue),
"magenta" => Some(Color::Magenta),
"cyan" => Some(Color::Cyan),
"white" => Some(Color::White),
"bright-black" => Some(Color::BrightBlack),
"bright-red" => Some(Color::BrightRed),
"bright-green" => Some(Color::BrightGreen),
"bright-yellow" => Some(Color::BrightYellow),
"bright-blue" => Some(Color::BrightBlue),
"bright-magenta" => Some(Color::BrightMagenta),
"bright-cyan" => Some(Color::BrightCyan),
"bright-white" => Some(Color::BrightWhite),
s if s.starts_with("#") => {
let s = &s[1..];
let r = u8::from_str_radix(&s[0..2], 16).context("Error parsing RGB color")?;
let g = u8::from_str_radix(&s[2..4], 16).context("Error parsing RGB color")?;
let b = u8::from_str_radix(&s[4..6], 16).context("Error parsing RGB color")?;
Some(Color::TrueColor { r, g, b })
}
_ => None,
};
color.ok_or_else(|| eyre::eyre!("Invalid color: '{}'", s))
}
}
bitflags! {
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct StyleAttributes: u8 {
const BOLD = 0b00000001;
const DIMMED = 0b00000010;
const ITALIC = 0b00000100;
const UNDERLINE = 0b00001000;
const BLINK = 0b00010000;
const REVERSED = 0b00100000;
const HIDDEN = 0b01000000;
const STRIKETHROUGH = 0b10000000;
}
}
impl StyleAttributes {
pub fn to_ansi_codes(&self) -> Vec<&'static str> {
let mut v = Vec::new();
if self.contains(StyleAttributes::BOLD) {
v.push("1");
}
if self.contains(StyleAttributes::DIMMED) {
v.push("2");
}
if self.contains(StyleAttributes::ITALIC) {
v.push("3");
}
if self.contains(StyleAttributes::UNDERLINE) {
v.push("4");
}
if self.contains(StyleAttributes::BLINK) {
v.push("5");
}
if self.contains(StyleAttributes::REVERSED) {
v.push("7");
}
if self.contains(StyleAttributes::HIDDEN) {
v.push("8");
}
if self.contains(StyleAttributes::STRIKETHROUGH) {
v.push("9");
}
v
}
}

187
lib/cli/colors/stylizer.rs Normal file
View File

@ -0,0 +1,187 @@
use super::style::{Color, StyleAttributes as StyleBitflags};
use eyre::Result;
pub mod instructions {
use super::{Result, StyleBitflags, Color};
use bitflags::parser::{from_str, ParseError};
pub struct StyledString {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub attrs: StyleBitflags,
}
impl Default for StyledString {
fn default() -> Self {
StyledString {
fg: None,
bg: None,
attrs: StyleBitflags::empty(),
}
}
}
/// parses the instruction into a StyledString
pub fn parse(instructions: &str) -> Result<StyledString> {
let mut styled_string = StyledString::default();
if instructions.starts_with("+") {
// only attributes
styled_string.attrs = attributes_from_str(instructions.trim_start_matches('+')).map_err(|e| eyre::eyre!(e))?;
return Ok(styled_string);
}
// try to separate the colors from the attributes
let parts = instructions.split('+').collect::<Vec<_>>();
match parts.len() {
1 => {
// no attributes
let (fg, bg) = parse_color_instruction(parts[0].trim())?;
styled_string.fg = fg;
styled_string.bg = bg;
}
2 => {
// attributes
let (fg, bg) = parse_color_instruction(parts[0].trim())?;
styled_string.fg = fg;
styled_string.bg = bg;
styled_string.attrs = attributes_from_str(parts[1]).map_err(|e| eyre::eyre!(e))?;
}
_ => {
return Err(eyre::eyre!("Invalid instruction: {}", instructions));
}
};
Ok(styled_string)
}
/// Parses the color instruction into a tuple of fg and bg colors.
pub fn parse_color_instruction(instruction: &str) -> Result<(Option<Color>, Option<Color>)> {
if instruction.starts_with("on ") {
// If instruction starts with "on ", bg color is provided
return Ok((None, Some(Color::from_str(&instruction[3..])?)));
}
let colors: Vec<&str> = instruction.split(" on ").collect();
match colors.len() {
1 => {
// Only fg color is provided
Ok((Some(Color::from_str(colors[0])?), None))
}
2 => {
// Both fg and bg colors are provided
Ok((Some(Color::from_str(colors[0])?), Some(Color::from_str(colors[1])?)))
}
_ => Err(eyre::eyre!("Invalid color instruction: {}", instruction)),
}
}
fn attributes_from_str (s: &str) -> Result<StyleBitflags, ParseError> {
from_str(s.to_uppercase().as_str())
}
}
/// 🧉 » Stylize Trait
/// --
///
/// `Trait` that extends `String` and `str` with the ability to stylize them
/// with ANSI color and attrs, using methods that return a new string with the given style.
pub trait Stylize {
/// Basic styling method, receives a styling instruction
/// see the `stylize` function for more information
fn stl(&self, instruction: &str) -> String;
fn black(&self) -> String { self.stl("black") }
fn red(&self) -> String { self.stl("red") }
fn green(&self) -> String { self.stl("green") }
fn yellow(&self) -> String { self.stl("yellow") }
fn blue(&self) -> String { self.stl("blue") }
fn magenta(&self) -> String { self.stl("magenta") }
fn cyan(&self) -> String { self.stl("cyan") }
fn white(&self) -> String { self.stl("white") }
fn bright_black(&self) -> String { self.stl("bright-black") }
fn bright_red(&self) -> String { self.stl("bright-red") }
fn bright_green(&self) -> String { self.stl("bright-green") }
fn bright_yellow(&self) -> String { self.stl("bright-yellow") }
fn bright_blue(&self) -> String { self.stl("bright-blue") }
fn bright_magenta(&self) -> String { self.stl("bright-magenta") }
fn bright_cyan(&self) -> String { self.stl("bright-cyan") }
fn bright_white(&self) -> String { self.stl("bright-white") }
fn rgb(&self, r: u8, g: u8, b: u8) -> String { self.stl(&format!("#{:02X}{:02X}{:02X}", r, g, b)) }
fn on_black(&self) -> String { self.stl("on black") }
fn on_red(&self) -> String { self.stl("on red") }
fn on_green(&self) -> String { self.stl("on green") }
fn on_yellow(&self) -> String { self.stl("on yellow") }
fn on_blue(&self) -> String { self.stl("on blue") }
fn on_magenta(&self) -> String { self.stl("on magenta") }
fn on_cyan(&self) -> String { self.stl("on cyan") }
fn on_white(&self) -> String { self.stl("on white") }
fn on_bright_black(&self) -> String { self.stl("on bright-black") }
fn on_bright_red(&self) -> String { self.stl("on bright-red") }
fn on_bright_green(&self) -> String { self.stl("on bright-green") }
fn on_bright_yellow(&self) -> String { self.stl("on bright-yellow") }
fn on_bright_blue(&self) -> String { self.stl("on bright-blue") }
fn on_bright_magenta(&self) -> String { self.stl("on bright-magenta") }
fn on_bright_cyan(&self) -> String { self.stl("on bright-cyan") }
fn on_bright_white(&self) -> String { self.stl("on bright-white") }
fn on_rgb(&self, r: u8, g: u8, b: u8) -> String { self.stl(&format!("on #{:02X}{:02X}{:02X}", r, g, b)) }
fn bold(&self) -> String { self.stl("+bold") }
fn dim(&self) -> String { self.stl("+dim") }
fn italic(&self) -> String { self.stl("+italic") }
fn underline(&self) -> String { self.stl("+underline") }
fn blink(&self) -> String { self.stl("+blink") }
fn reverse(&self) -> String { self.stl("+reverse") }
fn hidden(&self) -> String { self.stl("+hidden") }
fn strikethrough(&self) -> String { self.stl("+strikethrough") }
}
impl Stylize for str {
fn stl(&self, instruction: &str) -> String {
stylize(self, instruction)
}
}
impl Stylize for String {
fn stl(&self, instruction: &str) -> String {
stylize(self, instruction)
}
}
/// 🧉 » stylize fn
/// --
///
/// Stylizes a string with optional ANSI color and attributes.
///
pub fn stylize<S: AsRef<str>>(s: S, instructions: &str) -> String {
let styled_string = instructions::parse(instructions);
if styled_string.is_err() {
return s.as_ref().to_string();
}
let styled_string = styled_string.unwrap();
let mut formatted = String::new();
if let Some(fg) = styled_string.fg {
formatted.push_str(&format!("\x1b[{}m", fg.to_fg_str()));
}
if let Some(bg) = styled_string.bg {
formatted.push_str(&format!("\x1b[{}m", bg.to_bg_str()));
}
if !styled_string.attrs.is_empty() {
let ansi_codes = styled_string.attrs.to_ansi_codes();
if !ansi_codes.is_empty() {
formatted.push_str(&format!("\x1b[{}m", ansi_codes.join(";")));
}
}
// Append the original string and clear the style
formatted.push_str(s.as_ref());
formatted.push_str("\x1b[0m");
formatted
}

View File

@ -0,0 +1 @@
mod stylizer;

View File

@ -0,0 +1,103 @@
#[cfg(test)]
mod parse_instruction {
use {
crate::cli::colors::{
stylizer::instructions::{parse, parse_color_instruction}, style::{Color, StyleAttributes}
}, eyre::{set_hook, DefaultHandler}
};
fn setup_eyre() {
let _ = set_hook(Box::new(DefaultHandler::default_with));
}
#[test]
fn test_parse_color_instruction() -> eyre::Result<()> {
setup_eyre();
assert_eq!(parse_color_instruction("red")?, (Some(Color::Red), None));
assert_eq!(parse_color_instruction("#FF0000")?, (Some(Color::TrueColor { r: 255, g: 0, b: 0 }), None));
assert_eq!(parse_color_instruction("on blue")?, (None, Some(Color::Blue)));
assert_eq!(parse_color_instruction("on #0000FF")?, (None, Some(Color::TrueColor { r: 0, g: 0, b: 255 })));
assert_eq!(parse_color_instruction("red on blue")?, (Some(Color::Red), Some(Color::Blue)));
assert_eq!(parse_color_instruction("#FF0000 on #0000FF")?, (Some(Color::TrueColor { r: 255, g: 0, b: 0 }), Some(Color::TrueColor { r: 0, g: 0, b: 255 })));
assert!(parse_color_instruction("red on blue on green").is_err());
assert!(parse_color_instruction("red on").is_err());
assert!(parse_color_instruction("on").is_err());
assert!(parse_color_instruction("on red blue").is_err());
assert!(parse_color_instruction("red on blue on green").is_err());
assert!(parse_color_instruction("invalid").is_err());
assert!(parse_color_instruction("red on invalid").is_err());
assert!(parse_color_instruction("invalid on blue").is_err());
Ok(())
}
#[test]
fn test_parse_instructions() -> eyre::Result<()> {
setup_eyre();
let styled_string = parse("red on blue")?;
assert_eq!(styled_string.fg, Some(Color::Red));
assert_eq!(styled_string.bg, Some(Color::Blue));
assert_eq!(styled_string.attrs, StyleAttributes::empty());
let styled_string = parse("on blue")?;
assert_eq!(styled_string.fg, None);
assert_eq!(styled_string.bg, Some(Color::Blue));
assert_eq!(styled_string.attrs, StyleAttributes::empty());
let styled_string = parse("red")?;
assert_eq!(styled_string.fg, Some(Color::Red));
assert_eq!(styled_string.bg, None);
assert_eq!(styled_string.attrs, StyleAttributes::empty());
// ##RRGGBB
let styled_string = parse("#FF0000 on #0000FF")?;
assert_eq!(styled_string.fg, Some(Color::TrueColor { r: 255, g: 0, b: 0 }));
assert_eq!(styled_string.bg, Some(Color::TrueColor { r: 0, g: 0, b: 255 }));
assert_eq!(styled_string.attrs, StyleAttributes::empty());
let styled_string = parse("red on blue+bold")?;
assert_eq!(styled_string.fg, Some(Color::Red));
assert_eq!(styled_string.bg, Some(Color::Blue));
assert_eq!(styled_string.attrs, StyleAttributes::BOLD);
// ##RRGGBB
let styled_string = parse("#FF0000 on #0000FF+bold")?;
assert_eq!(styled_string.fg, Some(Color::TrueColor { r: 255, g: 0, b: 0 }));
assert_eq!(styled_string.bg, Some(Color::TrueColor { r: 0, g: 0, b: 255 }));
assert_eq!(styled_string.attrs, StyleAttributes::BOLD);
let styled_string = parse("red on blue+bold|underline")?;
assert_eq!(styled_string.fg, Some(Color::Red));
assert_eq!(styled_string.bg, Some(Color::Blue));
assert_eq!(styled_string.attrs, StyleAttributes::BOLD | StyleAttributes::UNDERLINE);
let styled_string = parse("red on #0000FF+bold|underline|italic")?;
assert_eq!(styled_string.fg, Some(Color::Red));
assert_eq!(styled_string.bg, Some(Color::TrueColor { r: 0, g: 0, b: 255 }));
assert_eq!(styled_string.attrs, StyleAttributes::BOLD | StyleAttributes::UNDERLINE | StyleAttributes::ITALIC);
let styled_string = parse("+bold")?;
assert_eq!(styled_string.fg, None);
assert_eq!(styled_string.bg, None);
assert_eq!(styled_string.attrs, StyleAttributes::BOLD);
let styled_string = parse("on red+bold")?;
assert_eq!(styled_string.fg, None);
assert_eq!(styled_string.bg, Some(Color::Red));
assert_eq!(styled_string.attrs, StyleAttributes::BOLD);
let styled_string = parse("red+bold")?;
assert_eq!(styled_string.fg, Some(Color::Red));
assert_eq!(styled_string.bg, None);
assert_eq!(styled_string.attrs, StyleAttributes::BOLD);
let styled_string = parse("red on blue+bold")?;
assert_eq!(styled_string.fg, Some(Color::Red));
assert_eq!(styled_string.bg, Some(Color::Blue));
Ok(())
}
}

1
lib/cli/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod colors;

1
lib/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod cli;

View File

@ -1,7 +0,0 @@
use eyre::Result;
fn main() -> Result<()> {
println!("Hello World");
Ok(())
}