diff options
author | Aram Drevekenin <aram@poor.dev> | 2023-11-05 15:32:05 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-05 15:32:05 +0100 |
commit | 28a165a969a9e1ee9f1fe66e4d63134b895b2ef7 (patch) | |
tree | 678ec1cd9d536d9cb7fcdb071e27cf12caf43afb | |
parent | 9eb9734bcca3cb0dcdff592da4d7a536d860c363 (diff) |
feat(ux): allow renaming sessions (#2903)
* change session name through the cli
* change session name from the session-manager
* style(fmt): rustfmt
23 files changed, 315 insertions, 16 deletions
diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs index 7e39307d9..8f70cc295 100644 --- a/default-plugins/session-manager/src/main.rs +++ b/default-plugins/session-manager/src/main.rs @@ -7,8 +7,8 @@ use std::collections::BTreeMap; use ui::{ components::{ - render_controls_line, render_new_session_line, render_prompt, render_resurrection_toggle, - Colors, + render_controls_line, render_error, render_new_session_line, render_prompt, + render_renaming_session_screen, render_resurrection_toggle, Colors, }, SessionUiInfo, }; @@ -23,6 +23,8 @@ struct State { resurrectable_sessions: ResurrectableSessions, search_term: String, new_session_name: Option<String>, + renaming_session_name: Option<String>, + error: Option<String>, browsing_resurrection_sessions: bool, colors: Colors, } @@ -67,6 +69,9 @@ impl ZellijPlugin for State { if self.browsing_resurrection_sessions { self.resurrectable_sessions.render(rows, cols); return; + } else if let Some(new_session_name) = self.renaming_session_name.as_ref() { + render_renaming_session_screen(&new_session_name, rows, cols); + return; } render_resurrection_toggle(cols, false); render_prompt( @@ -87,7 +92,11 @@ impl ZellijPlugin for State { self.sessions.is_searching, self.colors, ); - render_controls_line(self.sessions.is_searching, rows, cols, self.colors); + if let Some(error) = self.error.as_ref() { + render_error(&error, rows, cols); + } else { + render_controls_line(self.sessions.is_searching, rows, cols, self.colors); + } } } @@ -96,6 +105,10 @@ impl State { self.sessions.reset_selected_index(); } fn handle_key(&mut self, key: Key) -> bool { + if self.error.is_some() { + self.error = None; + return true; + } let mut should_render = false; if let Key::Right = key { if self.new_session_name.is_none() { @@ -110,14 +123,14 @@ impl State { } else if let Key::Down = key { if self.browsing_resurrection_sessions { self.resurrectable_sessions.move_selection_down(); - } else if self.new_session_name.is_none() { + } else if self.new_session_name.is_none() && self.renaming_session_name.is_none() { self.sessions.move_selection_down(); } should_render = true; } else if let Key::Up = key { if self.browsing_resurrection_sessions { self.resurrectable_sessions.move_selection_up(); - } else if self.new_session_name.is_none() { + } else if self.new_session_name.is_none() && self.renaming_session_name.is_none() { self.sessions.move_selection_up(); } should_render = true; @@ -126,6 +139,8 @@ impl State { self.handle_selection(); } else if let Some(new_session_name) = self.new_session_name.as_mut() { new_session_name.push(character); + } else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() { + renaming_session_name.push(character); } else if self.browsing_resurrection_sessions { self.resurrectable_sessions.handle_character(character); } else { @@ -141,6 +156,12 @@ impl State { } else { new_session_name.pop(); } + } else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() { + if renaming_session_name.is_empty() { + self.renaming_session_name = None; + } else { + renaming_session_name.pop(); + } } else if self.browsing_resurrection_sessions { self.resurrectable_sessions.handle_backspace(); } else { @@ -150,7 +171,7 @@ impl State { } should_render = true; } else if let Key::Ctrl('w') = key { - if self.sessions.is_searching { + if self.sessions.is_searching || self.browsing_resurrection_sessions { // no-op } else if self.new_session_name.is_some() { self.new_session_name = None; @@ -158,6 +179,15 @@ impl State { self.new_session_name = Some(String::new()); } should_render = true; + } else if let Key::Ctrl('r') = key { + if self.sessions.is_searching || self.browsing_resurrection_sessions { + // no-op + } else if self.renaming_session_name.is_some() { + self.renaming_session_name = None; + } else { + self.renaming_session_name = Some(String::new()); + } + should_render = true; } else if let Key::Ctrl('c') = key { if let Some(new_session_name) = self.new_session_name.as_mut() { if new_session_name.is_empty() { @@ -165,6 +195,12 @@ impl State { } else { new_session_name.clear() } + } else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() { + if renaming_session_name.is_empty() { + self.renaming_session_name = None; + } else { + renaming_session_name.clear() + } } else if !self.search_term.is_empty() { self.search_term.clear(); self.sessions @@ -190,7 +226,15 @@ impl State { should_render = true; } } else if let Key::Esc = key { - hide_self(); + if self.renaming_session_name.is_some() { + self.renaming_session_name = None; + should_render = true; + } else if self.new_session_name.is_some() { + self.new_session_name = None; + should_render = true; + } else { + hide_self(); + } } should_render } @@ -210,6 +254,29 @@ impl State { } else { switch_session(Some(new_session_name)); } + } else if let Some(renaming_session_name) = &self.renaming_session_name.take() { + if renaming_session_name.is_empty() { + // TODO: implement these, then implement the error UI, then implement the renaming + // session screen, then test it + self.show_error("New name must not be empty."); + return; // s that we don't hide self + } else if self.session_name.as_ref() == Some(renaming_session_name) { + // noop - we're already called that! + return; // s that we don't hide self + } else if self.sessions.has_session(&renaming_session_name) { + self.show_error("A session by this name already exists."); + return; // s that we don't hide self + } else if self + .resurrectable_sessions + .has_session(&renaming_session_name) + { + self.show_error("A resurrectable session by this name already exists."); + return; // s that we don't hide self + } else { + self.update_current_session_name_in_ui(&renaming_session_name); + rename_session(&renaming_session_name); + return; // s that we don't hide self + } } else if let Some(selected_session_name) = self.sessions.get_selected_session_name() { let selected_tab = self.sessions.get_selected_tab_position(); let selected_pane = self.sessions.get_selected_pane_id(); @@ -235,6 +302,16 @@ impl State { .update_search_term(&self.search_term, &self.colors); hide_self(); } + fn show_error(&mut self, error_text: &str) { + self.error = Some(error_text.to_owned()); + } + fn update_current_session_name_in_ui(&mut self, new_name: &str) { + if let Some(old_session_name) = self.session_name.as_ref() { + self.sessions + .update_session_name(&old_session_name, new_name); + } + self.session_name = Some(new_name.to_owned()); + } fn update_session_infos(&mut self, session_infos: Vec<SessionInfo>) { let session_infos: Vec<SessionUiInfo> = session_infos .iter() diff --git a/default-plugins/session-manager/src/resurrectable_sessions.rs b/default-plugins/session-manager/src/resurrectable_sessions.rs index beed254fd..dfbb86f0d 100644 --- a/default-plugins/session-manager/src/resurrectable_sessions.rs +++ b/default-plugins/session-manager/src/resurrectable_sessions.rs @@ -308,6 +308,11 @@ impl ResurrectableSessions { self.search_term.pop(); self.update_search_term(); } + pub fn has_session(&self, session_name: &str) -> bool { + self.all_resurrectable_sessions + .iter() + .any(|s| s.0 == session_name) + } fn update_search_term(&mut self) { let mut matches = vec![]; let matcher = SkimMatcherV2::default().use_cache(true); diff --git a/default-plugins/session-manager/src/session_list.rs b/default-plugins/session-manager/src/session_list.rs index 788d324f7..fa63571e8 100644 --- a/default-plugins/session-manager/src/session_list.rs +++ b/default-plugins/session-manager/src/session_list.rs @@ -314,6 +314,15 @@ impl SessionList { pub fn reset_selected_index(&mut self) { self.selected_index.reset(); } + pub fn has_session(&self, session_name: &str) -> bool { + self.session_ui_infos.iter().any(|s| s.name == session_name) + } + pub fn update_session_name(&mut self, old_name: &str, new_name: &str) { + self.session_ui_infos + .iter_mut() + .find(|s| s.name == old_name) + .map(|s| s.name = new_name.to_owned()); + } } #[derive(Debug, Clone, Default)] diff --git a/default-plugins/session-manager/src/ui/components.rs b/default-plugins/session-manager/src/ui/components.rs index be3da41df..4d9839c0f 100644 --- a/default-plugins/session-manager/src/ui/components.rs +++ b/default-plugins/session-manager/src/ui/components.rs @@ -558,29 +558,68 @@ pub fn render_new_session_line(session_name: &Option<String>, is_searching: bool } } +pub fn render_error(error_text: &str, rows: usize, columns: usize) { + print_text_with_coordinates( + Text::new(format!("Error: {}", error_text)).color_range(3, ..), + 0, + rows, + Some(columns), + None, + ); +} + +pub fn render_renaming_session_screen(new_session_name: &str, rows: usize, columns: usize) { + if rows == 0 || columns == 0 { + return; + } + let prompt_text = "NEW NAME FOR CURRENT SESSION"; + let new_session_name = format!("{}_", new_session_name); + let prompt_y_location = (rows / 2).saturating_sub(1); + let session_name_y_location = (rows / 2) + 1; + let prompt_x_location = columns.saturating_sub(prompt_text.chars().count()) / 2; + let session_name_x_location = columns.saturating_sub(new_session_name.chars().count()) / 2; + print_text_with_coordinates( + Text::new(prompt_text).color_range(0, ..), + prompt_x_location, + prompt_y_location, + None, + None, + ); + print_text_with_coordinates( + Text::new(new_session_name).color_range(3, ..), + session_name_x_location, + session_name_y_location, + None, + None, + ); +} + pub fn render_controls_line(is_searching: bool, row: usize, max_cols: usize, colors: Colors) { let (arrows, navigate) = if is_searching { (colors.magenta("<↓↑>"), colors.bold("Navigate")) } else { (colors.magenta("<←↓↑→>"), colors.bold("Navigate and Expand")) }; + let rename = colors.magenta("<Ctrl r>"); + let rename_text = colors.bold("Rename session"); let enter = colors.magenta("<ENTER>"); let select = colors.bold("Switch to selected"); let esc = colors.magenta("<ESC>"); let to_hide = colors.bold("Hide"); - if max_cols >= 80 { + if max_cols >= 104 { print!( - "\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {esc} - {to_hide}" + "\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}" ); - } else if max_cols >= 57 { + } else if max_cols >= 73 { let navigate = colors.bold("Navigate"); let select = colors.bold("Switch"); + let rename_text = colors.bold("Rename"); print!( - "\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {esc} - {to_hide}" + "\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}" ); - } else if max_cols >= 20 { - print!("\u{1b}[m\u{1b}[{row}H{arrows}/{enter}/{esc}"); + } else if max_cols >= 28 { + print!("\u{1b}[m\u{1b}[{row}H{arrows}/{enter}/{rename}/{esc}"); } } diff --git a/zellij-client/src/cli_client.rs b/zellij-client/src/cli_client.rs index 6ebc10aeb..ef9f9122f 100644 --- a/zellij-client/src/cli_client.rs +++ b/zellij-client/src/cli_client.rs @@ -35,6 +35,10 @@ pub fn start_cli_client(os_input: Box<dyn ClientOsApi>, session_name: &str, acti log_lines.iter().for_each(|line| println!("{line}")); process::exit(0); }, + Some((ServerToClientMsg::LogError(log_lines), _)) => { + log_lines.iter().for_each(|line| eprintln!("{line}")); + process::exit(2); + }, _ => {}, } } diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 6cb855118..c704ad84a 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -46,6 +46,7 @@ pub(crate) enum ClientInstruction { StartedParsingStdinQuery, DoneParsingStdinQuery, Log(Vec<String>), + LogError(Vec<String>), SwitchSession(ConnectToSession), SetSynchronizedOutput(Option<SyncOutput>), } @@ -62,6 +63,7 @@ impl From<ServerToClientMsg> for ClientInstruction { ServerToClientMsg::Connected => ClientInstruction::Connected, ServerToClientMsg::ActiveClients(clients) => ClientInstruction::ActiveClients(clients), ServerToClientMsg::Log(log_lines) => ClientInstruction::Log(log_lines), + ServerToClientMsg::LogError(log_lines) => ClientInstruction::LogError(log_lines), ServerToClientMsg::SwitchSession(connect_to_session) => { ClientInstruction::SwitchSession(connect_to_session) }, @@ -80,6 +82,7 @@ impl From<&ClientInstruction> for ClientContext { ClientInstruction::Connected => ClientContext::Connected, ClientInstruction::ActiveClients(_) => ClientContext::ActiveClients, ClientInstruction::Log(_) => ClientContext::Log, + ClientInstruction::LogError(_) => ClientContext::LogError, ClientInstruction::StartedParsingStdinQuery => ClientContext::StartedParsingStdinQuery, ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery, ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession, @@ -473,6 +476,11 @@ pub fn start_client( log::info!("{line}"); } }, + ClientInstruction::LogError(lines_to_log) => { + for line in lines_to_log { + log::error!("{line}"); + } + }, ClientInstruction::SwitchSession(connect_to_session) => { reconnect_to_session = Some(connect_to_session); os_input.send_to_server(ClientToServerMsg::ClientExited); diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 38b89ceac..ca24c015a 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -237,6 +237,9 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) { PluginCommand::OpenCommandPaneInPlace(command_to_run) => { open_command_pane_in_place(env, command_to_run) }, + PluginCommand::RenameSession(new_session_name) => { + rename_session(env, new_session_name) + }, }, (PermissionStatus::Denied, permission) => { log::error!( @@ -1188,6 +1191,17 @@ fn rename_tab(env: &ForeignFunctionEnv, tab_index: u32, new_name: &str) { apply_action!(rename_tab_action, error_msg, env); } +fn rename_session(env: &ForeignFunctionEnv, new_session_name: String) { + let error_msg = || { + format!( + "failed to rename session in plugin {}", + env.plugin_env.name() + ) + }; + let action = Action::RenameSession(new_session_name); + apply_action!(action, error_msg, env); +} + // Custom panic handler for plugins. // // This is called when a panic occurs in a plugin. Since most panics will likely originate in the @@ -1315,6 +1329,7 @@ fn check_command_permission( | PluginCommand::SwitchSession(..) | PluginCommand::DeleteDeadSession(..) | PluginCommand::DeleteAllDeadSessions + | PluginCommand::RenameSession(..) | PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState, _ => return (PermissionStatus::Granted, None), }; diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index a7d85e4ac..fdc83ac64 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -784,6 +784,11 @@ pub(crate) fn route_action( .send_to_screen(ScreenInstruction::BreakPaneLeft(client_id)) .with_context(err_context)?; }, + Action::RenameSession(name) => { + senders + .send_to_screen(ScreenInstruction::RenameSession(name, client_id)) + .with_context(err_context)?; + }, } Ok(should_break) } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index fbc355666..1adc2b1ac 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -15,6 +15,8 @@ use zellij_utils::input::command::RunCommand; use zellij_utils::input::options::Clipboard; use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::{ + consts::{session_info_folder_for_session, ZELLIJ_SOCK_DIR}, + envs::set_session_name, input::command::TerminalAction, input::layout::{ FloatingPaneLayout, Layout, PluginUserConfiguration, Run, RunPlugin, RunPluginLocation, @@ -314,6 +316,7 @@ pub enum ScreenInstruction { ClientTabIndexOrPaneId, ), DumpLayoutToHd, + RenameSession(String, ClientId), // String -> new name } impl From<&ScreenInstruction> for ScreenContext { @@ -492,6 +495,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ReplacePane(..) => ScreenContext::ReplacePane, ScreenInstruction::NewInPlacePluginPane(..) => ScreenContext::NewInPlacePluginPane, ScreenInstruction::DumpLayoutToHd => ScreenContext::DumpLayoutToHd, + ScreenInstruction::RenameSession(..) => ScreenContext::RenameSession, } } } @@ -1520,7 +1524,10 @@ impl Screen { } } - pub fn change_mode(&mut self, mode_info: ModeInfo, client_id: ClientId) -> Result<()> { + pub fn change_mode(&mut self, mut mode_info: ModeInfo, client_id: ClientId) -> Result<()> { + if mode_info.session_name.as_ref() != Some(&self.session_name) { + mode_info.session_name = Some(self.session_name.clone()); + } let previous_mode = self .mode_info .get(&client_id) @@ -3471,6 +3478,69 @@ pub(crate) fn screen_thread_main( screen.dump_layout_to_hd()?; } }, + ScreenInstruction::RenameSession(name, client_id) => { + if screen.session_infos_on_machine.contains_key(&name) { + let error_text = "A session by this name already exists."; + log::error!("{}", error_text); + if let Some(os_input) = &mut screen.bus.os_input { + let _ = os_input.send_to_client( + client_id, + ServerToClientMsg::LogError(vec![error_text.to_owned()]), + ); + } + } else if screen.resurrectable_sessions.contains_key(&name) { + let error_text = + "A resurrectable session by this name exists, cannot use this name."; + log::error!("{}", error_text); + if let Some(os_input) = &mut screen.bus.os_input { + let _ = os_input.send_to_client( + client_id, + ServerToClientMsg::LogError(vec![error_text.to_owned()]), + ); + } + } else { + let err_context = || format!("Failed to rename session"); + let old_session_name = screen.session_name.clone(); + + // update state + screen.session_name = name.clone(); + screen.default_mode_info.session_name = Some(name.clone()); + for (_client_id, mut mode_info) in screen.mode_info.iter_mut() { + mode_info.session_name = Some(name.clone()); + } + for (_, tab) in screen.tabs.iter_mut() { + tab.rename_session(name.clone()).with_context(err_context)?; + } + + // rename socket file + let old_socket_file_path = ZELLIJ_SOCK_DIR.join(&old_session_name); + let new_socket_file_path = ZELLIJ_SOCK_DIR.join(&name); + if let Err(e) = std::fs::rename(old_socket_file_path, new_socket_file_path) { |