Skip to main content

nu_command/conversions/
fill.rs

1use nu_cmd_base::input_handler::{CmdArgument, operate};
2use nu_engine::command_prelude::*;
3
4use nu_protocol::FromValue;
5use print_positions::print_positions;
6
7#[derive(Clone)]
8pub struct Fill;
9
10struct Arguments {
11    width: usize,
12    alignment: FillAlignment,
13    character: String,
14    cell_paths: Option<Vec<CellPath>>,
15}
16
17impl CmdArgument for Arguments {
18    fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
19        self.cell_paths.take()
20    }
21}
22
23#[derive(Clone, Copy, Default)]
24enum FillAlignment {
25    #[default]
26    Left,
27    Right,
28    Middle,
29    MiddleRight,
30}
31
32impl FromValue for FillAlignment {
33    fn from_value(v: Value) -> Result<Self, ShellError> {
34        Ok(match String::from_value(v)?.to_ascii_lowercase().as_str() {
35            "l" | "left" => Self::Left,
36            "r" | "right" => Self::Right,
37            "c" | "center" | "m" | "middle" => Self::Middle,
38            "cr" | "centerright" | "mr" | "middleright" => Self::MiddleRight,
39            // TODO: This should probably be an error
40            _ => Self::Left,
41        })
42    }
43}
44
45impl Command for Fill {
46    fn name(&self) -> &str {
47        "fill"
48    }
49
50    fn description(&self) -> &str {
51        "Fill and align text in columns."
52    }
53
54    fn signature(&self) -> nu_protocol::Signature {
55        Signature::build("fill")
56            .input_output_types(vec![
57                (Type::Int, Type::String),
58                (Type::Float, Type::String),
59                (Type::String, Type::String),
60                (Type::Filesize, Type::String),
61                (
62                    Type::List(Box::new(Type::Int)),
63                    Type::List(Box::new(Type::String)),
64                ),
65                (
66                    Type::List(Box::new(Type::Float)),
67                    Type::List(Box::new(Type::String)),
68                ),
69                (
70                    Type::List(Box::new(Type::String)),
71                    Type::List(Box::new(Type::String)),
72                ),
73                (
74                    Type::List(Box::new(Type::Filesize)),
75                    Type::List(Box::new(Type::String)),
76                ),
77                // General case for heterogeneous lists
78                (
79                    Type::List(Box::new(Type::Any)),
80                    Type::List(Box::new(Type::String)),
81                ),
82            ])
83            .allow_variants_without_examples(true)
84            .named(
85                "width",
86                SyntaxShape::Int,
87                "The width of the output. Defaults to 1.",
88                Some('w'),
89            )
90            .param(
91                Flag::new("alignment")
92                    .short('a')
93                    .arg(SyntaxShape::String)
94                    .desc(
95                        "The alignment of the output. Defaults to Left (Left(l), Right(r), Center(c/m), MiddleRight(cr/mr)).",
96                    )
97                    .completion(Completion::new_list(&[
98                        "left",
99                        "right",
100                        "middle",
101                        "middleright",
102                    ])),
103            )
104            .named(
105                "character",
106                SyntaxShape::String,
107                "The character to fill with. Defaults to ' ' (space).",
108                Some('c'),
109            )
110            .category(Category::Conversions)
111    }
112
113    fn search_terms(&self) -> Vec<&str> {
114        vec!["display", "render", "format", "pad", "align", "repeat"]
115    }
116
117    fn examples(&self) -> Vec<Example<'_>> {
118        vec![
119            Example {
120                description: "Fill a string on the left side to a width of 15 with the character '─'.",
121                example: "'nushell' | fill --alignment l --character '─' --width 15",
122                result: Some(Value::string("nushell────────", Span::test_data())),
123            },
124            Example {
125                description: "Fill a string on the right side to a width of 15 with the character '─'.",
126                example: "'nushell' | fill --alignment r --character '─' --width 15",
127                result: Some(Value::string("────────nushell", Span::test_data())),
128            },
129            Example {
130                description: "Fill an empty string with 10 '─' characters.",
131                example: "'' | fill --character '─' --width 10",
132                result: Some(Value::string("──────────", Span::test_data())),
133            },
134            Example {
135                description: "Fill a number on the left side to a width of 5 with the character '0'.",
136                example: "1 | fill --alignment right --character '0' --width 5",
137                result: Some(Value::string("00001", Span::test_data())),
138            },
139            Example {
140                description: "Fill a number on both sides to a width of 5 with the character '0'.",
141                example: "1.1 | fill --alignment center --character '0' --width 5",
142                result: Some(Value::string("01.10", Span::test_data())),
143            },
144            Example {
145                description: "Fill a filesize on both sides to a width of 10 with the character '0'.",
146                example: "1kib | fill --alignment middle --character '0' --width 10",
147                result: Some(Value::string("0001024000", Span::test_data())),
148            },
149        ]
150    }
151
152    fn run(
153        &self,
154        engine_state: &EngineState,
155        stack: &mut Stack,
156        call: &Call,
157        input: PipelineData,
158    ) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
159        fill(engine_state, stack, call, input)
160    }
161}
162
163fn fill(
164    engine_state: &EngineState,
165    stack: &mut Stack,
166    call: &Call,
167    input: PipelineData,
168) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
169    let width_arg: Option<usize> = call.get_flag(engine_state, stack, "width")?;
170    let alignment = call
171        .get_flag::<FillAlignment>(engine_state, stack, "alignment")?
172        .unwrap_or_default();
173    let character_arg: Option<String> = call.get_flag(engine_state, stack, "character")?;
174    let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
175    let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
176
177    let width = width_arg.unwrap_or(1);
178
179    let character = character_arg.unwrap_or_else(|| " ".to_string());
180
181    let arg = Arguments {
182        width,
183        alignment,
184        character,
185        cell_paths,
186    };
187
188    operate(action, arg, input, call.head, engine_state.signals())
189}
190
191fn action(input: &Value, args: &Arguments, span: Span) -> Value {
192    match input {
193        Value::Int { val, .. } => fill_int(*val, args, span),
194        Value::Filesize { val, .. } => fill_int(val.get(), args, span),
195        Value::Float { val, .. } => fill_float(*val, args, span),
196        Value::String { val, .. } => fill_string(val, args, span),
197        // Propagate errors by explicitly matching them before the final case.
198        Value::Error { .. } => input.clone(),
199        other => Value::error(
200            ShellError::OnlySupportsThisInputType {
201                exp_input_type: "int, filesize, float, string".into(),
202                wrong_type: other.get_type().to_string(),
203                dst_span: span,
204                src_span: other.span(),
205            },
206            span,
207        ),
208    }
209}
210
211fn fill_float(num: f64, args: &Arguments, span: Span) -> Value {
212    let s = num.to_string();
213    let out_str = pad(&s, args.width, &args.character, args.alignment, false);
214
215    Value::string(out_str, span)
216}
217fn fill_int(num: i64, args: &Arguments, span: Span) -> Value {
218    let s = num.to_string();
219    let out_str = pad(&s, args.width, &args.character, args.alignment, false);
220
221    Value::string(out_str, span)
222}
223fn fill_string(s: &str, args: &Arguments, span: Span) -> Value {
224    let out_str = pad(s, args.width, &args.character, args.alignment, false);
225
226    Value::string(out_str, span)
227}
228
229fn pad(s: &str, width: usize, pad_char: &str, alignment: FillAlignment, truncate: bool) -> String {
230    // Attribution: Most of this function was taken from https://github.com/ogham/rust-pad and tweaked. Thank you!
231    // Use width instead of len for graphical display
232
233    let cols = print_positions(s).count();
234
235    if cols >= width {
236        if truncate {
237            return s[..width].to_string();
238        } else {
239            return s.to_string();
240        }
241    }
242
243    let diff = width - cols;
244
245    let (left_pad, right_pad) = match alignment {
246        FillAlignment::Left => (0, diff),
247        FillAlignment::Right => (diff, 0),
248        FillAlignment::Middle => (diff / 2, diff - diff / 2),
249        FillAlignment::MiddleRight => (diff - diff / 2, diff / 2),
250    };
251
252    let mut new_str = String::new();
253    for _ in 0..left_pad {
254        new_str.push_str(pad_char)
255    }
256    new_str.push_str(s);
257    for _ in 0..right_pad {
258        new_str.push_str(pad_char)
259    }
260    new_str
261}
262
263#[cfg(test)]
264mod test {
265    use super::*;
266
267    #[test]
268    fn test_examples() -> nu_test_support::Result {
269        nu_test_support::test().examples(Fill)
270    }
271}