Skip to content

Commit

Permalink
feat(sessions): welcome screen (#3112)
Browse files Browse the repository at this point in the history
* prototype - can send layout name for new session from session-manager

* feat(sessions): ui for selecting layout for new session in the session-manager

* fix: send available layouts to plugins

* make tests compile

* fix tests

* improve ui

* fix: respect built-in layouts

* ui for built-in layouts

* some cleanups

* style(fmt): rustfmt

* welcome screen ui

* fix: make sure layout config is not shared between sessions

* allow disconnecting other users from current session and killing other sessions

* fix: respect default layout

* add welcome screen layout

* tests(plugins): new api methods

* fix(session-manager): do not quit welcome screen on esc and break

* fix(plugins): adjust permissions

* style(fmt): rustfmt

* style(fmt): fix warnings
  • Loading branch information
imsnif committed Feb 6, 2024
1 parent 286f7cc commit 6b20a95
Show file tree
Hide file tree
Showing 36 changed files with 1,935 additions and 394 deletions.
10 changes: 10 additions & 0 deletions default-plugins/fixture-plugin-for-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,16 @@ impl ZellijPlugin for State {
context,
);
},
Key::Ctrl('5') => {
switch_session(Some("my_new_session"));
},
Key::Ctrl('6') => disconnect_other_clients(),
Key::Ctrl('7') => {
switch_session_with_layout(
Some("my_other_new_session"),
LayoutInfo::BuiltIn("compact".to_owned()),
);
},
_ => {},
},
Event::CustomMessage(message, payload) => {
Expand Down
473 changes: 314 additions & 159 deletions default-plugins/session-manager/src/main.rs

Large diffs are not rendered by default.

265 changes: 265 additions & 0 deletions default-plugins/session-manager/src/new_session_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use std::cmp::Ordering;
use zellij_tile::prelude::*;

#[derive(Default)]
pub struct NewSessionInfo {
name: String,
layout_list: LayoutList,
entering_new_session_info: EnteringState,
}

#[derive(Eq, PartialEq)]
enum EnteringState {
EnteringName,
EnteringLayoutSearch,
}

impl Default for EnteringState {
fn default() -> Self {
EnteringState::EnteringName
}
}

impl NewSessionInfo {
pub fn name(&self) -> &str {
&self.name
}
pub fn layout_search_term(&self) -> &str {
&self.layout_list.layout_search_term
}
pub fn entering_new_session_info(&self) -> bool {
true
}
pub fn entering_new_session_name(&self) -> bool {
self.entering_new_session_info == EnteringState::EnteringName
}
pub fn entering_layout_search_term(&self) -> bool {
self.entering_new_session_info == EnteringState::EnteringLayoutSearch
}
pub fn add_char(&mut self, character: char) {
match self.entering_new_session_info {
EnteringState::EnteringName => {
self.name.push(character);
},
EnteringState::EnteringLayoutSearch => {
self.layout_list.layout_search_term.push(character);
self.update_layout_search_term();
},
}
}
pub fn handle_backspace(&mut self) {
match self.entering_new_session_info {
EnteringState::EnteringName => {
self.name.pop();
},
EnteringState::EnteringLayoutSearch => {
self.layout_list.layout_search_term.pop();
self.update_layout_search_term();
},
}
}
pub fn handle_break(&mut self) {
match self.entering_new_session_info {
EnteringState::EnteringName => {
self.name.clear();
},
EnteringState::EnteringLayoutSearch => {
self.layout_list.layout_search_term.clear();
self.entering_new_session_info = EnteringState::EnteringName;
self.update_layout_search_term();
},
}
}
pub fn handle_key(&mut self, key: Key) {
match key {
Key::Backspace => {
self.handle_backspace();
},
Key::Ctrl('c') | Key::Esc => {
self.handle_break();
},
Key::Char(character) => {
self.add_char(character);
},
Key::Up => {
self.move_selection_up();
},
Key::Down => {
self.move_selection_down();
},
_ => {},
}
}
pub fn handle_selection(&mut self, current_session_name: &Option<String>) {
match self.entering_new_session_info {
EnteringState::EnteringLayoutSearch => {
let new_session_layout: Option<LayoutInfo> = self.selected_layout_info();
let new_session_name = if self.name.is_empty() {
None
} else {
Some(self.name.as_str())
};
if new_session_name != current_session_name.as_ref().map(|s| s.as_str()) {
match new_session_layout {
Some(new_session_layout) => {
switch_session_with_layout(new_session_name, new_session_layout)
},
None => {
switch_session(new_session_name);
},
}
}
self.name.clear();
self.layout_list.clear_selection();
hide_self();
},
EnteringState::EnteringName => {
self.entering_new_session_info = EnteringState::EnteringLayoutSearch;
},
}
}
pub fn update_layout_list(&mut self, layout_info: Vec<LayoutInfo>) {
self.layout_list.update_layout_list(layout_info);
}
pub fn layout_list(&self) -> Vec<(LayoutInfo, bool)> {
// bool - is_selected
self.layout_list
.layout_list
.iter()
.enumerate()
.map(|(i, l)| (l.clone(), i == self.layout_list.selected_layout_index))
.collect()
}
pub fn layouts_to_render(&self) -> Vec<(LayoutInfo, Vec<usize>, bool)> {
// (layout_info,
// search_indices,
// is_selected)
if self.is_searching() {
self.layout_search_results()
.into_iter()
.map(|(layout_search_result, is_selected)| {
(
layout_search_result.layout_info,
layout_search_result.indices,
is_selected,
)
})
.collect()
} else {
self.layout_list()
.into_iter()
.map(|(layout_info, is_selected)| (layout_info, vec![], is_selected))
.collect()
}
}
pub fn layout_search_results(&self) -> Vec<(LayoutSearchResult, bool)> {
// bool - is_selected
self.layout_list
.layout_search_results
.iter()
.enumerate()
.map(|(i, l)| (l.clone(), i == self.layout_list.selected_layout_index))
.collect()
}
pub fn is_searching(&self) -> bool {
!self.layout_list.layout_search_term.is_empty()
}
pub fn layout_count(&self) -> usize {
self.layout_list.layout_list.len()
}
pub fn selected_layout_info(&self) -> Option<LayoutInfo> {
self.layout_list.selected_layout_info()
}
fn update_layout_search_term(&mut self) {
if self.layout_list.layout_search_term.is_empty() {
self.layout_list.clear_selection();
self.layout_list.layout_search_results = vec![];
} else {
let mut matches = vec![];
let matcher = SkimMatcherV2::default().use_cache(true);
for layout_info in &self.layout_list.layout_list {
if let Some((score, indices)) =
matcher.fuzzy_indices(&layout_info.name(), &self.layout_list.layout_search_term)
{
matches.push(LayoutSearchResult {
layout_info: layout_info.clone(),
score,
indices,
});
}
}
matches.sort_by(|a, b| b.score.cmp(&a.score));
self.layout_list.layout_search_results = matches;
self.layout_list.clear_selection();
}
}
fn move_selection_up(&mut self) {
self.layout_list.move_selection_up();
}
fn move_selection_down(&mut self) {
self.layout_list.move_selection_down();
}
}

#[derive(Default)]
pub struct LayoutList {
layout_list: Vec<LayoutInfo>,
layout_search_results: Vec<LayoutSearchResult>,
selected_layout_index: usize,
layout_search_term: String,
}

impl LayoutList {
pub fn update_layout_list(&mut self, layout_list: Vec<LayoutInfo>) {
let old_layout_length = self.layout_list.len();
self.layout_list = layout_list;
if old_layout_length != self.layout_list.len() {
// honestly, this is just the UX choice that sucks the least...
self.clear_selection();
}
}
pub fn selected_layout_info(&self) -> Option<LayoutInfo> {
if !self.layout_search_term.is_empty() {
self.layout_search_results
.get(self.selected_layout_index)
.map(|l| l.layout_info.clone())
} else {
self.layout_list.get(self.selected_layout_index).cloned()
}
}
pub fn clear_selection(&mut self) {
self.selected_layout_index = 0;
}
fn max_index(&self) -> usize {
if self.layout_search_term.is_empty() {
self.layout_list.len().saturating_sub(1)
} else {
self.layout_search_results.len().saturating_sub(1)
}
}
fn move_selection_up(&mut self) {
let max_index = self.max_index();
if self.selected_layout_index > 0 {
self.selected_layout_index -= 1;
} else {
self.selected_layout_index = max_index;
}
}
fn move_selection_down(&mut self) {
let max_index = self.max_index();
if self.selected_layout_index < max_index {
self.selected_layout_index += 1;
} else {
self.selected_layout_index = 0;
}
}
}

#[derive(Clone)]
pub struct LayoutSearchResult {
pub layout_info: LayoutInfo,
pub score: i64,
pub indices: Vec<usize>,
}
37 changes: 13 additions & 24 deletions default-plugins/session-manager/src/resurrectable_sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ 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::*;
Expand All @@ -24,23 +22,22 @@ impl ResurrectableSessions {
list.sort_by(|a, b| a.1.cmp(&b.1));
self.all_resurrectable_sessions = list;
}
pub fn render(&self, rows: usize, columns: usize) {
pub fn render(&self, rows: usize, columns: usize, x: usize, y: usize) {
if self.delete_all_dead_sessions_warning {
self.render_delete_all_sessions_warning(rows, columns);
self.render_delete_all_sessions_warning(rows, columns, x, y);
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 search_indication =
Text::new(format!("Search: {}_", self.search_term)).color_range(2, ..7);
let table_rows = rows.saturating_sub(5); // search row, toggle row and some padding
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);
print_text_with_coordinates(search_indication, x.saturating_sub(1), y + 2, None, None);
print_table_with_coordinates(table, x, y + 3, Some(table_columns), Some(table_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
Expand Down Expand Up @@ -103,7 +100,7 @@ impl ResurrectableSessions {
}
table
}
fn render_delete_all_sessions_warning(&self, rows: usize, columns: usize) {
fn render_delete_all_sessions_warning(&self, rows: usize, columns: usize, x: usize, y: usize) {
if rows == 0 || columns == 0 {
return;
}
Expand All @@ -112,11 +109,12 @@ impl ResurrectableSessions {
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_y_location = y + (rows / 2).saturating_sub(1);
let confirmation_y_location = y + (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;
x + columns.saturating_sub(warning_description_text.chars().count()) / 2;
let confirmation_x_location =
x + 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,
Expand Down Expand Up @@ -200,15 +198,6 @@ impl ResurrectableSessions {
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() {
Expand Down
12 changes: 12 additions & 0 deletions default-plugins/session-manager/src/session_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,18 @@ impl SessionList {
.find(|s| s.name == old_name)
.map(|s| s.name = new_name.to_owned());
}
pub fn all_other_sessions(&self) -> Vec<String> {
self.session_ui_infos
.iter()
.filter_map(|s| {
if !s.is_current_session {
Some(s.name.clone())
} else {
None
}
})
.collect()
}
}

#[derive(Debug, Clone, Default)]
Expand Down

0 comments on commit 6b20a95

Please sign in to comment.