use crate::app::window_state::WindowState;
use crate::profile::actions::{ProfileAction, ProfileSplit};
use crate::profile_launcher_ui::{LaunchTarget, LauncherChoice};
impl WindowState {
fn launcher_rows(&self) -> Vec<crate::profile_launcher_ui::LauncherRow> {
let registry = &self.keybinding_registry;
crate::profile_launcher_ui::launcher_rows(&self.overlay_ui.profile_manager, |action| {
registry
.chord_for_action(action)
.map(|combo| crate::command_palette::catalog::chord_display(&combo))
})
}
pub(crate) fn open_profile_launcher(&mut self) {
let rows = self.launcher_rows();
let chord = self.live_chord_hint("toggle_profile_drawer");
self.overlay_ui.profile_launcher_ui.open(rows, chord);
self.focus_state.needs_redraw = true;
self.request_redraw();
}
pub fn toggle_profile_drawer(&mut self) {
if self.overlay_ui.profile_launcher_ui.visible {
self.overlay_ui.profile_launcher_ui.close();
} else {
self.open_profile_launcher();
}
self.focus_state.needs_redraw = true;
self.request_redraw();
}
pub(crate) fn toggle_profiles_drawer(&mut self) {
let drawer = &mut self.overlay_ui.profile_drawer_ui;
drawer.toggle();
if drawer.expanded {
let rows = self.launcher_rows();
let launcher = &mut self.overlay_ui.profile_launcher_ui;
launcher.set_rows(rows);
launcher.prepare_pinned();
}
self.sync_ai_inspector_width();
self.focus_state.needs_redraw = true;
self.request_redraw();
}
pub(crate) fn mouse_over_profiles_drawer(&self, position: (f64, f64)) -> bool {
let drawer = &self.overlay_ui.profile_drawer_ui;
if !drawer.expanded {
return false;
}
self.with_window(|window| {
let scale = window.scale_factor();
let right = window.inner_size().width as f64
- f64::from(self.overlay_ui.ai_inspector.consumed_width()) * scale;
position.0 >= right - f64::from(drawer.consumed_width()) * scale
})
.unwrap_or(true)
}
pub(crate) fn refresh_profiles_drawer_rows(&mut self) {
if self.overlay_ui.profile_drawer_ui.expanded {
let rows = self.launcher_rows();
self.overlay_ui.profile_launcher_ui.set_rows(rows);
}
}
pub(crate) fn launcher_takes_split(&mut self, action: &str) -> bool {
if !self.overlay_ui.profile_launcher_ui.visible {
return false;
}
let target = match action {
"split_right" => LaunchTarget::SplitRight,
"split_down" => LaunchTarget::SplitDown,
_ => return false,
};
if let Some(choice) = self.overlay_ui.profile_launcher_ui.choose_selected(target) {
self.run_launcher_choice(choice);
}
true
}
pub(crate) fn run_launcher_choice(&mut self, choice: LauncherChoice) {
match choice {
LauncherChoice::Open { profiles, target } => {
for profile in profiles {
self.launch_profile_at(profile, target);
}
}
LauncherChoice::Edit(_) => self.manage_profiles(),
LauncherChoice::Manage => self.manage_profiles(),
LauncherChoice::Duplicate(id) => self.duplicate_profile(id),
}
self.focus_state.needs_redraw = true;
self.request_redraw();
}
fn launch_profile_at(
&mut self,
profile: Option<crate::profile::ProfileId>,
target: LaunchTarget,
) {
let Some(id) = profile else {
match target {
LaunchTarget::NewTab => self.new_tab(),
LaunchTarget::NewWindow => {
crate::menu::dispatch(crate::menu::MenuAction::NewWindow);
}
LaunchTarget::SplitRight => {
self.execute_keybinding_action("split_right");
}
LaunchTarget::SplitDown => {
self.execute_keybinding_action("split_down");
}
LaunchTarget::ThisTab => self.clear_tab_profile(),
}
return;
};
let action = match target {
LaunchTarget::NewTab => ProfileAction::OpenTab(id),
LaunchTarget::NewWindow => ProfileAction::OpenWindow(id),
LaunchTarget::SplitRight => ProfileAction::Split(id, ProfileSplit::Right),
LaunchTarget::SplitDown => ProfileAction::Split(id, ProfileSplit::Down),
LaunchTarget::ThisTab => ProfileAction::SetTabProfile(id),
};
self.execute_keybinding_action(&action.id());
}
fn clear_tab_profile(&mut self) {
let Some(tab) = self.tab_manager.active_tab_mut() else {
return;
};
if let crate::tab::TitleChange::Set(title) = tab.profile.revert_auto() {
tab.set_title(&title);
}
tab.profile.source_profile_id = None;
tab.profile.profile_icon = None;
tab.profile.badge_override = None;
tab.profile.pinned = true;
self.badge_tab = None;
self.show_toast("Tab profile: Default");
}
pub(crate) fn duplicate_profile(&mut self, id: crate::profile::ProfileId) {
let Some(original) = self.overlay_ui.profile_manager.get(&id).cloned() else {
return;
};
let copy = duplicate_of(&original);
let name = copy.name.clone();
let mut profiles = self.overlay_ui.profile_manager.to_vec();
let at = profiles
.iter()
.position(|p| p.id == id)
.map_or(profiles.len(), |i| i + 1);
profiles.insert(at, copy);
for (order, profile) in profiles.iter_mut().enumerate() {
profile.order = order;
}
self.apply_profile_changes(profiles);
self.show_toast(format!("Duplicated as {name}"));
}
}
pub(crate) fn duplicate_of(profile: &crate::profile::Profile) -> crate::profile::Profile {
let mut copy = profile.clone();
copy.id = uuid::Uuid::new_v4();
copy.name = format!("{} (copy)", profile.name);
copy.source = crate::profile::ProfileSource::Local;
copy.keyboard_shortcut = None;
copy
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::profile::Profile;
use crate::tab::Tab;
use std::sync::Arc;
fn window() -> WindowState {
let runtime = Arc::new(
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime"),
);
let mut ws = WindowState::new(Config::default(), runtime);
ws.overlay_ui.profile_manager = crate::profile::ProfileManager::new();
ws
}
#[test]
fn the_open_profiles_chord_toggles_the_launcher() {
let mut ws = window();
ws.overlay_ui.profile_manager.add(Profile::new("Work"));
assert!(ws.execute_keybinding_action("toggle_profile_drawer"));
assert!(ws.overlay_ui.profile_launcher_ui.visible);
assert!(ws.overlay_is_open(crate::app::overlay::OverlayId::ProfileLauncher));
assert!(
ws.any_modal_ui_visible(),
"the launcher joins the key guard"
);
assert!(ws.execute_keybinding_action("toggle_profile_drawer"));
assert!(!ws.overlay_ui.profile_launcher_ui.visible);
}
#[test]
fn new_tab_with_the_profiles_setting_opens_the_launcher() {
let mut config = Config::default();
config.tabs.new_tab_shortcut_shows_profiles = true;
let runtime = Arc::new(
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime"),
);
let mut ws = WindowState::new(config, runtime);
ws.overlay_ui.profile_manager = crate::profile::ProfileManager::new();
ws.overlay_ui.profile_manager.add(Profile::new("Work"));
assert!(ws.execute_keybinding_action("new_tab"));
assert!(ws.overlay_ui.profile_launcher_ui.visible);
}
#[test]
fn a_native_menu_split_while_the_launcher_is_open_splits_with_the_profile() {
let mut ws = window();
let mut work = Profile::new("Work");
work.command = Some("/bin/sh".to_string());
ws.overlay_ui.profile_manager.add(work);
ws.tab_manager.push_tab_for_test(Tab::new_stub(1, 1));
ws.open_profile_launcher();
ws.overlay_ui.profile_launcher_ui.set_query_for_test("work");
assert!(ws.launcher_takes_split("split_right"));
assert!(
!ws.overlay_ui.profile_launcher_ui.visible,
"the launcher closed: it took the split"
);
assert!(!ws.launcher_takes_split("split_right"));
}
#[test]
fn the_drawer_is_the_launcher_pinned_and_a_panel_in_the_stack() {
let mut ws = window();
ws.overlay_ui.profile_manager.add(Profile::new("Work"));
assert!(ws.execute_keybinding_action("toggle_profiles_panel"));
assert!(ws.overlay_ui.profile_drawer_ui.expanded);
assert!(ws.overlay_ui.profile_drawer_ui.consumed_width() > 0.0);
assert!(ws.overlay_is_open(crate::app::overlay::OverlayId::ProfileDrawer));
assert!(
!ws.any_modal_ui_visible(),
"an unfocused drawer does not block the terminal"
);
let labels: Vec<String> = ws
.overlay_ui
.profile_launcher_ui
.filtered()
.iter()
.map(|r| r.label.clone())
.collect();
assert_eq!(labels, ["Default", "Work"]);
assert!(ws.execute_keybinding_action("toggle_profiles_panel"));
assert_eq!(ws.overlay_ui.profile_drawer_ui.consumed_width(), 0.0);
}
#[test]
fn duplicate_copies_with_a_new_id_and_no_shortcut() {
let mut original = Profile::new("Work");
original.keyboard_shortcut = Some("Ctrl+Alt+W".to_string());
original.tags = vec!["dev".to_string()];
let copy = duplicate_of(&original);
assert_ne!(copy.id, original.id);
assert_eq!(copy.name, "Work (copy)");
assert_eq!(copy.tags, original.tags);
assert_eq!(copy.keyboard_shortcut, None);
}
}