zellij/xtask/src/build.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
6.9 KiB
Rust

//! Subcommands for building.
//!
//! Currently has the following functions:
//!
//! - [`build`]: Builds general cargo projects (i.e. zellij components) with `cargo build`
//! - [`manpage`]: Builds the manpage with `mandown`
use crate::{flags, metadata, WorkspaceMember};
use anyhow::Context;
use std::path::{Path, PathBuf};
use xshell::{cmd, Shell};
/// Build members of the zellij workspace.
///
/// Build behavior is controlled by the [`flags`](flags::Build). Calls some variation of `cargo
/// build` under the hood.
pub fn build(sh: &Shell, flags: flags::Build) -> anyhow::Result<()> {
let _pd = sh.push_dir(crate::project_root());
let cargo = crate::cargo()?;
if flags.no_plugins && flags.plugins_only {
eprintln!("Cannot use both '--no-plugins' and '--plugins-only'");
std::process::exit(1);
}
for WorkspaceMember { crate_name, .. } in crate::workspace_members()
.iter()
.filter(|member| member.build)
{
let err_context = || format!("failed to build '{crate_name}'");
if crate_name.contains("plugins") {
if flags.no_plugins {
continue;
}
} else if flags.plugins_only {
continue;
}
// zellij-utils requires protobuf definition files to be present. Usually these are
// auto-generated with `build.rs`-files, but this is currently broken for us.
// See [this PR][1] for details.
//
// [1]: https://github.com/zellij-org/zellij/pull/2711#issuecomment-1695015818
{
let zellij_utils_basedir = crate::project_root().join("zellij-utils");
let _pd = sh.push_dir(zellij_utils_basedir);
let prost_asset_dir = sh.current_dir().join("assets").join("prost");
let protobuf_source_dir = sh.current_dir().join("src").join("plugin_api");
std::fs::create_dir_all(&prost_asset_dir).unwrap();
let mut prost = prost_build::Config::new();
let last_generated = prost_asset_dir
.join("generated_plugin_api.rs")
.metadata()
.and_then(|m| m.modified());
let mut needs_regeneration = false;
prost.out_dir(prost_asset_dir);
prost.include_file("generated_plugin_api.rs");
let mut proto_files = vec![];
for entry in std::fs::read_dir(&protobuf_source_dir).unwrap() {
let entry_path = entry.unwrap().path();
if entry_path.is_file() {
if !entry_path
.extension()
.map(|e| e == "proto")
.unwrap_or(false)
{
continue;
}
proto_files.push(entry_path.display().to_string());
let modified = entry_path.metadata().and_then(|m| m.modified());
needs_regeneration |= match (&last_generated, modified) {
(Ok(last_generated), Ok(modified)) => modified >= *last_generated,
// Couldn't read some metadata, assume needs update
_ => true,
}
}
}
if needs_regeneration {
prost
.compile_protos(&proto_files, &[protobuf_source_dir])
.unwrap();
}
}
let _pd = sh.push_dir(Path::new(crate_name));
// Tell the user where we are now
println!();
let msg = format!(">> Building '{crate_name}'");
crate::status(&msg);
println!("{}", msg);
let mut base_cmd = cmd!(sh, "{cargo} build");
if flags.release {
base_cmd = base_cmd.arg("--release");
}
if flags.no_web {
// Check if this crate has web features that need modification
match metadata::get_no_web_features(sh, crate_name)
.context("Failed to check web features")?
{
Some(features) => {
base_cmd = base_cmd.arg("--no-default-features");
if !features.is_empty() {
base_cmd = base_cmd.arg("--features");
base_cmd = base_cmd.arg(features);
}
},
None => {
// Crate doesn't have web features, build normally
},
}
}
base_cmd.run().with_context(err_context)?;
if crate_name.contains("plugins") {
let (_, plugin_name) = crate_name
.rsplit_once('/')
.context("Cannot determine plugin name from '{subcrate}'")?;
if flags.release {
// Move plugin into assets folder
move_plugin_to_assets(sh, plugin_name)?;
}
}
}
Ok(())
}
fn move_plugin_to_assets(sh: &Shell, plugin_name: &str) -> anyhow::Result<()> {
let err_context = || format!("failed to move plugin '{plugin_name}' to assets folder");
// Get asset path
let asset_name = crate::asset_dir()
.join("plugins")
.join(plugin_name)
.with_extension("wasm");
// Get plugin path
let plugin = PathBuf::from(
std::env::var_os("CARGO_TARGET_DIR")
.unwrap_or(crate::project_root().join("target").into_os_string()),
)
.join("wasm32-wasip1")
.join("release")
.join(plugin_name)
.with_extension("wasm");
if !plugin.is_file() {
return Err(anyhow::anyhow!("No plugin found at '{}'", plugin.display()))
.with_context(err_context);
}
// This is a plugin we want to move
let from = plugin.as_path();
let to = asset_name.as_path();
sh.copy_file(from, to).with_context(err_context)
}
/// Build the manpage with `mandown`.
// mkdir -p ${root_dir}/assets/man
// mandown ${root_dir}/docs/MANPAGE.md 1 > ${root_dir}/assets/man/zellij.1
pub fn manpage(sh: &Shell) -> anyhow::Result<()> {
let err_context = "failed to generate manpage";
let mandown = mandown(sh).context(err_context)?;
let project_root = crate::project_root();
let asset_dir = &project_root.join("assets").join("man");
sh.create_dir(asset_dir).context(err_context)?;
let _pd = sh.push_dir(asset_dir);
cmd!(sh, "{mandown} {project_root}/docs/MANPAGE.md 1")
.read()
.and_then(|text| sh.write_file("zellij.1", text))
.context(err_context)
}
/// Get the path to a `mandown` executable.
///
/// If the executable isn't found, an error is returned instead.
fn mandown(_sh: &Shell) -> anyhow::Result<PathBuf> {
match which::which("mandown") {
Ok(path) => Ok(path),
Err(e) => {
eprintln!("!! 'mandown' wasn't found but is needed for this build step.");
eprintln!("!! Please install it with: `cargo install mandown`");
Err(e).context("Couldn't find 'mandown' executable")
},
}
}