//! The Open source licenses page (General → Open source licenses), like T3 //! Code's: a searchable list of notices, each folding open to its text. //! //! The crate list, `assets/licenses.tsv`, comes from `cargo metadata`: //! every package the app is built from, with its license and repository. //! Regenerate it after changing dependencies. use std::collections::HashSet; use gpui_kit::component::{ Icon, IconName, h_flex, input::{InputEvent, InputState}, v_flex, }; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::*; use super::SettingsView; use super::widgets::{Section, empty_row, focus_later, search_field}; use crate::ui::metal::{self, MetalButton, MetalStyled as _}; use crate::ui::workspace; const CRATES: &str = include_str!("../../../assets/licenses.tsv"); const OFL: &str = include_str!("../../../assets/fonts/OFL.txt"); const LUCIDE: &str = "ISC License\n\nCopyright (c) Lucide Icons and Contributors\n\nPermission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nSome icons are derived from Feather (MIT License, Copyright (c) 2013-present Cole Bemis)."; /// One notice on the page. struct Notice { name: String, license: String, /// What it is to the app: "Fonts", "Icons", "Rust crate", "Optional tool". bundle: &'static str, source: String, text: String, } fn notices() -> Vec { let notice = |name: &str, license: &str, bundle, source: &str, text: &str| Notice { name: name.into(), license: license.into(), bundle, source: source.into(), text: text.into(), }; let mut notices = vec![ notice("Geist, Geist Mono and Geist Pixel", "OFL-1.1", "Fonts", "https://github.com/vercel/geist-font", OFL), notice("Lucide", "ISC", "Icons", "https://github.com/lucide-icons/lucide", LUCIDE), notice( "Claude Code", "See repository", "Optional tool", "https://github.com/anthropics/claude-code", "Not bundled. Vectrum Code runs the claude CLI you install; its own terms apply.", ), notice( "Pi", "See repository", "Optional tool", "https://github.com/earendil-works/pi", "Not bundled. Vectrum Code runs the pi CLI you install; its own license applies.", ), notice( "jcode", "See repository", "Optional tool", "https://github.com/1jehuang/jcode", "Not bundled. Vectrum Code runs the jcode CLI you install; its own license applies.", ), notice( "agent-browser", "See repository", "Optional tool", "https://github.com/vercel-labs/agent-browser", "Not bundled. The browser panel runs the agent-browser CLI you install; its own license applies.", ), notice( "expo-device-hub", "MIT", "Optional tool", "https://github.com/expo/expo-device-hub", "Not bundled. Once you turn on the device hub, Vectrum Code installs this npm package and runs it to stream simulators and emulators; its own license applies.", ), notice( "agent-device", "MIT", "Optional tool", "https://github.com/callstack/agent-device", "Not bundled. Once you allow agent device access, Vectrum Code installs this npm package for agents to drive devices; its own license applies.", ), notice( "OpenH264", "BSD-2-Clause", "Compiled in", "https://github.com/cisco/openh264", "Cisco's H.264 codec, compiled from source through the openh264 crate, decodes Android Emulator video in the Device panel.", ), ]; notices.extend(CRATES.lines().filter(|line| !line.starts_with('#') && !line.trim().is_empty()).map(|line| { let mut fields = line.split('\t'); let (name, version, license, source) = (fields.next().unwrap_or_default(), fields.next().unwrap_or_default(), fields.next().unwrap_or_default(), fields.next().unwrap_or_default()); Notice { name: format!("{name} {version}"), license: license.into(), bundle: "Rust crate", source: source.into(), text: format!("Licensed under {license}. The full license text ships with the crate's source."), } })); notices } pub(super) struct LicensesState { query: Entity, searching: bool, expanded: HashSet, _subscription: Subscription, } impl LicensesState { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let query = cx.new(|cx| InputState::new(window, cx).placeholder("Search licenses")); let subscription = cx.subscribe_in(&query, window, |_, _, event: &InputEvent, _, cx| { if matches!(event, InputEvent::Change) { cx.notify(); } }); Self { query, searching: false, expanded: HashSet::new(), _subscription: subscription } } } impl SettingsView { pub(super) fn render_licenses(&mut self, cx: &mut Context) -> Vec { let p = metal::palette(cx); let all = notices(); let total = all.len(); let query = self.licenses.query.read(cx).value().trim().to_lowercase(); let shown: Vec = all .into_iter() .filter(|notice| { query.is_empty() || notice.name.to_lowercase().contains(&query) || notice.license.to_lowercase().contains(&query) }) .collect(); let count = if query.is_empty() { format!("{total} notices") } else { format!("{} of {total}", shown.len()) }; let header = h_flex() .gap(px(8.)) .child(div().text_size(px(12.)).text_color(p.ink3).child(count)) .map(|this| { if self.licenses.searching { this.child(search_field(&self.licenses.query, 200., cx)) } else { this.child( MetalButton::new("search-licenses") .ghost() .mini() .icon(IconName::Search) .tooltip("Search licenses") .on_click(cx.listener(|this, _, window, cx| { this.licenses.searching = true; focus_later(&this.licenses.query, window); cx.notify(); })), ) } }); let mut rows: Vec = shown .into_iter() .enumerate() .map(|(ix, notice)| { let open = self.licenses.expanded.contains(¬ice.name); let name = notice.name.clone(); let source = notice.source.clone(); v_flex() .child( h_flex() .id(("license", ix)) .px(px(16.)) .py(px(10.)) .gap(px(10.)) .cursor_pointer() .hover(|style| style.bg(p.row_hover)) .on_click(cx.listener(move |this, _, _, cx| { if !this.licenses.expanded.remove(&name) { this.licenses.expanded.insert(name.clone()); } cx.notify(); })) .child( Icon::new(IconName::ChevronRight) .size(px(14.)) .text_color(p.ink3) .when(open, |icon| icon.rotate(Radians(std::f32::consts::PI / 2.))), ) .child( v_flex() .flex_1() .min_w_0() .child(div().truncate().text_size(px(13.5)).font_weight(FontWeight::MEDIUM).text_color(p.ink).child(notice.name.clone())) .child(div().text_size(px(12.)).text_color(p.ink3).child(format!("{} · {}", notice.license, notice.bundle))), ) .when(!source.is_empty(), |this| { this.child( MetalButton::new(("license-source", ix)) .ghost() .mini() .icon(IconName::ExternalLink) .tooltip("Source") .on_click(move |_, _, _| open_url(&source)), ) }), ) .when(open, |this| { this.child( div() .mx(px(16.)) .mb(px(12.)) .px(px(12.)) .py(px(10.)) .rounded(px(12.)) .well(p) .font_family(metal::MONO) .text_size(px(11.)) .line_height(px(16.)) .text_color(p.ink2) .child(notice.text.clone()), ) }) .into_any_element() }) .collect(); if rows.is_empty() { rows.push(empty_row("No notices match your search.", p)); } vec![Section::new("Third-party notices").header_action(header).rows(rows).render(cx)] } } fn open_url(url: &str) { if let Some(program) = workspace::find_program(&["xdg-open"]) && let Ok(mut child) = std::process::Command::new(program).arg(url).spawn() { std::thread::spawn(move || child.wait()); } }