rmux-server 0.5.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
use rmux_core::formats::{is_known_format_variable_name, FormatVariable, FormatVariables};
use rmux_core::input::mode;
use rmux_core::{Session, Window, WINLINK_ACTIVITY, WINLINK_BELL, WINLINK_SILENCE};
use rmux_proto::OptionName;

use crate::hook_runtime::current_hook_format_value;

use crate::host_name::local_hostname;

use super::{bool_string, server_start_time, RuntimeFormatContext};

impl RuntimeFormatContext<'_> {
    fn pane_history_all_bytes(&self) -> Option<String> {
        let session = self.session?;
        let pane = self.pane?;
        let stats = self.state?.pane_history_stats(session.name(), pane.id())?;
        Some(stats.all_bytes)
    }

    fn pane_pipe(&self) -> Option<String> {
        let session = self.session?;
        let pane = self.pane?;
        let window_index = self.window_index?;
        Some(bool_string(self.state.is_some_and(|state| {
            state.pane_has_pipe(session.name(), window_index, pane.id())
        })))
    }

    fn pane_synchronized(&self) -> Option<String> {
        let session = self.session?;
        let window_index = self.window_index?;
        Some(bool_string(
            self.options?.resolve_for_window(
                session.name(),
                window_index,
                OptionName::SynchronizePanes,
            ) == Some("on"),
        ))
    }

    fn window_stack_index(&self) -> Option<String> {
        let session = self.session?;
        let window_index = self.window_index?;
        window_stack(session)
            .iter()
            .position(|candidate| *candidate == window_index)
            .map(|index| index.to_string())
    }
}

