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 self.recalculate_geometry();
324 if self.zoomed {
325 self.apply_zoom_geometry();
326 }
327 }
328
329 pub(crate) fn set_name(&mut self, name: String) {
330 self.name = Some(name);
331 self.automatic_rename = false;
332 }
333
334 pub fn enable_automatic_rename(&mut self) {
336 self.automatic_rename = true;
337 }
338
339 pub fn set_automatic_name(&mut self, name: String) {
341 self.name = Some(name);
342 self.automatic_rename = true;
343 }
344
345 pub(crate) fn rotate_panes(&mut self, direction: RotateWindowDirection) {
346 self.rotate_panes_with_zoom(direction, false);
347 }
348
349 pub(crate) fn rotate_panes_with_zoom(
350 &mut self,
351 direction: RotateWindowDirection,
352 restore_zoom: bool,
353 ) {
354 if self.panes.len() <= 1 {
355 return;
356 }
357
358 self.push_zoom(restore_zoom);
359 let previous_active_pane_id = self
360 .active_pane()
361 .expect("active pane must exist before pane rotation")
362 .id();
363 let active_position = self
364 .panes
365 .iter()
366 .position(|pane| pane.index() == self.active_pane)
367 .expect("active pane must exist in window order");
368
369 match direction {
370 RotateWindowDirection::Down => self.panes.rotate_right(1),
371 RotateWindowDirection::Up => self.panes.rotate_left(1),
372 }
373 for (index, pane) in self.panes.iter_mut().enumerate() {
374 pane.set_index(index as u32);
375 }
376
377 self.apply_layout_tree();
378
379 self.active_pane = active_position as u32;
382 self.last_pane = self
383 .pane_index_for_id(previous_active_pane_id)
384 .filter(|pane_index| *pane_index != self.active_pane);
385 self.mark_pane_active(self.active_pane);
386
387 self.pop_zoom();
388 }
389
390 pub(crate) fn insert_pane_at_position(
391 &mut self,
392 position: usize,
393 pane: Pane,
394 direction: SplitDirection,
395 ) -> Result<(), RmuxError> {
396 if position > self.panes.len() {
397 return Err(RmuxError::Server(format!(
398 "cannot insert pane at position {position} in a {}-pane window",
399 self.panes.len()
400 )));
401 }
402
403 self.ensure_accepts_pane(&pane, None)?;
404 self.auto_unzoom();
405 self.layout = layout_for_split(direction);
406 self.bump_next_pane_index(pane.index());
407 let inserted_index = pane.index();
408 self.panes.insert(position, pane);
409 if self.panes.len() == 1 {
410 self.active_pane = inserted_index;
411 self.last_pane = None;
412 }
413 if self.panes.len() == 1 {
414 self.layout_tree = Some(LayoutTree::single(self.size));
415 self.apply_layout_tree();
416 return Ok(());
417 }
418
419 let (target_leaf, insert_before_target) = if position == 0 {
420 (0, true)
421 } else {
422 (position - 1, false)
423 };
424 let inserted = self.layout_tree.as_mut().is_some_and(|tree| {
425 tree.split_leaf(
426 target_leaf,
427 LayoutDirection::from_split_direction(direction),
428 insert_before_target,
429 )
430 });
431 if !inserted {
432 self.rebuild_named_layout_tree(self.layout);
433 } else {
434 self.apply_layout_tree();
435 }
436 Ok(())
437 }
438
439 pub(crate) fn move_pane_by_splitting_target(
440 &mut self,
441 source_position: usize,
442 target_position: usize,
443 final_insert_position: usize,
444 direction: SplitDirection,
445 insert_before_target: bool,
446 ) -> Result<PaneId, RmuxError> {
447 let pane_count = self.panes.len();
448 if source_position >= pane_count {
449 return Err(RmuxError::Server(format!(
450 "cannot move missing pane at position {source_position}"
451 )));
452 }
453 if target_position >= pane_count {
454 return Err(RmuxError::Server(format!(
455 "cannot split missing target pane at position {target_position}"
456 )));
457 }
458 if final_insert_position > pane_count.saturating_sub(1) {
459 return Err(RmuxError::Server(format!(
460 "cannot insert moved pane at position {final_insert_position} in a {}-pane window",
461 pane_count.saturating_sub(1)
462 )));
463 }
464
465 self.auto_unzoom();
466 self.layout = layout_for_split(direction);
467
468 let split_insert_position = if insert_before_target {
469 target_position
470 } else {
471 target_position + 1
472 };
473 let source_leaf_after_split = if split_insert_position <= source_position {
474 source_position + 1
475 } else {
476 source_position
477 };
478 let tree = self.layout_tree.as_mut().ok_or_else(|| {
479 RmuxError::Server("cannot move pane without a layout tree".to_owned())
480 })?;
481 if !tree.split_leaf(
482 target_position,
483 LayoutDirection::from_split_direction(direction),
484 insert_before_target,
485 ) {
486 return Err(RmuxError::Server(format!(
487 "cannot split target pane at position {target_position}"
488 )));
489 }
490 if !tree.remove_leaf(source_leaf_after_split) {
491 return Err(RmuxError::Server(format!(
492 "cannot remove source pane leaf at position {source_leaf_after_split}"
493 )));
494 }
495
496 let moved_pane = self.panes.remove(source_position);
497 let moved_pane_id = moved_pane.id();
498 self.panes.insert(final_insert_position, moved_pane);
499 self.apply_layout_tree();
500 Ok(moved_pane_id)
501 }
502
503 pub(crate) fn insert_pane_full_size(
504 &mut self,
505 pane: Pane,
506 direction: SplitDirection,
507 insert_before_target: bool,
508 ) -> Result<(), RmuxError> {
509 self.ensure_accepts_pane(&pane, None)?;
510 self.auto_unzoom();
511 self.layout = layout_for_split(direction);
512 self.bump_next_pane_index(pane.index());
513
514 if insert_before_target {
515 self.panes.insert(0, pane);
516 } else {
517 self.panes.push(pane);
518 }
519
520 let split = self.layout_tree.as_mut().is_some_and(|tree| {
521 tree.split_root(
522 LayoutDirection::from_split_direction(direction),
523 insert_before_target,
524 )
525 });
526 if !split {
527 self.rebuild_named_layout_tree(self.layout);
528 } else {
529 self.apply_layout_tree();
530 }
531 Ok(())
532 }
533
534 pub(crate) fn replace_pane_for_swap(
535 &mut self,
536 pane_index: u32,
537 pane: Pane,
538 ) -> Result<(), RmuxError> {
539 let position = self.pane_position(pane_index).ok_or_else(|| {
540 RmuxError::Server(format!(
541 "cannot replace missing pane index {pane_index} in window {}",
542 self.id
543 ))
544 })?;
545 for (existing_position, existing) in self.panes.iter().enumerate() {
546 if existing_position != position && existing.id() == pane.id() {
547 return Err(RmuxError::Server(format!(
548 "pane id {} already exists in window {}",
549 pane.id().as_u32(),
550 self.id
551 )));
552 }
553 }
554 self.bump_next_pane_index(pane.index());
555 self.panes[position] = pane;
556 self.apply_layout_tree();
557 Ok(())
558 }
559
560 pub(crate) fn swap_panes(&mut self, source_pane_index: u32, target_pane_index: u32) -> bool {
561 let Some(source_position) = self.pane_position(source_pane_index) else {
562 return false;
563 };
564 let Some(target_position) = self.pane_position(target_pane_index) else {
565 return false;
566 };
567 if source_position == target_position {
568 return true;
569 }
570
571 self.auto_unzoom();
572 let active_pane_id = self
573 .active_pane()
574 .expect("active pane must exist before pane swap")
575 .id();
576 let last_pane_id = self
577 .last_pane
578 .and_then(|pane_index| self.pane(pane_index).map(Pane::id));
579 self.panes.swap(source_position, target_position);
580 self.apply_layout_tree();
581 self.renumber_panes_by_position(active_pane_id, last_pane_id);
582 true
583 }
584}
585
586fn current_unix_timestamp() -> i64 {
587 SystemTime::now()
588 .duration_since(UNIX_EPOCH)
589 .ok()
590 .and_then(|duration| i64::try_from(duration.as_secs()).ok())
591 .unwrap_or_default()
592}
593
594#[cfg(test)]
595#[path = "window/tests.rs"]
596mod tests;