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",
"chrono",
"fuzzy-matcher",
"humantime",
"unicode-width",
"zellij-tile",
]

View file

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

View file

@ -1,3 +1,4 @@
mod resurrectable_sessions;
mod session_list;
mod ui;
use zellij_tile::prelude::*;
@ -5,18 +6,24 @@ use zellij_tile::prelude::*;
use std::collections::BTreeMap;
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,
};
use resurrectable_sessions::ResurrectableSessions;
use session_list::SessionList;
#[derive(Default)]
struct State {
session_name: Option<String>,
sessions: SessionList,
resurrectable_sessions: ResurrectableSessions,
search_term: String,
new_session_name: Option<String>,
browsing_resurrection_sessions: bool,
colors: Colors,
}
@ -45,7 +52,9 @@ impl ZellijPlugin for State {
Event::PermissionRequestResult(_result) => {
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);
should_render = true;
},
@ -55,6 +64,11 @@ impl ZellijPlugin for State {
}
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(
self.new_session_name.is_some(),
&self.search_term,
@ -94,12 +108,16 @@ impl State {
}
should_render = true;
} 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();
}
should_render = true;
} 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();
}
should_render = true;
@ -108,6 +126,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 self.browsing_resurrection_sessions {
self.resurrectable_sessions.handle_character(character);
} else {
self.search_term.push(character);
self.sessions
@ -121,6 +141,8 @@ impl State {
} else {
new_session_name.pop();
}
} else if self.browsing_resurrection_sessions {
self.resurrectable_sessions.handle_backspace();
} else {
self.search_term.pop();
self.sessions
@ -153,13 +175,33 @@ impl State {
hide_self();
}
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 {
hide_self();
}
should_render
}
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() {
switch_session(None);
} 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) {
if !typing_session_name {
let prompt = colors.bold(&format!("> {}_", search_term));
println!("{}\n", prompt);
println!("\u{1b}[H{}\n", prompt);
} else {
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) {
if is_searching {
return;

View file

@ -1,12 +1,11 @@
use zellij_utils::async_std::task;
use zellij_utils::consts::{
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::errors::{prelude::*, BackgroundJobContext, ContextType};
use zellij_utils::surf::{
self,
http::{Method, Url},
RequestBuilder,
};
@ -35,7 +34,7 @@ pub enum BackgroundJob {
StopPluginLoadingAnimation(u32), // u32 - plugin_id
ReadAllSessionInfosOnMachine, // u32 - plugin_id
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(
PluginId,
ClientId,
@ -163,89 +162,23 @@ pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> {
let current_session_layout = current_session_layout.clone();
async move {
loop {
// write state of current session
// write it to disk
let current_session_name =
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_layout, layout_files_to_write) =
let current_session_layout =
current_session_layout.lock().unwrap().clone();
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
write_session_state_to_disk(
current_session_name.clone(),
current_session_info,
current_session_layout,
);
},
);
}
Ok(())
});
}
// 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 session_infos_on_machine =
read_other_live_session_states(&current_session_name);
let resurrectable_sessions =
find_resurrectable_sessions(&session_infos_on_machine);
let _ = senders.send_to_screen(ScreenInstruction::UpdateSessionInfos(
session_infos_on_machine,
resurrectable_sessions,
));
let _ = senders.send_to_screen(ScreenInstruction::DumpLayoutToHd);
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 zellij_utils::{
consts::VERSION,
consts::{VERSION, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR},
data::{
CommandToRun, Direction, Event, EventType, FileToOpen, InputMode, PluginCommand, PluginIds,
PluginMessage, Resize, ResizeStrategy,
@ -224,6 +224,10 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
connect_to_session.tab_position,
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) => {
open_file_in_place(env, file_to_open)
},
@ -837,6 +841,52 @@ fn switch_session(
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) {
let action = Action::EditScrollback;
let error_msg = || format!("Failed to edit scrollback");
@ -1263,6 +1313,8 @@ fn check_command_permission(
| PluginCommand::RenameTerminalPane(..)
| PluginCommand::RenamePluginPane(..)
| PluginCommand::SwitchSession(..)
| PluginCommand::DeleteDeadSession(..)
| PluginCommand::DeleteAllDeadSessions
| PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState,
_ => return (PermissionStatus::Granted, None),
};

View file

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

View file

@ -666,6 +666,22 @@ pub fn switch_session_with_focus(
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
#[allow(unused)]

View file

@ -55,6 +55,8 @@ pub mod event {
pub struct SessionUpdatePayload {
#[prost(message, repeated, tag = "1")]
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)]
#[derive(Clone, PartialEq, ::prost::Message)]
@ -173,6 +175,14 @@ pub struct SessionManifest {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[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 {
#[prost(uint32, tag = "1")]
pub id: u32,

View file

@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32,
#[prost(
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>,
}
@ -100,6 +100,8 @@ pub mod plugin_command {
RunCommandPayload(super::RunCommandPayload),
#[prost(message, tag = "44")]
WebRequestPayload(super::WebRequestPayload),
#[prost(string, tag = "45")]
DeleteDeadSessionPayload(::prost::alloc::string::String),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@ -311,6 +313,8 @@ pub enum CommandName {
OpenFileInPlace = 70,
RunCommand = 71,
WebRequest = 72,
DeleteDeadSession = 73,
DeleteAllDeadSessions = 74,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@ -392,6 +396,8 @@ impl CommandName {
CommandName::OpenFileInPlace => "OpenFileInPlace",
CommandName::RunCommand => "RunCommand",
CommandName::WebRequest => "WebRequest",
CommandName::DeleteDeadSession => "DeleteDeadSession",
CommandName::DeleteAllDeadSessions => "DeleteAllDeadSessions",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@ -470,6 +476,8 @@ impl CommandName {
"OpenFileInPlace" => Some(Self::OpenFileInPlace),
"RunCommand" => Some(Self::RunCommand),
"WebRequest" => Some(Self::WebRequest),
"DeleteDeadSession" => Some(Self::DeleteDeadSession),
"DeleteAllDeadSessions" => Some(Self::DeleteAllDeadSessions),
_ => None,
}
}

View file

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

View file

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

View file

@ -6,6 +6,7 @@ pub use super::generated_api::api::{
EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload,
PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
ResurrectableSession as ProtobufResurrectableSession,
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
},
input_mode::InputMode as ProtobufInputMode,
@ -23,6 +24,7 @@ use crate::input::actions::Action;
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::path::PathBuf;
use std::time::Duration;
impl TryFrom<ProtobufEvent> for Event {
type Error = &'static str;
@ -176,10 +178,19 @@ impl TryFrom<ProtobufEvent> for Event {
protobuf_session_update_payload,
)) => {
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 {
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"),
},
@ -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![];
for session_info in session_infos {
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 {
session_manifests: protobuf_session_manifests,
resurrectable_sessions: protobuf_resurrectable_sessions,
};
Ok(ProtobufEvent {
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]
fn serialize_mode_update_event() {
use prost::Message;
@ -1249,7 +1283,7 @@ fn serialize_file_system_delete_event() {
#[test]
fn serialize_session_update_event() {
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 serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =
@ -1360,8 +1394,9 @@ fn serialize_session_update_event_with_non_default_values() {
is_current_session: false,
};
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 serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =

View file

@ -84,6 +84,8 @@ enum CommandName {
OpenFileInPlace = 70;
RunCommand = 71;
WebRequest = 72;
DeleteDeadSession = 73;
DeleteAllDeadSessions = 74;
}
message PluginCommand {
@ -132,6 +134,7 @@ message PluginCommand {
OpenCommandPanePayload open_command_pane_in_place_payload = 42;
RunCommandPayload run_command_payload = 43;
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"),
},
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"),
}
}
@ -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,
}),
}
}
}