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