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