1use std::time::{SystemTime, UNIX_EPOCH};
2
3use rmux_proto::{LayoutName, RmuxError, RotateWindowDirection, SplitDirection, TerminalSize};
4
5use crate::layout::{LayoutDirection, LayoutTree};
6use crate::{Pane, PaneGeometry, PaneId, WindowId};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub struct AlertFlags(u8);
11
12impl AlertFlags {
13 #[must_use]
15 pub const fn empty() -> Self {
16 Self(0)
17 }
18
19 #[must_use]
21 pub const fn contains(self, other: Self) -> bool {
22 (self.0 & other.0) == other.0
23 }
24
25 #[must_use]
27 pub const fn intersects(self, other: Self) -> bool {
28 (self.0 & other.0) != 0
29 }
30
31 #[must_use]
33 pub const fn is_empty(self) -> bool {
34 self.0 == 0
35 }
36
37 #[must_use]
39 pub const fn union(self, other: Self) -> Self {
40 Self(self.0 | other.0)
41 }
42
43 pub fn insert(&mut self, other: Self) {
45 self.0 |= other.0;
46 }
47
48 pub fn remove(&mut self, other: Self) {
50 self.0 &= !other.0;
51 }
52}
53
54pub const WINDOW_BELL: AlertFlags = AlertFlags(0x1);
56pub const WINDOW_ACTIVITY: AlertFlags = AlertFlags(0x2);
58pub const WINDOW_SILENCE: AlertFlags = AlertFlags(0x4);
60pub const WINDOW_ALERTFLAGS: AlertFlags =
62 AlertFlags(WINDOW_BELL.0 | WINDOW_ACTIVITY.0 | WINDOW_SILENCE.0);
63
64pub const WINLINK_BELL: AlertFlags = AlertFlags(0x1);
66pub const WINLINK_ACTIVITY: AlertFlags = AlertFlags(0x2);
68pub const WINLINK_SILENCE: AlertFlags = AlertFlags(0x4);
70pub const WINLINK_ALERTFLAGS: AlertFlags =
72 AlertFlags(WINLINK_BELL.0 | WINLINK_ACTIVITY.0 | WINLINK_SILENCE.0);
73
74#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct Window {
82 id: WindowId,
83 panes: Vec<Pane>,
84 next_pane_index: u32,
85 active_pane: u32,
86 last_pane: Option<u32>,
87 layout: LayoutName,
88 last_layout: Option<LayoutName>,
89 layout_tree: Option<LayoutTree>,
90 custom_layout: bool,
91 old_layout: Option<String>,
92 size: TerminalSize,
93 name: Option<String>,
94 automatic_rename: bool,
95 zoomed: bool,
96 zoom_restore_pending: bool,
97 alert_flags: AlertFlags,
98 alerts_queued: bool,
99 created_at: i64,
100 activity_at: i64,
101 requested_main_width: Option<u16>,
103 requested_main_height: Option<u16>,
105}
106
107#[path = "window/layout_cycle.rs"]
108mod layout_cycle;
109#[path = "window/layout_ops.rs"]
110mod layout_ops;
111#[path = "window/panes.rs"]
112mod panes;
113#[path = "window/zoom.rs"]
114mod zoom;
115
116use panes::layout_for_split;
117
118impl Window {
119 #[must_use]
121 pub fn new(size: TerminalSize) -> Self {
122 Self::new_with_initial_pane(size, PaneId::new(0), WindowId::new(0))
123 }
124
125 pub(crate) fn new_with_initial_pane(size: TerminalSize, pane_id: PaneId, id: WindowId) -> Self {
126 let now = current_unix_timestamp();
127 let mut window = Self {
128 id,
129 panes: vec![Pane::new_with_id(
130 pane_id,
131 0,
132 PaneGeometry::new(0, 0, size.cols, size.rows),
133 )],
134 next_pane_index: 1,
135 active_pane: 0,
136 last_pane: None,
137 layout: LayoutName::MainVertical,
138 last_layout: None,
139 layout_tree: Some(LayoutTree::single(size)),
140 custom_layout: false,
141 old_layout: None,
142 size,
143 name: None,
144 automatic_rename: true,
145 zoomed: false,
146 zoom_restore_pending: false,
147 alert_flags: AlertFlags::empty(),
148 alerts_queued: false,
149 created_at: now,
150 activity_at: now,
151 requested_main_width: None,
152 requested_main_height: None,
153 };
154 window.recalculate_geometry();
155 window
156 }
157
158 #[must_use]
160 pub const fn id(&self) -> WindowId {
161 self.id
162 }
163
164 #[must_use]
166 pub const fn created_at(&self) -> i64 {
167 self.created_at
168 }
169
170 #[must_use]
172 pub const fn activity_at(&self) -> i64 {
173 self.activity_at
174 }
175
176 pub fn touch_activity_for_pane(&mut self, pane_index: u32) -> bool {
178 let Some(position) = self
179 .panes
180 .iter()
181 .position(|pane| pane.index() == pane_index)
182 else {
183 return false;
184 };
185 self.activity_at = current_unix_timestamp();
186 self.panes[position].touch_activity();
187 true
188 }
189
190 #[must_use]
192 pub fn panes(&self) -> &[Pane] {
193 &self.panes
194 }
195
196 #[must_use]
198 pub fn pane(&self, pane_index: u32) -> Option<&Pane> {
199 self.panes.iter().find(|pane| pane.index() == pane_index)
200 }
201
202 #[must_use]
204 pub fn pane_mut(&mut self, pane_index: u32) -> Option<&mut Pane> {
205 self.panes
206 .iter_mut()
207 .find(|pane| pane.index() == pane_index)
208 }
209
210 #[must_use]
212 pub const fn active_pane_index(&self) -> u32 {
213 self.active_pane
214 }
215
216 #[must_use]
218 pub const fn last_pane_index(&self) -> Option<u32> {
219 self.last_pane
220 }
221
222 #[must_use]
224 pub fn active_pane(&self) -> Option<&Pane> {
225 self.pane(self.active_pane)
226 }
227
228 #[must_use]
230 pub fn pane_id(&self, pane_index: u32) -> Option<PaneId> {
231 self.pane(pane_index).map(Pane::id)
232 }
233
234 #[must_use]
236 pub const fn layout(&self) -> LayoutName {
237 self.layout
238 }
239
240 #[must_use]
242 pub const fn size(&self) -> TerminalSize {
243 self.size
244 }
245
246 #[must_use]
248 pub fn layout_dump(&self) -> String {
249 self.layout_tree
250 .as_ref()
251 .map_or_else(String::new, |tree| tree.dump(&self.panes))
252 }
253
254 pub fn save_old_layout(&mut self) {
256 self.old_layout = Some(self.layout_dump());
257 }
258
259 #[must_use]
261 pub fn old_layout(&self) -> Option<&str> {
262 self.old_layout.as_deref()
263 }
264
265 #[must_use]
267 pub fn name(&self) -> Option<&str> {
268 self.name.as_deref()
269 }
270
271 #[must_use]
273 pub const fn automatic_rename(&self) -> bool {
274 self.automatic_rename
275 }
276
277 #[must_use]
279 pub const fn alert_flags(&self) -> AlertFlags {
280 self.alert_flags
281 }
282
283 #[must_use]
285 pub const fn alerts_queued(&self) -> bool {
286 self.alerts_queued
287 }
288
289 #[must_use]
291 pub fn pane_count(&self) -> usize {
292 self.panes.len()
293 }
294
295 pub fn queue_alerts(&mut self, flags: AlertFlags) {
297 self.alert_flags.insert(flags);
298 }
299
300 pub fn take_alert_flags(&mut self) -> AlertFlags {
302 let flags = self.alert_flags;
303 self.alert_flags = AlertFlags::empty();
304 flags
305 }
306
307 pub fn clear_alert_flags(&mut self, flags: AlertFlags) {
309 self.alert_flags.remove(flags);
310 }
311
312 pub fn set_alerts_queued(&mut self, queued: bool) {
314 self.alerts_queued = queued;
315 }
316
317 pub(crate) fn respawn(&mut self, pane_id: PaneId) -> PaneId {
320 let size = self.size;
321 self.panes = vec![Pane::new_with_id(
322 pane_id,
323 0,
324 PaneGeometry::new(0, 0, size.cols, size.rows),
325 )];
326 self.next_pane_index = 1;
327 self.active_pane = 0;
328 self.last_pane = None;
329 self.layout = LayoutName::MainVertical;
330 self.last_layout = None;
331 self.layout_tree = Some(LayoutTree::single(size));
332 self.custom_layout = false;
333 self.old_layout = None;
334 self.automatic_rename = true;
335 self.zoomed = false;
336 self.zoom_restore_pending = false;
337 self.alert_flags = AlertFlags::empty();
338 self.alerts_queued = false;
339 self.requested_main_width = None;
340 self.requested_main_height = None;
341 pane_id
342 }
343
344 pub(crate) fn set_size(&mut self, size: TerminalSize) {
345 self.size = size;
346 if self.zoomed {
347 self.apply_zoom_geometry();
348 } else {
349 self.recalculate_geometry();
350 }
351 }
352
353 pub(crate) fn set_name(&mut self, name: String) {
354 self.name = Some(name);
355 self.automatic_rename = false;
356 }
357
358 pub fn enable_automatic_rename(&mut self) {
360 self.automatic_rename = true;
361 }
362
363 pub fn set_automatic_name(&mut self, name: String) {
365 self.name = Some(name);
366 self.automatic_rename = true;
367 }
368
369 pub(crate) fn rotate_panes(&mut self, direction: RotateWindowDirection) {
370 self.rotate_panes_with_zoom(direction, false);
371 }
372
373 pub(crate) fn rotate_panes_with_zoom(
374 &mut self,
375 direction: RotateWindowDirection,
376 restore_zoom: bool,
377 ) {
378 if self.panes.len() <= 1 {
379 return;
380 }
381
382 self.push_zoom(restore_zoom);
383 let previous_active_pane_id = self
384 .active_pane()
385 .expect("active pane must exist before pane rotation")
386 .id();
387 let active_position = self
388 .panes
389 .iter()
390 .position(|pane| pane.index() == self.active_pane)
391 .expect("active pane must exist in window order");
392
393 match direction {
394 RotateWindowDirection::Down => self.panes.rotate_right(1),
395 RotateWindowDirection::Up => self.panes.rotate_left(1),
396 }
397 for (index, pane) in self.panes.iter_mut().enumerate() {
398 pane.set_index(index as u32);
399 }
400
401 self.apply_layout_tree();
402
403 self.active_pane = active_position as u32;
406 self.last_pane = self
407 .pane_index_for_id(previous_active_pane_id)
408 .filter(|pane_index| *pane_index != self.active_pane);
409 self.mark_pane_active(self.active_pane);
410
411 self.pop_zoom();
412 }
413
414 pub(crate) fn insert_pane_at_position(
415 &mut self,
416 position: usize,
417 pane: Pane,
418 direction: SplitDirection,
419 ) -> Result<(), RmuxError> {
420 if position > self.panes.len() {
421 return Err(RmuxError::Server(format!(
422 "cannot insert pane at position {position} in a {}-pane window",
423 self.panes.len()
424 )));
425 }
426
427 self.ensure_accepts_pane(&pane, None)?;
428 self.auto_unzoom();
429 self.layout = layout_for_split(direction);
430 self.bump_next_pane_index(pane.index());
431 let inserted_index = pane.index();
432 self.panes.insert(position, pane);
433 if self.panes.len() == 1 {
434 self.active_pane = inserted_index;
435 self.last_pane = None;
436 }
437 if self.panes.len() == 1 {
438 self.layout_tree = Some(LayoutTree::single(self.size));
439 self.apply_layout_tree();
440 return Ok(());
441 }
442
443 let (target_leaf, insert_before_target) = if position == 0 {
444 (0, true)
445 } else {
446 (position - 1, false)
447 };
448 let inserted = self.layout_tree.as_mut().is_some_and(|tree| {
449 tree.split_leaf(
450 target_leaf,
451 LayoutDirection::from_split_direction(direction),
452 insert_before_target,
453 )
454 });
455 if !inserted {
456 self.rebuild_named_layout_tree(self.layout);
457 } else {
458 self.apply_layout_tree();
459 }
460 Ok(())
461 }
462
463 pub(crate) fn move_pane_by_splitting_target(
464 &mut self,
465 source_position: usize,
466 target_position: usize,
467 final_insert_position: usize,
468 direction: SplitDirection,
469 insert_before_target: bool,
470 ) -> Result<PaneId, RmuxError> {
471 let pane_count = self.panes.len();
472 if source_position >= pane_count {
473 return Err(RmuxError::Server(format!(
474 "cannot move missing pane at position {source_position}"
475 )));
476 }
477 if target_position >= pane_count {
478 return Err(RmuxError::Server(format!(
479 "cannot split missing target pane at position {target_position}"
480 )));
481 }
482 if final_insert_position > pane_count.saturating_sub(1) {
483 return Err(RmuxError::Server(format!(
484 "cannot insert moved pane at position {final_insert_position} in a {}-pane window",
485 pane_count.saturating_sub(1)
486 )));
487 }
488
489 self.auto_unzoom();
490 self.layout = layout_for_split(direction);
491
492 let split_insert_position = if insert_before_target {
493 target_position
494 } else {
495 target_position + 1
496 };
497 let source_leaf_after_split = if split_insert_position <= source_position {
498 source_position + 1
499 } else {
500 source_position
501 };
502 let tree = self.layout_tree.as_mut().ok_or_else(|| {
503 RmuxError::Server("cannot move pane without a layout tree".to_owned())
504 })?;
505 if !tree.split_leaf(
506 target_position,
507 LayoutDirection::from_split_direction(direction),
508 insert_before_target,
509 ) {
510 return Err(RmuxError::Server(format!(
511 "cannot split target pane at position {target_position}"
512 )));
513 }
514 if !tree.remove_leaf(source_leaf_after_split) {
515 return Err(RmuxError::Server(format!(
516 "cannot remove source pane leaf at position {source_leaf_after_split}"
517 )));
518 }
519
520 let moved_pane = self.panes.remove(source_position);
521 let moved_pane_id = moved_pane.id();
522 self.panes.insert(final_insert_position, moved_pane);
523 self.apply_layout_tree();
524 Ok(moved_pane_id)
525 }
526
527 pub(crate) fn insert_pane_full_size(
528 &mut self,
529 pane: Pane,
530 direction: SplitDirection,
531 insert_before_target: bool,
532 ) -> Result<(), RmuxError> {
533 self.ensure_accepts_pane(&pane, None)?;
534 self.auto_unzoom();
535 self.layout = layout_for_split(direction);
536 self.bump_next_pane_index(pane.index());
537
538 if insert_before_target {
539 self.panes.insert(0, pane);
540 } else {
541 self.panes.push(pane);
542 }
543
544 let split = self.layout_tree.as_mut().is_some_and(|tree| {
545 tree.split_root(
546 LayoutDirection::from_split_direction(direction),
547 insert_before_target,
548 )
549 });
550 if !split {
551 self.rebuild_named_layout_tree(self.layout);
552 } else {
553 self.apply_layout_tree();
554 }
555 Ok(())
556 }
557
558 pub(crate) fn replace_pane_for_swap(
559 &mut self,
560 pane_index: u32,
561 pane: Pane,
562 ) -> Result<(), RmuxError> {
563 let position = self.pane_position(pane_index).ok_or_else(|| {
564 RmuxError::Server(format!(
565 "cannot replace missing pane index {pane_index} in window {}",
566 self.id
567 ))
568 })?;
569 for (existing_position, existing) in self.panes.iter().enumerate() {
570 if existing_position != position && existing.id() == pane.id() {
571 return Err(RmuxError::Server(format!(
572 "pane id {} already exists in window {}",
573 pane.id().as_u32(),
574 self.id
575 )));
576 }
577 }
578 self.bump_next_pane_index(pane.index());
579 self.panes[position] = pane;
580 self.apply_layout_tree();
581 Ok(())
582 }
583
584 pub(crate) fn swap_panes(&mut self, source_pane_index: u32, target_pane_index: u32) -> bool {
585 let Some(source_position) = self.pane_position(source_pane_index) else {
586 return false;
587 };
588 let Some(target_position) = self.pane_position(target_pane_index) else {
589 return false;
590 };
591 if source_position == target_position {
592 return true;
593 }
594
595 self.auto_unzoom();
596 let active_pane_id = self
597 .active_pane()
598 .expect("active pane must exist before pane swap")
599 .id();
600 let last_pane_id = self
601 .last_pane
602 .and_then(|pane_index| self.pane(pane_index).map(Pane::id));
603 self.panes.swap(source_position, target_position);
604 self.apply_layout_tree();
605 self.renumber_panes_by_position(active_pane_id, last_pane_id);
606 true
607 }
608}
609
610fn current_unix_timestamp() -> i64 {
611 SystemTime::now()
612 .duration_since(UNIX_EPOCH)
613 .ok()
614 .and_then(|duration| i64::try_from(duration.as_secs()).ok())
615 .unwrap_or_default()
616}
617
618#[cfg(test)]
619#[path = "window/tests.rs"]
620mod tests;