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 _ => 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 (
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 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 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}