impl FormatVariables for RuntimeFormatContext<'_> {
    fn format_value(&self, variable: FormatVariable) -> Option<String> {
        if let Some(value) = self.runtime_format_value(variable) {
            return Some(value);
        }
        self.base.format_value(variable)
    }

    fn format_loop(&self, scope: char, body: &str, count_only: bool) -> Option<String> {
        match scope {
            'S' => self.render_session_loop(body, count_only),
            'W' => self.render_window_loop(body, count_only),
            'P' => self.render_pane_loop(body, count_only),
            _ => None,
        }
    }

    fn format_name_exists(&self, scope: Option<char>, name: &str) -> Option<bool> {
        match scope {
            Some('s') => Some(
                rmux_proto::SessionName::new(name)
                    .ok()
                    .and_then(|session_name| {
                        self.session_store
                            .map(|sessions| sessions.contains_session(&session_name))
                    })
                    .unwrap_or(false),
            ),
            None | Some('w') => Some(
                self.session
                    .map(|session| {
                        session.windows().iter().any(|(window_index, window)| {
                            self.rendered_window_name(*window_index, window).as_deref()
                                == Some(name)
                        })
                    })
                    .unwrap_or(false),
            ),
            Some(_) => None,
        }
    }

    fn format_value_by_name(&self, name: &str) -> Option<String> {
        if let Some(variable) = FormatVariable::from_name(name) {
            if let Some(value) = self.runtime_format_value(variable) {
                return Some(value);
            }
        }

        let runtime_value = match name {
            "pane_at_bottom" => self.visible_pane_snapshot().map(|pane| {
                bool_string(self.visible_window_snapshot().is_some_and(|window| {
                    pane.geometry().y() + pane.geometry().rows() >= window.size().rows
                }))
            }),
            "pane_bottom" => self.visible_pane_snapshot().map(|pane| {
                (pane.geometry().y() + pane.geometry().rows())
                    .saturating_sub(1)
                    .to_string()
            }),
            "pane_height" => self
                .visible_pane_snapshot()
                .map(|pane| pane.geometry().rows().to_string()),
            "pane_width" => self
                .visible_pane_snapshot()
                .map(|pane| pane.geometry().cols().to_string()),
            "session_activity_flag" => self.session_flag(WINLINK_ACTIVITY),
            "session_alert" => self.session_alert(),
            "session_alerts" => self.session_alerts(),
            "session_bell_flag" => self.session_flag(WINLINK_BELL),
            "session_height" => (self.hide_session_size || self.session_attached_count() == 0)
                .then(String::new)
                .or_else(|| {
                    self.visible_session_snapshot()
                        .map(|session| session.window().size().rows.to_string())
                }),
            "session_silence_flag" => self.session_flag(WINLINK_SILENCE),
            "session_width" => (self.hide_session_size || self.session_attached_count() == 0)
                .then(String::new)
                .or_else(|| {
                    self.visible_session_snapshot()
                        .map(|session| session.window().size().cols.to_string())
                }),
            "window_activity_flag" => self.window_flag(WINLINK_ACTIVITY),
            "window_bell_flag" => self.window_flag(WINLINK_BELL),
            "window_flags" => self.window_flags(),
            "window_height" => self
                .visible_window_snapshot()
                .map(|window| window.size().rows.to_string()),
            "window_layout" | "window_visible_layout" => self
                .layout_window_snapshot()
                .map(|window| window.layout_dump()),
            "window_linked" => self.window_linked(),
            "window_linked_sessions" => self.window_linked_sessions(),
            "window_linked_sessions_list" => self.window_linked_sessions_list(),
            "window_name" => self.window_name(),
            "window_raw_flags" => Some(self.printable_window_flags(false)),
            "window_silence_flag" => self.window_flag(WINLINK_SILENCE),
            "window_width" => self
                .visible_window_snapshot()
                .map(|window| window.size().cols.to_string()),
            _ => None,
        };
        if let Some(value) = runtime_value {
            return Some(value);
        }
        if let Some(value) = self.base.format_value_by_name(name) {
            return Some(value);
        }
        if let Some(value) = current_hook_format_value(name) {
            return Some(value);
        }

        let value = match name {
            "active_window_index" => self
                .session
                .map(|session| session.active_window_index().to_string()),
            "alternate_saved_x" | "alternate_saved_y" => Some(u32::MAX.to_string()),
            "buffer_full" => self
                .buffer_head()
                .map(|(_, content)| String::from_utf8_lossy(&content).into_owned()),
            "buffer_mode_format" => Some("#{t/p:buffer_created}: #{buffer_sample}".to_owned()),
            "buffer_name" => self.buffer_head().map(|(name, _)| name),
            "buffer_sample" => self.buffer_head().map(|(_, content)| {
                let text = String::from_utf8_lossy(&content);
                text.chars().take(50).collect()
            }),
            "buffer_size" => self
                .buffer_head()
                .map(|(_, content)| content.len().to_string()),
            "client_mode_format" => Some("#{t/p:client_activity}: session #{session_name}".to_owned()),
            "client_height" => self.client_size.map(|size| size.rows.to_string()),
            "client_width" => self.client_size.map(|size| size.cols.to_string()),
            "command" => Some("display-message".to_owned()),
            "config_files" => Some("/dev/null".to_owned()),
            "cursor_character" => Some(" ".to_owned()),
            "cursor_flag" => Some("1".to_owned()),
            "cursor_x" => self
                .pane_cursor_position()
                .map(|(cursor_x, _)| cursor_x.to_string()),
            "cursor_y" => self
                .pane_cursor_position()
                .map(|(_, cursor_y)| cursor_y.to_string()),
            "history_bytes" => self.pane_history_bytes(),
            "history_all_bytes" => self.pane_history_all_bytes(),
            "history_limit" => self.pane_history_limit(),
            "history_size" => self.pane_history_size(),
            "host" => local_hostname(),
            "host_short" => {
                local_hostname().map(|host| host.split('.').next().unwrap_or_default().to_owned())
            }
            "insert_flag" | "keypad_cursor_flag" | "keypad_flag" | "origin_flag" => {
                Some("0".to_owned())
            }
            "last_window_index" => self
                .session
                .and_then(|session| session.windows().keys().next_back().copied())
                .map(|value| value.to_string()),
            "next_session_id" => self
                .session_store
                .map(|sessions| sessions.next_session_id().to_string()),
            "pane_at_left" => self.pane.map(|pane| bool_string(pane.geometry().x() == 0)),
            "pane_at_right" => self.pane.map(|pane| {
                bool_string(self.window.is_some_and(|window| {
                    pane.geometry().x() + pane.geometry().cols() >= window.size().cols
                }))
            }),
            "pane_at_top" => self.pane.map(|pane| bool_string(pane.geometry().y() == 0)),
            "alternate_on" => self.pane_alternate_on(),
            "bracket_paste_flag" => self.pane_mode_flag(mode::MODE_BRACKETPASTE),
            "mouse_all_flag" => self.pane_mode_flag(mode::MODE_MOUSE_ALL),
            "mouse_any_flag" => self
                .pane_screen_mode()
                .map(|mode_value| bool_string(mode_value & mode::ALL_MOUSE_MODES != 0)),
            "mouse_button_flag" => self.pane_mode_flag(mode::MODE_MOUSE_BUTTON),
            "mouse_sgr_flag" => self.pane_mode_flag(mode::MODE_MOUSE_SGR),
            "mouse_standard_flag" => self.pane_mode_flag(mode::MODE_MOUSE_STANDARD),
            "mouse_utf8_flag" => self.pane_mode_flag(mode::MODE_MOUSE_UTF8),
            "pane_current_path" | "session_path" => self
                .pane_current_path()
                .or_else(|| self.environment_value_by_name("PWD"))
                .or_else(|| self.environment_value_by_name("HOME")),
            "pane_path" => Some(String::new()),
            "pane_current_command" => self.pane_current_command(),
            "pane_dead" => Some(bool_string(self.pane_dead())),
            "pane_dead_signal" => self.pane_dead_signal(),
            "pane_dead_status" => self.pane_dead_status(),
            "pane_dead_time" => self.pane_dead_time(),
            "pane_bg" | "pane_fg" => Some("default".to_owned()),
            "pane_flags" => self.pane.map(|pane| {
                let mut flags = String::new();
                if self
                    .base
                    .format_value(FormatVariable::PaneActive)
                    .is_some_and(|value| value == "1")
                {
                    flags.push('*');
                }
                if self.window.is_some_and(Window::is_zoomed)
                    && self
                        .window
                        .is_some_and(|window| window.active_pane_index() == pane.index())
                {
                    flags.push('Z');
                }
                flags
            }),
            "pane_format" => Some(bool_string(self.pane.is_some())),
            "pane_in_mode" => Some(bool_string(self.pane_in_mode())),
            "pane_input_off" => Some("0".to_owned()),
            "pane_marked" => self.pane_marked(),
            "pane_marked_set" => Some(bool_string(self.marked_pane_set())),
            "pane_last" => self.pane.map(|pane| {
                bool_string(
                    self.window
                        .and_then(Window::last_pane_index)
                        .is_some_and(|last| last == pane.index()),
                )
            }),
            "pane_left" => self.pane.map(|pane| pane.geometry().x().to_string()),
            "pane_mode" => self.pane_mode_name(),
            "pane_lifecycle_generation" | "pane_generation" => self.pane_lifecycle_generation(),
            "pane_lifecycle_revision" | "pane_revision" => self.pane_lifecycle_revision(),
            "pane_output_sequence" => self.pane_output_sequence(),
            "pane_pid" => self.pane_pid(),
            "pane_pipe" => self.pane_pipe(),
            "pane_right" => self.pane.map(|pane| {
                (pane.geometry().x() + pane.geometry().cols())
                    .saturating_sub(1)
                    .to_string()
            }),
            "pane_search_string" => self
                .pane_copy_mode_summary()
                .map(|summary| summary.pane_search_string),
            "pane_start_command" => self.pane_start_command(),
            "pane_start_path" => self
                .pane_start_path()
                .or_else(|| self.environment_value_by_name("PWD"))
                .or_else(|| self.environment_value_by_name("HOME")),
            "pane_synchronized" => self.pane_synchronized(),
            "pane_tabs" => Some("8,16,24,32,40,48,56,64,72,80,88,96,104,112".to_owned()),
            "pane_tty" => self.pane_tty(),
            "pane_title" => self.pane_title(),
            "pane_top" => self.pane.map(|pane| pane.geometry().y().to_string()),
            "pane_unseen_changes" => Some("0".to_owned()),
            "pane_zoomed_flag" => Some(bool_string(self.window.is_some_and(Window::is_zoomed))),
            "pid" => Some(std::process::id().to_string()),
            "scroll_region_lower" => self
                .visible_window_snapshot()
                .map(|window| window.size().rows.saturating_sub(1).to_string()),
            "scroll_region_upper" => Some("0".to_owned()),
            "scroll_position" => self
                .pane_copy_mode_summary()
                .map(|summary| summary.scroll_position.to_string()),
            "rectangle_toggle" => self
                .pane_copy_mode_summary()
                .map(|summary| bool_string(summary.rectangle_toggle)),
            "copy_cursor_x" => self
                .pane_copy_mode_summary()
                .map(|summary| summary.cursor_x.to_string()),
            "copy_cursor_y" => self
                .pane_copy_mode_summary()
                .map(|summary| summary.cursor_y.to_string()),
            "selection_start_x" => self.pane_copy_mode_summary().and_then(|summary| {
                summary
                    .selection_start
                    .map(|position| position.x.to_string())
            }),
            "selection_start_y" => self.pane_copy_mode_summary().and_then(|summary| {
                summary
                    .selection_start
                    .map(|position| position.y.to_string())
            }),
            "selection_end_x" => self
                .pane_copy_mode_summary()
                .and_then(|summary| summary.selection_end.map(|position| position.x.to_string())),
            "selection_end_y" => self
                .pane_copy_mode_summary()
                .and_then(|summary| summary.selection_end.map(|position| position.y.to_string())),
            "selection_active" => self
                .pane_copy_mode_summary()
                .map(|summary| bool_string(summary.selection_active)),
            "selection_present" => self
                .pane_copy_mode_summary()
                .map(|summary| bool_string(summary.selection_present)),
            "selection_mode" => self.pane_copy_mode_summary().and_then(|summary| {
                summary
                    .selection_mode
                    .map(|selection_mode| selection_mode.as_str().to_owned())
            }),
            "search_present" => self
                .pane_copy_mode_summary()
                .map(|summary| bool_string(summary.search_present)),
            "search_timed_out" => self
                .pane_copy_mode_summary()
                .map(|summary| bool_string(summary.search_timed_out)),
            "search_count" => self
                .pane_copy_mode_summary()
                .map(|summary| summary.search_count.to_string()),
            "search_count_partial" => self
                .pane_copy_mode_summary()
                .map(|summary| bool_string(summary.search_count_partial)),
            "search_match" => self
                .pane_copy_mode_summary()
                .and_then(|summary| summary.search_match),
            "copy_cursor_word" => self
                .pane_copy_mode_summary()
                .map(|summary| summary.copy_cursor_word),
            "copy_cursor_line" => self
                .pane_copy_mode_summary()
                .map(|summary| summary.copy_cursor_line),
            "copy_cursor_hyperlink" => self
                .pane_copy_mode_summary()
                .map(|summary| summary.copy_cursor_hyperlink),
            "top_line_time" => self
                .pane_copy_mode_summary()
                .map(|summary| summary.top_line_time.to_string()),
            "server_sessions" => self
                .session_store
                .map(|sessions| sessions.len().to_string()),
            "session_active" => Some(bool_string(self.session.is_some())),
            "session_activity" => self.session.map(|session| session.activity_at().to_string()),
            "session_created" => self.session.map(|session| session.created_at().to_string()),
            "session_format" => Some(bool_string(
                self.session.is_some() && self.window.is_none() && self.pane.is_none(),
            )),
            "session_group" => Some(
                self.session_group_name()
                    .map(|name| name.to_string())
                    .unwrap_or_default(),
            ),
            "session_group_attached" => Some(self.session_attached_count().to_string()),
            "session_group_attached_list" => Some(
                (self.session_attached_count() > 0)
                    .then(|| self.session_name().map(ToString::to_string))
                    .flatten()
                    .unwrap_or_default(),
            ),
            "session_group_list" => Some(
                self.session_group_members()
                    .into_iter()
                    .map(|session_name| session_name.to_string())
                    .collect::<Vec<_>>()
                    .join(","),
            ),
            "session_group_many_attached" => Some(bool_string(self.session_attached_count() > 1)),
            "session_group_size" => Some(self.session_group_members().len().to_string()),
            "session_grouped" => Some(bool_string(self.session_group_name().is_some())),
            "session_id" => self.session.map(|session| session.id().to_string()),
            "session_last_attached" => self
                .session
                .and_then(Session::last_attached_at)
                .map(|timestamp| timestamp.to_string()),
            "session_many_attached" => Some(bool_string(self.session_attached_count() > 1)),
            "session_marked" => Some(bool_string(self.session_marked())),
            "session_stack" => self.session.map(session_stack),
            "socket_path" => Some(String::new()),
            "start_time" => Some(server_start_time().to_string()),
            "tree_mode_format" => Some("#{?pane_format,#{?pane_marked,#[reverse],}#{pane_current_command}#{?pane_active,*,}#{?pane_marked,M,}#{?#{&&:#{pane_title},#{!=:#{pane_title},#{host_short}}},: \"#{pane_title}\",},#{?window_format,#{?window_marked_flag,#[reverse],}#{window_name}#{window_flags}#{?#{&&:#{==:#{window_panes},1},#{&&:#{pane_title},#{!=:#{pane_title},#{host_short}}}},: \"#{pane_title}\",},#{session_windows} windows#{?session_grouped, (group #{session_group}: #{session_group_list}),}#{?session_attached, (attached),}}}".to_owned()),
            "uid" => Some(crate::server_access::current_owner_uid().to_string()),
            "user" => current_user_name(),
            "version" => Some("3.4".to_owned()),
            "window_active_clients" => Some(
                if self
                    .base
                    .format_value(FormatVariable::WindowActive)
                    .is_some_and(|value| value == "1")
                {
                    self.session_attached_count().to_string()
                } else {
                    "0".to_owned()
                },
            ),
            "window_active_sessions" => Some(bool_string(
                self.base
                    .format_value(FormatVariable::WindowActive)
                    .is_some_and(|value| value == "1"),
            )),
            "window_active_sessions_list" => Some(
                self.base
                    .format_value(FormatVariable::WindowActive)
                    .filter(|value| value == "1")
                    .and_then(|_| self.session_name().map(ToString::to_string))
                    .unwrap_or_default(),
            ),
            "window_activity" => self.session.map(|session| session.activity_at().to_string()),
            "window_bigger" => Some("0".to_owned()),
            "window_cell_height" => Some("32".to_owned()),
            "window_cell_width" => Some("16".to_owned()),
            "window_end_flag" => self.window_index.map(|window_index| {
                bool_string(
                    self.session
                        .and_then(|session| session.windows().keys().next_back().copied())
                        .is_some_and(|last| last == window_index),
                )
            }),
            "window_format" => Some(bool_string(self.window.is_some() && self.pane.is_none())),
            "window_marked_flag" => Some(bool_string(self.window_marked())),
            "window_offset_x" | "window_offset_y" => Some("0".to_owned()),
            "window_stack_index" => self.window_stack_index(),
            "window_start_flag" => self.window_index.map(|window_index| {
                bool_string(
                    self.session
                        .and_then(|session| session.windows().keys().next().copied())
                        .is_some_and(|first| first == window_index),
                )
            }),
            "window_zoomed_flag" => Some(bool_string(self.window.is_some_and(Window::is_zoomed))),
            "wrap_flag" => Some("1".to_owned()),
            _ => None,
        };

        if let Some(value) = value {
            return Some(value);
        }

        if let Some(value) = self.option_value_by_name(name) {
            return Some(value);
        }

        if let Some(value) = self.environment_value_by_name(name) {
            return Some(value);
        }

        if is_known_format_variable_name(name) {
            return Some(String::new());
        }

        None
    }
}

