Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7039563f23 | ||
|
|
6d78be7aa2 | ||
|
|
1a1b58e9a8 | ||
|
|
6ac02cdda5 |
@@ -17,6 +17,44 @@ Nexus Terminal helps you manage many connections at once, keep terminal work org
|
|||||||
- Session recording (asciicast v2) and playback
|
- Session recording (asciicast v2) and playback
|
||||||
- Secure credential storage with the OS keychain first and an encrypted vault fallback
|
- Secure credential storage with the OS keychain first and an encrypted vault fallback
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
**macOS** (Homebrew):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew install --cask andydixon/tap/nexusterminal
|
||||||
|
```
|
||||||
|
|
||||||
|
**Debian, Ubuntu** (22.04 and later):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo install -d -m 0755 /etc/apt/keyrings
|
||||||
|
curl -fsSL https://repo.dixon.cx/dixon.gpg | sudo tee /etc/apt/keyrings/dixon.gpg >/dev/null
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/dixon.gpg] https://repo.dixon.cx/apt stable main" |
|
||||||
|
sudo tee /etc/apt/sources.list.d/dixon.list
|
||||||
|
sudo apt update && sudo apt install nexusterminal
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fedora, RHEL, openSUSE:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo curl -fsSL -o /etc/yum.repos.d/dixon.repo https://repo.dixon.cx/rpm/dixon.repo
|
||||||
|
sudo dnf install nexusterminal # or: sudo zypper install nexusterminal
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arch Linux:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://repo.dixon.cx/dixon.asc | sudo pacman-key --add -
|
||||||
|
sudo pacman-key --lsign-key C22FF7330668417C62C7A304CBC7951D7F2234D1
|
||||||
|
printf '\n[dixon]\nServer = https://repo.dixon.cx/arch/$arch\n' | sudo tee -a /etc/pacman.conf
|
||||||
|
sudo pacman -Sy nexusterminal
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows**, or any platform by hand: download a zip from
|
||||||
|
[GitHub Releases](https://github.com/andydixon/nexus/releases) and check it against `SHA256SUMS`.
|
||||||
|
Builds aren't code-signed yet, so macOS and Windows ask you to confirm the first launch.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
1. Launch Nexus Terminal.
|
1. Launch Nexus Terminal.
|
||||||
|
|||||||
Generated
+2
@@ -3723,6 +3723,8 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"libc",
|
"libc",
|
||||||
"nexus-core",
|
"nexus-core",
|
||||||
|
"objc2 0.6.5",
|
||||||
|
"objc2-app-kit 0.3.2",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"regex",
|
"regex",
|
||||||
"rfd",
|
"rfd",
|
||||||
|
|||||||
@@ -35,3 +35,7 @@ winresource = "0.1"
|
|||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
objc2 = "0.6.4"
|
||||||
|
objc2-app-kit = { version = "0.3.2", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
|
||||||
|
|||||||
@@ -992,9 +992,31 @@ impl eframe::App for App {
|
|||||||
self.screenshot.is_none()
|
self.screenshot.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||||
|
if raw.events.iter().any(|e| matches!(e, egui::Event::WindowFocused(false))) {
|
||||||
|
// winit can miss mouse-up events when macOS hides or minimizes the window.
|
||||||
|
// Release egui's pointer buttons too, so every widget remains interactive on return.
|
||||||
|
let pos = raw.screen_rect.map_or(egui::pos2(-1000.0, -1000.0), |r| r.max + egui::vec2(100.0, 100.0));
|
||||||
|
for button in [egui::PointerButton::Primary, egui::PointerButton::Secondary, egui::PointerButton::Middle] {
|
||||||
|
raw.events.push(egui::Event::PointerButton { pos, button, pressed: false, modifiers: egui::Modifiers::default() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn ui(&mut self, ui: &mut Ui, _frame: &mut eframe::Frame) {
|
fn ui(&mut self, ui: &mut Ui, _frame: &mut eframe::Frame) {
|
||||||
let ctx = ui.ctx().clone();
|
let ctx = ui.ctx().clone();
|
||||||
let t = Tokens::get(&ctx);
|
let t = Tokens::get(&ctx);
|
||||||
|
// If the window is hidden or minimized during a drag, macOS may not deliver the
|
||||||
|
// matching mouse-up. Clear terminal-side pointer capture on every pane, including
|
||||||
|
// panes that are currently hidden behind another tab.
|
||||||
|
let lost_focus = ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::WindowFocused(false))));
|
||||||
|
if lost_focus {
|
||||||
|
for pane in self.s.panes.values_mut() {
|
||||||
|
if let crate::views::View::Terminal(term) = &mut pane.view {
|
||||||
|
term.cancel_pointer_interaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
self.s.now = ctx.input(|i| i.time);
|
self.s.now = ctx.input(|i| i.time);
|
||||||
self.drain_events();
|
self.drain_events();
|
||||||
self.handle_requests();
|
self.handle_requests();
|
||||||
|
|||||||
+14
-1
@@ -238,7 +238,7 @@ fn window_controls(ui: &mut Ui, area: Rect) {
|
|||||||
ui.painter().text(r.center(), Align2::CENTER_CENTER, glyph, egui::FontId::proportional(14.0), c);
|
ui.painter().text(r.center(), Align2::CENTER_CENTER, glyph, egui::FontId::proportional(14.0), c);
|
||||||
if resp.clicked() {
|
if resp.clicked() {
|
||||||
match k {
|
match k {
|
||||||
0 => ctx.send_viewport_cmd(ViewportCommand::Minimized(true)),
|
0 => minimize_window(&ctx),
|
||||||
1 => ctx.send_viewport_cmd(ViewportCommand::Maximized(!max)),
|
1 => ctx.send_viewport_cmd(ViewportCommand::Maximized(!max)),
|
||||||
_ => ctx.send_viewport_cmd(ViewportCommand::Close),
|
_ => ctx.send_viewport_cmd(ViewportCommand::Close),
|
||||||
}
|
}
|
||||||
@@ -246,6 +246,19 @@ fn window_controls(ui: &mut Ui, area: Rect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `ViewportCommand::Minimized(true)` can leave eframe unresponsive after restore on macOS.
|
||||||
|
/// Hiding the app follows the normal macOS app lifecycle and avoids that minimize event path.
|
||||||
|
fn minimize_window(_ctx: &egui::Context) {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
if let Some(mtm) = objc2::MainThreadMarker::new() {
|
||||||
|
objc2_app_kit::NSApplication::sharedApplication(mtm).hide(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
_ctx.send_viewport_cmd(ViewportCommand::Minimized(true));
|
||||||
|
}
|
||||||
|
|
||||||
/// Invisible window edges that resize the undecorated window.
|
/// Invisible window edges that resize the undecorated window.
|
||||||
pub fn resize_edges(ctx: &egui::Context) {
|
pub fn resize_edges(ctx: &egui::Context) {
|
||||||
use egui::{CursorIcon, ResizeDirection as D};
|
use egui::{CursorIcon, ResizeDirection as D};
|
||||||
|
|||||||
@@ -396,6 +396,13 @@ impl TermView {
|
|||||||
self.focus_requested = true;
|
self.focus_requested = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A window can lose its mouse-up event when it is hidden or minimized mid-drag.
|
||||||
|
/// Drop the transient pointer state while keeping any text already selected.
|
||||||
|
pub fn cancel_pointer_interaction(&mut self) {
|
||||||
|
self.drag = None;
|
||||||
|
self.mouse_down = None;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selection_text(&self) -> Option<String> {
|
pub fn selection_text(&self) -> Option<String> {
|
||||||
self.term.selection_to_string().filter(|s| !s.is_empty())
|
self.term.selection_to_string().filter(|s| !s.is_empty())
|
||||||
}
|
}
|
||||||
@@ -780,13 +787,31 @@ impl TermView {
|
|||||||
self.term.scroll_display(Scroll::Delta(n));
|
self.term.scroll_display(Scroll::Delta(n));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::PointerButton { pos, button, pressed, modifiers } if resp.rect.contains(*pos) => {
|
Event::PointerButton { pos, button, pressed, modifiers } => {
|
||||||
let b = match button {
|
let b = match button {
|
||||||
PointerButton::Primary => 0,
|
PointerButton::Primary => 0,
|
||||||
PointerButton::Middle => 1,
|
PointerButton::Middle => 1,
|
||||||
PointerButton::Secondary => 2,
|
PointerButton::Secondary => 2,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
// A mouse-up outside this widget still ends a drag that began here.
|
||||||
|
// Without this, the next pointer movement keeps extending the selection.
|
||||||
|
let was_mouse_down = self.mouse_down == Some(b);
|
||||||
|
if !*pressed {
|
||||||
|
if b == 0 {
|
||||||
|
self.drag = None;
|
||||||
|
}
|
||||||
|
if was_mouse_down {
|
||||||
|
self.mouse_down = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !resp.rect.contains(*pos) {
|
||||||
|
if !*pressed && was_mouse_down {
|
||||||
|
let (c, r) = to_cell(*pos);
|
||||||
|
out.input.extend(mouse_report(b, c, r, false, mode));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if reporting && b != 2 || reporting && mode.contains(TermMode::MOUSE_MODE) && b == 2 && !*pressed {
|
if reporting && b != 2 || reporting && mode.contains(TermMode::MOUSE_MODE) && b == 2 && !*pressed {
|
||||||
let (c, r) = to_cell(*pos);
|
let (c, r) = to_cell(*pos);
|
||||||
out.input.extend(mouse_report(b, c, r, *pressed, mode));
|
out.input.extend(mouse_report(b, c, r, *pressed, mode));
|
||||||
|
|||||||
+21
-14
@@ -16,7 +16,7 @@
|
|||||||
# The publish half is the same as in ../../../mapsize/packaging/repo.sh and friends: every index
|
# The publish half is the same as in ../../../mapsize/packaging/repo.sh and friends: every index
|
||||||
# is rebuilt over all packages in the repository, so other projects' packages are kept.
|
# is rebuilt over all packages in the repository, so other projects' packages are kept.
|
||||||
#
|
#
|
||||||
# Needs: gh (or curl), unzip, curl (for the pinned nfpm), docker, gpg, apt-ftparchive, flock.
|
# Needs: curl, unzip, curl (for the pinned nfpm), docker, gpg, apt-ftparchive, flock.
|
||||||
# REPO_DIR where the repository lives (default below)
|
# REPO_DIR where the repository lives (default below)
|
||||||
# REPO_GNUPGHOME GnuPG home holding the signing key (default below)
|
# REPO_GNUPGHOME GnuPG home holding the signing key (default below)
|
||||||
# ARCHS package architectures (default: amd64 arm64)
|
# ARCHS package architectures (default: amd64 arm64)
|
||||||
@@ -31,12 +31,17 @@ LICENSE=AGPL-3.0-only
|
|||||||
ARCHS=${ARCHS:-"amd64 arm64"}
|
ARCHS=${ARCHS:-"amd64 arm64"}
|
||||||
# The binaries come from ubuntu-22.04 runners: glibc 2.35. Windowing, GL and audio libraries are
|
# The binaries come from ubuntu-22.04 runners: glibc 2.35. Windowing, GL and audio libraries are
|
||||||
# loaded at run time, so they're recommended rather than hard dependencies (any desktop has them).
|
# loaded at run time, so they're recommended rather than hard dependencies (any desktop has them).
|
||||||
DEB_DEPENDS='"libc6 (>= 2.35)", "libgcc-s1", "zlib1g"'
|
# ALSA is linked directly (RDP audio), so it's a hard dependency. libasound2t64 comes first: on
|
||||||
DEB_RECOMMENDS='"libxkbcommon0", "libwayland-client0", "libx11-6", "libxcursor1", "libxrandr2", "libxi6", "libgl1", "libegl1", "libasound2", "xdg-desktop-portal"'
|
# Ubuntu 24.04 and later plain libasound2 is only a virtual name, which apt may satisfy with the
|
||||||
RPM_DEPENDS='"glibc >= 2.35", "libgcc", "zlib"'
|
# OSS shim liboss4-salsa-asound2 (missing real ALSA symbols); older Debian falls through to it.
|
||||||
RPM_RECOMMENDS='"libxkbcommon", "libwayland-client", "libX11", "libXcursor", "libXrandr", "libXi", "mesa-libGL", "mesa-libEGL", "alsa-lib", "xdg-desktop-portal"'
|
DEB_DEPENDS='"libc6 (>= 2.35)", "libgcc-s1", "zlib1g", "libasound2t64 | libasound2"'
|
||||||
ARCH_DEPENDS='"glibc", "gcc-libs", "zlib", "libxkbcommon", "libglvnd"'
|
DEB_RECOMMENDS='"libxkbcommon0", "libwayland-client0", "libx11-6", "libxcursor1", "libxrandr2", "libxi6", "libgl1", "libegl1", "xdg-desktop-portal"'
|
||||||
ARCH_OPTDEPENDS='"wayland: Wayland sessions", "libx11: X11 sessions", "libxcursor: X11 sessions", "libxrandr: X11 sessions", "libxi: X11 sessions", "alsa-lib: RDP audio", "xdg-desktop-portal: file dialogs"'
|
RPM_DEPENDS='"glibc >= 2.35", "libgcc", "zlib", "alsa-lib"'
|
||||||
|
RPM_RECOMMENDS='"libxkbcommon", "libwayland-client", "libX11", "libXcursor", "libXrandr", "libXi", "mesa-libGL", "mesa-libEGL", "xdg-desktop-portal"'
|
||||||
|
ARCH_DEPENDS='"glibc", "gcc-libs", "zlib", "alsa-lib", "libxkbcommon", "libglvnd"'
|
||||||
|
ARCH_SUGGESTS='"wayland", "libx11", "libxcursor", "libxrandr", "libxi", "xdg-desktop-portal"'
|
||||||
|
# Package revision: bump to republish the same Nexus version with packaging fixes.
|
||||||
|
PKGREL=${PKGREL:-1}
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
REPO_DIR=${REPO_DIR:-/home/andy/domains/repo.dixon.cx/public_html}
|
REPO_DIR=${REPO_DIR:-/home/andy/domains/repo.dixon.cx/public_html}
|
||||||
@@ -49,7 +54,7 @@ HERE=$(cd "$(dirname "$0")" && pwd)
|
|||||||
say() { echo "repo: $*" >&2; }
|
say() { echo "repo: $*" >&2; }
|
||||||
die() { say "$*"; exit 1; }
|
die() { say "$*"; exit 1; }
|
||||||
|
|
||||||
for tool in gh unzip curl docker gpg apt-ftparchive flock sha256sum; do
|
for tool in unzip curl docker gpg apt-ftparchive flock sha256sum; do
|
||||||
command -v $tool >/dev/null || die "needs $tool"
|
command -v $tool >/dev/null || die "needs $tool"
|
||||||
done
|
done
|
||||||
[[ -d $REPO_DIR ]] || die "$REPO_DIR does not exist"
|
[[ -d $REPO_DIR ]] || die "$REPO_DIR does not exist"
|
||||||
@@ -85,9 +90,10 @@ NFPM=$NFPM_DIR/nfpm
|
|||||||
# ---- fetch and verify the release binaries ---------------------------------
|
# ---- fetch and verify the release binaries ---------------------------------
|
||||||
say "fetching $TAG from github.com/$GITHUB"
|
say "fetching $TAG from github.com/$GITHUB"
|
||||||
mkdir -p "$WORK/dl"
|
mkdir -p "$WORK/dl"
|
||||||
patterns=(--pattern SHA256SUMS)
|
base=https://github.com/$GITHUB/releases/download/$TAG
|
||||||
for arch in $ARCHS; do patterns+=(--pattern "NexusTerminal-linux-$arch.zip"); done
|
for f in SHA256SUMS $(for arch in $ARCHS; do echo "NexusTerminal-linux-$arch.zip"; done); do
|
||||||
gh release download "$TAG" -R "$GITHUB" -D "$WORK/dl" "${patterns[@]}" || die "release $TAG has no Linux assets yet"
|
curl -fsSL -o "$WORK/dl/$f" "$base/$f" || die "release $TAG has no $f yet"
|
||||||
|
done
|
||||||
(cd "$WORK/dl" && grep -E "NexusTerminal-linux-($(tr ' ' '|' <<<"$ARCHS"))\.zip" SHA256SUMS | sha256sum -c --quiet) || die "checksum mismatch"
|
(cd "$WORK/dl" && grep -E "NexusTerminal-linux-($(tr ' ' '|' <<<"$ARCHS"))\.zip" SHA256SUMS | sha256sum -c --quiet) || die "checksum mismatch"
|
||||||
|
|
||||||
# ---- package -----------------------------------------------------------------
|
# ---- package -----------------------------------------------------------------
|
||||||
@@ -104,7 +110,7 @@ for arch in $ARCHS; do
|
|||||||
arch: $arch
|
arch: $arch
|
||||||
platform: linux
|
platform: linux
|
||||||
version: $VERSION
|
version: $VERSION
|
||||||
release: 1
|
release: $PKGREL
|
||||||
section: net
|
section: net
|
||||||
maintainer: Andy Dixon <andy@dixon.cx>
|
maintainer: Andy Dixon <andy@dixon.cx>
|
||||||
description: |
|
description: |
|
||||||
@@ -144,15 +150,16 @@ for arch in $ARCHS; do
|
|||||||
recommends: [$RPM_RECOMMENDS]
|
recommends: [$RPM_RECOMMENDS]
|
||||||
archlinux:
|
archlinux:
|
||||||
depends: [$ARCH_DEPENDS]
|
depends: [$ARCH_DEPENDS]
|
||||||
|
suggests: [$ARCH_SUGGESTS]
|
||||||
archlinux:
|
archlinux:
|
||||||
packager: Andy Dixon <andy@dixon.cx>
|
packager: Andy Dixon <andy@dixon.cx>
|
||||||
optdepends: [$ARCH_OPTDEPENDS]
|
|
||||||
rpm:
|
rpm:
|
||||||
signature:
|
signature:
|
||||||
key_file: $WORK/signing.asc
|
key_file: $WORK/signing.asc
|
||||||
EOF
|
EOF
|
||||||
for fmt in deb rpm archlinux; do
|
for fmt in deb rpm archlinux; do
|
||||||
"$NFPM" pkg -f "$WORK/nfpm-$arch.yaml" -p $fmt -t "$WORK/out/" >/dev/null
|
"$NFPM" pkg -f "$WORK/nfpm-$arch.yaml" -p $fmt -t "$WORK/out/" >"$WORK/nfpm.log" 2>&1 ||
|
||||||
|
{ cat "$WORK/nfpm.log" >&2; die "nfpm $fmt for $arch failed"; }
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user