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