aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/share/src/token_screen.rs
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-06-23 19:19:37 +0200
committerGitHub <noreply@github.com>2025-06-23 19:19:37 +0200
commitc5ac7968809ddb1033b625562eae22019cccc2f4 (patch)
tree932cf07d898dfbcc6ec6a9d4cf5353c06a47accc /default-plugins/share/src/token_screen.rs
parentfeat(ui): add optional tooltip with key hints to the compact bar (#4225) (diff)
downloadsl0ppij-c5ac7968809ddb1033b625562eae22019cccc2f4.tar.gz
sl0ppij-c5ac7968809ddb1033b625562eae22019cccc2f4.tar.xz
sl0ppij-c5ac7968809ddb1033b625562eae22019cccc2f4.zip
Feature: web-client/server to share your sessions in the browser (#4242)
* work * moar work * notes * work * separate to terminal and control channels * stdin working * serve html web client initial * serve static assets loaded with include_dir * merge * enable_web_server config parameter * compile time flag to disable web server capability * rustfmt * add license to all xterm.js assets * mouse working except copy/paste * helpful comment * web client improvements - move script to js file - add favicon - add nerd font - change title TODO: investigate if font license embedded in otf is sufficient * get mouse to work properly * kitty keyboard support initial * fix wrong type in preload link * wip axum websocket handlers - upgrade axum to v0.8.1, enable ws feature - begin setup of websocket handlers - tidy up imports * replace control listener * handle terminal websocket with axum * cleanup Cargo.toml * kitty fixes and bracketed paste * fix(mouse): pane not found crash * initial session switching infra * add `web_client_font` option * session switching, creation and resurrection working through the session manager * move session module to zellij-utils and share logic with web-client * some cleanups * require restart for enable-web-server * use session name from router * write config to disk and watch for config changes * rename session name to ipc path * add basic panic handler, make render_to_client exit on channel close * use while let instead of loop * handle websocket close * add mouse motions * make clipboard work * add weblink handling and webgl rendering * add todo * fix: use session name instead of patch on session switch * use "default" layout for new sessions * ui indication for session being shared * share this session ui * plugin assets * Fix process crash on mac with notify watcher. Use poll watcher instead of recommended as a workaround. * make url session switching and creation work * start welcome screen on root url * scaffold control messages, set font from config * set dimensions on session start * bring back session name from url * send bytes on terminal websocket instead of json - create web client os input and id before websocket connection * draft ui * work * refactor ui * remove otf font, remove margins to avoid scrollbar * version query endpoint for server status * web session info query endpoint * refactor: move stuff around * add web client info to session metadata * make tests pass * populate real data in session list * remove unnecessary endpoint * add web_client node to config, add font option * remove web_client_font * allow disabling the web session through the config - WIP * formalize sharing/not-sharing configuration * fix tests * allow shutting down web server * display error when web clients are forbidden to attach * only show sessions that allow web clients if this is a web client * style(fmt): rustfmt * fix: query web server from Zellij rather than from each plugin * remove log spam * handle some error paths better in the web client * allow controlling the web server through the cli * allow configuring the web server's ip/port * fix tests and format code * use direct WebServerStatus event instead of piggy-backing on SessionInfo * plugin revamp initial * make plugin responsive * adjust plugin title * refactor: share plugin * refactor: share plugin * add cors middleware * some fixes for running without a compiled web server capability * display error when starting the share plugin without web server support * clarify config * add pipelines to compile zellij without web support * display error when unable to start web server * only query web server when share plugin is running * refactor(web-client): connection table * give zellij_server_listener access to the control channel * fixes and clarifications * refactor: consolidate generate_unique_session_name * give proper error when trying to attach to a forbidden session * change browser URL when switching sessions * add keyboard shortcut * enforce https when bound to non-loopback ip * initial authentication token implementation * background color from theme * initial web client theme config * basic token generation ui * refactor set config message creation * also set body background * allow editing scrollback for plugins too * set scrollback to 0 * properly parse colors in config * generate token from plugin * nice login modals * initial token management screen * implement token authentication * refactor(share): token management screen * style(fmt): rustfmt * fix(plugin): some minor bugs * refactor(share): main screen * refactor(share): token screen * refactor(share): main * refactor(share): ui components * fix(responsiveness): properly send usage_width to the render function * fix cli commands and add some verbosity * add support for settings ansi and selection colors * add cursor and cursor accent * basic web client tests * fix tests * refactor: web client * use session tokens for authentication * improve modals * move shutdown to ipc * refactor: ipc logic * serialize theme config for web client * update tests * refactor: move some stuff around to prepare for config hot reload * config live reloading for the web clients * change remember-me UI wording * improve xterm.js link handling * make sure terminal is focused on mousemove * remove deprecated sharing indication from compact-bar * gate deps and functionality behind the web_server_compatibility feature * feat(build): add --no-web flag in all the places * fix some other build flows * add new assets * update CI for no-web (untested) * make more dependencies optional * update axum-extra * add web client configuration options * gracefully close connections on server exit * tests for graceful connection closing * handle client-side reconnect when server is down * fix: make sure ipc bus folder exists before starting * add commands to manage login tokens from the cli * style(fmt): rustfmt * some cleanups * fix(ux): allow alt-right-click on the web client without opening the context menu * fix: prevent attaching to welcome screen * fix: reload config issues * fix long socket path on macos * normalize config conversion and fix color gap in browser * revoke session_token cookie if it is not valid * fix: visual bug with multiple clients in extremely small screen sizes * fix: only include rusqlite for the web server capability builds * update e2e snapshots * refactor(web): client side js * some cleanups * moar cleanups * fix(tests): wait for server instead of using a fixed timeout * debug CI * fix(tests): use spawn_blocking for running the test web server * fix(tests): wait for http rather than tcp port * fix(tests): properly pass config path - hopefully this is the issue... * success! bring back the rest of the tests * attempt to fix the macos CI issue * docs(changelog): add PR --------- Co-authored-by: Thomas Linford <linford.t@gmail.com>
Diffstat (limited to 'default-plugins/share/src/token_screen.rs')
-rw-r--r--default-plugins/share/src/token_screen.rs192
1 files changed, 192 insertions, 0 deletions
diff --git a/default-plugins/share/src/token_screen.rs b/default-plugins/share/src/token_screen.rs
new file mode 100644
index 00000000..461b199d
--- /dev/null
+++ b/default-plugins/share/src/token_screen.rs
@@ -0,0 +1,192 @@
+use zellij_tile::prelude::*;
+
+// Constants for text content
+const TOKEN_LABEL_LONG: &str = "New log-in token: ";
+const TOKEN_LABEL_SHORT: &str = "Token: ";
+const EXPLANATION_1_LONG: &str = "Use this token to log-in from the browser.";
+const EXPLANATION_1_SHORT: &str = "Use to log-in from the browser.";
+const EXPLANATION_2_LONG: &str =
+ "Copy this token, because it will not be saved and can't be retrieved.";
+const EXPLANATION_2_SHORT: &str = "It will not be saved and can't be retrieved.";
+const EXPLANATION_3_LONG: &str = "If lost, it can always be revoked and a new one generated.";
+const EXPLANATION_3_SHORT: &str = "It can always be revoked and a regenerated.";
+const ESC_INSTRUCTION: &str = "<Esc> - go back";
+
+// Screen layout constants
+const SCREEN_HEIGHT: usize = 7;
+const TOKEN_Y_OFFSET: usize = 0;
+const EXPLANATION_1_Y_OFFSET: usize = 2;
+const EXPLANATION_2_Y_OFFSET: usize = 4;
+const EXPLANATION_3_Y_OFFSET: usize = 5;
+const ESC_Y_OFFSET: usize = 7;
+const ERROR_Y_OFFSET: usize = 8;
+
+struct TextVariant {
+ long: &'static str,
+ short: &'static str,
+}
+
+impl TextVariant {
+ fn select(&self, cols: usize) -> &'static str {
+ if cols >= self.long.chars().count() {
+ self.long
+ } else {
+ self.short
+ }
+ }
+}
+
+pub struct TokenScreen {
+ token: String,
+ web_server_error: Option<String>,
+ rows: usize,
+ cols: usize,
+}
+
+impl TokenScreen {
+ pub fn new(token: String, web_server_error: Option<String>, rows: usize, cols: usize) -> Self {
+ Self {
+ token,
+ web_server_error,
+ rows,
+ cols,
+ }
+ }
+
+ pub fn render(&self) {
+ let elements = self.prepare_screen_elements();
+ let width = self.calculate_max_width(&elements);
+ let (base_x, base_y) = self.calculate_base_position(width);
+
+ self.render_elements(&elements, base_x, base_y);
+ self.render_error_if_present(base_x, base_y);
+ }
+
+ fn prepare_screen_elements(&self) -> ScreenElements {
+ let token_variant = TextVariant {
+ long: TOKEN_LABEL_LONG,
+ short: TOKEN_LABEL_SHORT,
+ };
+
+ let explanation_variants = [
+ TextVariant {
+ long: EXPLANATION_1_LONG,
+ short: EXPLANATION_1_SHORT,
+ },
+ TextVariant {
+ long: EXPLANATION_2_LONG,
+ short: EXPLANATION_2_SHORT,
+ },
+ TextVariant {
+ long: EXPLANATION_3_LONG,
+ short: EXPLANATION_3_SHORT,
+ },
+ ];
+
+ let token_label = token_variant.select(
+ self.cols
+ .saturating_sub(self.token.chars().count().saturating_sub(1)),
+ );
+ let token_text = format!("{}{}", token_label, self.token);
+ let token_element = self.create_token_text_element(&token_text, token_label);
+
+ let explanation_texts: Vec<&str> = explanation_variants
+ .iter()
+ .map(|variant| variant.select(self.cols))
+ .collect();
+
+ let explanation_elements: Vec<Text> = explanation_texts
+ .iter()
+ .enumerate()
+ .map(|(i, &text)| {
+ if i == 0 {
+ Text::new(text).color_range(0, ..)
+ } else {
+ Text::new(text)
+ }
+ })
+ .collect();
+
+ let esc_element = Text::new(ESC_INSTRUCTION).color_range(3, ..=4);
+
+ ScreenElements {
+ token: token_element,
+ token_text,
+ explanation_texts,
+ explanations: explanation_elements,
+ esc: esc_element,
+ }
+ }
+
+ fn create_token_text_element(&self, token_text: &str, token_label: &str) -> Text {
+ Text::new(token_text).color_range(2, ..token_label.chars().count())
+ }
+
+ fn calculate_max_width(&self, elements: &ScreenElements) -> usize {
+ let token_width = elements.token_text.chars().count();
+ let explanation_widths = elements
+ .explanation_texts
+ .iter()
+ .map(|text| text.chars().count());
+ let esc_width = ESC_INSTRUCTION.chars().count();
+
+ [token_width, esc_width]
+ .into_iter()
+ .chain(explanation_widths)
+ .max()
+ .unwrap_or(0)
+ }
+
+ fn calculate_base_position(&self, width: usize) -> (usize, usize) {
+ let base_x = self.cols.saturating_sub(width) / 2;
+ let base_y = self.rows.saturating_sub(SCREEN_HEIGHT) / 2;
+ (base_x, base_y)
+ }
+
+ fn render_elements(&self, elements: &ScreenElements, base_x: usize, base_y: usize) {
+ print_text_with_coordinates(
+ elements.token.clone(),
+ base_x,
+ base_y + TOKEN_Y_OFFSET,
+ None,
+ None,
+ );
+
+ let y_offsets = [
+ EXPLANATION_1_Y_OFFSET,
+ EXPLANATION_2_Y_OFFSET,
+ EXPLANATION_3_Y_OFFSET,
+ ];
+ for (explanation, &y_offset) in elements.explanations.iter().zip(y_offsets.iter()) {
+ print_text_with_coordinates(explanation.clone(), base_x, base_y + y_offset, None, None);
+ }
+
+ print_text_with_coordinates(
+ elements.esc.clone(),
+ base_x,
+ base_y + ESC_Y_OFFSET,
+ None,
+ None,
+ );
+ }
+
+ fn render_error_if_present(&self, base_x: usize, base_y: usize) {
+ if let Some(error) = &self.web_server_error {
+ print_text_with_coordinates(
+ Text::new(error).color_range(3, ..),
+ base_x,
+ base_y + ERROR_Y_OFFSET,
+ None,
+ None,
+ );
+ }
+ }
+}
+
+struct ScreenElements {
+ token: Text,
+ token_text: String,
+ explanation_texts: Vec<&'static str>,
+ explanations: Vec<Text>,
+ esc: Text,
+}