feat: preferences for remote clipboard writes, multi-line paste confirmation and the launch update check
OSC 52 and VNC ServerCutText now go through one handler that honours 'allow remote clipboard' and always toasts the first line, so a silent clipboard swap can't set up a paste into another shell (SEC-20261012-010). Multi-line pastes into a terminal without bracketed paste ask first, with a preview (SEC-20261012-017). The launch-time update check can be turned off and the updater docs no longer claim it is opt-in; the unused GitHub fallback is removed (SEC-20261012-013, F15).
This commit is contained in:
@@ -538,6 +538,12 @@ pub struct AppPreferences {
|
||||
pub default_cursor_style: String,
|
||||
pub use_native_paste: bool,
|
||||
pub local_shell: String,
|
||||
/// Check the update manifest once at launch.
|
||||
pub check_for_updates: bool,
|
||||
/// Let remote hosts set the local clipboard (terminal OSC 52, VNC ServerCutText).
|
||||
pub allow_remote_clipboard: bool,
|
||||
/// Ask before pasting multi-line text into a terminal that isn't in bracketed-paste mode.
|
||||
pub confirm_multiline_paste: bool,
|
||||
}
|
||||
|
||||
impl Default for AppPreferences {
|
||||
@@ -556,6 +562,9 @@ impl Default for AppPreferences {
|
||||
default_cursor_style: DEFAULT_CURSOR_STYLE.into(),
|
||||
use_native_paste: false,
|
||||
local_shell: String::new(),
|
||||
check_for_updates: true,
|
||||
allow_remote_clipboard: true,
|
||||
confirm_multiline_paste: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Update checks against a self-hosted manifest (or, as a fallback, GitHub's latest release).
|
||||
//! Opt-in and check-only: nothing is downloaded or installed. Calls block; run them off the UI
|
||||
//! thread.
|
||||
//! Update checks against a self-hosted manifest. Check-only: nothing is downloaded or installed.
|
||||
//! The launch-time check honours `AppPreferences::check_for_updates`. Calls block; run them off
|
||||
//! the UI thread.
|
||||
|
||||
use crate::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -9,8 +9,6 @@ use std::time::Duration;
|
||||
|
||||
/// Manifest URL template; expand with [`expand_update_url`].
|
||||
pub const DEFAULT_UPDATE_URL: &str = "https://dixon.cx/downloads/nexus/manifest-{platform}-{arch}.json";
|
||||
const GITHUB_OWNER: &str = "nexus-terminal";
|
||||
const GITHUB_REPO: &str = "nexus-terminal";
|
||||
const USER_AGENT: &str = "NexusTerminal-UpdateChecker";
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const MAX_UPDATE_RESPONSE_BYTES: usize = 1 << 20;
|
||||
@@ -28,18 +26,6 @@ pub struct UpdateInfo {
|
||||
pub published_at: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
struct Release {
|
||||
tag_name: String,
|
||||
name: String,
|
||||
html_url: String,
|
||||
published_at: String,
|
||||
body: String,
|
||||
prerelease: bool,
|
||||
draft: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
struct Manifest {
|
||||
@@ -109,34 +95,6 @@ pub fn check_url(url: &str, current_version: &str) -> Result<UpdateInfo> {
|
||||
Ok(UpdateInfo { available: is_newer(&latest, ¤t), current_version: current, latest_version: latest, ..Default::default() })
|
||||
}
|
||||
|
||||
/// Checks the latest GitHub release of `nexus-terminal/nexus-terminal`.
|
||||
pub fn check_github(current_version: &str) -> Result<UpdateInfo> {
|
||||
let current = normalise_version(current_version);
|
||||
let url = format!("https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest");
|
||||
let (status, body) = fetch(&url, "application/vnd.github+json")?;
|
||||
if status == 404 {
|
||||
// No releases yet.
|
||||
return Ok(UpdateInfo { latest_version: current.clone(), current_version: current, ..Default::default() });
|
||||
}
|
||||
if status != 200 {
|
||||
return Err(format!("updater: GitHub API returned {status}").into());
|
||||
}
|
||||
let release: Release = serde_json::from_slice(&body).map_err(|e| format!("updater: failed to decode response: {e}"))?;
|
||||
let latest = normalise_version(&release.tag_name);
|
||||
if release.draft || release.prerelease {
|
||||
return Ok(UpdateInfo { current_version: current, latest_version: latest, ..Default::default() });
|
||||
}
|
||||
Ok(UpdateInfo {
|
||||
available: is_newer(&latest, ¤t),
|
||||
current_version: current,
|
||||
latest_version: latest,
|
||||
release_url: release.html_url,
|
||||
release_name: release.name,
|
||||
release_notes: truncate_notes(&release.body, 500),
|
||||
published_at: release.published_at,
|
||||
})
|
||||
}
|
||||
|
||||
fn normalise_version(v: &str) -> String {
|
||||
let v = v.trim();
|
||||
v.strip_prefix('v').unwrap_or(v).to_string()
|
||||
@@ -184,17 +142,6 @@ fn leading_int(s: Option<&&str>) -> i64 {
|
||||
}
|
||||
|
||||
/// Cuts at `max` bytes (backing off to a char boundary) and appends "…".
|
||||
fn truncate_notes(body: &str, max: usize) -> String {
|
||||
if body.len() <= max {
|
||||
return body.into();
|
||||
}
|
||||
let mut i = max;
|
||||
while !body.is_char_boundary(i) {
|
||||
i -= 1;
|
||||
}
|
||||
format!("{}…", &body[..i])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -247,15 +194,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncates_notes() {
|
||||
assert_eq!(truncate_notes("Short note", 100), "Short note");
|
||||
let got = truncate_notes(&"x".repeat(600), 500);
|
||||
assert!(got.ends_with('…'));
|
||||
assert_eq!(got.chars().count(), 501);
|
||||
assert_eq!(truncate_notes("aé", 2), "a…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expands_url_template() {
|
||||
let u = expand_update_url(DEFAULT_UPDATE_URL);
|
||||
|
||||
Reference in New Issue
Block a user