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