feat: add latency probing, platform installers (deb/rpm/pkg/dmg/exe)
Release / Build aarch64-unknown-linux-gnu (push) Has been cancelled
Release / Build aarch64-apple-darwin (push) Has been cancelled
Release / Build aarch64-pc-windows-msvc (push) Has been cancelled
Release / Build x86_64-unknown-linux-gnu (push) Has been cancelled
Release / Build x86_64-apple-darwin (push) Has been cancelled
Release / Build x86_64-pc-windows-msvc (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Release / Build aarch64-unknown-linux-gnu (push) Has been cancelled
Release / Build aarch64-apple-darwin (push) Has been cancelled
Release / Build aarch64-pc-windows-msvc (push) Has been cancelled
Release / Build x86_64-unknown-linux-gnu (push) Has been cancelled
Release / Build x86_64-apple-darwin (push) Has been cancelled
Release / Build x86_64-pc-windows-msvc (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
This commit is contained in:
@@ -22,9 +22,11 @@ jobs:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
artifact: sshell-x86_64-linux
|
||||
nfpm_arch: amd64
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
artifact: sshell-aarch64-linux
|
||||
nfpm_arch: arm64
|
||||
# macOS
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
@@ -35,10 +37,10 @@ jobs:
|
||||
# Windows
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
artifact: sshell-x86_64-windows.exe
|
||||
artifact: sshell-x86_64-windows
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
artifact: sshell-aarch64-windows.exe
|
||||
artifact: sshell-aarch64-windows
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -60,19 +62,77 @@ jobs:
|
||||
- name: Build
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Package (Unix)
|
||||
- name: Set version from tag
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
echo "SHELL_VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Package binary archive (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
cp target/${{ matrix.target }}/release/sshell sshell
|
||||
chmod +x sshell
|
||||
tar czf ${{ matrix.artifact }}.tar.gz sshell
|
||||
|
||||
- name: Package (Windows)
|
||||
- name: Package binary archive (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
copy target\${{ matrix.target }}\release\sshell.exe sshell.exe
|
||||
Compress-Archive -Path sshell.exe -DestinationPath ${{ matrix.artifact }}.zip
|
||||
|
||||
# --- Linux installer packages (nfpm) ---
|
||||
- name: Install nfpm
|
||||
if: runner.os == 'Linux'
|
||||
uses: jaxxstorm/action-install-gh-release@v1.14.0
|
||||
with:
|
||||
repo: goreleaser/nfpm/v2
|
||||
cache: enable
|
||||
|
||||
- name: Package .deb
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
NFPM_ARCH: ${{ matrix.nfpm_arch }}
|
||||
NFPM_VERSION: ${{ env.SHELL_VERSION }}
|
||||
run: nfpm pkg --packager deb --target ${{ matrix.artifact }}.deb
|
||||
|
||||
- name: Package .rpm
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
NFPM_ARCH: ${{ matrix.nfpm_arch }}
|
||||
NFPM_VERSION: ${{ env.SHELL_VERSION }}
|
||||
run: nfpm pkg --packager rpm --target ${{ matrix.artifact }}.rpm
|
||||
|
||||
- name: Package .pkg.tar.zst
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
NFPM_ARCH: ${{ matrix.nfpm_arch }}
|
||||
NFPM_VERSION: ${{ env.SHELL_VERSION }}
|
||||
run: nfpm pkg --packager archlinux --target ${{ matrix.artifact }}.pkg.tar.zst
|
||||
|
||||
# --- macOS DMG ---
|
||||
- name: Package .dmg
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
STAGING_DIR=$(mktemp -d)
|
||||
cp sshell "$STAGING_DIR/sshell"
|
||||
chmod +x "$STAGING_DIR/sshell"
|
||||
hdiutil create -volname "sshell" -srcfolder "$STAGING_DIR" -ov -format UDZO ${{ matrix.artifact }}.dmg
|
||||
rm -rf "$STAGING_DIR"
|
||||
|
||||
# --- Windows installer (Inno Setup) ---
|
||||
- name: Compile Inno Setup installer
|
||||
if: runner.os == 'Windows'
|
||||
uses: Minionguyjpro/Inno-Setup-Action@v1.2.8
|
||||
env:
|
||||
SHELL_VERSION: ${{ env.SHELL_VERSION }}
|
||||
with:
|
||||
path: sshell.iss
|
||||
options: >-
|
||||
/DMyArch=${{ matrix.target == 'x86_64-pc-windows-msvc' && 'x64compatible' || 'arm64' }}
|
||||
/DMyArchAllowed=${{ matrix.target == 'x86_64-pc-windows-msvc' && 'x64compatible' || 'arm64' }}
|
||||
/DMyArchInstall64=${{ matrix.target == 'x86_64-pc-windows-msvc' && 'x64compatible' || 'arm64' }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -80,6 +140,11 @@ jobs:
|
||||
path: |
|
||||
${{ matrix.artifact }}.tar.gz
|
||||
${{ matrix.artifact }}.zip
|
||||
${{ matrix.artifact }}.deb
|
||||
${{ matrix.artifact }}.rpm
|
||||
${{ matrix.artifact }}.pkg.tar.zst
|
||||
${{ matrix.artifact }}.dmg
|
||||
sshell-*-windows-setup.exe
|
||||
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
@@ -100,3 +165,8 @@ jobs:
|
||||
files: |
|
||||
artifacts/*.tar.gz
|
||||
artifacts/*.zip
|
||||
artifacts/*.deb
|
||||
artifacts/*.rpm
|
||||
artifacts/*.pkg.tar.zst
|
||||
artifacts/*.dmg
|
||||
artifacts/*-windows-setup.exe
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
name: sshell
|
||||
arch: ${NFPM_ARCH}
|
||||
platform: linux
|
||||
version: ${NFPM_VERSION}
|
||||
section: utils
|
||||
priority: optional
|
||||
maintainer: rain-bus <rainandbus@gmail.com>
|
||||
description: |
|
||||
A personal SSH and shell connection manager with a terminal user interface (TUI).
|
||||
Manage SSH and local shell profiles with tagging, usage tracking, and smart sorting.
|
||||
vendor: rain-bus
|
||||
homepage: https://github.com/Rain-Bus/sshell
|
||||
license: MIT
|
||||
|
||||
contents:
|
||||
- src: sshell
|
||||
dst: /usr/bin/sshell
|
||||
file_info:
|
||||
mode: 0755
|
||||
|
||||
deb:
|
||||
compression: gz
|
||||
|
||||
rpm:
|
||||
compression: gz
|
||||
|
||||
archlinux:
|
||||
pkgbase: sshell
|
||||
packager: rain-bus <rainandbus@gmail.com>
|
||||
@@ -1,4 +1,5 @@
|
||||
mod home_ops;
|
||||
pub mod latency;
|
||||
mod profile_ext;
|
||||
mod types;
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Result of a single latency probe.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum LatencyStatus {
|
||||
/// Host reachable, round-trip in milliseconds.
|
||||
Reachable { ms: u64 },
|
||||
/// TCP connect failed or timed out.
|
||||
Unreachable,
|
||||
/// No latency check applicable (shell connections).
|
||||
Local,
|
||||
/// Not yet checked.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Shared latency cache keyed by "host:port" strings.
|
||||
/// Background threads write, draw reads.
|
||||
pub type LatencyCache = Arc<Mutex<HashMap<String, LatencyStatus>>>;
|
||||
|
||||
/// Perform a TCP connect to measure latency.
|
||||
/// Timeout is 3 seconds. Called from spawned threads.
|
||||
pub fn probe(host: &str, port: u16) -> LatencyStatus {
|
||||
let target = format!("{host}:{port}");
|
||||
let start = Instant::now();
|
||||
|
||||
let addr = match std::net::ToSocketAddrs::to_socket_addrs(&target) {
|
||||
Ok(mut addrs) => match addrs.next() {
|
||||
Some(a) => a,
|
||||
None => return LatencyStatus::Unreachable,
|
||||
},
|
||||
Err(_) => return LatencyStatus::Unreachable,
|
||||
};
|
||||
|
||||
match TcpStream::connect_timeout(&addr, Duration::from_secs(3)) {
|
||||
Ok(_) => {
|
||||
let ms = start.elapsed().as_millis() as u64;
|
||||
LatencyStatus::Reachable { ms }
|
||||
}
|
||||
Err(_) => LatencyStatus::Unreachable,
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use crate::import::ImportCandidate;
|
||||
|
||||
use super::cred::CredFormState;
|
||||
use super::form::FormState;
|
||||
use super::latency::LatencyCache;
|
||||
use super::settings::SettingsState;
|
||||
|
||||
// ── Text editing trait ──────────────────────────────────────
|
||||
@@ -183,6 +184,7 @@ pub struct Session {
|
||||
pub toast: Option<Toast>,
|
||||
pub should_quit: bool,
|
||||
pub settings: SettingsState,
|
||||
pub latency: LatencyCache,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -219,6 +221,7 @@ impl Session {
|
||||
toast: None,
|
||||
should_quit: false,
|
||||
settings: SettingsState::default(),
|
||||
latency: LatencyCache::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::app::{App, Mode};
|
||||
use crate::config::ConnectionType;
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
cursor::{Hide, Show},
|
||||
@@ -7,6 +8,7 @@ use crossterm::{
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
use ratatui::{Terminal, backend::CrosstermBackend};
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
@@ -24,6 +26,8 @@ pub fn run() -> Result<()> {
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
let mut app = App::load()?;
|
||||
|
||||
spawn_latency_probes(&app);
|
||||
|
||||
loop {
|
||||
terminal.draw(|frame| super::draw(frame, &mut app))?;
|
||||
if app.session.should_quit {
|
||||
@@ -34,6 +38,7 @@ pub fn run() -> Result<()> {
|
||||
&& key.kind == KeyEventKind::Press
|
||||
{
|
||||
handle_key(&mut app, key)?;
|
||||
spawn_latency_probes(&app);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,3 +88,27 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Result<()> {
|
||||
Mode::Settings => SettingsView.handle_key(app, key),
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_latency_probes(app: &App) {
|
||||
let cache = app.session.latency.lock().unwrap();
|
||||
let existing: HashSet<String> = cache.keys().cloned().collect();
|
||||
drop(cache);
|
||||
|
||||
for (_, profile) in app.entries() {
|
||||
if let ConnectionType::Ssh { host, port, .. } = &profile.kind {
|
||||
let key = format!("{host}:{port}");
|
||||
if existing.contains(&key) {
|
||||
continue;
|
||||
}
|
||||
let cache_clone = app.session.latency.clone();
|
||||
let host = host.clone();
|
||||
let port = *port;
|
||||
std::thread::spawn(move || {
|
||||
let status = crate::app::latency::probe(&host, port);
|
||||
if let Ok(mut cache) = cache_clone.lock() {
|
||||
cache.insert(key, status);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-14
@@ -1,8 +1,9 @@
|
||||
use crate::app::{App, Mode};
|
||||
use crate::app::display_name;
|
||||
use crate::app::latency::LatencyStatus;
|
||||
use crate::config::{ConnectionSource, ConnectionType, CredentialEntry};
|
||||
use crate::ui::component::{badge_span, draw_input, panel, tag_badge};
|
||||
use crate::ui::{ACCENT, BLUE, GREEN, MUTED, PANEL_ALT, PURPLE, RED, SELECTED_BG, TEXT};
|
||||
use crate::ui::{ACCENT, BLUE, GREEN, MUTED, PANEL_ALT, PURPLE, RED, SELECTED_BG, TEXT, YELLOW};
|
||||
|
||||
use super::View;
|
||||
|
||||
@@ -193,7 +194,7 @@ pub fn draw_connection_list(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||
],
|
||||
)
|
||||
.header(
|
||||
Row::new([" Name", "Type", "Target", "Auth"])
|
||||
Row::new([" Name", "Type", "Target", "Ping"])
|
||||
.style(Style::default().fg(BLUE).add_modifier(Modifier::BOLD)),
|
||||
)
|
||||
.block(panel(title))
|
||||
@@ -264,15 +265,27 @@ fn connection_row(
|
||||
}
|
||||
};
|
||||
|
||||
let auth_state = profile
|
||||
.auth_ref()
|
||||
.and_then(|auth| app.config.credential(auth))
|
||||
.map(|cred| if cred.has_value() { "ready" } else { "empty" })
|
||||
.unwrap_or("none");
|
||||
let auth_color = match auth_state {
|
||||
"ready" => GREEN,
|
||||
"empty" => RED,
|
||||
_ => MUTED,
|
||||
let (ping_text, ping_color) = match &profile.kind {
|
||||
ConnectionType::Ssh { host, port, .. } => {
|
||||
let key = format!("{host}:{port}");
|
||||
let cache = app.session.latency.lock().unwrap();
|
||||
match cache.get(&key) {
|
||||
Some(LatencyStatus::Reachable { ms }) => {
|
||||
let text = format!("{ms} ms");
|
||||
let color = if *ms < 100 {
|
||||
GREEN
|
||||
} else if *ms < 300 {
|
||||
YELLOW
|
||||
} else {
|
||||
RED
|
||||
};
|
||||
(text, color)
|
||||
}
|
||||
Some(LatencyStatus::Unreachable) => ("timeout".to_string(), RED),
|
||||
_ => ("...".to_string(), MUTED),
|
||||
}
|
||||
}
|
||||
ConnectionType::Shell { .. } => ("-".to_string(), MUTED),
|
||||
};
|
||||
|
||||
let badge_style = if selected {
|
||||
@@ -287,10 +300,10 @@ fn connection_row(
|
||||
Span::styled(format!(" {} ", type_badge), badge_style),
|
||||
])).style(row_style),
|
||||
Cell::from(target).style(row_style),
|
||||
Cell::from(auth_state).style(if selected {
|
||||
Style::default().fg(auth_color).bg(SELECTED_BG)
|
||||
Cell::from(ping_text).style(if selected {
|
||||
Style::default().fg(ping_color).bg(SELECTED_BG)
|
||||
} else {
|
||||
Style::default().fg(auth_color)
|
||||
Style::default().fg(ping_color)
|
||||
}),
|
||||
])
|
||||
.height(1)
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#define MyAppName "sshell"
|
||||
#define MyAppVersion GetEnv("SHELL_VERSION")
|
||||
#ifndef MyAppVersion
|
||||
#define MyAppVersion "0.1.0"
|
||||
#endif
|
||||
#define MyAppPublisher "rain-bus"
|
||||
#define MyAppURL "https://github.com/Rain-Bus/sshell"
|
||||
#define MyAppExeName "sshell.exe"
|
||||
|
||||
[Setup]
|
||||
AppId={{sshell-2024-1}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputBaseFilename=sshell-{#MyAppVersion}-{#MyArch}-windows-setup
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
ArchitecturesAllowed={#MyArchAllowed}
|
||||
ArchitecturesInstallIn64BitMode={#MyArchInstall64}
|
||||
OutputDir=.
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "addtopath"; Description: "Add sshell to PATH"; GroupDescription: "Environment:"; Flags: checked
|
||||
|
||||
[Files]
|
||||
Source: "sshell.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Registry]
|
||||
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
|
||||
ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \
|
||||
Flags: preservestringtype uninsdeletevalue; \
|
||||
Tasks: addtopath
|
||||
|
||||
[UninstallDelete]
|
||||
Type: files; Name: "{app}\sshell.exe"
|
||||
|
||||
[Code]
|
||||
const
|
||||
WM_SETTINGCHANGE = $001A;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
if CurStep = ssPostInstall then
|
||||
begin
|
||||
// Notify the system that environment variables have changed
|
||||
RegWriteStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
|
||||
'Path', RegQueryStringValue(HKLM, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path'));
|
||||
Exec('cmd.exe', '/C echo %Path%', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
end;
|
||||
Reference in New Issue
Block a user