use super::CoordinatesInLine; use crate::ui_components::{ hovering_on_line, render_text_with_underline, CurrentSessionSection, Usage, WebServerStatusSection, }; use zellij_tile::prelude::*; use std::collections::HashMap; use url::Url; pub struct MainScreenState { pub currently_hovering_over_link: bool, pub currently_hovering_over_unencrypted: bool, pub clickable_urls: HashMap, } pub struct MainScreen<'a> { token_list_is_empty: bool, web_server_started: bool, web_server_error: &'a Option, web_server_different_version_error: &'a Option, web_server_base_url: &'a String, web_server_ip: Option, web_server_port: Option, session_name: &'a Option, web_sharing: WebSharing, hover_coordinates: Option<(usize, usize)>, info: &'a Option, link_executable: &'a Option<&'a str>, } impl<'a> MainScreen<'a> { const TITLE_TEXT: &'static str = "Share Session Locally in the Browser"; const WARNING_TEXT: &'static str = "[*] Connection unencrypted. Consider using an SSL certificate."; const MORE_INFO_TEXT: &'static str = "More info: "; const SSL_URL: &'static str = "https://zellij.dev/documentation/web-client.html#https"; const HELP_TEXT_WITH_CLICK: &'static str = "Help: Click or Shift-Click to open in browser"; const HELP_TEXT_SHIFT_ONLY: &'static str = "Help: Shift-Click to open in browser"; pub fn new( token_list_is_empty: bool, web_server_started: bool, web_server_error: &'a Option, web_server_different_version_error: &'a Option, web_server_base_url: &'a String, web_server_ip: Option, web_server_port: Option, session_name: &'a Option, web_sharing: WebSharing, hover_coordinates: Option<(usize, usize)>, info: &'a Option, link_executable: &'a Option<&'a str>, ) -> Self { Self { token_list_is_empty, web_server_started, web_server_error, web_server_different_version_error, web_server_base_url, web_server_ip, web_server_port, session_name, web_sharing, hover_coordinates, info, link_executable, } } pub fn render(self, rows: usize, cols: usize) -> MainScreenState { let mut state = MainScreenState { currently_hovering_over_link: false, currently_hovering_over_unencrypted: false, clickable_urls: HashMap::new(), }; let layout = self.calculate_layout(rows, cols); self.render_content(&layout, &mut state); state } fn calculate_layout<'b>(&'b self, rows: usize, cols: usize) -> Layout<'b> { let usage = Usage::new(!self.token_list_is_empty); let web_server_status_section = WebServerStatusSection::new( self.web_server_started, self.web_server_error.clone(), self.web_server_different_version_error.clone(), self.web_server_base_url.clone(), self.connection_is_unencrypted(), ); let current_session_section = CurrentSessionSection::new( self.web_server_started, self.web_server_ip, self.web_server_port, self.session_name.clone(), self.web_sharing, self.connection_is_unencrypted(), ); let title_width = Self::TITLE_TEXT.chars().count(); let (web_server_width, web_server_height) = web_server_status_section.web_server_status_width_and_height(); let (current_session_width, current_session_height) = current_session_section.current_session_status_width_and_height(); let (usage_width, usage_height) = usage.usage_width_and_height(cols); let mut max_width = title_width .max(web_server_width) .max(current_session_width) .max(usage_width); let mut total_height = 2 + web_server_height + 1 + current_session_height + 1 + usage_height; if self.connection_is_unencrypted() { let warning_width = self.unencrypted_warning_width(); max_width = max_width.max(warning_width); total_height += 3; } Layout { base_x: cols.saturating_sub(max_width) / 2, base_y: rows.saturating_sub(total_height) / 2, title_text: Self::TITLE_TEXT, web_server_height, usage_height, usage_width, } } fn render_content(&self, layout: &Layout, state: &mut MainScreenState) { let mut current_y = layout.base_y; self.render_title(layout, current_y); current_y += 2; current_y = self.render_web_server_section(layout, current_y, state); current_y = self.render_current_session_section(layout, current_y, state); current_y = self.render_usage_section(layout, current_y); current_y = self.render_warnings_and_help(layout, current_y, state); self.render_info(layout, current_y); } fn render_title(&self, layout: &Layout, y: usize) { let title = Text::new(layout.title_text).color_range(2, ..); print_text_with_coordinates(title, layout.base_x, y, None, None); } fn render_web_server_section( &self, layout: &Layout, y: usize, state: &mut MainScreenState, ) -> usize { let mut web_server_status_section = WebServerStatusSection::new( self.web_server_started, self.web_server_error.clone(), self.web_server_different_version_error.clone(), self.web_server_base_url.clone(), self.connection_is_unencrypted(), ); web_server_status_section.render_web_server_status( layout.base_x, y, self.hover_coordinates, ); state.currently_hovering_over_link |= web_server_status_section.currently_hovering_over_link; state.currently_hovering_over_unencrypted |= web_server_status_section.currently_hovering_over_unencrypted; for (coordinates, url) in web_server_status_section.clickable_urls { state.clickable_urls.insert(coordinates, url); } y + layout.web_server_height + 1 } fn render_current_session_section( &self, layout: &Layout, y: usize, state: &mut MainScreenState, ) -> usize { let mut current_session_section = CurrentSessionSection::new( self.web_server_started, self.web_server_ip, self.web_server_port, self.session_name.clone(), self.web_sharing, self.connection_is_unencrypted(), ); current_session_section.render_current_session_status( layout.base_x, y, self.hover_coordinates, ); state.currently_hovering_over_link |= current_session_section.currently_hovering_over_link; for (coordinates, url) in current_session_section.clickable_urls { state.clickable_urls.insert(coordinates, url); } y + layout.web_server_height + 1 } fn render_usage_section(&self, layout: &Layout, y: usize) -> usize { let usage = Usage::new(!self.token_list_is_empty); usage.render_usage(layout.base_x, y, layout.usage_width); y + layout.usage_height + 1 } fn render_warnings_and_help( &self, layout: &Layout, mut y: usize, state: &mut MainScreenState, ) -> usize { if self.connection_is_unencrypted() && self.web_server_started { self.render_unencrypted_warning(layout.base_x, y, state); y += 3; } if state.currently_hovering_over_link { self.render_link_help(layout.base_x, y); y += 3; } y } fn render_info(&self, layout: &Layout, y: usize) { if let Some(info) = self.info { let info_text = Text::new(info).color_range(1, ..); print_text_with_coordinates(info_text, layout.base_x, y, None, None); } } fn unencrypted_warning_width(&self) -> usize { let more_info_line = format!("{}{}", Self::MORE_INFO_TEXT, Self::SSL_URL); std::cmp::max( Self::WARNING_TEXT.chars().count(), more_info_line.chars().count(), ) } fn render_unencrypted_warning(&self, x: usize, y: usize, state: &mut MainScreenState) { let warning_text = Text::new(Self::WARNING_TEXT).color_range(1, ..3); let more_info_line = Text::new(format!("{}{}", Self::MORE_INFO_TEXT, Self::SSL_URL)); let url_x = x + Self::MORE_INFO_TEXT.chars().count(); let url_y = y + 1; let url_width = Self::SSL_URL.chars().count(); state.clickable_urls.insert( CoordinatesInLine::new(url_x, url_y, url_width), Self::SSL_URL.to_owned(), ); print_text_with_coordinates(warning_text, x, y, None, None); print_text_with_coordinates(more_info_line, x, y + 1, None, None); if hovering_on_line(url_x, url_y, url_width, self.hover_coordinates) { state.currently_hovering_over_link = true; render_text_with_underline(url_x, url_y, Self::SSL_URL); } } fn render_link_help(&self, x: usize, y: usize) { let help_text = if self.link_executable.is_some() { Text::new(Self::HELP_TEXT_WITH_CLICK) .color_range(3, 6..=10) .color_range(3, 15..=25) } else { Text::new(Self::HELP_TEXT_SHIFT_ONLY).color_range(3, 6..=16) }; print_text_with_coordinates(help_text, x, y, None, None); } pub fn connection_is_unencrypted(&self) -> bool { Url::parse(&self.web_server_base_url) .ok() .map(|b| b.scheme() == "http") .unwrap_or(false) } } struct Layout<'a> { base_x: usize, base_y: usize, title_text: &'a str, web_server_height: usize, usage_height: usize, usage_width: usize, }