4 Commits
Author SHA1 Message Date
Andy Dixon 7039563f23 Fix hanging when minimised on OSX 2026-10-20 15:02:30 +01:00
Andy Dixon 6d78be7aa2 Freeze issue experimental fix due to mouse 2026-10-20 13:27:39 +01:00
Andy Dixon 1a1b58e9a8 Merge pull request #7 from andydixon/packaging-nexusterminal
Publish nexusterminal to repo.dixon.cx and Homebrew; install instructions
2026-10-13 15:57:36 +01:00
Andy Dixon 6ac02cdda5 packaging: publish nexusterminal to repo.dixon.cx and Homebrew; install instructions
- packaging/repo.sh: download release assets over HTTPS (no GitHub API),
  show nfpm errors, PKGREL for packaging-only revisions, and declare ALSA
  as a hard dependency (the release binary links libasound.so.2), with
  libasound2t64 preferred so Ubuntu 24.04 doesn't pick the OSS shim.
  Verified by clean installs on Debian 12/13, Ubuntu 22.04/24.04,
  Fedora 42 and Arch.
- README: Install section (Homebrew cask, apt, dnf/zypper, pacman, zips).
2026-10-13 13:57:25 +01:00
7 changed files with 127 additions and 16 deletions
+38
View File
@@ -17,6 +17,44 @@ Nexus Terminal helps you manage many connections at once, keep terminal work org
- Session recording (asciicast v2) and playback
- 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
1. Launch Nexus Terminal.
+2
View File
@@ -3723,6 +3723,8 @@ dependencies = [
"image",
"libc",
"nexus-core",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"parking_lot",
"regex",
"rfd",
+4
View File
@@ -35,3 +35,7 @@ winresource = "0.1"
[target.'cfg(unix)'.dependencies]
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"] }
+22
View File
@@ -992,9 +992,31 @@ impl eframe::App for App {
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) {
let ctx = ui.ctx().clone();
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.drain_events();
self.handle_requests();
+14 -1
View File
@@ -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);
if resp.clicked() {
match k {
0 => ctx.send_viewport_cmd(ViewportCommand::Minimized(true)),
0 => minimize_window(&ctx),
1 => ctx.send_viewport_cmd(ViewportCommand::Maximized(!max)),
_ => 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.
pub fn resize_edges(ctx: &egui::Context) {
use egui::{CursorIcon, ResizeDirection as D};
+26 -1
View File
@@ -396,6 +396,13 @@ impl TermView {
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> {
self.term.selection_to_string().filter(|s| !s.is_empty())
}
@@ -780,13 +787,31 @@ impl TermView {
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 {
PointerButton::Primary => 0,
PointerButton::Middle => 1,
PointerButton::Secondary => 2,
_ => 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 {
let (c, r) = to_cell(*pos);
out.input.extend(mouse_report(b, c, r, *pressed, mode));
+21 -14
View File
@@ -16,7 +16,7 @@
# 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.
#
# 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_GNUPGHOME GnuPG home holding the signing key (default below)
# ARCHS package architectures (default: amd64 arm64)
@@ -31,12 +31,17 @@ LICENSE=AGPL-3.0-only
ARCHS=${ARCHS:-"amd64 arm64"}
# 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).
DEB_DEPENDS='"libc6 (>= 2.35)", "libgcc-s1", "zlib1g"'
DEB_RECOMMENDS='"libxkbcommon0", "libwayland-client0", "libx11-6", "libxcursor1", "libxrandr2", "libxi6", "libgl1", "libegl1", "libasound2", "xdg-desktop-portal"'
RPM_DEPENDS='"glibc >= 2.35", "libgcc", "zlib"'
RPM_RECOMMENDS='"libxkbcommon", "libwayland-client", "libX11", "libXcursor", "libXrandr", "libXi", "mesa-libGL", "mesa-libEGL", "alsa-lib", "xdg-desktop-portal"'
ARCH_DEPENDS='"glibc", "gcc-libs", "zlib", "libxkbcommon", "libglvnd"'
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"'
# ALSA is linked directly (RDP audio), so it's a hard dependency. libasound2t64 comes first: on
# Ubuntu 24.04 and later plain libasound2 is only a virtual name, which apt may satisfy with the
# OSS shim liboss4-salsa-asound2 (missing real ALSA symbols); older Debian falls through to it.
DEB_DEPENDS='"libc6 (>= 2.35)", "libgcc-s1", "zlib1g", "libasound2t64 | libasound2"'
DEB_RECOMMENDS='"libxkbcommon0", "libwayland-client0", "libx11-6", "libxcursor1", "libxrandr2", "libxi6", "libgl1", "libegl1", "xdg-desktop-portal"'
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}
@@ -49,7 +54,7 @@ HERE=$(cd "$(dirname "$0")" && pwd)
say() { echo "repo: $*" >&2; }
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"
done
[[ -d $REPO_DIR ]] || die "$REPO_DIR does not exist"
@@ -85,9 +90,10 @@ NFPM=$NFPM_DIR/nfpm
# ---- fetch and verify the release binaries ---------------------------------
say "fetching $TAG from github.com/$GITHUB"
mkdir -p "$WORK/dl"
patterns=(--pattern SHA256SUMS)
for arch in $ARCHS; do patterns+=(--pattern "NexusTerminal-linux-$arch.zip"); done
gh release download "$TAG" -R "$GITHUB" -D "$WORK/dl" "${patterns[@]}" || die "release $TAG has no Linux assets yet"
base=https://github.com/$GITHUB/releases/download/$TAG
for f in SHA256SUMS $(for arch in $ARCHS; do echo "NexusTerminal-linux-$arch.zip"; done); do
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"
# ---- package -----------------------------------------------------------------
@@ -104,7 +110,7 @@ for arch in $ARCHS; do
arch: $arch
platform: linux
version: $VERSION
release: 1
release: $PKGREL
section: net
maintainer: Andy Dixon <andy@dixon.cx>
description: |
@@ -144,15 +150,16 @@ for arch in $ARCHS; do
recommends: [$RPM_RECOMMENDS]
archlinux:
depends: [$ARCH_DEPENDS]
suggests: [$ARCH_SUGGESTS]
archlinux:
packager: Andy Dixon <andy@dixon.cx>
optdepends: [$ARCH_OPTDEPENDS]
rpm:
signature:
key_file: $WORK/signing.asc
EOF
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