aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/session-manager/src/ui
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2023-08-24 13:36:24 +0200
committerGitHub <noreply@github.com>2023-08-24 13:36:24 +0200
commitbc628abc1266cdc0dbce4f19a89727527a3e39a8 (patch)
tree6a0fcac6ec35c71a237bca0128a57a5b40afb0e3 /default-plugins/session-manager/src/ui
parentdocs(changelog): permission system (diff)
downloadsl0ppij-bc628abc1266cdc0dbce4f19a89727527a3e39a8.tar.gz
sl0ppij-bc628abc1266cdc0dbce4f19a89727527a3e39a8.tar.xz
sl0ppij-bc628abc1266cdc0dbce4f19a89727527a3e39a8.zip
feat(sessions): add a session manager to switch between sessions, tabs and panes and create new ones (#2721)
* write/read session metadata to disk for all sessions * switch session client side * fix tests * various adjustments * fix full screen focus bug in tiled panes * fix tests * fix permission sorting issue * cleanups * add session manager * fix tests * various cleanups * style(fmt): rustfmt * clear screen before switching sessions * I hate you clippy * truncate controls line to width * version session cache * attempt to fix plugin tests * style(fmt): rustfmt * another attempt to fix the tests in the ci
Diffstat (limited to 'default-plugins/session-manager/src/ui')
-rw-r--r--default-plugins/session-manager/src/ui/components.rs577
-rw-r--r--default-plugins/session-manager/src/ui/mod.rs357
2 files changed, 934 insertions, 0 deletions
diff --git a/default-plugins/session-manager/src/ui/components.rs b/default-plugins/session-manager/src/ui/components.rs
new file mode 100644
index 00000000..6b606589
--- /dev/null
+++ b/default-plugins/session-manager/src/ui/components.rs
@@ -0,0 +1,577 @@
+use unicode_width::UnicodeWidthChar;
+use unicode_width::UnicodeWidthStr;
+use zellij_tile::prelude::*;
+
+use crate::ui::{PaneUiInfo, SessionUiInfo, TabUiInfo};
+
+#[derive(Debug)]
+pub struct ListItem {
+ pub name: String,
+ pub session_name: Option<Vec<UiSpan>>,
+ pub tab_name: Option<Vec<UiSpan>>,
+ pub pane_name: Option<Vec<UiSpan>>,
+ colors: Colors,
+}
+
+impl ListItem {
+ pub fn from_session_info(session_ui_info: &SessionUiInfo, colors: Colors) -> Self {
+ let session_ui_line = build_session_ui_line(session_ui_info, colors);
+ ListItem {
+ name: session_ui_info.name.clone(),
+ session_name: Some(session_ui_line),
+ tab_name: None,
+ pane_name: None,
+ colors,
+ }
+ }
+ pub fn from_tab_info(
+ session_ui_info: &SessionUiInfo,
+ tab_ui_info: &TabUiInfo,
+ colors: Colors,
+ ) -> Self {
+ let session_ui_line = build_session_ui_line(session_ui_info, colors);
+ let tab_ui_line = build_tab_ui_line(tab_ui_info, colors);
+ ListItem {
+ name: tab_ui_info.name.clone(),
+ session_name: Some(session_ui_line),
+ tab_name: Some(tab_ui_line),
+ pane_name: None,
+ colors,
+ }
+ }
+ pub fn from_pane_info(
+ session_ui_info: &SessionUiInfo,
+ tab_ui_info: &TabUiInfo,
+ pane_ui_info: &PaneUiInfo,
+ colors: Colors,
+ ) -> Self {
+ let session_ui_line = build_session_ui_line(session_ui_info, colors);
+ let tab_ui_line = build_tab_ui_line(tab_ui_info, colors);
+ let pane_ui_line = build_pane_ui_line(pane_ui_info, colors);
+ ListItem {
+ name: pane_ui_info.name.clone(),
+ session_name: Some(session_ui_line),
+ tab_name: Some(tab_ui_line),
+ pane_name: Some(pane_ui_line),
+ colors,
+ }
+ }
+ pub fn line_count(&self) -> usize {
+ let mut line_count = 0;
+ if self.session_name.is_some() {
+ line_count += 1
+ };
+ if self.tab_name.is_some() {
+ line_count += 1
+ };
+ if self.pane_name.is_some() {
+ line_count += 1
+ };
+ line_count
+ }
+ pub fn render(&self, indices: Option<Vec<usize>>, max_cols: usize) -> Vec<LineToRender> {
+ let mut lines_to_render = vec![];
+ if let Some(session_name) = &self.session_name {
+ let indices = if self.tab_name.is_none() && self.pane_name.is_none() {
+ indices.clone()
+ } else {
+ None
+ };
+ let mut line_to_render = LineToRender::new(self.colors);
+ let mut remaining_cols = max_cols;
+ for span in session_name {
+ span.render(
+ indices
+ .clone()
+ .map(|i| (SpanStyle::ForegroundBold(self.colors.palette.magenta), i)),
+ &mut line_to_render,
+ &mut remaining_cols,
+ );
+ }
+ lines_to_render.push(line_to_render);
+ }
+ if let Some(tab_name) = &self.tab_name {
+ let indices = if self.pane_name.is_none() {
+ indices.clone()
+ } else {
+ None
+ };
+ let mut line_to_render = LineToRender::new(self.colors);
+ let mut remaining_cols = max_cols;
+ for span in tab_name {
+ span.render(
+ indices
+ .clone()
+ .map(|i| (SpanStyle::ForegroundBold(self.colors.palette.magenta), i)),
+ &mut line_to_render,
+ &mut remaining_cols,
+ );
+ }
+ lines_to_render.push(line_to_render);
+ }
+ if let Some(pane_name) = &self.pane_name {
+ let mut line_to_render = LineToRender::new(self.colors);
+ let mut remaining_cols = max_cols;
+ for span in pane_name {
+ span.render(
+ indices
+ .clone()
+ .map(|i| (SpanStyle::ForegroundBold(self.colors.palette.magenta), i)),
+ &mut line_to_render,
+ &mut remaining_cols,
+ );
+ }
+ lines_to_render.push(line_to_render);
+ }
+ lines_to_render
+ }
+}
+
+#[derive(Debug)]
+pub enum UiSpan {
+ UiSpanTelescope(UiSpanTelescope),
+ TruncatableUiSpan(TruncatableUiSpan),
+}
+
+impl UiSpan {
+ pub fn render(
+ &self,
+ indices: Option<(SpanStyle, Vec<usize>)>,
+ line_to_render: &mut LineToRender,
+ remaining_cols: &mut usize,
+ ) {
+ match self {
+ UiSpan::UiSpanTelescope(ui_span_telescope) => {
+ ui_span_telescope.render(line_to_render, remaining_cols)
+ },
+ UiSpan::TruncatableUiSpan(truncatable_ui_span) => {
+ truncatable_ui_span.render(indices, line_to_render, remaining_cols)
+ },
+ }
+ }
+}
+
+#[allow(dead_code)] // in the future this will be moved to be its own component
+#[derive(Debug)]
+pub enum SpanStyle {
+ None,
+ Bold,
+ Foreground(PaletteColor),
+ ForegroundBold(PaletteColor),
+}
+
+impl SpanStyle {
+ pub fn style_string(&self, to_style: &str) -> String {
+ match self {
+ SpanStyle::None => to_style.to_owned(),
+ SpanStyle::Bold => format!("\u{1b}[1m{}\u{1b}[22m", to_style),
+ SpanStyle::Foreground(color) => match color {
+ PaletteColor::EightBit(byte) => {
+ format!("\u{1b}[38;5;{byte}m{}\u{1b}[39m", to_style)
+ },
+ PaletteColor::Rgb((r, g, b)) => {
+ format!("\u{1b}[38;2;{};{};{}m{}\u{1b}[39m", r, g, b, to_style)
+ },
+ },
+ SpanStyle::ForegroundBold(color) => match color {
+ PaletteColor::EightBit(byte) => {
+ format!("\u{1b}[38;5;{byte};1m{}\u{1b}[39;22m", to_style)
+ },
+ PaletteColor::Rgb((r, g, b)) => {
+ format!("\u{1b}[38;2;{};{};{};1m{}\u{1b}[39;22m", r, g, b, to_style)
+ },
+ },
+ }
+ }
+}
+
+impl Default for SpanStyle {
+ fn default() -> Self {
+ SpanStyle::None
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct TruncatableUiSpan {
+ text: String,
+ style: SpanStyle,
+}
+
+impl TruncatableUiSpan {
+ pub fn new(text: String, style: SpanStyle) -> Self {
+ TruncatableUiSpan { text, style }
+ }
+ pub fn render(
+ &self,
+ indices: Option<(SpanStyle, Vec<usize>)>,
+ line_to_render: &mut LineToRender,
+ remaining_cols: &mut usize,
+ ) {
+ let mut rendered = String::new();
+ let truncated = if *remaining_cols >= self.text.width() {
+ self.text.clone()
+ } else {
+ let mut truncated = String::new();
+ for character in self.text.chars() {
+ if truncated.width() + character.width().unwrap_or(0) <= *remaining_cols {
+ truncated.push(character);
+ } else {
+ break;
+ }
+ }
+ truncated
+ };
+ match indices {
+ Some((index_style, indices)) => {
+ for (i, character) in truncated.chars().enumerate() {
+ // TODO: optimize this by splitting the string up by its indices and only pushing those
+ // chu8nks
+ if indices.contains(&i) {
+ rendered.push_str(&index_style.style_string(&character.to_string()));
+ } else {
+ rendered.push_str(&self.style.style_string(&character.to_string()));
+ }
+ }
+ },
+ None => {
+ rendered.push_str(&self.style.style_string(&truncated));
+ },
+ }
+ *remaining_cols = remaining_cols.saturating_sub(truncated.width());
+ line_to_render.append(&rendered);
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct UiSpanTelescope(Vec<StringAndLength>);
+
+impl UiSpanTelescope {
+ pub fn new(string_and_lengths: Vec<StringAndLength>) -> Self {
+ UiSpanTelescope(string_and_lengths)
+ }
+ pub fn render(&self, line_to_render: &mut LineToRender, remaining_cols: &mut usize) {
+ for string_and_length in &self.0 {
+ if string_and_length.length < *remaining_cols {
+ line_to_render.append(&string_and_length.string);
+ *remaining_cols -= string_and_length.length;
+ break;
+ }
+ }
+ }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct StringAndLength {
+ pub string: String,
+ pub length: usize,
+}
+
+impl StringAndLength {
+ pub fn new(string: String, length: usize) -> Self {
+ StringAndLength { string, length }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct LineToRender {
+ line: String,
+ is_selected: bool,
+ truncated_result_count: usize,
+ colors: Colors,
+}
+
+impl LineToRender {
+ pub fn new(colors: Colors) -> Self {
+ LineToRender {
+ line: String::default(),
+ is_selected: false,
+ truncated_result_count: 0,
+ colors,
+ }
+ }
+ pub fn append(&mut self, to_append: &str) {
+ self.line.push_str(to_append)
+ }
+ pub fn make_selected(&mut self) {
+ self.is_selected = true;
+ match self.colors.palette.gray {
+ PaletteColor::EightBit(byte) => {
+ self.line = format!(
+ "\u{1b}[48;5;{byte}m\u{1b}[K\r\u{1b}[48;5;{byte}m{}",
+ self.line
+ );
+ },
+ PaletteColor::Rgb((r, g, b)) => {
+ self.line = format!(
+ "\u{1b}[48;2;{};{};{}m\u{1b}[K\r\u{1b}[48;5;{};{};{}m{}",
+ r, g, b, r, g, b, self.line
+ );
+ },
+ }
+ }
+ pub fn render(&self) -> String {
+ let mut line = self.line.clone();
+
+ let more = if self.truncated_result_count > 0 {
+ self.colors
+ .red(&format!(" [+{}]", self.truncated_result_count))
+ } else {
+ String::new()
+ };
+
+ line.push_str(&more);
+ if self.is_selected {
+ self.line.clone()
+ } else {
+ format!("\u{1b}[49m{}", line)
+ }
+ }
+ pub fn add_truncated_results(&mut self, result_count: usize) {
+ self.truncated_result_count += result_count;
+ }
+}
+
+pub fn build_session_ui_line(session_ui_info: &SessionUiInfo, colors: Colors) -> Vec<UiSpan> {
+ let mut ui_spans = vec![];
+ let tab_count_text = session_ui_info.tabs.len();
+ let total_pane_count_text = session_ui_info
+ .tabs
+ .iter()
+ .fold(0, |acc, tab| acc + tab.panes.len());
+ let tab_count = format!("{}", tab_count_text);
+ let tab_count_styled = colors.cyan(&tab_count);
+ let total_pane_count = format!("{}", total_pane_count_text);
+ let total_pane_count_styled = colors.green(&total_pane_count);
+ let session_name = &session_ui_info.name;
+ let connected_users = format!("{}", session_ui_info.connected_users);
+ let connected_users_styled = colors.orange(&connected_users);
+ let session_bullet_span =
+ UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![StringAndLength::new(
+ format!(" > "),
+ 3,
+ )]));
+ let session_name_span = UiSpan::TruncatableUiSpan(TruncatableUiSpan::new(
+ session_name.clone(),
+ SpanStyle::ForegroundBold(colors.palette.orange),
+ ));
+ let tab_and_pane_count = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![
+ StringAndLength::new(
+ format!(" ({tab_count_styled} tabs, {total_pane_count_styled} panes)"),
+ 2 + tab_count.width() + 7 + total_pane_count.width() + 7,
+ ),
+ StringAndLength::new(
+ format!(" ({tab_count_styled}, {total_pane_count_styled})"),
+ 2 + tab_count.width() + 2 + total_pane_count.width() + 3,
+ ),
+ ]));
+ let connected_users_count = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![
+ StringAndLength::new(
+ format!(" [{connected_users_styled} connected users]"),
+ 2 + connected_users.width() + 17,
+ ),
+ StringAndLength::new(
+ format!(" [{connected_users_styled}]"),
+ 2 + connected_users.width() + 1,
+ ),
+ ]));
+ ui_spans.push(session_bullet_span);
+ ui_spans.push(session_name_span);
+ ui_spans.push(tab_and_pane_count);
+ ui_spans.push(connected_users_count);
+ if session_ui_info.is_current_session {
+ let current_session_indication = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![
+ StringAndLength::new(colors.orange(&format!(" <CURRENT SESSION>")), 18),
+ StringAndLength::new(colors.orange(&format!(" <CURRENT>")), 10),
+ StringAndLength::new(colors.orange(&format!(" <C>")), 4),
+ ]));
+ ui_spans.push(current_session_indication);
+ }
+ ui_spans
+}
+
+pub fn build_tab_ui_line(tab_ui_info: &TabUiInfo, colors: Colors) -> Vec<UiSpan> {
+ let mut ui_spans = vec![];
+ let tab_name = &tab_ui_info.name;
+ let pane_count_text = tab_ui_info.panes.len();
+ let pane_count = format!("{}", pane_count_text);
+ let pane_count_styled = colors.green(&pane_count);
+ let tab_bullet_span =
+ UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![StringAndLength::new(
+ format!(" - "),
+ 4,
+ )]));
+ let tab_name_span = UiSpan::TruncatableUiSpan(TruncatableUiSpan::new(
+ tab_name.clone(),
+ SpanStyle::ForegroundBold(colors.palette.cyan),
+ ));
+ let connected_users_count_span = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![
+ StringAndLength::new(
+ format!(" ({pane_count_styled} panes)"),
+ 2 + pane_count.width() + 7,
+ ),
+ StringAndLength::new(
+ format!(" ({pane_count_styled})"),
+ 2 + pane_count.width() + 1,
+ ),
+ ]));
+ ui_spans.push(tab_bullet_span);
+ ui_spans.push(tab_name_span);
+ ui_spans.push(connected_users_count_span);
+ ui_spans
+}
+
+pub fn build_pane_ui_line(pane_ui_info: &PaneUiInfo, colors: Colors) -> Vec<UiSpan> {
+ let mut ui_spans = vec![];
+ let pane_name = pane_ui_info.name.clone();
+ let exit_code = pane_ui_info.exit_code.map(|exit_code_number| {
+ let exit_code = format!("{}", exit_code_number);
+ let exit_code = if exit_code_number == 0 {
+ colors.green(&exit_code)
+ } else {
+ colors.red(&exit_code)
+ };
+ exit_code
+ });
+ let pane_bullet_span =
+ UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![StringAndLength::new(
+ format!(" > "),
+ 6,
+ )]));
+ ui_spans.push(pane_bullet_span);
+ let pane_name_span =
+ UiSpan::TruncatableUiSpan(TruncatableUiSpan::new(pane_name, SpanStyle::Bold));
+ ui_spans.push(pane_name_span);
+ if let Some(exit_code) = exit_code {
+ let pane_name_span = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![
+ StringAndLength::new(
+ format!(" (EXIT CODE: {exit_code})"),
+ 13 + exit_code.width() + 1,
+ ),
+ StringAndLength::new(format!(" ({exit_code})"), 2 + exit_code.width() + 1),
+ ]));
+ ui_spans.push(pane_name_span);
+ }
+ ui_spans
+}
+
+pub fn minimize_lines(
+ total_count: usize,
+ line_count_to_remove: usize,
+ selected_index: Option<usize>,
+) -> (usize, usize, usize, usize) {
+ // returns: (start_index, anchor_index, end_index, lines_left_to_remove)
+ let (count_to_render, line_count_to_remove) = if line_count_to_remove > total_count {
+ (1, line_count_to_remove.saturating_sub(total_count) + 1)
+ } else {
+ (total_count.saturating_sub(line_count_to_remove), 0)
+ };
+ let anchor_index = selected_index.unwrap_or(0); // 5
+ let mut start_index = anchor_index.saturating_sub(count_to_render / 2);
+ let mut end_index = start_index + count_to_render;
+ if end_index > total_count {
+ start_index = start_index.saturating_sub(end_index - total_count);
+ end_index = total_count;
+ }
+ (start_index, anchor_index, end_index, line_count_to_remove)
+}
+
+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);
+ } else {
+ println!("\n");
+ }
+}
+
+pub fn render_new_session_line(session_name: &Option<String>, is_searching: bool, colors: Colors) {
+ if is_searching {
+ return;
+ }
+ let new_session_shortcut_text = "<Ctrl w>";
+ let new_session_shortcut = colors.magenta(new_session_shortcut_text);
+ let new_session = colors.bold("New session");
+ let enter = colors.magenta("<ENTER>");
+ match session_name {
+ Some(session_name) => {
+ println!(
+ "\u{1b}[m > {}_ ({}, {} when done)",
+ colors.orange(session_name),
+ colors.bold("Type optional name"),
+ enter
+ );
+ },
+ None => {
+ println!("\u{1b}[m > {new_session_shortcut} - {new_session}");
+ },
+ }
+}
+
+pub fn render_controls_line(is_searching: bool, row: usize, max_cols: usize, colors: Colors) {
+ let (arrows, navigate) = if is_searching {
+ (colors.magenta("<↓↑>"), colors.bold("Navigate"))
+ } else {
+ (colors.magenta("<←↓↑→>"), colors.bold("Navigate and Expand"))
+ };
+ let enter = colors.magenta("<ENTER>");
+ let select = colors.bold("Switch to selected");
+ let esc = colors.magenta("<ESC>");
+ let to_hide = colors.bold("Hide");
+
+ if max_cols >= 80 {
+ print!(
+ "\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {esc} - {to_hide}"
+ );
+ } else if max_cols >= 57 {
+ let navigate = colors.bold("Navigate");
+ let select = colors.bold("Switch");
+ print!(
+ "\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {esc} - {to_hide}"
+ );
+ } else if max_cols >= 20 {
+ print!("\u{1b}[m\u{1b}[{row}H{arrows}/{enter}/{esc}");
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+pub struct Colors {
+ pub palette: Palette,
+}
+impl Colors {
+ pub fn new(palette: Palette) -> Self {
+ Colors { palette }
+ }
+ pub fn bold(&self, text: &str) -> String {
+ format!("\u{1b}[1m{}\u{1b}[22m", text)
+ }
+
+ fn color(&self, color: &PaletteColor, text: &str) -> String {
+ match color {
+ PaletteColor::EightBit(byte) => {
+ format!("\u{1b}[38;5;{};1m{}\u{1b}[39;22m", byte, text)
+ },
+ PaletteColor::Rgb((r, g, b)) => {
+ format!("\u{1b}[38;2;{};{};{};1m{}\u{1b}[39;22m", r, g, b, text)
+ },
+ }
+ }
+ pub fn orange(&self, text: &str) -> String {
+ self.color(&self.palette.orange, text)
+ }
+
+ pub fn green(&self, text: &str) -> String {
+ self.color(&self.palette.green, text)
+ }
+
+ pub fn red(&self, text: &str) -> String {
+ self.color(&self.palette.red, text)
+ }
+
+ pub fn cyan(&self, text: &str) -> String {
+ self.color(&self.palette.cyan, text)
+ }
+
+ pub fn magenta(&self, text: &str) -> String {
+ self.color(&self.palette.magenta, text)
+ }
+}
diff --git a/default-plugins/session-manager/src/ui/mod.rs b/default-plugins/session-manager/src/ui/mod.rs
new file mode 100644
index 00000000..94a1a8a7
--- /dev/null
+++ b/default-plugins/session-manager/src/ui/mod.rs
@@ -0,0 +1,357 @@
+pub mod components;
+use zellij_tile::prelude::*;
+
+use crate::session_list::{SelectedIndex, SessionList};
+use components::{
+ build_pane_ui_line, build_session_ui_line, build_tab_ui_line, minimize_lines, Colors,
+ LineToRender,
+};
+
+macro_rules! render_assets {
+ ($assets:expr, $line_count_to_remove:expr, $selected_index:expr, $to_render_until_selected: expr, $to_render_after_selected:expr, $has_deeper_selected_assets:expr, $max_cols:expr, $colors:expr) => {{
+ let (start_index, anchor_asset_index, end_index, line_count_to_remove) =
+ minimize_lines($assets.len(), $line_count_to_remove, $selected_index);
+ let mut truncated_result_count_above = start_index;
+ let mut truncated_result_count_below = $assets.len().saturating_sub(end_index);
+ let mut current_index = 1;
+ if let Some(assets_to_render_before_selected) = $assets.get(start_index..anchor_asset_index)
+ {
+ for asset in assets_to_render_before_selected {
+ let mut asset: LineToRender =
+ asset.as_line_to_render(current_index, $max_cols, $colors);
+ asset.add_truncated_results(truncated_result_count_above);
+ truncated_result_count_above = 0;
+ current_index += 1;
+ $to_render_until_selected.push(asset);
+ }
+ }
+ if let Some(selected_asset) = $assets.get(anchor_asset_index) {
+ if $selected_index.is_some() && !$has_deeper_selected_assets {
+ let mut selected_asset: LineToRender =
+ selected_asset.as_line_to_render(current_index, $max_cols, $colors);
+ selected_asset.make_selected();
+ selected_asset.add_truncated_results(truncated_result_count_above);
+ if anchor_asset_index + 1 >= end_index {
+ // no more results below, let's add the more indication if we need to
+ selected_asset.add_truncated_results(truncated_result_count_below);
+ }
+ current_index += 1;
+ $to_render_until_selected.push(selected_asset);
+ } else {
+ $to_render_until_selected.push(selected_asset.as_line_to_render(
+ current_index,
+ $max_cols,
+ $colors,
+ ));
+ current_index += 1;
+ }
+ }
+ if let Some(assets_to_render_after_selected) =
+ $assets.get(anchor_asset_index + 1..end_index)
+ {
+ for asset in assets_to_render_after_selected.iter().rev() {
+ let mut asset: LineToRender =
+ asset.as_line_to_render(current_index, $max_cols, $colors);
+ asset.add_truncated_results(truncated_result_count_below);
+ truncated_result_count_below = 0;
+ current_index += 1;
+ $to_render_after_selected.insert(0, asset.into());
+ }
+ }
+ line_count_to_remove
+ }};
+}
+
+impl SessionList {
+ pub fn render(&self, max_rows: usize, max_cols: usize, colors: Colors) -> Vec<LineToRender> {
+ if self.is_searching {
+ self.render_search_results(max_rows, max_cols)
+ } else {
+ self.render_list(max_rows, max_cols, colors)
+ }
+ }
+ fn render_search_results(&self, max_rows: usize, max_cols: usize) -> Vec<LineToRender> {
+ let mut lines_to_render = vec![];
+ for (i, result) in self.search_results.iter().enumerate() {
+ if lines_to_render.len() + result.lines_to_render() <= max_rows {
+ let mut result_lines = result.render(max_cols);
+ if Some(i) == self.selected_search_index {
+ for line_to_render in result_lines.iter_mut() {
+ line_to_render.make_selected();
+ }
+ }
+ lines_to_render.append(&mut result_lines);
+ } else {
+ break;
+ }
+ }
+ lines_to_render
+ }
+ fn render_list(&self, max_rows: usize, max_cols: usize, colors: Colors) -> Vec<LineToRender> {
+ let mut lines_to_render_until_selected = vec![];
+ let mut lines_to_render_after_selected = vec![];
+ let total_lines_to_render = self.total_lines_to_render();
+ let line_count_to_remove = total_lines_to_render.saturating_sub(max_rows);
+ let line_count_to_remove = self.render_sessions(
+ &mut lines_to_render_until_selected,
+ &mut lines_to_render_after_selected,
+ line_count_to_remove,
+ max_cols,
+ colors,
+ );
+ let line_count_to_remove = self.render_tabs(
+ &mut lines_to_render_until_selected,
+ &mut lines_to_render_after_selected,
+ line_count_to_remove,
+ max_cols,
+ colors,
+ );
+ self.render_panes(
+ &mut lines_to_render_until_selected,
+ &mut lines_to_render_after_selected,
+ line_count_to_remove,
+ max_cols,
+ colors,
+ );
+ let mut lines_to_render = lines_to_render_until_selected;
+ lines_to_render.append(&mut lines_to_render_after_selected);
+ lines_to_render
+ }
+ fn render_sessions(
+ &self,
+ to_render_until_selected: &mut Vec<LineToRender>,
+ to_render_after_selected: &mut Vec<LineToRender>,
+ line_count_to_remove: usize,
+ max_cols: usize,
+ colors: Colors,
+ ) -> usize {
+ render_assets!(
+ self.session_ui_infos,
+ line_count_to_remove,
+ self.selected_index.0,
+ to_render_until_selected,
+ to_render_after_selected,
+ self.selected_index.1.is_some(),
+ max_cols,
+ colors
+ )
+ }
+ fn render_tabs(
+ &self,
+ to_render_until_selected: &mut Vec<LineToRender>,
+ to_render_after_selected: &mut Vec<LineToRender>,
+ line_count_to_remove: usize,
+ max_cols: usize,
+ colors: Colors,
+ ) -> usize {
+ if self.selected_index.1.is_none() {
+ return line_count_to_remove;
+ }
+ if let Some(tabs_in_session) = self
+ .selected_index
+ .0
+ .and_then(|i| self.session_ui_infos.get(i))
+ .map(|s| &s.tabs)
+ {
+ render_assets!(
+ tabs_in_session,
+ line_count_to_remove,
+ self.selected_index.1,
+ to_render_until_selected,
+ to_render_after_selected,
+ self.selected_index.2.is_some(),
+ max_cols,
+ colors
+ )
+ } else {
+ line_count_to_remove
+ }
+ }
+ fn render_panes(
+ &self,
+ to_render_until_selected: &mut Vec<LineToRender>,
+ to_render_after_selected: &mut Vec<LineToRender>,
+ line_count_to_remove: usize,
+ max_cols: usize,
+ colors: Colors,
+ ) -> usize {
+ if self.selected_index.2.is_none() {
+ return line_count_to_remove;
+ }
+ if let Some(panes_in_session) = self
+ .selected_index
+ .0
+ .and_then(|i| self.session_ui_infos.get(i))
+ .map(|s| &s.tabs)
+ .and_then(|tabs| {
+ self.selected_index
+ .1
+ .and_then(|i| tabs.get(i))
+ .map(|t| &t.panes)
+ })
+ {
+ render_assets!(
+ panes_in_session,
+ line_count_to_remove,
+ self.selected_index.2,
+ to_render_until_selected,
+ to_render_after_selected,
+ false,
+ max_cols,
+ colors
+ )
+ } else {
+ line_count_to_remove
+ }
+ }
+ fn total_lines_to_render(&self) -> usize {
+ self.session_ui_infos
+ .iter()
+ .enumerate()
+ .fold(0, |acc, (index, s)| {
+ if self.selected_index.session_index_is_selected(index) {
+ acc + s.line_count(&self.selected_index)
+ } else {
+ acc + 1
+ }
+ })
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct SessionUiInfo {
+ pub name: String,
+ pub tabs: Vec<TabUiInfo>,
+ pub connected_users: usize,
+ pub is_current_session: bool,
+}
+
+impl SessionUiInfo {
+ pub fn from_session_info(session_info: &SessionInfo) -> Self {
+ SessionUiInfo {
+ name: session_info.name.clone(),
+ tabs: session_info
+ .tabs
+ .iter()
+ .map(|t| TabUiInfo::new(t, &session_info.panes))
+ .collect(),
+ connected_users: session_info.connected_clients,
+ is_current_session: session_info.is_current_session,
+ }
+ }
+ pub fn line_count(&self, selected_index: &SelectedIndex) -> usize {
+ let mut line_count = 1; // self
+ if selected_index.tabs_are_visible() {
+ match selected_index
+ .selected_tab_index()
+ .and_then(|i| self.tabs.get(i))
+ .map(|t| t.line_count(&selected_index))
+ {
+ Some(line_count_of_selected_tab) => {
+ // we add the line count in the selected tab minus 1 because we will account
+ // for the selected tab line itself in self.tabs.len() below
+ line_count += line_count_of_selected_tab.saturating_sub(1);
+ line_count += self.tabs.len();
+ },
+ None => {
+ line_count += self.tabs.len();
+ },
+ }
+ }
+ line_count
+ }
+ fn as_line_to_render(
+ &self,
+ _session_index: u8,
+ mut max_cols: usize,
+ colors: Colors,
+ ) -> LineToRender {
+ let mut line_to_render = LineToRender::new(colors);
+ let ui_spans = build_session_ui_line(&self, colors);
+ for span in ui_spans {
+ span.render(None, &mut line_to_render, &mut max_cols);
+ }
+ line_to_render
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct TabUiInfo {
+ pub name: String,
+ pub panes: Vec<PaneUiInfo>,
+ pub position: usize,
+}
+
+impl TabUiInfo {
+ pub fn new(tab_info: &TabInfo, pane_manifest: &PaneManifest) -> Self {
+ let panes = pane_manifest
+ .panes
+ .get(&tab_info.position)
+ .map(|p| {
+ p.iter()
+ .filter_map(|pane_info| {
+ if pane_info.is_selectable {
+ Some(PaneUiInfo {
+ name: pane_info.title.clone(),
+ exit_code: pane_info.exit_status.clone(),
+ pane_id: pane_info.id,
+ is_plugin: pane_info.is_plugin,
+ })
+ } else {
+ None
+ }
+ })
+ .collect()
+ })
+ .unwrap_or_default();
+ TabUiInfo {
+ name: tab_info.name.clone(),
+ panes,
+ position: tab_info.position,
+ }
+ }
+ pub fn line_count(&self, selected_index: &SelectedIndex) -> usize {
+ let mut line_count = 1; // self
+ if selected_index.panes_are_visible() {
+ line_count += self.panes.len()
+ }
+ line_count
+ }
+ fn as_line_to_render(
+ &self,
+ _session_index: u8,
+ mut max_cols: usize,
+ colors: Colors,
+ ) -> LineToRender {
+ let mut line_to_render = LineToRender::new(colors);
+ let ui_spans = build_tab_ui_line(&self, colors);
+ for span in ui_spans {
+ span.render(None, &mut line_to_render, &mut max_cols);
+ }
+ line_to_render
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct PaneUiInfo {
+ pub name: String,
+ pub exit_code: Option<i32>,
+ pub pane_id: u32,
+ pub is_plugin: bool,
+}
+
+impl PaneUiInfo {
+ fn as_line_to_render(
+ &self,
+ _session_index: u8,
+ mut max_cols: usize,
+ colors: Colors,
+ ) -> LineToRender {
+ let mut line_to_render = LineToRender::new(colors);
+ let ui_spans = build_pane_ui_line(&self, colors);
+ for span in ui_spans {
+ span.render(None, &mut line_to_render, &mut max_cols);
+ }
+ line_to_render
+ }
+}