1use crate::style::parse_colour;
14use crate::utf8::Utf8Config;
15#[path = "formats/colour.rs"]
16mod colour;
17#[path = "formats/condition.rs"]
18mod condition;
19#[path = "formats/context.rs"]
20mod context;
21#[path = "formats/expand.rs"]
22mod expand;
23#[path = "formats/expression.rs"]
24mod expression;
25#[path = "formats/glob.rs"]
26mod glob;
27#[path = "formats/modifiers.rs"]
28mod modifiers;
29#[path = "formats/regex_cache.rs"]
30mod regex_cache;
31#[path = "formats/scan.rs"]
32mod scan;
33#[path = "formats/styled_text.rs"]
34mod styled_text;
35#[path = "formats/time.rs"]
36mod time;
37#[path = "formats/transforms.rs"]
38mod transforms;
39
40use condition::{format_bool_op, format_conditional};
41pub use context::{
42 is_known_format_variable_name, FormatContext, FormatVariable, FormatVariables,
43 DEFAULT_DISPLAY_MESSAGE_FORMAT, DEFAULT_LIST_PANES_ALL_FORMAT, DEFAULT_LIST_PANES_FORMAT,
44 DEFAULT_LIST_PANES_SESSION_FORMAT, DEFAULT_LIST_PANES_WINDOW_FORMAT,
45 DEFAULT_LIST_SESSIONS_FORMAT, DEFAULT_LIST_WINDOWS_ALL_FORMAT, DEFAULT_LIST_WINDOWS_FORMAT,
46 FORMAT_VARIABLES, TMUX_FORMAT_TABLE_NAMES, TMUX_TIME_FORMAT_VARIABLE_NAMES,
47};
48use expand::{format_expand1, FORMAT_LOOP_LIMIT};
49use expression::format_expression;
50use glob::format_fnmatch;
51use modifiers::{parse_modifiers, FormatModifier};
52use scan::format_skip;
53pub use scan::format_skip_delimiter;
54pub use styled_text::{styled_text_width, truncate_styled_text_to_width};
55pub use time::expand_time_tokens;
56use time::format_time_string;
57use transforms::{
58 apply_substitution, format_unescape, shell_quote, style_quote, truncate_left, truncate_right,
59};
60
61const FORMAT_REPEAT_BYTES_LIMIT: usize = 5 * 1024 * 1024;
62
63#[must_use]
69pub fn render_template<V>(template: &str, variables: &V) -> String
70where
71 V: FormatVariables + ?Sized,
72{
73 let mut state = ExpandState {
74 loop_depth: 0,
75 expand_time: false,
76 stop_expansion: false,
77 preserve_jobs: false,
78 };
79 format_expand1(&mut state, template, variables)
80}
81
82#[must_use]
88pub fn render_template_preserving_jobs<V>(template: &str, variables: &V) -> String
89where
90 V: FormatVariables + ?Sized,
91{
92 let mut state = ExpandState {
93 loop_depth: 0,
94 expand_time: false,
95 stop_expansion: false,
96 preserve_jobs: true,
97 };
98 format_expand1(&mut state, template, variables)
99}
100
101#[must_use]
103pub fn render_list_windows_line<V>(variables: &V, format: Option<&str>) -> String
104where
105 V: FormatVariables + ?Sized,
106{
107 render_template(format.unwrap_or(DEFAULT_LIST_WINDOWS_FORMAT), variables)
108}
109
110#[must_use]
112pub fn render_list_sessions_line<V>(variables: &V, format: Option<&str>) -> String
113where
114 V: FormatVariables + ?Sized,
115{
116 render_template(format.unwrap_or(DEFAULT_LIST_SESSIONS_FORMAT), variables)
117}
118
119#[must_use]
121pub fn render_list_panes_line<V>(variables: &V, format: Option<&str>) -> String
122where
123 V: FormatVariables + ?Sized,
124{
125 render_template(format.unwrap_or(DEFAULT_LIST_PANES_FORMAT), variables)
126}
127
128#[must_use]
132pub fn is_truthy(value: &str) -> bool {
133 !value.is_empty() && value != "0"
134}
135
136struct ExpandState {
141 loop_depth: u32,
142 expand_time: bool,
143 stop_expansion: bool,
144 preserve_jobs: bool,
145}
146
147fn format_choose<V>(state: &mut ExpandState, body: &str, variables: &V) -> Option<(String, String)>
154where
155 V: FormatVariables + ?Sized,
156{
157 let bytes = body.as_bytes();
158 let pos = format_skip(bytes, b",")?;
159 let left_raw = &body[..pos];
160 let right_raw = &body[pos + 1..];
161 let left = format_expand1(state, left_raw, variables);
162 let right = format_expand1(state, right_raw, variables);
163 Some((left, right))
164}
165
166const MOD_LITERAL: u32 = 1 << 0;
172const MOD_EXPAND: u32 = 1 << 1;
173const MOD_QUOTE_SHELL: u32 = 1 << 4;
174const MOD_QUOTE_STYLE: u32 = 1 << 5;
175const MOD_BASENAME: u32 = 1 << 6;
176const MOD_DIRNAME: u32 = 1 << 7;
177const MOD_LENGTH: u32 = 1 << 8;
178const MOD_EXPAND_TIME: u32 = 1 << 9;
179const FORMAT_PADDING_LIMIT: usize = 10_000;
180
181fn format_replace<V>(state: &mut ExpandState, key: &str, variables: &V) -> String
184where
185 V: FormatVariables + ?Sized,
186{
187 let (modifiers, body) = parse_modifiers(state, key, variables);
188
189 let mut flags: u32 = 0;
191 let mut cmp: Option<&FormatModifier> = None;
192 let mut bool_op_n: Option<&FormatModifier> = None;
193 let mut limit: i32 = 0;
194 let mut limit_marker: Option<&str> = None;
195 let mut width: i32 = 0;
196 let mut subs: Vec<&FormatModifier> = Vec::new();
197 let mut time_string = false;
198 let mut time_pretty = false;
199 let mut time_format: Option<String> = None;
200 let mut deferred_loop_scope = None;
201 let mut ascii_char = false;
202 let mut colour_hex = false;
203 let mut display_width = false;
204 let mut name_exists: Option<&FormatModifier> = None;
205 let mut expression: Option<&FormatModifier> = None;
206 let mut search: Option<&FormatModifier> = None;
207 let mut bool_unary: Option<&FormatModifier> = None;
208 let mut repeat = false;
209
210 for fm in &modifiers {
211 if fm.modifier.len() == 1 {
212 match fm.modifier.as_bytes()[0] {
213 b'm' | b'<' | b'>' => cmp = Some(fm),
214 b'a' => ascii_char = true,
215 b'c' => colour_hex = true,
216 b's' if fm.argv.len() >= 2 => {
217 subs.push(fm);
218 }
219 b'=' => {
220 if let Some(arg) = fm.argv.first() {
221 limit = arg.parse::<i32>().unwrap_or(0);
222 if fm.argv.len() >= 2 {
223 limit_marker = fm.argv.get(1).map(String::as_str);
224 }
225 }
226 }
227 b'p' => {
228 if let Some(arg) = fm.argv.first() {
229 width = arg.parse::<i32>().unwrap_or(0);
230 }
231 }
232 b'l' => flags |= MOD_LITERAL,
233 b'b' => flags |= MOD_BASENAME,
234 b'd' => flags |= MOD_DIRNAME,
235 b'n' => flags |= MOD_LENGTH,
236 b'q' => {
237 if fm.argv.is_empty() {
238 flags |= MOD_QUOTE_SHELL;
239 } else if let Some(arg) = fm.argv.first() {
240 if arg.contains('e') || arg.contains('h') {
241 flags |= MOD_QUOTE_STYLE;
242 }
243 }
244 }
245 b'E' => flags |= MOD_EXPAND,
246 b'T' => flags |= MOD_EXPAND_TIME,
247 b't' => {
248 time_string = true;
249 if let Some(arg) = fm.argv.first() {
250 if arg.contains('p') {
251 time_pretty = true;
252 } else if arg.contains('f') {
253 time_format = fm.argv.get(1).cloned();
254 }
255 }
256 }
257 b'S' | b'W' | b'P' | b'L' => {
258 deferred_loop_scope = Some(fm.modifier.as_bytes()[0] as char)
259 }
260 b'N' => name_exists = Some(fm),
261 b'e' => expression = Some(fm),
262 b'C' => search = Some(fm),
263 b'w' => display_width = true,
264 b'!' => bool_unary = Some(fm),
265 b'R' => repeat = true,
266 _ => {}
267 }
268 } else if fm.modifier.len() == 2 {
269 match fm.modifier.as_str() {
270 "||" | "&&" => bool_op_n = Some(fm),
271 "!!" => bool_unary = Some(fm),
272 "==" | "!=" | "<=" | ">=" => cmp = Some(fm),
273 _ => {}
274 }
275 }
276 }
277
278 if let Some(scope) = deferred_loop_scope {
279 let (body, current_body) = if scope == 'S' {
280 (body, None)
281 } else {
282 split_loop_body(body)
283 };
284 if let Some(value) = variables.format_loop(scope, body, current_body, false) {
285 return value;
286 }
287 }
288 if let Some(modifier) = name_exists {
289 let scope = match modifier.argv.first().map(String::as_str) {
290 None | Some("") | Some("w") => None,
291 Some("s") => Some('s'),
292 Some(_) => return String::new(),
293 };
294 let operand = format_expand1(state, body, variables);
295 return variables
296 .format_name_exists(scope, &operand)
297 .map(bool_value)
298 .unwrap_or_default();
299 }
300 if let Some(modifier) = search {
301 let options = modifier
302 .argv
303 .first()
304 .map(String::as_str)
305 .unwrap_or_default();
306 let pattern = format_expand1(state, body, variables);
307 return variables
308 .format_search(options, &pattern)
309 .unwrap_or_default();
310 }
311
312 if flags & MOD_LITERAL != 0 {
316 return format_unescape(body);
317 }
318
319 let value;
320
321 if let Some(op) = bool_unary {
322 let truthy = is_truthy(&format_expand1(state, body, variables));
323 value = match op.modifier.as_str() {
324 "!" => bool_value(!truthy),
325 "!!" => bool_value(truthy),
326 _ => String::new(),
327 };
328 } else if repeat {
329 value = format_repeat(state, body, variables);
330 } else if let Some(op) = bool_op_n {
331 let is_and = op.modifier == "&&";
333 value = format_bool_op(state, body, is_and, variables);
334 } else if let Some(cmp_mod) = cmp {
335 value = match format_choose(state, body, variables) {
337 Some((left, right)) => {
338 let result = match cmp_mod.modifier.as_str() {
339 "==" => left == right,
340 "!=" => left != right,
341 "<" => left < right,
342 "<=" => left <= right,
343 ">" => left > right,
344 ">=" => left >= right,
345 "m" => format_fnmatch(&left, &right, cmp_mod),
346 _ => false,
347 };
348 if result { "1" } else { "0" }.to_owned()
349 }
350 None => String::new(),
351 };
352 } else if let Some(cond_body) = body.strip_prefix('?') {
353 value = format_conditional(state, cond_body, variables);
355 } else if let Some(expression) = expression {
356 value = format_expression(state, body, expression, variables);
357 } else {
358 if body.contains("#{") {
360 value = format_expand1(state, body, variables);
361 } else {
362 value = resolve_variable(body, variables);
363 }
364 }
365
366 let mut result = value;
368
369 if time_string {
370 result =
371 format_time_string(&result, time_pretty, time_format.as_deref()).unwrap_or_default();
372 }
373
374 if flags & MOD_EXPAND != 0 {
376 result = format_expand1(state, &result, variables);
377 result = expand_time_tokens(&result);
378 } else if flags & MOD_EXPAND_TIME != 0 {
379 let previous = state.expand_time;
380 state.expand_time = true;
381 result = format_expand1(state, &result, variables);
382 state.expand_time = previous;
383 }
384
385 for sub in &subs {
387 if sub.argv.len() >= 2 {
388 result = apply_substitution(&result, sub);
389 }
390 }
391
392 if limit > 0 {
394 let truncated = truncate_left(&result, limit as usize);
395 if truncated != result {
396 if let Some(marker) = limit_marker {
397 result = format!("{truncated}{marker}");
398 } else {
399 result = truncated;
400 }
401 } else {
402 result = truncated;
403 }
404 } else if limit < 0 {
405 let truncated = truncate_right(&result, signed_i32_abs_usize(limit));
406 if truncated != result {
407 if let Some(marker) = limit_marker {
408 result = format!("{marker}{truncated}");
409 } else {
410 result = truncated;
411 }
412 } else {
413 result = truncated;
414 }
415 }
416
417 if width > 0 {
419 if let Some(w) = bounded_format_padding_width(width) {
420 let current_width = styled_text_width(&result, &Utf8Config::default());
421 if current_width < w {
422 result.push_str(&" ".repeat(w - current_width));
423 }
424 }
425 } else if width < 0 {
426 if let Some(w) = bounded_format_padding_width(width) {
427 let current_width = styled_text_width(&result, &Utf8Config::default());
428 if current_width < w {
429 result = format!("{}{result}", " ".repeat(w - current_width));
430 }
431 }
432 }
433
434 if flags & MOD_BASENAME != 0 {
436 if let Some(pos) = result.rfind('/') {
437 result = result[pos + 1..].to_owned();
438 }
439 }
440
441 if flags & MOD_DIRNAME != 0 {
443 result = format_dirname(&result);
444 }
445
446 if flags & MOD_LENGTH != 0 {
448 result = result.len().to_string();
449 }
450
451 if display_width {
452 result = format_display_width(&result, body, state, variables);
453 }
454
455 if ascii_char {
456 result = format_ascii_character(&result, body, state, variables);
457 }
458
459 if colour_hex {
460 result = format_colour_hex(&result, body, state, variables);
461 }
462
463 if flags & MOD_QUOTE_SHELL != 0 && !is_single_nested_expansion(body) {
465 result = shell_quote(&result);
466 } else if flags & MOD_QUOTE_STYLE != 0 {
467 result = style_quote(&result);
468 }
469
470 result
471}
472
473fn signed_i32_abs_usize(value: i32) -> usize {
474 value.unsigned_abs() as usize
475}
476
477fn bounded_format_padding_width(value: i32) -> Option<usize> {
478 let width = signed_i32_abs_usize(value);
479 (width <= FORMAT_PADDING_LIMIT).then_some(width)
480}
481
482fn format_repeat<V>(state: &mut ExpandState, body: &str, variables: &V) -> String
483where
484 V: FormatVariables + ?Sized,
485{
486 let Some((unit, count)) = format_choose(state, body, variables) else {
487 state.stop_expansion = true;
488 return String::new();
489 };
490 let Ok(count) = count.parse::<usize>() else {
491 return String::new();
492 };
493 if count == 0 || count > FORMAT_PADDING_LIMIT || unit.is_empty() {
494 return String::new();
495 }
496 let Some(capacity) = unit.len().checked_mul(count) else {
497 return String::new();
498 };
499 if capacity > FORMAT_REPEAT_BYTES_LIMIT {
500 return String::new();
501 }
502 let mut repeated = String::with_capacity(capacity);
503 for _ in 0..count {
504 repeated.push_str(&unit);
505 }
506 repeated
507}
508
509fn split_loop_body(body: &str) -> (&str, Option<&str>) {
510 format_skip(body.as_bytes(), b",")
511 .map(|offset| (&body[..offset], Some(&body[offset + 1..])))
512 .unwrap_or((body, None))
513}
514
515fn is_single_nested_expansion(body: &str) -> bool {
516 body.starts_with("#{") && format_skip(body.as_bytes(), b"}") == Some(body.len() - 1)
517}
518
519fn format_ascii_character<V>(
520 _result: &str,
521 body: &str,
522 state: &mut ExpandState,
523 variables: &V,
524) -> String
525where
526 V: FormatVariables + ?Sized,
527{
528 let operand = format_expand1(state, body, variables);
529 operand
530 .parse::<u32>()
531 .ok()
532 .and_then(|value| u8::try_from(value).ok())
533 .map(char::from)
534 .map(|character| character.to_string())
535 .unwrap_or_default()
536}
537
538fn format_colour_hex<V>(_result: &str, body: &str, state: &mut ExpandState, variables: &V) -> String
539where
540 V: FormatVariables + ?Sized,
541{
542 let operand = format_expand1(state, body, variables);
543 parse_colour(&operand)
544 .ok()
545 .and_then(colour::tmux_colour_to_rgb)
546 .map(|(red, green, blue)| format!("{red:02x}{green:02x}{blue:02x}"))
547 .unwrap_or_default()
548}
549
550fn format_display_width<V>(
551 result: &str,
552 _body: &str,
553 _state: &mut ExpandState,
554 _variables: &V,
555) -> String
556where
557 V: FormatVariables + ?Sized,
558{
559 styled_text_width(result, &Utf8Config::default()).to_string()
560}
561
562fn format_dirname(value: &str) -> String {
563 if value.is_empty() {
564 return String::new();
565 }
566 if value.chars().all(|character| character == '/') {
567 return value.to_owned();
568 }
569
570 let trimmed = value.trim_end_matches('/');
571 match trimmed.rfind('/') {
572 Some(0) => "/".to_owned(),
573 Some(position) => trimmed[..position].to_owned(),
574 None => ".".to_owned(),
575 }
576}
577
578fn resolve_variable<V>(name: &str, variables: &V) -> String
583where
584 V: FormatVariables + ?Sized,
585{
586 variables.format_value_by_name(name).unwrap_or_default()
587}
588
589fn window_raw_flags(active: bool, last: bool) -> &'static str {
590 if active {
591 "*"
592 } else if last {
593 "-"
594 } else {
595 ""
596 }
597}
598
599fn bool_value(value: bool) -> String {
600 if value {
601 "1".to_owned()
602 } else {
603 "0".to_owned()
604 }
605}
606
607#[cfg(test)]
608#[path = "formats/tests.rs"]
609mod tests;