Skip to main content

rmux_core/formats/
context.rs

1use rmux_proto::TerminalSize;
2
3use super::{bool_value, window_raw_flags};
4use crate::{Pane, PaneId, Session, Window, WindowId};
5
6/// The default `list-windows` line format.
7pub const DEFAULT_LIST_WINDOWS_FORMAT: &str =
8    "#{window_index}: #{window_name}#{window_raw_flags} (#{window_panes} panes) [#{window_width}x#{window_height}] [layout #{window_layout}] #{window_id}#{?window_active, (active),}";
9
10/// The default `list-windows -a` line format.
11pub const DEFAULT_LIST_WINDOWS_ALL_FORMAT: &str =
12    "#{session_name}:#{window_index}: #{window_name}#{window_raw_flags} (#{window_panes} panes) [#{window_width}x#{window_height}] ";
13
14/// The default `list-sessions` line format.
15pub const DEFAULT_LIST_SESSIONS_FORMAT: &str =
16    "#{session_name}: #{session_windows} windows (created #{t:session_created})#{?session_grouped, (group #{session_group}),}#{?session_attached, (attached),}";
17
18/// The default `list-panes` line format for a single window.
19pub const DEFAULT_LIST_PANES_WINDOW_FORMAT: &str =
20    "#{pane_index}: [#{pane_width}x#{pane_height}] [history #{history_size}/#{history_limit}, #{history_bytes} bytes] #{pane_id}#{?pane_active, (active),}#{?pane_dead, (dead),}";
21
22/// The default `list-panes -s` line format for a single session.
23pub const DEFAULT_LIST_PANES_SESSION_FORMAT: &str =
24    "#{window_index}.#{pane_index}: [#{pane_width}x#{pane_height}] [history #{history_size}/#{history_limit}, #{history_bytes} bytes] #{pane_id}#{?pane_active, (active),}#{?pane_dead, (dead),}";
25
26/// The default `list-panes -a` line format.
27pub const DEFAULT_LIST_PANES_ALL_FORMAT: &str =
28    "#{session_name}:#{window_index}.#{pane_index}: [#{pane_width}x#{pane_height}] [history #{history_size}/#{history_limit}, #{history_bytes} bytes] #{pane_id}#{?pane_active, (active),}#{?pane_dead, (dead),}";
29
30/// Backward-compatible alias for callers that need the fully qualified form.
31pub const DEFAULT_LIST_PANES_FORMAT: &str = DEFAULT_LIST_PANES_ALL_FORMAT;
32
33/// The default `display-message` format.
34pub const DEFAULT_DISPLAY_MESSAGE_FORMAT: &str =
35    "[#{session_name}] #{window_index}:#{window_name}, current pane #{pane_index} - (%H:%M %d-%b-%y)";
36
37/// The frozen tmux `format_table[]` variable inventory from the reference source.
38pub const TMUX_FORMAT_TABLE_NAMES: [&str; 192] = [
39    "active_window_index",
40    "alternate_on",
41    "alternate_saved_x",
42    "alternate_saved_y",
43    "bracket_paste_flag",
44    "buffer_created",
45    "buffer_full",
46    "buffer_mode_format",
47    "buffer_name",
48    "buffer_sample",
49    "buffer_size",
50    "client_activity",
51    "client_cell_height",
52    "client_cell_width",
53    "client_control_mode",
54    "client_created",
55    "client_discarded",
56    "client_flags",
57    "client_height",
58    "client_key_table",
59    "client_last_session",
60    "client_mode_format",
61    "client_name",
62    "client_pid",
63    "client_prefix",
64    "client_readonly",
65    "client_session",
66    "client_termfeatures",
67    "client_termname",
68    "client_termtype",
69    "client_theme",
70    "client_tty",
71    "client_uid",
72    "client_user",
73    "client_utf8",
74    "client_width",
75    "client_written",
76    "config_files",
77    "cursor_blinking",
78    "cursor_character",
79    "cursor_colour",
80    "cursor_flag",
81    "cursor_shape",
82    "cursor_very_visible",
83    "cursor_x",
84    "cursor_y",
85    "history_all_bytes",
86    "history_bytes",
87    "history_limit",
88    "history_size",
89    "host",
90    "host_short",
91    "insert_flag",
92    "keypad_cursor_flag",
93    "keypad_flag",
94    "last_window_index",
95    "loop_last_flag",
96    "mouse_all_flag",
97    "mouse_any_flag",
98    "mouse_button_flag",
99    "mouse_hyperlink",
100    "mouse_line",
101    "mouse_pane",
102    "mouse_sgr_flag",
103    "mouse_standard_flag",
104    "mouse_status_line",
105    "mouse_status_range",
106    "mouse_utf8_flag",
107    "mouse_word",
108    "mouse_x",
109    "mouse_y",
110    "next_session_id",
111    "origin_flag",
112    "pane_active",
113    "pane_at_bottom",
114    "pane_at_left",
115    "pane_at_right",
116    "pane_at_top",
117    "pane_bg",
118    "pane_bottom",
119    "pane_current_command",
120    "pane_current_path",
121    "pane_dead",
122    "pane_dead_signal",
123    "pane_dead_status",
124    "pane_dead_time",
125    "pane_fg",
126    "pane_flags",
127    "pane_floating_flag",
128    "pane_format",
129    "pane_height",
130    "pane_id",
131    "pane_in_mode",
132    "pane_index",
133    "pane_input_off",
134    "pane_key_mode",
135    "pane_last",
136    "pane_left",
137    "pane_marked",
138    "pane_marked_set",
139    "pane_mode",
140    "pane_path",
141    "pane_pb_progress",
142    "pane_pb_state",
143    "pane_pid",
144    "pane_pipe",
145    "pane_pipe_pid",
146    "pane_right",
147    "pane_search_string",
148    "pane_start_command",
149    "pane_start_path",
150    "pane_synchronized",
151    "pane_tabs",
152    "pane_title",
153    "pane_top",
154    "pane_tty",
155    "pane_unseen_changes",
156    "pane_width",
157    "pane_zoomed_flag",
158    "pid",
159    "scroll_region_lower",
160    "scroll_region_upper",
161    "server_sessions",
162    "session_active",
163    "session_activity",
164    "session_activity_flag",
165    "session_alert",
166    "session_alerts",
167    "session_attached",
168    "session_attached_list",
169    "session_bell_flag",
170    "session_created",
171    "session_format",
172    "session_group",
173    "session_group_attached",
174    "session_group_attached_list",
175    "session_group_list",
176    "session_group_many_attached",
177    "session_group_size",
178    "session_grouped",
179    "session_id",
180    "session_last_attached",
181    "session_many_attached",
182    "session_marked",
183    "session_name",
184    "session_path",
185    "session_silence_flag",
186    "session_stack",
187    "session_windows",
188    "sixel_support",
189    "socket_path",
190    "start_time",
191    "synchronized_output_flag",
192    "tree_mode_format",
193    "uid",
194    "user",
195    "version",
196    "window_active",
197    "window_active_clients",
198    "window_active_clients_list",
199    "window_active_sessions",
200    "window_active_sessions_list",
201    "window_activity",
202    "window_activity_flag",
203    "window_bell_flag",
204    "window_bigger",
205    "window_cell_height",
206    "window_cell_width",
207    "window_end_flag",
208    "window_flags",
209    "window_format",
210    "window_height",
211    "window_id",
212    "window_index",
213    "window_last_flag",
214    "window_layout",
215    "window_linked",
216    "window_linked_sessions",
217    "window_linked_sessions_list",
218    "window_marked_flag",
219    "window_name",
220    "window_offset_x",
221    "window_offset_y",
222    "window_panes",
223    "window_raw_flags",
224    "window_silence_flag",
225    "window_stack_index",
226    "window_start_flag",
227    "window_visible_layout",
228    "window_width",
229    "window_zoomed_flag",
230    "wrap_flag",
231];
232
233/// Frozen tmux `FORMAT_TABLE_TIME` variable names.
234pub const TMUX_TIME_FORMAT_VARIABLE_NAMES: [&str; 9] = [
235    "buffer_created",
236    "client_activity",
237    "client_created",
238    "pane_dead_time",
239    "session_activity",
240    "session_created",
241    "session_last_attached",
242    "start_time",
243    "window_activity",
244];
245
246/// The closed set of format variables supported by RMUX.
247pub const FORMAT_VARIABLES: [FormatVariable; 20] = [
248    FormatVariable::SessionName,
249    FormatVariable::SessionWindows,
250    FormatVariable::SessionAttached,
251    FormatVariable::SessionWidth,
252    FormatVariable::SessionHeight,
253    FormatVariable::WindowIndex,
254    FormatVariable::WindowId,
255    FormatVariable::WindowName,
256    FormatVariable::WindowRawFlags,
257    FormatVariable::WindowPanes,
258    FormatVariable::WindowWidth,
259    FormatVariable::WindowHeight,
260    FormatVariable::WindowLayout,
261    FormatVariable::WindowActive,
262    FormatVariable::WindowLastFlag,
263    FormatVariable::PaneIndex,
264    FormatVariable::PaneId,
265    FormatVariable::PaneActive,
266    FormatVariable::PaneWidth,
267    FormatVariable::PaneHeight,
268];
269
270/// A supported format variable name.
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
272pub enum FormatVariable {
273    /// The session name.
274    SessionName,
275    /// The number of windows in the session.
276    SessionWindows,
277    /// The number of clients attached to the session.
278    SessionAttached,
279    /// The session width in columns.
280    SessionWidth,
281    /// The session height in rows.
282    SessionHeight,
283    /// The window index within the session.
284    WindowIndex,
285    /// The stable window identifier, rendered with an `@` prefix.
286    WindowId,
287    /// The window name, or an empty string when unnamed.
288    WindowName,
289    /// The derived active or last-window marker.
290    WindowRawFlags,
291    /// The number of panes in the window.
292    WindowPanes,
293    /// The window width in columns.
294    WindowWidth,
295    /// The window height in rows.
296    WindowHeight,
297    /// The window layout name.
298    WindowLayout,
299    /// Whether the window is active, rendered as `1` or `0`.
300    WindowActive,
301    /// Whether the window is the last window, rendered as `1` or `0`.
302    WindowLastFlag,
303    /// The pane index within the window.
304    PaneIndex,
305    /// The stable pane identifier, rendered with a `%` prefix.
306    PaneId,
307    /// Whether the pane is active, rendered as `1` or `0`.
308    PaneActive,
309    /// The pane width in columns.
310    PaneWidth,
311    /// The pane height in rows.
312    PaneHeight,
313}
314
315impl FormatVariable {
316    /// Parses a supported format variable name.
317    #[must_use]
318    pub fn from_name(name: &str) -> Option<Self> {
319        Some(match name {
320            "session_name" => Self::SessionName,
321            "session_windows" => Self::SessionWindows,
322            "session_attached" => Self::SessionAttached,
323            "session_width" => Self::SessionWidth,
324            "session_height" => Self::SessionHeight,
325            "window_index" => Self::WindowIndex,
326            "window_id" => Self::WindowId,
327            "window_name" => Self::WindowName,
328            "window_raw_flags" => Self::WindowRawFlags,
329            "window_panes" => Self::WindowPanes,
330            "window_width" => Self::WindowWidth,
331            "window_height" => Self::WindowHeight,
332            "window_layout" => Self::WindowLayout,
333            "window_active" => Self::WindowActive,
334            "window_last_flag" => Self::WindowLastFlag,
335            "pane_index" => Self::PaneIndex,
336            "pane_id" => Self::PaneId,
337            "pane_active" => Self::PaneActive,
338            "pane_width" => Self::PaneWidth,
339            "pane_height" => Self::PaneHeight,
340            _ => return None,
341        })
342    }
343
344    /// Returns the canonical supported variable name.
345    #[must_use]
346    pub const fn name(self) -> &'static str {
347        match self {
348            Self::SessionName => "session_name",
349            Self::SessionWindows => "session_windows",
350            Self::SessionAttached => "session_attached",
351            Self::SessionWidth => "session_width",
352            Self::SessionHeight => "session_height",
353            Self::WindowIndex => "window_index",
354            Self::WindowId => "window_id",
355            Self::WindowName => "window_name",
356            Self::WindowRawFlags => "window_raw_flags",
357            Self::WindowPanes => "window_panes",
358            Self::WindowWidth => "window_width",
359            Self::WindowHeight => "window_height",
360            Self::WindowLayout => "window_layout",
361            Self::WindowActive => "window_active",
362            Self::WindowLastFlag => "window_last_flag",
363            Self::PaneIndex => "pane_index",
364            Self::PaneId => "pane_id",
365            Self::PaneActive => "pane_active",
366            Self::PaneWidth => "pane_width",
367            Self::PaneHeight => "pane_height",
368        }
369    }
370}
371
372/// Returns whether a variable name is part of the supported format inventory.
373#[must_use]
374pub fn is_known_format_variable_name(name: &str) -> bool {
375    FormatVariable::from_name(name).is_some()
376        || TMUX_FORMAT_TABLE_NAMES.binary_search(&name).is_ok()
377}
378
379/// A source of values for supported format variables.
380pub trait FormatVariables {
381    /// Resolves a supported format variable to its rendered value.
382    fn format_value(&self, variable: FormatVariable) -> Option<String>;
383
384    /// Resolves an arbitrary string-keyed variable name.
385    ///
386    /// The default implementation delegates to the enum-based lookup. Runtime
387    /// implementors can override this to support `@user` options and the full
388    /// dynamic variable inventory.
389    fn format_value_by_name(&self, name: &str) -> Option<String> {
390        FormatVariable::from_name(name).and_then(|v| self.format_value(v))
391    }
392
393    /// Checks whether a window or session name exists for the `N:` modifier.
394    ///
395    /// `scope` is `None` or `Some('w')` for windows and `Some('s')` for
396    /// sessions.
397    fn format_name_exists(&self, _scope: Option<char>, _name: &str) -> Option<bool> {
398        None
399    }
400
401    /// Searches the current runtime content for the `C:` modifier.
402    ///
403    /// Implementors that do not have a pane screen should return `None`.
404    fn format_search(&self, _options: &str, _pattern: &str) -> Option<String> {
405        None
406    }
407
408    /// Expands runtime loop modifiers such as `S`, `W`, and `P`.
409    ///
410    /// The default implementation leaves these modifiers unsupported.
411    fn format_loop(
412        &self,
413        _scope: char,
414        _body: &str,
415        _current_body: Option<&str>,
416        _count_only: bool,
417    ) -> Option<String> {
418        None
419    }
420}
421
422/// Format values populated from session, window, pane, and server-side context.
423#[derive(Debug, Clone, Default, PartialEq, Eq)]
424pub struct FormatContext {
425    session_name: Option<String>,
426    session_windows: Option<usize>,
427    session_attached: Option<usize>,
428    session_size: Option<TerminalSize>,
429    window_index: Option<u32>,
430    window_id: Option<WindowId>,
431    window_name: Option<String>,
432    window_panes: Option<usize>,
433    window_size: Option<TerminalSize>,
434    window_layout: Option<String>,
435    window_active: Option<bool>,
436    window_last_flag: Option<bool>,
437    pane_index: Option<u32>,
438    pane_id: Option<PaneId>,
439    pane_active: Option<bool>,
440    pane_size: Option<TerminalSize>,
441    named_values: Vec<(String, String)>,
442}
443
444impl FormatContext {
445    /// Creates an empty format context.
446    #[must_use]
447    pub fn new() -> Self {
448        Self::default()
449    }
450
451    /// Creates a format context populated from a session.
452    #[must_use]
453    pub fn from_session(session: &Session) -> Self {
454        Self::new().with_session(session)
455    }
456
457    /// Populates session-level variables from a session.
458    #[must_use]
459    pub fn with_session(mut self, session: &Session) -> Self {
460        self.session_name = Some(session.name().to_string());
461        self.session_windows = Some(session.windows().len());
462        self.session_size = Some(session.window().size());
463        self
464    }
465
466    /// Populates the externally tracked attached-client count.
467    #[must_use]
468    pub const fn with_session_attached(mut self, attached_count: usize) -> Self {
469        self.session_attached = Some(attached_count);
470        self
471    }
472
473    /// Populates window-level variables from a window and its session-local state.
474    #[must_use]
475    pub fn with_window(
476        mut self,
477        window_index: u32,
478        window: &Window,
479        active: bool,
480        last: bool,
481    ) -> Self {
482        self.window_index = Some(window_index);
483        self.window_id = Some(window.id());
484        self.window_name = window.name().map(str::to_owned);
485        self.window_panes = Some(window.pane_count());
486        self.window_size = Some(window.size());
487        self.window_layout = Some(window.layout_dump());
488        self.window_active = Some(active);
489        self.window_last_flag = Some(last);
490        self
491    }
492
493    /// Populates pane-level variables from a pane and its window-local state.
494    #[must_use]
495    pub fn with_pane(mut self, pane: &Pane, active: bool) -> Self {
496        let geometry = pane.geometry();
497        self.pane_index = Some(pane.index());
498        self.pane_id = Some(pane.id());
499        self.pane_active = Some(active);
500        self.pane_size = Some(TerminalSize {
501            cols: geometry.cols(),
502            rows: geometry.rows(),
503        });
504        self
505    }
506
507    /// Populates pane-level variables, deriving active state from the owning window.
508    #[must_use]
509    pub fn with_window_pane(self, window: &Window, pane: &Pane) -> Self {
510        self.with_pane(pane, pane.index() == window.active_pane_index())
511    }
512
513    /// Populates an arbitrary named format variable.
514    #[must_use]
515    pub fn with_named_value(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
516        self.named_values.push((name.into(), value.into()));
517        self
518    }
519}
520
521impl FormatVariables for FormatContext {
522    fn format_value(&self, variable: FormatVariable) -> Option<String> {
523        match variable {
524            FormatVariable::SessionName => self.session_name.clone(),
525            FormatVariable::SessionWindows => self.session_windows.map(|value| value.to_string()),
526            FormatVariable::SessionAttached => self.session_attached.map(|value| value.to_string()),
527            FormatVariable::SessionWidth => self.session_size.map(|size| size.cols.to_string()),
528            FormatVariable::SessionHeight => self.session_size.map(|size| size.rows.to_string()),
529            FormatVariable::WindowIndex => self.window_index.map(|value| value.to_string()),
530            FormatVariable::WindowId => self.window_id.map(|value| value.to_string()),
531            FormatVariable::WindowName => self.window_name.clone(),
532            FormatVariable::WindowRawFlags => {
533                if self.window_active.is_some() || self.window_last_flag.is_some() {
534                    Some(
535                        window_raw_flags(
536                            self.window_active.unwrap_or(false),
537                            self.window_last_flag.unwrap_or(false),
538                        )
539                        .to_owned(),
540                    )
541                } else {
542                    None
543                }
544            }
545            FormatVariable::WindowPanes => self.window_panes.map(|value| value.to_string()),
546            FormatVariable::WindowWidth => self.window_size.map(|size| size.cols.to_string()),
547            FormatVariable::WindowHeight => self.window_size.map(|size| size.rows.to_string()),
548            FormatVariable::WindowLayout => self.window_layout.clone(),
549            FormatVariable::WindowActive => self.window_active.map(bool_value),
550            FormatVariable::WindowLastFlag => self.window_last_flag.map(bool_value),
551            FormatVariable::PaneIndex => self.pane_index.map(|value| value.to_string()),
552            FormatVariable::PaneId => self.pane_id.map(|value| value.to_string()),
553            FormatVariable::PaneActive => self.pane_active.map(bool_value),
554            FormatVariable::PaneWidth => self.pane_size.map(|size| size.cols.to_string()),
555            FormatVariable::PaneHeight => self.pane_size.map(|size| size.rows.to_string()),
556        }
557    }
558
559    fn format_value_by_name(&self, name: &str) -> Option<String> {
560        self.named_values
561            .iter()
562            .rev()
563            .find(|(candidate, _)| candidate == name)
564            .map(|(_, value)| value.clone())
565            .or_else(|| {
566                FormatVariable::from_name(name).and_then(|variable| self.format_value(variable))
567            })
568    }
569}