Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.36] — 2026-06-12

### Added
- **Scrollback paging keys** — Shift+PageUp / Shift+PageDown page through the terminal scrollback (main buffer only; alternate-screen apps still receive the keys)
- **Reset Terminal** — right-click action that recovers a session stuck in the alternate screen with mouse reporting left on (full-screen app crashed / SSH dropped mid-TUI): returns to the main buffer, disables mouse mode, re-shows the cursor — the local equivalent of `reset`

### Changed
- **Terminal render performance** — visible lines are painted via cached per-line pictures re-recorded only when a line's content changes (new `BufferLine.version`), turning a steady frame from O(visible cells) paragraph draws into O(visible lines) picture replays (~7× less per-frame paint work); keyword-highlight regexes now run on line change instead of every frame

### Fixed
- **Mouse wheel inside mouse-aware TUIs** (#65) — wheel up/down were reported with button codes 68/69 instead of the standard 64/65, so claude CLI, htop, `vim mouse=a`, lazygit, and tmux (mouse on) ignored every wheel event; legacy-mode reports also placed events one row below the pointer
- **Scrollback drift at the cap** (#66) — once the buffer hit `maxLines`, content a scrolled-up reader was viewing streamed past as lines were trimmed from the top; the viewport now compensates for trimmed lines and stays pinned to the text
- **Decomposed (NFD) Vietnamese text** (#67) — combining marks are now canonically composed into the preceding cell (every Vietnamese letter has a precomposed form), so macOS `ls` filenames and other NFD sources render correctly instead of one displaced cell per diacritic

---

## [0.1.35] — 2026-06-11

### Added
- **Breadcrumb path jump** — the shared `PathBreadcrumb` gains an inline path editor: an edit affordance opens a text field seeded with the current path; Enter navigates there, Escape cancels. Wired into both the remote SFTP panel and the local file panel; remote-typed paths are normalized to absolute POSIX (trailing slash dropped) so derived child paths don't double up
- **macOS universal build** — Intel Macs are now supported: one universal (arm64 + x86_64) artifact (`YourSSH-x.x.x-macOS-universal.dmg/zip`) built on the arm64 runner; `build.sh` lipos both Rust dylib targets and the release workflow asserts both arches via `lipo -archs` so an arm64-only artifact can never ship under the universal name; the in-app updater matches the universal DMG on both archs (Intel falls back to the browser against pre-universal releases)

### Changed
- **Performance pass** — compiled keyword-highlight rules are memoized in `SettingsProvider` (previously every terminal build recompiled each rule's RegExp, duplicated across three widgets); `SessionProvider.setActive` no longer notifies when re-clicking the already-active tab; SSH agent messages are built with a direct buffer write instead of double list spreads
- **Smaller bundles** — removed the unused `local_auth` dependency (pulled native plugins into macOS/Windows bundles); dropped the MesloLGS NF Italic / Bold Italic faces (−4.8 MB per bundle; the engine falls back to a synthetic slant); desktop release builds use `--split-debug-info`, with per-platform symbols zips attached to each release for `flutter symbolize`

### Fixed
- **Non-ASCII terminal input** — typed input was sent to the SSH shell via truncated UTF-16 code units, corrupting any character above U+00FF (e.g. Vietnamese: "ế" arrived as a single garbage byte). Input is now UTF-8 encoded at every user-text write site (keystroke/IME, `terminal.input` plugin hook, startup command, snippet insert), matching the local-shell path
- **Windows build on VS 2026** — unblocked the MSVC STL1011 coroutine error

---

Expand Down Expand Up @@ -614,7 +636,10 @@ Initial release of YourSSH — a cross-platform SSH client for macOS, Windows, a
- **Host management** — CRUD for SSH host profiles with `StorageService`
- **Known hosts** — TOFU dialog for host-key verification; `KnownHostsProvider`

[Unreleased]: https://github.com/YoursshLabs/yourssh/compare/v0.1.32...HEAD
[0.1.36]: https://github.com/YoursshLabs/yourssh/compare/v0.1.35...v0.1.36
[0.1.35]: https://github.com/YoursshLabs/yourssh/compare/v0.1.34...v0.1.35
[0.1.34]: https://github.com/YoursshLabs/yourssh/compare/v0.1.33...v0.1.34
[0.1.33]: https://github.com/YoursshLabs/yourssh/compare/v0.1.32...v0.1.33
[0.1.32]: https://github.com/YoursshLabs/yourssh/compare/v0.1.31...v0.1.32
[0.1.31]: https://github.com/YoursshLabs/yourssh/compare/v0.1.30...v0.1.31
[0.1.30]: https://github.com/YoursshLabs/yourssh/compare/v0.1.29...v0.1.30
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ The active codebase is `app/` — a Flutter app targeting macOS, Windows, and Li
- `packages/yourssh_rdp` — **Rust RDP client** (IronRDP 0.15, flutter_rust_bridge v2); exposes `RdpClient` + `RdpConfig`, a `StreamSink<RdpEvent>` event bus, and `rdp_lib_version()`; `RdpClient.ensureInitialized()` lazily loads the native library + inits the FRB runtime on first RDP connect (no init in main.dart); `RdpConfig.expectedFingerprint` carries the pinned cert fingerprint — the Rust engine verifies it post-TLS / **pre-CredSSP** and aborts with `RdpEvent.certMismatch` before any credentials are sent (TLS itself uses no cert verification — the pin is the only server check); `RdpEvent.connected` carries the **server-negotiated** desktop size (may differ from the request); dirty rects are inclusive-rectangle corrected (+1) and clamped before extraction; the run loop peeks every X224 frame for an MCS Disconnect Provider Ultimatum (server-side session end: remote sign-out / session takeover / admin disconnect) and turns it into a graceful `RdpEvent.disconnected` — ironrdp-session 0.9's x224 processor would otherwise surface it as a raw decode error; build scripts produce `libyourssh_rdp.dylib` / `.so` / `.dll` which the Dart `NativeLoader` resolves at runtime from the app bundle (release) or `assets/native/` (dev); the built libraries are **not tracked in git** (`assets/native/` is gitignored — run `build.sh`/`build.ps1` once after clone; CI builds them fresh)
- `packages/dartssh2` — **local fork** of dartssh2; overrides the pub.dev version via `dependency_overrides` in `app/pubspec.yaml`; adds `signAsync()` for agent-backed auth and SSH agent forwarding (sends `auth-agent-req@openssh.com` on shell channels only — never exec; accepts server-opened `auth-agent@openssh.com` channels via `SSHClient.agentHandler`; a refused request is non-fatal and surfaces as `SSHSession.agentForwardingRefused`); implements strict KEX (`kex-strict-c/s-v00@openssh.com`, CVE-2023-48795 "Terrapin" mitigation: sequence numbers reset after every NEWKEYS, non-KEX messages during the initial key exchange terminate the connection, KEXINIT must be the first packet); adds `OpenSSHEd25519KeyPair.generate()` and a passphrase-encrypting `toPem({passphrase})` (bcrypt-pbkdf + aes256-ctr; null passphrase output unchanged, interop-verified against real `ssh-keygen -y`)
- `packages/flutter_pty` — **local fork** of flutter_pty 0.4.2 (also via `dependency_overrides`); patches `src/flutter_pty_win.c` so the Windows command line doesn't duplicate `argv[0]` (upstream issue #19 — broke keyboard input in the local PowerShell terminal)
- `packages/xterm` — **local fork** of xterm 4.0.0 (also via `dependency_overrides`); patches: (1) `lib/src/ui/custom_text_edit.dart` passes `viewId: View.maybeOf(context)?.viewId` to `TextInputConfiguration` (upstream issue #207 — newer Flutter engines on Windows reject a text-input client without a viewId, so printable keys never reached any `TerminalView` while Enter/Tab/paste still worked); (2) copy/paste reachability (issue #43) — `shortcut/shortcuts.dart` adds Ctrl+C → `TerminalCopyAndClearIntent` and Ctrl+Shift+V paste alias on Windows/Linux, `shortcut/actions.dart` adds `_CopySelectionAndClearAction` (enabled only with an active selection; copies then clears it so the next Ctrl+C reaches the shell as SIGINT — without a selection `ShortcutManager` ignores the key and it falls through as ^C), `ui/gesture/gesture_handler.dart` un-aliases tertiary taps from the secondary-tap callbacks (middle clicks also reported to mouse-mode apps as `TerminalMouseButton.middle`, not right), and `terminal_view.dart` pastes the clipboard on middle-click unless `readOnly`
- `packages/xterm` — **local fork** of xterm 4.0.0 (also via `dependency_overrides`); patches: (1) `lib/src/ui/custom_text_edit.dart` passes `viewId: View.maybeOf(context)?.viewId` to `TextInputConfiguration` (upstream issue #207 — newer Flutter engines on Windows reject a text-input client without a viewId, so printable keys never reached any `TerminalView` while Enter/Tab/paste still worked); (2) copy/paste reachability (issue #43) — `shortcut/shortcuts.dart` adds Ctrl+C → `TerminalCopyAndClearIntent` and Ctrl+Shift+V paste alias on Windows/Linux, `shortcut/actions.dart` adds `_CopySelectionAndClearAction` (enabled only with an active selection; copies then clears it so the next Ctrl+C reaches the shell as SIGINT — without a selection `ShortcutManager` ignores the key and it falls through as ^C), `ui/gesture/gesture_handler.dart` un-aliases tertiary taps from the secondary-tap callbacks (middle clicks also reported to mouse-mode apps as `TerminalMouseButton.middle`, not right), and `terminal_view.dart` pastes the clipboard on middle-click unless `readOnly`; (3) scrollback-trim scroll compensation — `IndexAwareCircularBuffer` exposes a monotonic `droppedLines` counter (bumped on overflow push/insert and `trimStart`) and `RenderTerminal._compensateScrollbackTrim()` shifts the scroll offset up by the trimmed amount each layout, so once the buffer hits `maxLines` the content a scrolled-up user is reading stays put instead of streaming past (offset clamped at 0; re-baselined on main↔alt buffer switches, never corrected across them); (4) per-line picture render cache — `BufferLine.version` (monotonic, bumped by every mutator) keys `TerminalPainter.getLinePicture()`, an LRU (`_linePictureCacheLimit` 1024, disposed on eviction) of per-line recorded `Picture`s replayed by `RenderTerminal._paint`, so scrolling/steady frames replay O(visible lines) pictures instead of issuing O(visible cells) `drawParagraph` calls (~7× less per-frame paint work); keyword highlights are baked into the cached picture (regex runs on line change, not per frame) — `RenderTerminal.keywordRules` forwards to `painter.keywordRules` on structural change, and the cache invalidates on textStyle/textScaler/theme/font-change/keyword-rule updates (pixel-equivalence + invalidation covered by `app/test/widgets/xterm_line_picture_cache_test.dart`, scroll behavior by `app/test/widgets/terminal_scroll_probe_test.dart`); (5) scrollback keyboard paging — Shift+PageUp/PageDown in `TerminalViewState._handleKeyEvent` page the viewport locally (main buffer only; in the alt buffer the keys still reach the application); (6) `Terminal.recoverFromStuckState()` — local `reset`-equivalent for a full-screen app that died uncleanly and left the terminal trapped in the alt screen with mouse reporting on (the cause of "wheel scrolling completely dead at a prompt"): returns to the main buffer, disables mouse mode/report mode, re-shows the cursor, resets main-buffer margins; wired to the right-click "Reset Terminal" item in `app/lib/widgets/terminal_context_menu.dart`; (7) mouse-report encoding fixes — wheel buttons reported as 64-67 (flag 64 + low two bits of X11 buttons 4-7; upstream sent 68-71, which no application recognizes, so wheel scrolling was dead inside every mouse-aware TUI: claude CLI, htop, vim `mouse=a`, tmux with mouse on), and normal/UTF report mode no longer adds an extra +1 to the row byte (every event was reported one row below the pointer); covered by `app/test/widgets/xterm_mouse_report_test.dart`; (8) combining-mark composition — `Buffer.writeChar` merges a zero-width combining mark into the preceding cell when a precomposed NFC form exists (`unorm_dart`; preserves the base cell's style via `setCodePoint`, skips wide-char continuation cells), so decomposed Vietnamese (macOS `ls` filenames, NFD output from many tools) renders correctly instead of one displaced cell per diacritic; marks with no precomposed form keep the legacy own-cell behavior (single-codepoint cell model); covered by `app/test/widgets/xterm_combining_chars_test.dart`
- `packages/yourssh_plugin_api` — abstract plugin interface (`YourSSHPlugin`, `YourSSHPluginContext`)
- `packages/yourssh_devops` — DevOps plugin (containers (Docker/K8s), network tools, Cloudflare tunnel, mail catcher, MCP server, S3 browser)
- `packages/yourssh_web_tools` — Web Tools plugin (in-app browser over port-forwarded HTTP)
Expand Down
13 changes: 12 additions & 1 deletion app/lib/widgets/terminal_context_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:xterm/xterm.dart';

/// Actions offered by the terminal right-click menu (issue #43).
enum TerminalMenuAction { copy, paste, selectAll }
enum TerminalMenuAction { copy, paste, selectAll, resetTerminal }

/// Shows the Copy / Paste / Select All context menu for a terminal at
/// [globalPosition] and performs the chosen action.
Expand Down Expand Up @@ -41,6 +41,12 @@ Future<void> showTerminalContextMenu({
height: 36,
child: const Text('Select All'),
),
const PopupMenuDivider(),
PopupMenuItem(
value: TerminalMenuAction.resetTerminal,
height: 36,
child: const Text('Reset Terminal'),
),
],
);

Expand All @@ -60,6 +66,11 @@ Future<void> showTerminalContextMenu({
}
case TerminalMenuAction.selectAll:
terminalSelectAll(terminal, controller);
case TerminalMenuAction.resetTerminal:
// A full-screen app that died uncleanly can leave the terminal stuck
// in the alternate screen with mouse reporting on — wheel scrolling
// goes dead until recovered (the `reset` command equivalent).
terminal.recoverFromStuckState();
case null:
break;
}
Expand Down
8 changes: 8 additions & 0 deletions app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.3"
unorm_dart:
dependency: transitive
description:
name: unorm_dart
sha256: "0c69186b03ca6addab0774bcc0f4f17b88d4ce78d9d4d8f0619e30a99ead58e7"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
url_launcher:
dependency: "direct main"
description:
Expand Down
2 changes: 1 addition & 1 deletion app/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: yourssh
description: YourSSH - A professional SSH client for macOS, Windows, and Linux with advanced features like SFTP, port forwarding, and a built-in code editor.
publish_to: 'none'
version: 0.1.35+1
version: 0.1.36+1

environment:
sdk: ^3.12.0
Expand Down
Loading
Loading