zellij/default-plugins/share/src/token_screen.rs
Aram Drevekenin c5ac796880
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>
2025-06-23 19:19:37 +02:00

192 lines
5.7 KiB
Rust

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,
}