feat(sessions): resurrect sessions through the session-manager (and plugin API) (#2902)

* working with table and scrolling

* ui and functionality complete

* fix formatting

* refactor: background jobs

* style(fmt): rustfmt
This commit is contained in:
Aram Drevekenin 2023-11-04 11:20:50 +01:00 committed by GitHub
parent 37bc6364fa
commit 4c6b03acc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 737 additions and 97 deletions

1
Cargo.lock generated
View file

@ -3325,6 +3325,7 @@ dependencies = [
"ansi_term", "ansi_term",
"chrono", "chrono",
"fuzzy-matcher", "fuzzy-matcher",
"humantime",
"unicode-width", "unicode-width",
"zellij-tile", "zellij-tile",
] ]

View file

@ -10,3 +10,4 @@ zellij-tile = { path = "../../zellij-tile" }
chrono = "0.4.0" chrono = "0.4.0"
fuzzy-matcher = "0.3.7" fuzzy-matcher = "0.3.7"
unicode-width = "0.1.10" unicode-width = "0.1.10"
humantime = "2.1.0"

View file

@ -1,3 +1,4 @@
mod resurrectable_sessions;
mod session_list; mod session_list;
mod ui; mod ui;
use zellij_tile::prelude::*; use zellij_tile::prelude::*;
@ -5,18 +6,24 @@ use zellij_tile::prelude::*;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use ui::{ use ui::{
components::{render_controls_line, render_new_session_line, render_prompt, Colors}, components::{
render_controls_line, render_new_session_line, render_prompt, render_resurrection_toggle,
Colors,
},
SessionUiInfo, SessionUiInfo,
}; };
use resurrectable_sessions::ResurrectableSessions;
use session_list::SessionList; use session_list::SessionList;
#[derive(Default)] #[derive(Default)]
struct State { struct State {
session_name: Option<String>, session_name: Option<String>,
sessions: SessionList, sessions: SessionList,
resurrectable_sessions: ResurrectableSessions,
search_term: String, search_term: String,
new_session_name: Option<String>, new_session_name: Option<String>,
browsing_resurrection_sessions: bool,
colors: Colors, colors: Colors,
} }
@ -45,7 +52,9 @@ impl ZellijPlugin for State {
Event::PermissionRequestResult(_result) => { Event::PermissionRequestResult(_result) => {
should_render = true; should_render = true;
}, },
Event::SessionUpdate(session_infos) => { Event::SessionUpdate(session_infos, resurrectable_session_list) => {
self.resurrectable_sessions
.update(resurrectable_session_list);
self.update_session_infos(session_infos); self.update_session_infos(session_infos);
should_render = true; should_render = true;
}, },
@ -55,6 +64,11 @@ impl ZellijPlugin for State {
} }
fn render(&mut self, rows: usize, cols: usize) { fn render(&mut self, rows: usize, cols: usize) {
if self.browsing_resurrection_sessions {
self.resurrectable_sessions.render(rows, cols);
return;
}
render_resurrection_toggle(cols, false);
render_prompt( render_prompt(
self.new_session_name.is_some(), self.new_session_name.is_some(),
&self.search_term, &self.search_term,
@ -94,12 +108,16 @@ impl State {
} }
should_render = true; should_render = true;
} else if let Key::Down = key { } else if let Key::Down = key {
if self.new_session_name.is_none() { if self.browsing_resurrection_sessions {
self.resurrectable_sessions.move_selection_down();
} else if self.new_session_name.is_none() {
self.sessions.move_selection_down(); self.sessions.move_selection_down();
} }
should_render = true; should_render = true;
} else if let Key::Up = key { } else if let Key::Up = key {
if self.new_session_name.is_none() { if self.browsing_resurrection_sessions {
self.resurrectable_sessions.move_selection_up();
} else if self.new_session_name.is_none() {
self.sessions.move_selection_up(); self.sessions.move_selection_up();
} }
should_render = true; should_render = true;
@ -108,6 +126,8 @@ impl State {
self.handle_selection(); self.handle_selection();
} else if let Some(new_session_name) = self.new_session_name.as_mut() { } else if let Some(new_session_name) = self.new_session_name.as_mut() {
new_session_name.push(character); new_session_name.push(character);
} else if self.browsing_resurrection_sessions {
self.resurrectable_sessions.handle_character(character);
} else { } else {
self.search_term.push(character); self.search_term.push(character);
self.sessions self.sessions
@ -121,6 +141,8 @@ impl State {
} else { } else {
new_session_name.pop(); new_session_name.pop();
} }
} else if self.browsing_resurrection_sessions {
self.resurrectable_sessions.handle_backspace();
} else { } else {
self.search_term.pop(); self.search_term.pop();
self.sessions self.sessions
@ -153,13 +175,33 @@ impl State {
hide_self(); hide_self();
} }
should_render = true; should_render = true;
} else if let Key::BackTab = key {
self.browsing_resurrection_sessions = !self.browsing_resurrection_sessions;
should_render = true;
} else if let Key::Delete = key {
if self.browsing_resurrection_sessions {
self.resurrectable_sessions.delete_selected_session();
should_render = true;
}
} else if let Key::Ctrl('d') = key {
if self.browsing_resurrection_sessions {
self.resurrectable_sessions
.show_delete_all_sessions_warning();
should_render = true;
}
} else if let Key::Esc = key { } else if let Key::Esc = key {
hide_self(); hide_self();
} }
should_render should_render
} }
fn handle_selection(&mut self) { fn handle_selection(&mut self) {
if let Some(new_session_name) = &self.new_session_name { if self.browsing_resurrection_sessions {
if let Some(session_name_to_resurrect) =
self.resurrectable_sessions.get_selected_session_name()
{
switch_session(Some(&session_name_to_resurrect));
}
} else if let Some(new_session_name) = &self.new_session_name {
if new_session_name.is_empty() { if new_session_name.is_empty() {
switch_session(None); switch_session(None);
} else if self.session_name.as_ref() == Some(new_session_name) { } else if self.session_name.as_ref() == Some(new_session_name) {

View file

@ -0,0 +1,338 @@
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use humantime::format_duration;
use crate::ui::components::render_resurrection_toggle;
use std::time::Duration;
use zellij_tile::shim::*;
#[derive(Debug, Default)]
pub struct ResurrectableSessions {
pub all_resurrectable_sessions: Vec<(String, Duration)>,
pub selected_index: Option<usize>,
pub selected_search_index: Option<usize>,
pub search_results: Vec<SearchResult>,
pub is_searching: bool,
pub search_term: String,
pub delete_all_dead_sessions_warning: bool,
}
impl ResurrectableSessions {
pub fn update(&mut self, mut list: Vec<(String, Duration)>) {
list.sort_by(|a, b| a.1.cmp(&b.1));
self.all_resurrectable_sessions = list;
}
pub fn render(&self, rows: usize, columns: usize) {
if self.delete_all_dead_sessions_warning {
self.render_delete_all_sessions_warning(rows, columns);
return;
}
render_resurrection_toggle(columns, true);
let search_indication = Text::new(format!("> {}_", self.search_term)).color_range(1, ..);
let table_rows = rows.saturating_sub(3);
let table_columns = columns;
let table = if self.is_searching {
self.render_search_results(table_rows, columns)
} else {
self.render_all_entries(table_rows, columns)
};
print_text_with_coordinates(search_indication, 0, 0, None, None);
print_table_with_coordinates(table, 0, 1, Some(table_columns), Some(table_rows));
self.render_controls_line(rows);
}
fn render_search_results(&self, table_rows: usize, _table_columns: usize) -> Table {
let mut table = Table::new().add_row(vec![" ", " ", " "]); // skip the title row
let (first_row_index_to_render, last_row_index_to_render) = self.range_to_render(
table_rows,
self.search_results.len(),
self.selected_search_index,
);
for i in first_row_index_to_render..last_row_index_to_render {
if let Some(search_result) = self.search_results.get(i) {
let is_selected = Some(i) == self.selected_search_index;
let mut table_cells = vec![
self.render_session_name(
&search_result.session_name,
Some(search_result.indices.clone()),
),
self.render_ctime(&search_result.ctime),
self.render_more_indication_or_enter_as_needed(
i,
first_row_index_to_render,
last_row_index_to_render,
self.search_results.len(),
is_selected,
),
];
if is_selected {
table_cells = table_cells.drain(..).map(|t| t.selected()).collect();
}
table = table.add_styled_row(table_cells);
}
}
table
}
fn render_all_entries(&self, table_rows: usize, _table_columns: usize) -> Table {
let mut table = Table::new().add_row(vec![" ", " ", " "]); // skip the title row
let (first_row_index_to_render, last_row_index_to_render) = self.range_to_render(
table_rows,
self.all_resurrectable_sessions.len(),
self.selected_index,
);
for i in first_row_index_to_render..last_row_index_to_render {
if let Some(session) = self.all_resurrectable_sessions.get(i) {
let is_selected = Some(i) == self.selected_index;
let mut table_cells = vec![
self.render_session_name(&session.0, None),
self.render_ctime(&session.1),
self.render_more_indication_or_enter_as_needed(
i,
first_row_index_to_render,
last_row_index_to_render,
self.all_resurrectable_sessions.len(),
is_selected,
),
];
if is_selected {
table_cells = table_cells.drain(..).map(|t| t.selected()).collect();
}
table = table.add_styled_row(table_cells);
}
}
table
}
fn render_delete_all_sessions_warning(&self, rows: usize, columns: usize) {
if rows == 0 || columns == 0 {
return;
}
let session_count = self.all_resurrectable_sessions.len();
let session_count_len = session_count.to_string().chars().count();
let warning_description_text =
format!("This will delete {} resurrectable sessions", session_count,);
let confirmation_text = "Are you sure? (y/n)";
let warning_y_location = (rows / 2).saturating_sub(1);
let confirmation_y_location = (rows / 2) + 1;
let warning_x_location =
columns.saturating_sub(warning_description_text.chars().count()) / 2;
let confirmation_x_location = columns.saturating_sub(confirmation_text.chars().count()) / 2;
print_text_with_coordinates(
Text::new(warning_description_text).color_range(0, 17..18 + session_count_len),
warning_x_location,
warning_y_location,
None,
None,
);
print_text_with_coordinates(
Text::new(confirmation_text).color_indices(2, vec![15, 17]),
confirmation_x_location,
confirmation_y_location,
None,
None,
);
}
fn range_to_render(
&self,
table_rows: usize,
results_len: usize,
selected_index: Option<usize>,
) -> (usize, usize) {
if table_rows <= results_len {
let row_count_to_render = table_rows.saturating_sub(1); // 1 for the title
let first_row_index_to_render = selected_index
.unwrap_or(0)
.saturating_sub(row_count_to_render / 2);
let last_row_index_to_render = first_row_index_to_render + row_count_to_render;
(first_row_index_to_render, last_row_index_to_render)
} else {
let first_row_index_to_render = 0;
let last_row_index_to_render = results_len;
(first_row_index_to_render, last_row_index_to_render)
}
}
fn render_session_name(&self, session_name: &str, indices: Option<Vec<usize>>) -> Text {
let text = Text::new(&session_name).color_range(0, ..);
match indices {
Some(indices) => text.color_indices(1, indices),
None => text,
}
}
fn render_ctime(&self, ctime: &Duration) -> Text {
let duration = format_duration(ctime.clone()).to_string();
let duration_parts = duration.split_whitespace();
let mut formatted_duration = String::new();
for part in duration_parts {
if !part.ends_with('s') {
if !formatted_duration.is_empty() {
formatted_duration.push(' ');
}
formatted_duration.push_str(part);
}
}
if formatted_duration.is_empty() {
formatted_duration.push_str("<1m");
}
let duration_len = formatted_duration.chars().count();
Text::new(format!("Created {} ago", formatted_duration)).color_range(2, 8..9 + duration_len)
}
fn render_more_indication_or_enter_as_needed(
&self,
i: usize,
first_row_index_to_render: usize,
last_row_index_to_render: usize,
results_len: usize,
is_selected: bool,
) -> Text {
if is_selected {
Text::new(format!("<ENTER> - Resurrect Session")).color_range(3, 0..7)
} else if i == first_row_index_to_render && i > 0 {
Text::new(format!("+ {} more", first_row_index_to_render)).color_range(1, ..)
} else if i == last_row_index_to_render.saturating_sub(1)
&& last_row_index_to_render < results_len
{
Text::new(format!(
"+ {} more",
results_len.saturating_sub(last_row_index_to_render)
))
.color_range(1, ..)
} else {
Text::new(" ")
}
}
fn render_controls_line(&self, rows: usize) {
let controls_line = Text::new(format!(
"Help: <↓↑> - Navigate, <DEL> - Delete Session, <Ctrl d> - Delete all sessions"
))
.color_range(3, 6..10)
.color_range(3, 23..29)
.color_range(3, 47..56);
print_text_with_coordinates(controls_line, 0, rows.saturating_sub(1), None, None);
}
pub fn move_selection_down(&mut self) {
if self.is_searching {
if let Some(selected_index) = self.selected_search_index.as_mut() {
if *selected_index == self.search_results.len().saturating_sub(1) {
*selected_index = 0;
} else {
*selected_index = *selected_index + 1;
}
} else {
self.selected_search_index = Some(0);
}
} else {
if let Some(selected_index) = self.selected_index.as_mut() {
if *selected_index == self.all_resurrectable_sessions.len().saturating_sub(1) {
*selected_index = 0;
} else {
*selected_index = *selected_index + 1;
}
} else {
self.selected_index = Some(0);
}
}
}
pub fn move_selection_up(&mut self) {
if self.is_searching {
if let Some(selected_index) = self.selected_search_index.as_mut() {
if *selected_index == 0 {
*selected_index = self.search_results.len().saturating_sub(1);
} else {
*selected_index = selected_index.saturating_sub(1);
}
} else {
self.selected_search_index = Some(self.search_results.len().saturating_sub(1));
}
} else {
if let Some(selected_index) = self.selected_index.as_mut() {
if *selected_index == 0 {
*selected_index = self.all_resurrectable_sessions.len().saturating_sub(1);
} else {
*selected_index = selected_index.saturating_sub(1);
}
} else {
self.selected_index = Some(self.all_resurrectable_sessions.len().saturating_sub(1));
}
}
}
pub fn get_selected_session_name(&self) -> Option<String> {
if self.is_searching {
self.selected_search_index
.and_then(|i| self.search_results.get(i))
.map(|search_result| search_result.session_name.clone())
} else {
self.selected_index
.and_then(|i| self.all_resurrectable_sessions.get(i))
.map(|session_name_and_creation_time| session_name_and_creation_time.0.clone())
}
}
pub fn delete_selected_session(&mut self) {
self.selected_index
.and_then(|i| {
if self.all_resurrectable_sessions.len() > i {
// optimistic update
if i == 0 {
self.selected_index = None;
} else if i == self.all_resurrectable_sessions.len().saturating_sub(1) {
self.selected_index = Some(i.saturating_sub(1));
}
Some(self.all_resurrectable_sessions.remove(i))
} else {
None
}
})
.map(|session_name_and_creation_time| {
delete_dead_session(&session_name_and_creation_time.0)
});
}
fn delete_all_sessions(&mut self) {
// optimistic update
self.all_resurrectable_sessions = vec![];
self.delete_all_dead_sessions_warning = false;
delete_all_dead_sessions();
}
pub fn show_delete_all_sessions_warning(&mut self) {
self.delete_all_dead_sessions_warning = true;
}
pub fn handle_character(&mut self, character: char) {
if self.delete_all_dead_sessions_warning && character == 'y' {
self.delete_all_sessions();
} else if self.delete_all_dead_sessions_warning && character == 'n' {
self.delete_all_dead_sessions_warning = false;
} else {
self.search_term.push(character);
self.update_search_term();
}
}
pub fn handle_backspace(&mut self) {
self.search_term.pop();
self.update_search_term();
}
fn update_search_term(&mut self) {
let mut matches = vec![];
let matcher = SkimMatcherV2::default().use_cache(true);
for (session_name, ctime) in &self.all_resurrectable_sessions {
if let Some((score, indices)) = matcher.fuzzy_indices(&session_name, &self.search_term)
{
matches.push(SearchResult {
session_name: session_name.to_owned(),
ctime: ctime.clone(),
score,
indices,
});
}
}
matches.sort_by(|a, b| b.score.cmp(&a.score));
self.search_results = matches;
self.is_searching = !self.search_term.is_empty();
self.selected_search_index = Some(0);
}
}
#[derive(Debug)]
pub struct SearchResult {
score: i64,
indices: Vec<usize>,
session_name: String,
ctime: Duration,
}

View file

@ -478,12 +478,63 @@ pub fn minimize_lines(
pub fn render_prompt(typing_session_name: bool, search_term: &str, colors: Colors) { pub fn render_prompt(typing_session_name: bool, search_term: &str, colors: Colors) {
if !typing_session_name { if !typing_session_name {
let prompt = colors.bold(&format!("> {}_", search_term)); let prompt = colors.bold(&format!("> {}_", search_term));
println!("{}\n", prompt); println!("\u{1b}[H{}\n", prompt);
} else { } else {
println!("\n"); println!("\n");
} }
} }
pub fn render_resurrection_toggle(cols: usize, resurrection_screen_is_active: bool) {
let key_indication_text = "<TAB>";
let running_sessions_text = "Running";
let exited_sessions_text = "Exited";
let key_indication_len = key_indication_text.chars().count() + 1;
let first_ribbon_length = running_sessions_text.chars().count() + 4;
let second_ribbon_length = exited_sessions_text.chars().count() + 4;
let key_indication_x =
cols.saturating_sub(key_indication_len + first_ribbon_length + second_ribbon_length);
let first_ribbon_x = key_indication_x + key_indication_len;
let second_ribbon_x = first_ribbon_x + first_ribbon_length;
print_text_with_coordinates(
Text::new(key_indication_text).color_range(3, ..),
key_indication_x,
0,
None,
None,
);
if resurrection_screen_is_active {
print_ribbon_with_coordinates(
Text::new(running_sessions_text),
first_ribbon_x,
0,
None,
None,
);
print_ribbon_with_coordinates(
Text::new(exited_sessions_text).selected(),
second_ribbon_x,
0,
None,
None,
);
} else {
print_ribbon_with_coordinates(
Text::new(running_sessions_text).selected(),
first_ribbon_x,
0,
None,
None,
);
print_ribbon_with_coordinates(
Text::new(exited_sessions_text),
second_ribbon_x,
0,
None,
None,
);
}
}
pub fn render_new_session_line(session_name: &Option<String>, is_searching: bool, colors: Colors) { pub fn render_new_session_line(session_name: &Option<String>, is_searching: bool, colors: Colors) {
if is_searching { if is_searching {
return; return;

View file

@ -1,12 +1,11 @@
use zellij_utils::async_std::task; use zellij_utils::async_std::task;
use zellij_utils::consts::{ use zellij_utils::consts::{
session_info_cache_file_name, session_info_folder_for_session, session_layout_cache_file_name, session_info_cache_file_name, session_info_folder_for_session, session_layout_cache_file_name,
ZELLIJ_SOCK_DIR, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR,
}; };
use zellij_utils::data::{Event, HttpVerb, SessionInfo}; use zellij_utils::data::{Event, HttpVerb, SessionInfo};
use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType}; use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType};
use zellij_utils::surf::{ use zellij_utils::surf::{
self,
http::{Method, Url}, http::{Method, Url},
RequestBuilder, RequestBuilder,
}; };
@ -35,7 +34,7 @@ pub enum BackgroundJob {
StopPluginLoadingAnimation(u32), // u32 - plugin_id StopPluginLoadingAnimation(u32), // u32 - plugin_id
ReadAllSessionInfosOnMachine, // u32 - plugin_id ReadAllSessionInfosOnMachine, // u32 - plugin_id
ReportSessionInfo(String, SessionInfo), // String - session name ReportSessionInfo(String, SessionInfo), // String - session name
ReportLayoutInfo((String, BTreeMap<String, String>)), // HashMap<file_name, pane_contents> ReportLayoutInfo((String, BTreeMap<String, String>)), // BTreeMap<file_name, pane_contents>
RunCommand( RunCommand(
PluginId, PluginId,
ClientId, ClientId,
@ -163,89 +162,23 @@ pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> {
let current_session_layout = current_session_layout.clone(); let current_session_layout = current_session_layout.clone();
async move { async move {
loop { loop {
// write state of current session
// write it to disk
let current_session_name = let current_session_name =
current_session_name.lock().unwrap().to_string(); current_session_name.lock().unwrap().to_string();
let metadata_cache_file_name =
session_info_cache_file_name(&current_session_name);
let current_session_info = current_session_info.lock().unwrap().clone(); let current_session_info = current_session_info.lock().unwrap().clone();
let (current_session_layout, layout_files_to_write) = let current_session_layout =
current_session_layout.lock().unwrap().clone(); current_session_layout.lock().unwrap().clone();
let _wrote_metadata_file = std::fs::create_dir_all( write_session_state_to_disk(
session_info_folder_for_session(&current_session_name).as_path(), current_session_name.clone(),
) current_session_info,
.and_then(|_| std::fs::File::create(metadata_cache_file_name)) current_session_layout,
.and_then(|mut f| write!(f, "{}", current_session_info.to_string()));
if !current_session_layout.is_empty() {
let layout_cache_file_name =
session_layout_cache_file_name(&current_session_name);
let _wrote_layout_file = std::fs::create_dir_all(
session_info_folder_for_session(&current_session_name)
.as_path(),
)
.and_then(|_| std::fs::File::create(layout_cache_file_name))
.and_then(|mut f| write!(f, "{}", current_session_layout))
.and_then(|_| {
let session_info_folder =
session_info_folder_for_session(&current_session_name);
for (external_file_name, external_file_contents) in
layout_files_to_write
{
std::fs::File::create(
session_info_folder.join(external_file_name),
)
.and_then(|mut f| write!(f, "{}", external_file_contents))
.unwrap_or_else(
|e| {
log::error!(
"Failed to write layout metadata file: {:?}",
e
); );
}, let session_infos_on_machine =
); read_other_live_session_states(&current_session_name);
} let resurrectable_sessions =
Ok(()) find_resurrectable_sessions(&session_infos_on_machine);
});
}
// start a background job (if not already running) that'll periodically read this and other
// sesion infos and report back
// read state of all sessions
let mut other_session_names = vec![];
let mut session_infos_on_machine = BTreeMap::new();
// we do this so that the session infos will be actual and we're
// reasonably sure their session is running
if let Ok(files) = fs::read_dir(&*ZELLIJ_SOCK_DIR) {
files.for_each(|file| {
if let Ok(file) = file {
if let Ok(file_name) = file.file_name().into_string() {
if file.file_type().unwrap().is_socket() {
other_session_names.push(file_name);
}
}
}
});
}
for session_name in other_session_names {
let session_cache_file_name =
session_info_cache_file_name(&session_name);
if let Ok(raw_session_info) =
fs::read_to_string(&session_cache_file_name)
{
if let Ok(session_info) = SessionInfo::from_string(
&raw_session_info,
&current_session_name,
) {
session_infos_on_machine.insert(session_name, session_info);
}
}
}
let _ = senders.send_to_screen(ScreenInstruction::UpdateSessionInfos( let _ = senders.send_to_screen(ScreenInstruction::UpdateSessionInfos(
session_infos_on_machine, session_infos_on_machine,
resurrectable_sessions,
)); ));
let _ = senders.send_to_screen(ScreenInstruction::DumpLayoutToHd); let _ = senders.send_to_screen(ScreenInstruction::DumpLayoutToHd);
task::sleep(std::time::Duration::from_millis(SESSION_READ_DURATION)) task::sleep(std::time::Duration::from_millis(SESSION_READ_DURATION))
@ -396,3 +329,110 @@ fn job_already_running(
}, },
} }
} }
fn write_session_state_to_disk(
current_session_name: String,
current_session_info: SessionInfo,
current_session_layout: (String, BTreeMap<String, String>),
) {
let metadata_cache_file_name = session_info_cache_file_name(&current_session_name);
let (current_session_layout, layout_files_to_write) = current_session_layout;
let _wrote_metadata_file =
std::fs::create_dir_all(session_info_folder_for_session(&current_session_name).as_path())
.and_then(|_| std::fs::File::create(metadata_cache_file_name))
.and_then(|mut f| write!(f, "{}", current_session_info.to_string()));
if !current_session_layout.is_empty() {
let layout_cache_file_name = session_layout_cache_file_name(&current_session_name);
let _wrote_layout_file = std::fs::create_dir_all(
session_info_folder_for_session(&current_session_name).as_path(),
)
.and_then(|_| std::fs::File::create(layout_cache_file_name))
.and_then(|mut f| write!(f, "{}", current_session_layout))
.and_then(|_| {
let session_info_folder = session_info_folder_for_session(&current_session_name);
for (external_file_name, external_file_contents) in layout_files_to_write {
std::fs::File::create(session_info_folder.join(external_file_name))
.and_then(|mut f| write!(f, "{}", external_file_contents))
.unwrap_or_else(|e| {
log::error!("Failed to write layout metadata file: {:?}", e);
});
}
Ok(())
});
}
}
fn read_other_live_session_states(current_session_name: &str) -> BTreeMap<String, SessionInfo> {
let mut other_session_names = vec![];
let mut session_infos_on_machine = BTreeMap::new();
// we do this so that the session infos will be actual and we're
// reasonably sure their session is running
if let Ok(files) = fs::read_dir(&*ZELLIJ_SOCK_DIR) {
files.for_each(|file| {
if let Ok(file) = file {
if let Ok(file_name) = file.file_name().into_string() {
if file.file_type().unwrap().is_socket() {
other_session_names.push(file_name);
}
}
}
});
}
for session_name in other_session_names {
let session_cache_file_name = session_info_cache_file_name(&session_name);
if let Ok(raw_session_info) = fs::read_to_string(&session_cache_file_name) {
if let Ok(session_info) =
SessionInfo::from_string(&raw_session_info, &current_session_name)
{
session_infos_on_machine.insert(session_name, session_info);
}
}
}
session_infos_on_machine
}
fn find_resurrectable_sessions(
session_infos_on_machine: &BTreeMap<String, SessionInfo>,
) -> BTreeMap<String, Duration> {
match fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) {
Ok(files_in_session_info_folder) => {
let files_that_are_folders = files_in_session_info_folder
.filter_map(|f| f.ok().map(|f| f.path()))
.filter(|f| f.is_dir());
files_that_are_folders
.filter_map(|folder_name| {
let session_name = folder_name.file_name()?.to_str()?.to_owned();
if session_infos_on_machine.contains_key(&session_name) {
// this is not a dead session...
return None;
}
let layout_file_name = session_layout_cache_file_name(&session_name);
let ctime = match std::fs::metadata(&layout_file_name)
.and_then(|metadata| metadata.created())
{
Ok(created) => Some(created),
Err(e) => {
log::error!(
"Failed to read created stamp of resurrection file: {:?}",
e
);
None
},
};
let elapsed_duration = ctime
.map(|ctime| {
Duration::from_secs(ctime.elapsed().ok().unwrap_or_default().as_secs())
})
.unwrap_or_default();
Some((session_name, elapsed_duration))
})
.collect()
},
Err(e) => {
log::error!("Failed to read session info cache dir: {:?}", e);
BTreeMap::new()
},
}
}

View file

@ -27,7 +27,7 @@ use url::Url;
use crate::{panes::PaneId, screen::ScreenInstruction}; use crate::{panes::PaneId, screen::ScreenInstruction};
use zellij_utils::{ use zellij_utils::{
consts::VERSION, consts::{VERSION, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR},
data::{ data::{
CommandToRun, Direction, Event, EventType, FileToOpen, InputMode, PluginCommand, PluginIds, CommandToRun, Direction, Event, EventType, FileToOpen, InputMode, PluginCommand, PluginIds,
PluginMessage, Resize, ResizeStrategy, PluginMessage, Resize, ResizeStrategy,
@ -224,6 +224,10 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
connect_to_session.tab_position, connect_to_session.tab_position,
connect_to_session.pane_id, connect_to_session.pane_id,
)?, )?,
PluginCommand::DeleteDeadSession(session_name) => {
delete_dead_session(session_name)?
},
PluginCommand::DeleteAllDeadSessions => delete_all_dead_sessions()?,
PluginCommand::OpenFileInPlace(file_to_open) => { PluginCommand::OpenFileInPlace(file_to_open) => {
open_file_in_place(env, file_to_open) open_file_in_place(env, file_to_open)
}, },
@ -837,6 +841,52 @@ fn switch_session(
Ok(()) Ok(())
} }
fn delete_dead_session(session_name: String) -> Result<()> {
std::fs::remove_dir_all(&*ZELLIJ_SESSION_INFO_CACHE_DIR.join(&session_name))
.with_context(|| format!("Failed to delete dead session: {:?}", &session_name))
}
fn delete_all_dead_sessions() -> Result<()> {
use std::os::unix::fs::FileTypeExt;
let mut live_sessions = vec![];
if let Ok(files) = std::fs::read_dir(&*ZELLIJ_SOCK_DIR) {
files.for_each(|file| {
if let Ok(file) = file {
if let Ok(file_name) = file.file_name().into_string() {
if file.file_type().unwrap().is_socket() {
live_sessions.push(file_name);
}
}
}
});
}
let dead_sessions: Vec<String> = match std::fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) {
Ok(files_in_session_info_folder) => {
let files_that_are_folders = files_in_session_info_folder
.filter_map(|f| f.ok().map(|f| f.path()))
.filter(|f| f.is_dir());
files_that_are_folders
.filter_map(|folder_name| {
let session_name = folder_name.file_name()?.to_str()?.to_owned();
if live_sessions.contains(&session_name) {
// this is not a dead session...
return None;
}
Some(session_name)
})
.collect()
},
Err(e) => {
log::error!("Failed to read session info cache dir: {:?}", e);
vec![]
},
};
for session in dead_sessions {
delete_dead_session(session)?;
}
Ok(())
}
fn edit_scrollback(env: &ForeignFunctionEnv) { fn edit_scrollback(env: &ForeignFunctionEnv) {
let action = Action::EditScrollback; let action = Action::EditScrollback;
let error_msg = || format!("Failed to edit scrollback"); let error_msg = || format!("Failed to edit scrollback");
@ -1263,6 +1313,8 @@ fn check_command_permission(
| PluginCommand::RenameTerminalPane(..) | PluginCommand::RenameTerminalPane(..)
| PluginCommand::RenamePluginPane(..) | PluginCommand::RenamePluginPane(..)
| PluginCommand::SwitchSession(..) | PluginCommand::SwitchSession(..)
| PluginCommand::DeleteDeadSession(..)
| PluginCommand::DeleteAllDeadSessions
| PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState, | PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState,
_ => return (PermissionStatus::Granted, None), _ => return (PermissionStatus::Granted, None),
}; };

View file

@ -5,6 +5,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use std::str; use std::str;
use std::time::Duration;
use zellij_utils::data::{ use zellij_utils::data::{
Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy, SessionInfo, Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy, SessionInfo,
@ -301,7 +302,10 @@ pub enum ScreenInstruction {
BreakPane(Box<Layout>, Option<TerminalAction>, ClientId), BreakPane(Box<Layout>, Option<TerminalAction>, ClientId),
BreakPaneRight(ClientId), BreakPaneRight(ClientId),
BreakPaneLeft(ClientId), BreakPaneLeft(ClientId),
UpdateSessionInfos(BTreeMap<String, SessionInfo>), // String is the session name UpdateSessionInfos(
BTreeMap<String, SessionInfo>, // String is the session name
BTreeMap<String, Duration>, // resurrectable sessions - <name, created>
),
ReplacePane( ReplacePane(
PaneId, PaneId,
HoldForCommand, HoldForCommand,
@ -558,6 +562,8 @@ pub(crate) struct Screen {
session_name: String, session_name: String,
session_infos_on_machine: BTreeMap<String, SessionInfo>, // String is the session name, can session_infos_on_machine: BTreeMap<String, SessionInfo>, // String is the session name, can
// also be this session // also be this session
resurrectable_sessions: BTreeMap<String, Duration>, // String is the session name, duration is
// its creation time
default_layout: Box<Layout>, default_layout: Box<Layout>,
default_shell: Option<PathBuf>, default_shell: Option<PathBuf>,
arrow_fonts: bool, arrow_fonts: bool,
@ -585,6 +591,7 @@ impl Screen {
let session_name = mode_info.session_name.clone().unwrap_or_default(); let session_name = mode_info.session_name.clone().unwrap_or_default();
let session_info = SessionInfo::new(session_name.clone()); let session_info = SessionInfo::new(session_name.clone());
let mut session_infos_on_machine = BTreeMap::new(); let mut session_infos_on_machine = BTreeMap::new();
let resurrectable_sessions = BTreeMap::new();
session_infos_on_machine.insert(session_name.clone(), session_info); session_infos_on_machine.insert(session_name.clone(), session_info);
Screen { Screen {
bus, bus,
@ -616,6 +623,7 @@ impl Screen {
serialize_pane_viewport, serialize_pane_viewport,
scrollback_lines_to_serialize, scrollback_lines_to_serialize,
arrow_fonts, arrow_fonts,
resurrectable_sessions,
} }
} }
@ -1417,14 +1425,22 @@ impl Screen {
pub fn update_session_infos( pub fn update_session_infos(
&mut self, &mut self,
new_session_infos: BTreeMap<String, SessionInfo>, new_session_infos: BTreeMap<String, SessionInfo>,
resurrectable_sessions: BTreeMap<String, Duration>,
) -> Result<()> { ) -> Result<()> {
self.session_infos_on_machine = new_session_infos; self.session_infos_on_machine = new_session_infos;
self.resurrectable_sessions = resurrectable_sessions;
self.bus self.bus
.senders .senders
.send_to_plugin(PluginInstruction::Update(vec![( .send_to_plugin(PluginInstruction::Update(vec![(
None, None,
None, None,
Event::SessionUpdate(self.session_infos_on_machine.values().cloned().collect()), Event::SessionUpdate(
self.session_infos_on_machine.values().cloned().collect(),
self.resurrectable_sessions
.iter()
.map(|(n, c)| (n.clone(), c.clone()))
.collect(),
),
)])) )]))
.context("failed to update session info")?; .context("failed to update session info")?;
Ok(()) Ok(())
@ -3421,8 +3437,8 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::BreakPaneLeft(client_id) => { ScreenInstruction::BreakPaneLeft(client_id) => {
screen.break_pane_to_new_tab(Direction::Left, client_id)?; screen.break_pane_to_new_tab(Direction::Left, client_id)?;
}, },
ScreenInstruction::UpdateSessionInfos(new_session_infos) => { ScreenInstruction::UpdateSessionInfos(new_session_infos, resurrectable_sessions) => {
screen.update_session_infos(new_session_infos)?; screen.update_session_infos(new_session_infos, resurrectable_sessions)?;
}, },
ScreenInstruction::ReplacePane( ScreenInstruction::ReplacePane(
new_pane_id, new_pane_id,

View file

@ -666,6 +666,22 @@ pub fn switch_session_with_focus(
unsafe { host_run_plugin_command() }; unsafe { host_run_plugin_command() };
} }
/// Permanently delete a resurrectable session with the given name
pub fn delete_dead_session(name: &str) {
let plugin_command = PluginCommand::DeleteDeadSession(name.to_owned());
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
/// Permanently delete aall resurrectable sessions on this machine
pub fn delete_all_dead_sessions() {
let plugin_command = PluginCommand::DeleteAllDeadSessions;
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
// Utility Functions // Utility Functions
#[allow(unused)] #[allow(unused)]

View file

@ -55,6 +55,8 @@ pub mod event {
pub struct SessionUpdatePayload { pub struct SessionUpdatePayload {
#[prost(message, repeated, tag = "1")] #[prost(message, repeated, tag = "1")]
pub session_manifests: ::prost::alloc::vec::Vec<SessionManifest>, pub session_manifests: ::prost::alloc::vec::Vec<SessionManifest>,
#[prost(message, repeated, tag = "2")]
pub resurrectable_sessions: ::prost::alloc::vec::Vec<ResurrectableSession>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
@ -173,6 +175,14 @@ pub struct SessionManifest {
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ResurrectableSession {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
#[prost(uint64, tag = "2")]
pub creation_time: u64,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PaneInfo { pub struct PaneInfo {
#[prost(uint32, tag = "1")] #[prost(uint32, tag = "1")]
pub id: u32, pub id: u32,

View file

@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32, pub name: i32,
#[prost( #[prost(
oneof = "plugin_command::Payload", oneof = "plugin_command::Payload",
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44" tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45"
)] )]
pub payload: ::core::option::Option<plugin_command::Payload>, pub payload: ::core::option::Option<plugin_command::Payload>,
} }
@ -100,6 +100,8 @@ pub mod plugin_command {
RunCommandPayload(super::RunCommandPayload), RunCommandPayload(super::RunCommandPayload),
#[prost(message, tag = "44")] #[prost(message, tag = "44")]
WebRequestPayload(super::WebRequestPayload), WebRequestPayload(super::WebRequestPayload),
#[prost(string, tag = "45")]
DeleteDeadSessionPayload(::prost::alloc::string::String),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
@ -311,6 +313,8 @@ pub enum CommandName {
OpenFileInPlace = 70, OpenFileInPlace = 70,
RunCommand = 71, RunCommand = 71,
WebRequest = 72, WebRequest = 72,
DeleteDeadSession = 73,
DeleteAllDeadSessions = 74,
} }
impl CommandName { impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -392,6 +396,8 @@ impl CommandName {
CommandName::OpenFileInPlace => "OpenFileInPlace", CommandName::OpenFileInPlace => "OpenFileInPlace",
CommandName::RunCommand => "RunCommand", CommandName::RunCommand => "RunCommand",
CommandName::WebRequest => "WebRequest", CommandName::WebRequest => "WebRequest",
CommandName::DeleteDeadSession => "DeleteDeadSession",
CommandName::DeleteAllDeadSessions => "DeleteAllDeadSessions",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -470,6 +476,8 @@ impl CommandName {
"OpenFileInPlace" => Some(Self::OpenFileInPlace), "OpenFileInPlace" => Some(Self::OpenFileInPlace),
"RunCommand" => Some(Self::RunCommand), "RunCommand" => Some(Self::RunCommand),
"WebRequest" => Some(Self::WebRequest), "WebRequest" => Some(Self::WebRequest),
"DeleteDeadSession" => Some(Self::DeleteDeadSession),
"DeleteAllDeadSessions" => Some(Self::DeleteAllDeadSessions),
_ => None, _ => None,
} }
} }

View file

@ -6,6 +6,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt; use std::fmt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration;
use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString, ToString}; use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString, ToString};
pub type ClientId = u16; // TODO: merge with crate type? pub type ClientId = u16; // TODO: merge with crate type?
@ -495,7 +496,10 @@ pub enum Event {
FileSystemDelete(Vec<PathBuf>), FileSystemDelete(Vec<PathBuf>),
/// A Result of plugin permission request /// A Result of plugin permission request
PermissionRequestResult(PermissionStatus), PermissionRequestResult(PermissionStatus),
SessionUpdate(Vec<SessionInfo>), SessionUpdate(
Vec<SessionInfo>,
Vec<(String, Duration)>, // resurrectable sessions
),
RunCommandResult(Option<i32>, Vec<u8>, Vec<u8>, BTreeMap<String, String>), // exit_code, STDOUT, STDERR, RunCommandResult(Option<i32>, Vec<u8>, Vec<u8>, BTreeMap<String, String>), // exit_code, STDOUT, STDERR,
// context // context
WebRequestResult( WebRequestResult(
@ -1082,6 +1086,8 @@ pub enum PluginCommand {
ReportPanic(String), // stringified panic ReportPanic(String), // stringified panic
RequestPluginPermissions(Vec<PermissionType>), RequestPluginPermissions(Vec<PermissionType>),
SwitchSession(ConnectToSession), SwitchSession(ConnectToSession),
DeleteDeadSession(String), // String -> session name
DeleteAllDeadSessions, // String -> session name
OpenTerminalInPlace(FileToOpen), // only used for the path as cwd OpenTerminalInPlace(FileToOpen), // only used for the path as cwd
OpenFileInPlace(FileToOpen), OpenFileInPlace(FileToOpen),
OpenCommandPaneInPlace(CommandToRun), OpenCommandPaneInPlace(CommandToRun),

View file

@ -70,6 +70,7 @@ message Event {
message SessionUpdatePayload { message SessionUpdatePayload {
repeated SessionManifest session_manifests = 1; repeated SessionManifest session_manifests = 1;
repeated ResurrectableSession resurrectable_sessions = 2;
} }
message RunCommandResultPayload { message RunCommandResultPayload {
@ -153,6 +154,11 @@ message SessionManifest {
bool is_current_session = 5; bool is_current_session = 5;
} }
message ResurrectableSession {
string name = 1;
uint64 creation_time = 2;
}
message PaneInfo { message PaneInfo {
uint32 id = 1; uint32 id = 1;
bool is_plugin = 2; bool is_plugin = 2;

View file

@ -6,6 +6,7 @@ pub use super::generated_api::api::{
EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds, EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload, KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload,
PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest, PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
ResurrectableSession as ProtobufResurrectableSession,
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *, SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
}, },
input_mode::InputMode as ProtobufInputMode, input_mode::InputMode as ProtobufInputMode,
@ -23,6 +24,7 @@ use crate::input::actions::Action;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration;
impl TryFrom<ProtobufEvent> for Event { impl TryFrom<ProtobufEvent> for Event {
type Error = &'static str; type Error = &'static str;
@ -176,10 +178,19 @@ impl TryFrom<ProtobufEvent> for Event {
protobuf_session_update_payload, protobuf_session_update_payload,
)) => { )) => {
let mut session_infos: Vec<SessionInfo> = vec![]; let mut session_infos: Vec<SessionInfo> = vec![];
let mut resurrectable_sessions: Vec<(String, Duration)> = vec![];
for protobuf_session_info in protobuf_session_update_payload.session_manifests { for protobuf_session_info in protobuf_session_update_payload.session_manifests {
session_infos.push(SessionInfo::try_from(protobuf_session_info)?); session_infos.push(SessionInfo::try_from(protobuf_session_info)?);
} }
Ok(Event::SessionUpdate(session_infos)) for protobuf_resurrectable_session in
protobuf_session_update_payload.resurrectable_sessions
{
resurrectable_sessions.push(protobuf_resurrectable_session.into());
}
Ok(Event::SessionUpdate(
session_infos,
resurrectable_sessions.into(),
))
}, },
_ => Err("Malformed payload for the SessionUpdate Event"), _ => Err("Malformed payload for the SessionUpdate Event"),
}, },
@ -359,13 +370,18 @@ impl TryFrom<Event> for ProtobufEvent {
)), )),
}) })
}, },
Event::SessionUpdate(session_infos) => { Event::SessionUpdate(session_infos, resurrectable_sessions) => {
let mut protobuf_session_manifests = vec![]; let mut protobuf_session_manifests = vec![];
for session_info in session_infos { for session_info in session_infos {
protobuf_session_manifests.push(session_info.try_into()?); protobuf_session_manifests.push(session_info.try_into()?);
} }
let mut protobuf_resurrectable_sessions = vec![];
for resurrectable_session in resurrectable_sessions {
protobuf_resurrectable_sessions.push(resurrectable_session.into());
}
let session_update_payload = SessionUpdatePayload { let session_update_payload = SessionUpdatePayload {
session_manifests: protobuf_session_manifests, session_manifests: protobuf_session_manifests,
resurrectable_sessions: protobuf_resurrectable_sessions,
}; };
Ok(ProtobufEvent { Ok(ProtobufEvent {
name: ProtobufEventType::SessionUpdate as i32, name: ProtobufEventType::SessionUpdate as i32,
@ -887,6 +903,24 @@ impl TryFrom<EventType> for ProtobufEventType {
} }
} }
impl From<ProtobufResurrectableSession> for (String, Duration) {
fn from(protobuf_resurrectable_session: ProtobufResurrectableSession) -> (String, Duration) {
(
protobuf_resurrectable_session.name,
Duration::from_secs(protobuf_resurrectable_session.creation_time),
)
}
}
impl From<(String, Duration)> for ProtobufResurrectableSession {
fn from(session_name_and_creation_time: (String, Duration)) -> ProtobufResurrectableSession {
ProtobufResurrectableSession {
name: session_name_and_creation_time.0,
creation_time: session_name_and_creation_time.1.as_secs(),
}
}
}
#[test] #[test]
fn serialize_mode_update_event() { fn serialize_mode_update_event() {
use prost::Message; use prost::Message;
@ -1249,7 +1283,7 @@ fn serialize_file_system_delete_event() {
#[test] #[test]
fn serialize_session_update_event() { fn serialize_session_update_event() {
use prost::Message; use prost::Message;
let session_update_event = Event::SessionUpdate(Default::default()); let session_update_event = Event::SessionUpdate(Default::default(), Default::default());
let protobuf_event: ProtobufEvent = session_update_event.clone().try_into().unwrap(); let protobuf_event: ProtobufEvent = session_update_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec(); let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent = let deserialized_protobuf_event: ProtobufEvent =
@ -1360,8 +1394,9 @@ fn serialize_session_update_event_with_non_default_values() {
is_current_session: false, is_current_session: false,
}; };
let session_infos = vec![session_info_1, session_info_2]; let session_infos = vec![session_info_1, session_info_2];
let resurrectable_sessions = vec![];
let session_update_event = Event::SessionUpdate(session_infos); let session_update_event = Event::SessionUpdate(session_infos, resurrectable_sessions);
let protobuf_event: ProtobufEvent = session_update_event.clone().try_into().unwrap(); let protobuf_event: ProtobufEvent = session_update_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec(); let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent = let deserialized_protobuf_event: ProtobufEvent =

View file

@ -84,6 +84,8 @@ enum CommandName {
OpenFileInPlace = 70; OpenFileInPlace = 70;
RunCommand = 71; RunCommand = 71;
WebRequest = 72; WebRequest = 72;
DeleteDeadSession = 73;
DeleteAllDeadSessions = 74;
} }
message PluginCommand { message PluginCommand {
@ -132,6 +134,7 @@ message PluginCommand {
OpenCommandPanePayload open_command_pane_in_place_payload = 42; OpenCommandPanePayload open_command_pane_in_place_payload = 42;
RunCommandPayload run_command_payload = 43; RunCommandPayload run_command_payload = 43;
WebRequestPayload web_request_payload = 44; WebRequestPayload web_request_payload = 44;
string delete_dead_session_payload = 45;
} }
} }

View file

@ -628,6 +628,13 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
}, },
_ => Err("Mismatched payload for WebRequest"), _ => Err("Mismatched payload for WebRequest"),
}, },
Some(CommandName::DeleteDeadSession) => match protobuf_plugin_command.payload {
Some(Payload::DeleteDeadSessionPayload(dead_session_name)) => {
Ok(PluginCommand::DeleteDeadSession(dead_session_name))
},
_ => Err("Mismatched payload for DeleteDeadSession"),
},
Some(CommandName::DeleteAllDeadSessions) => Ok(PluginCommand::DeleteAllDeadSessions),
None => Err("Unrecognized plugin command"), None => Err("Unrecognized plugin command"),
} }
} }
@ -1044,6 +1051,14 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
})), })),
}) })
}, },
PluginCommand::DeleteDeadSession(dead_session_name) => Ok(ProtobufPluginCommand {
name: CommandName::DeleteDeadSession as i32,
payload: Some(Payload::DeleteDeadSessionPayload(dead_session_name)),
}),
PluginCommand::DeleteAllDeadSessions => Ok(ProtobufPluginCommand {
name: CommandName::DeleteAllDeadSessions as i32,
payload: None,
}),
} }
} }
} }