fn session_stack(session: &Session) -> String {
    window_stack(session)
        .into_iter()
        .map(|index| index.to_string())
        .collect::<Vec<_>>()
        .join(",")
}

fn window_stack(session: &Session) -> Vec<u32> {
    let mut stack = Vec::with_capacity(session.windows().len());
    push_existing_window(&mut stack, session, session.active_window_index());
    if let Some(last_window) = session.last_window_index() {
        push_existing_window(&mut stack, session, last_window);
    }
    for window_index in session.windows().keys().copied() {
        push_existing_window(&mut stack, session, window_index);
    }
    stack
}

fn push_existing_window(stack: &mut Vec<u32>, session: &Session, window_index: u32) {
    if session.window_at(window_index).is_some() && !stack.contains(&window_index) {
        stack.push(window_index);
    }
}

fn current_user_name() -> Option<String> {
    user_name_from_env(|name| std::env::var(name).ok())
}

fn user_name_from_env(mut lookup: impl FnMut(&str) -> Option<String>) -> Option<String> {
    lookup("USER")
        .filter(|value| !value.is_empty())
        .or_else(|| lookup("USERNAME").filter(|value| !value.is_empty()))
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::user_name_from_env;

    fn lookup<'a>(values: &'a HashMap<&str, &str>) -> impl FnMut(&str) -> Option<String> + 'a {
        |name| values.get(name).map(|value| (*value).to_owned())
    }

    #[test]
    fn user_name_prefers_unix_user_when_present() {
        let values = HashMap::from([("USER", "alice"), ("USERNAME", "windows-alice")]);

        assert_eq!(
            user_name_from_env(lookup(&values)).as_deref(),
            Some("alice")
        );
    }

    #[test]
    fn user_name_falls_back_to_windows_username() {
        let values = HashMap::from([("USERNAME", "shadow")]);

        assert_eq!(
            user_name_from_env(lookup(&values)).as_deref(),
            Some("shadow")
        );
    }

    #[test]
    fn user_name_ignores_empty_values() {
        let values = HashMap::from([("USER", ""), ("USERNAME", "shadow")]);

        assert_eq!(
            user_name_from_env(lookup(&values)).as_deref(),
            Some("shadow")
        );
    }
}