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/activity.rs"]
108mod activity;
109#[path = "window/layout_cycle.rs"]
110mod layout_cycle;
111#[path = "window/layout_ops.rs"]
112mod layout_ops;
113#[path = "window/panes.rs"]
114mod panes;
115#[path = "window/zoom.rs"]
116mod zoom;
117
118pub use activity::WindowPaneActivity;
119use panes::layout_for_split;
120
121impl Window {
122 #[must_use]
124 pub fn new(size: TerminalSize) -> Self {
125 Self::new_with_initial_pane(size, PaneId::new(0), WindowId::new(0))
126 }
127
128 pub(crate) fn new_with_initial_pane(size: TerminalSize, pane_id: PaneId, id: WindowId) -> Self {
129 let now = current_unix_timestamp();
130 let mut window = Self {
131 id,
132 panes: vec![Pane::new_with_id(
133 pane_id,
134 0,
135 PaneGeometry::new(0, 0, size.cols, size.rows),
136 )],
137 next_pane_index: 1,
138 active_pane: 0,
139 last_pane: None,
140 layout: LayoutName::MainVertical,
141 last_layout: None,
142 layout_tree: Some(LayoutTree::single(size)),
143 custom_layout: false,
144 old_layout: None,
145 size,
146 name: None,
147 automatic_rename: true,
148 zoomed: false,
149 zoom_restore_pending: false,
150 alert_flags: AlertFlags::empty(),
151 alerts_queued: false,
152 created_at: now,
153 activity_at: now,
154 requested_main_width: None,
155 requested_main_height: None,
156 };
157 window.recalculate_geometry();
158 window
159 }
160
161 #[must_use]
163 pub const fn id(&self) -> WindowId {
164 self.id
165 }
166
167 #[must_use]
169 pub fn panes(&self) -> &[Pane] {
170 &self.panes
171 }
172
173 #[must_use]
175 pub fn pane(&self, pane_index: u32) -> Option<&Pane> {
176 self.panes.iter().find(|pane| pane.index() == pane_index)
177 }
178
179 #[must_use]
181 pub fn pane_mut(&mut self, pane_index: u32) -> Option<&mut Pane> {
182 self.panes
183 .iter_mut()
184 .find(|pane| pane.index() == pane_index)
185 }
186
187 #[must_use]
189 pub const fn active_pane_index(&self) -> u32 {
190 self.active_pane
191 }
192
193 #[must_use]
195 pub const fn last_pane_index(&self) -> Option<u32> {
196 self.last_pane
197 }
198
199 #[must_use]
201 pub fn active_pane(&self) -> Option<&Pane> {
202 self.pane(self.active_pane)
203 }
204
205 #[must_use]
207 pub fn pane_id(&self, pane_index: u32) -> Option<PaneId> {
208 self.pane(pane_index).map(Pane::id)
209 }
210
211 #[must_use]
213 pub const fn layout(&self) -> LayoutName {
214 self.layout
215 }
216
217 #[must_use]
219 pub const fn size(&self) -> TerminalSize {
220 self.size
221 }
222
223 #[must_use]
225 pub fn layout_dump(&self) -> String {
226 self.layout_tree
227 .as_ref()
228 .map_or_else(String::new, |tree| tree.dump(&self.panes))
229 }
230
231 pub fn save_old_layout(&mut self) {
233 self.old_layout = Some(self.layout_dump());
234 }
235
236 #[must_use]
238 pub fn old_layout(&self) -> Option<&str> {
239 self.old_layout.as_deref()
240 }
241
242 #[must_use]
244 pub fn name(&self) -> Option<&str> {
245 self.name.as_deref()
246 }
247
248 #[must_use]
250 pub const fn automatic_rename(&self) -> bool {
251 self.automatic_rename
252 }
253
254 #[must_use]
256 pub const fn alert_flags(&self) -> AlertFlags {
257 self.alert_flags
258 }
259
260 #[must_use]
262 pub const fn alerts_queued(&self) -> bool {
263 self.alerts_queued
264 }
265
266 #[must_use]
268 pub fn pane_count(&self) -> usize {
269 self.panes.len()
270 }
271
272 pub fn queue_alerts(&mut self, flags: AlertFlags) {
274 self.alert_flags.insert(flags);
275 }
276
277 pub fn take_alert_flags(&mut self) -> AlertFlags {
279 let flags = self.alert_flags;
280 self.alert_flags = AlertFlags::empty();
281 flags
282 }
283
284 pub fn clear_alert_flags(&mut self, flags: AlertFlags) {
286 self.alert_flags.remove(flags);
287 }
288
289 pub fn set_alerts_queued(&mut self, queued: bool) {
291 self.alerts_queued = queued;
292 }
293
294 pub(crate) fn respawn(&mut self, pane_id: PaneId) -> PaneId {
297 let size = self.size;
298 self.panes = vec![Pane::new_with_id(
299 pane_id,
300 0,
301 PaneGeometry::new(0, 0, size.cols, size.rows),
302 )];
303 self.next_pane_index = 1;
304 self.active_pane = 0;
305 self.last_pane = None;
306 self.layout = LayoutName::MainVertical;
307 self.last_layout = None;
308 self.layout_tree = Some(LayoutTree::single(size));
309 self.custom_layout = false;
310 self.old_layout = None;
311 self.automatic_rename = true;
312 self.zoomed = false;
313 self.zoom_restore_pending = false;
314 self.alert_flags = AlertFlags::empty();
315 self.alerts_queued = false;
316 self.requested_main_width = None;
317 self.requested_main_height = None;
318 pane_id
319 }
320
321 pub(crate) fn set_size(&mut self, size: TerminalSize) {
322 self.size = size;
323 if self.zoomed {
324 self.apply_zoom_geometry();
325 } else {
326 self.recalculate_geometry();
327 }
328 }
329
330 pub(crate) fn set_name(&mut self, name: String) {
331 self.name = Some(name);
332 self.automatic_rename = false;
333 }
334
335 pub fn enable_automatic_rename(&mut self) {
337 self.automatic_rename = true;
338 }
339
340 pub fn set_automatic_name(&mut self, name: String) {
342 self.name = Some(name);
343 self.automatic_rename = true;
344 }
345
346 pub(crate) fn rotate_panes(&mut self, direction: RotateWindowDirection) {
347 self.rotate_panes_with_zoom(direction, false);
348 }
349
350 pub(crate) fn rotate_panes_with_zoom(
351 &mut self,
352 direction: RotateWindowDirection,
353 restore_zoom: bool,
354 ) {
355 if self.panes.len() <= 1 {
356 return;
357 }
358
359 self.push_zoom(restore_zoom);
360 let previous_active_pane_id = self
361 .active_pane()
362 .expect("active pane must exist before pane rotation")
363 .id();
364 let active_position = self
365 .panes
366 .iter()
367 .position(|pane| pane.index() == self.active_pane)
368 .expect("active pane must exist in window order");
369
370 match direction {
371 RotateWindowDirection::Down => self.panes.rotate_right(1),
372 RotateWindowDirection::Up => self.panes.rotate_left(1),
373 }
374 for (index, pane) in self.panes.iter_mut().enumerate() {
375 pane.set_index(index as u32);
376 }
377
378 self.apply_layout_tree();
379
380 self.active_pane = active_position as u32;
383 self.last_pane = self
384 .pane_index_for_id(previous_active_pane_id)
385 .filter(|pane_index| *pane_index != self.active_pane);
386 self.mark_pane_active(self.active_pane);
387
388 self.pop_zoom();
389 }
390
391 pub(crate) fn insert_pane_at_position(
392 &mut self,
393 position: usize,
394 pane: Pane,
395 direction: SplitDirection,
396 ) -> Result<(), RmuxError> {
397 if position > self.panes.len() {
398 return Err(RmuxError::Server(format!(
399 "cannot insert pane at position {position} in a {}-pane window",
400 self.panes.len()
401 )));
402 }
403
404 self.ensure_accepts_pane(&pane, None)?;
405 self.auto_unzoom();
406 self.layout = layout_for_split(direction);
407 self.bump_next_pane_index(pane.index());
408 let inserted_index = pane.index();
409 self.panes.insert(position, pane);
410 if self.panes.len() == 1 {
411 self.active_pane = inserted_index;
412 self.last_pane = None;
413 }
414 if self.panes.len() == 1 {
415 self.layout_tree = Some(LayoutTree::single(self.size));
416 self.apply_layout_tree();
417 return Ok(());
418 }
419
420 let (target_leaf, insert_before_target) = if position == 0 {
421 (0, true)
422 } else {
423 (position - 1, false)
424 };
425 let inserted = self.layout_tree.as_mut().is_some_and(|tree| {
426 tree.split_leaf(
427 target_leaf,
428 LayoutDirection::from_split_direction(direction),
429 insert_before_target,
430 )
431 });
432 if !inserted {
433 self.rebuild_named_layout_tree(self.layout);
434 } else {
435 self.apply_layout_tree();
436 }
437 Ok(())
438 }
439
440 pub(crate) fn move_pane_by_splitting_target(
441 &mut self,
442 source_position: usize,
443 target_position: usize,
444 final_insert_position: usize,
445 direction: SplitDirection,
446 insert_before_target: bool,
447 ) -> Result<PaneId, RmuxError> {
448 let pane_count = self.panes.len();
449 if source_position >= pane_count {
450 return Err(RmuxError::Server(format!(
451 "cannot move missing pane at position {source_position}"
452 )));
453 }
454 if target_position >= pane_count {
455 return Err(RmuxError::Server(format!(
456 "cannot split missing target pane at position {target_position}"
457 )));
458 }
459 if final_insert_position > pane_count.saturating_sub(1) {
460 return Err(RmuxError::Server(format!(
461 "cannot insert moved pane at position {final_insert_position} in a {}-pane window",
462 pane_count.saturating_sub(1)
463 )));
464 }
465
466 self.auto_unzoom();
467 self.layout = layout_for_split(direction);
468
469 let split_insert_position = if insert_before_target {
470 target_position
471 } else {
472 target_position + 1
473 };
474 let source_leaf_after_split = if split_insert_position <= source_position {
475 source_position + 1
476 } else {
477 source_position
478 };
479 let tree = self.layout_tree.as_mut().ok_or_else(|| {
480 RmuxError::Server("cannot move pane without a layout tree".to_owned())
481 })?;
482 if !tree.split_leaf(
483 target_position,
484 LayoutDirection::from_split_direction(direction),
485 insert_before_target,
486 ) {
487 return Err(RmuxError::Server(format!(
488 "cannot split target pane at position {target_position}"
489 )));
490 }
491 if !tree.remove_leaf(source_leaf_after_split) {
492 return Err(RmuxError::Server(format!(
493 "cannot remove source pane leaf at position {source_leaf_after_split}"
494 )));
495 }
496
497 let moved_pane = self.panes.remove(source_position);
498 let moved_pane_id = moved_pane.id();
499 self.panes.insert(final_insert_position, moved_pane);
500 self.apply_layout_tree();
501 Ok(moved_pane_id)
502 }
503
504 pub(crate) fn insert_pane_full_size(
505 &mut self,
506 pane: Pane,
507 direction: SplitDirection,
508 insert_before_target: bool,
509 ) -> Result<(), RmuxError> {
510 self.ensure_accepts_pane(&pane, None)?;
511 self.auto_unzoom();
512 self.layout = layout_for_split(direction);
513 self.bump_next_pane_index(pane.index());
514
515 if insert_before_target {
516 self.panes.insert(0, pane);
517 } else {
518 self.panes.push(pane);
519 }
520
521 let split = self.layout_tree.as_mut().is_some_and(|tree| {
522 tree.split_root(
523 LayoutDirection::from_split_direction(direction),
524 insert_before_target,
525 )
526 });
527 if !split {
528 self.rebuild_named_layout_tree(self.layout);
529 } else {
530 self.apply_layout_tree();
531 }
532 Ok(())
533 }
534
535 pub(crate) fn replace_pane_for_swap(
536 &mut self,
537 pane_index: u32,
538 pane: Pane,
539 ) -> Result<(), RmuxError> {
540 let position = self.pane_position(pane_index).ok_or_else(|| {
541 RmuxError::Server(format!(
542 "cannot replace missing pane index {pane_index} in window {}",
543 self.id
544 ))
545 })?;
546 for (existing_position, existing) in self.panes.iter().enumerate() {
547 if existing_position != position && existing.id() == pane.id() {
548 return Err(RmuxError::Server(format!(
549 "pane id {} already exists in window {}",
550 pane.id().as_u32(),
551 self.id
552 )));
553 }
554 }
555 self.bump_next_pane_index(pane.index());
556 self.panes[position] = pane;
557 self.apply_layout_tree();
558 Ok(())
559 }
560
561 pub(crate) fn swap_panes(&mut self, source_pane_index: u32, target_pane_index: u32) -> bool {
562 let Some(source_position) = self.pane_position(source_pane_index) else {
563 return false;
564 };
565 let Some(target_position) = self.pane_position(target_pane_index) else {
566 return false;
567 };
568 if source_position == target_position {
569 return true;
570 }
571
572 self.auto_unzoom();
573 let active_pane_id = self
574 .active_pane()
575 .expect("active pane must exist before pane swap")
576 .id();
577 let last_pane_id = self
578 .last_pane
579 .and_then(|pane_index| self.pane(pane_index).map(Pane::id));
580 self.panes.swap(source_position, target_position);
581 self.apply_layout_tree();
582 self.renumber_panes_by_position(active_pane_id, last_pane_id);
583 true
584 }
585}
586
587fn current_unix_timestamp() -> i64 {
588 SystemTime::now()
589 .duration_since(UNIX_EPOCH)
590 .ok()
591 .and_then(|duration| i64::try_from(duration.as_secs()).ok())
592 .unwrap_or_default()
593}
594
595#[cfg(test)]
596#[path = "window/tests.rs"]
597mod tests;