diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cecd7694..1c2caf9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,73 @@ # Changelog +## 1.4.1 - 2026-08-25 + +Stable production release of the **Pane Search** generation and **Click to +position cursor**. This release contains the complete application delta from +stable v1.3.0, promotes the field-tested 1.4 search line without a prerelease +suffix, and publishes as GitHub Latest with application version `1.4.1`, +package/file version `1.4.1.0`, PowerShell module version `1.4.1`, and tag +`v1.4.1`. + +### Added + +- **Pane Search:** `Ctrl+F` opens Find in the focused pane only + (`Ctrl+Shift+F` remains a remappable compatibility alias). Search covers the + full active-pane scrollback, updates while typing, highlights all matches, + navigates with `Enter` / `Shift+Enter`, supports case-sensitive and regular + expression modes, shows a compact `current / total` counter, and renders a + pane-local match overview on the scrollbar. +- **Click to position cursor:** a plain single left click inside the current + shell-integrated editable command sends the established LEFT/RIGHT input + events needed to move the shell cursor to that position. It is enabled by + default in winTerm, remains configurable under Profile > Advanced, and an + explicit existing value of `false` continues to win. + +### Improved + +- Pane Search coalesces rapid typing, converges while output streams + continuously, and avoids repainting an unchanged scrollbar overview. Search + state remains isolated per pane and safe across reflow, scrollback eviction, + and main/alternate-buffer transitions. +- Cursor positioning now occurs on mouse release only after the existing drag + threshold has not been crossed. Starting a drag therefore establishes text + selection without first moving the shell cursor. +- Cursor movement counts rendered glyphs rather than the trailing halves of + double-cell glyphs, so CJK and other full-width input does not generate an + extra arrow event for the second cell. + +### Fixed + +- Backported the safety intent of Microsoft Terminal PR #20442 + (`de3fc87d186e5da1d5ccd8731412905f5e2aba30`): negative, padding, overflow, + and out-of-buffer mouse coordinates are validated before any TextBuffer + access, preventing malformed boundary clicks from reaching invalid buffer + iterators. +- Click positioning now requires an unfinished final shell-integration mark + and a trustworthy editable range. Previous commands, completed command + output, unrelated scrollback rows, invalid resize-era coordinates, and + unsupported positions safely do nothing. +- Pane Search resize no longer converts stale pre-reflow spans into a stray + selection; main/alternate-buffer switches discard stale highlights; and + closing search releases its terminal-side highlight span copy. + +### Compatibility + +- Ctrl+Click hyperlinks, VT mouse reporting, double-click word selection, + triple-click line selection, Shift+Click, drag selection, and split-pane + connection isolation keep precedence over click positioning. Mouse-aware + terminal applications retain ownership while VT mouse reporting is active. +- Cursor positioning uses shell/readline input events; it does not directly + move the renderer cursor, rewrite or execute the command, or guess an + editable range when shell integration is unavailable. + +### Release + +- The stable Release provides the Setup EXE, Portable ZIP, checksums, release + notes, third-party notices, SPDX and CycloneDX SBOMs, and release metadata + through the guarded tag workflow. The installer is not Authenticode-signed, + so Windows can show Unknown Publisher or a SmartScreen warning. + ## 1.4.0-beta - 2026-08-13 Beta prerelease of the Pane Search release. Manual validation of diff --git a/README.ja.md b/README.ja.md index 6891ac799..2148e74eb 100644 --- a/README.ja.md +++ b/README.ja.md @@ -13,12 +13,8 @@ [**最新の安定版リリースページを開いて、x64版のwinTermをダウンロードする**](https://github.com/HelloThisWorld/winTerm/releases/latest) 最新の安定版リリースページには、インストーラー(Setup EXE)、ポータブルZIP、 -リリースノート、チェックサムがまとめて掲載されています。 - -Pane Search(ペイン検索)を先行して試せる**ベータ版**のプレリリースもあります。 -[v1.4.0-beta](https://github.com/HelloThisWorld/winTerm/releases/tag/v1.4.0-beta) -はGitHubのプレリリースとして公開されており、資産の構成は安定版と同じです。 -プレリリースは動作確認を目的とした配布のため、通常の利用には上記の安定版を推奨します。 +リリースノート、チェックサムがまとめて掲載されています。現在の安定版は +**winTerm 1.4.1** です。 インストーラーは署名されていないため、Windowsに「不明な発行元」または SmartScreenの警告が表示される場合があります。必ず上記の公式リリースから @@ -38,8 +34,8 @@ Microsoft、Windows、Windows Terminalのロゴも使用していません。 - `winTerm--setup-x64.exe` — 現在のユーザー、または全ユーザーへのインストール用; - `winTerm--portable-x64.zip` — 展開してそのまま実行する用。 -現在のソースバージョンは `1.4.0-beta` で、 -最新の安定版リリースは `1.3.0` です。公開されている資産の一覧とチェックサムの全体は、 +現在のソースバージョンは `1.4.1` で、 +最新の安定版リリースも `1.4.1` です。公開されている資産の一覧とチェックサムの全体は、 [最新の公式リリース](https://github.com/HelloThisWorld/winTerm/releases/latest) を参照してください。 @@ -49,11 +45,20 @@ Windowsに「不明な発行元」またはSmartScreenの警告が表示され リリースEXEのインストールに、証明書のインポート、開発者モード、Visual Studio、 Windows SDK、`Add-AppxPackage` は一切必要ありません。 -[インストール手順(英語)](docs/user/installation.md)と -[1.2.0のリリースノート(英語)](docs/releases/1.2.0.md)もあわせて参照してください。 +[インストール手順(英語)](docs/user/installation.md)、 +[1.4.1のリリースノート(英語)](docs/releases/1.4.1.md)、 +[1.3.0のリリースノート(英語)](docs/releases/1.3.0.md)もあわせて参照してください。 ## 主な機能 +- ペインごとの**Pane Search(ペイン検索)**(`Ctrl+F`): スクロールバック全体を + 検索し、入力中のライブ更新、全一致箇所の強調、`Enter`/`Shift+Enter` での移動、 + 大文字と小文字の区別、正規表現、件数表示、スクロールバー上の概要表示に対応します。 + 出力が流れ続けている間も検索結果が更新されます; +- **クリックした位置へカーソルを移動**: シェル統合で安全に確認できる現在の編集中 + コマンド内をクリックすると、シェルのテキストカーソルがその位置へ移動します。 + 既定で有効で、Profile > Advancedから無効にできます。ドラッグ、Shift、単語/行選択、 + ハイパーリンク、VTマウス対応アプリの操作が優先され、明示的な `false` 設定も維持されます; - ペインごとの**Command Timeline**(`Ctrl+Tab`、またはターミナル左端の細い ハンドル): そのペインで実行したコマンドをメモリ上に保持する一覧です。 OSC 133のシェル統合の情報だけから構築され、実行せずに入力欄へ読み込む操作、 @@ -111,8 +116,8 @@ PowerShell 7と、[ビルド手順(英語)](docs/build.md)に記載された .\scripts\winterm\build.ps1 -Configuration Release -Platform x64 -IncludeTests .\scripts\winterm\test.ps1 -Suite Relevant -Configuration Release -Platform x64 .\scripts\winterm\build-unpackaged.ps1 -Configuration Release -Platform x64 -.\scripts\winterm\build-installer.ps1 -Version 1.4.0-beta -Platform x64 -.\scripts\winterm\build-portable.ps1 -Version 1.4.0-beta -Platform x64 +.\scripts\winterm\build-installer.ps1 -Version 1.4.1 -Platform x64 +.\scripts\winterm\build-portable.ps1 -Version 1.4.1 -Platform x64 ``` アンパッケージ形式の生成処理では、統合されたリソースインデックスを作るための diff --git a/README.md b/README.md index f955592b9..44a92924f 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,7 @@ English | [日本語](README.ja.md) [**Open the latest stable release and download winTerm for x64**](https://github.com/HelloThisWorld/winTerm/releases/latest) The latest stable Release page provides the Setup EXE, Portable ZIP, release -notes, and checksums together. - -A **beta** prerelease is also available for early access to Pane Search: -[v1.4.0-beta](https://github.com/HelloThisWorld/winTerm/releases/tag/v1.4.0-beta) -is published as a GitHub prerelease with the same asset layout. Prereleases -are for testing; the stable release above stays the recommended download. +notes, and checksums together. The current stable release is **winTerm 1.4.1**. The installer is unsigned, so Windows may display Unknown Publisher or a SmartScreen warning. Download only from the official release above and verify @@ -36,8 +31,8 @@ application downloads are: - `winTerm--setup-x64.exe` — current-user or all-users installation; - `winTerm--portable-x64.zip` — extract and run without installation. -The current source version is `1.4.0-beta`; -the latest stable release is `1.3.0`. See the +The current source version is `1.4.1`; +the latest stable release is `1.4.1`. See the [latest official Release](https://github.com/HelloThisWorld/winTerm/releases/latest) for the complete published asset list and checksums. @@ -46,11 +41,20 @@ Unknown Publisher or SmartScreen warning; verify `SHA256SUMS.txt` from the same Release. No MSIX certificate, Developer Mode, Visual Studio, Windows SDK, or `Add-AppxPackage` is required to install a release EXE. -See [installation guidance](docs/user/installation.md) and the -[1.3.0 release notes](https://github.com/HelloThisWorld/winTerm/releases/tag/v1.3.0), and the earlier [1.2.0 notes](docs/releases/1.2.0.md). +See [installation guidance](docs/user/installation.md), the +[1.4.1 release notes](docs/releases/1.4.1.md), and the earlier +[1.3.0 notes](docs/releases/1.3.0.md). ## Core features +- per-pane **Pane Search** (`Ctrl+F`) over the full scrollback, with live + all-match highlighting, `Enter` / `Shift+Enter` navigation, case-sensitive + and regex modes, a compact match counter, and a scrollbar overview that + stays current while output streams; +- **Click to position cursor** inside the current shell-integrated editable + command, enabled by default and available under Profile > Advanced; normal + drag, Shift, word/line selection, hyperlinks, and VT mouse applications + retain precedence, and an explicit setting of `false` remains respected; - a per-pane **Command Timeline** (`Ctrl+Tab`, or the thin handle on the terminal's left edge): an in-memory list of the commands that pane has run, built from OSC 133 shell integration only, with load-without-executing onto @@ -105,8 +109,8 @@ Use PowerShell 7 and the Microsoft Terminal toolchain described in .\scripts\winterm\build.ps1 -Configuration Release -Platform x64 -IncludeTests .\scripts\winterm\test.ps1 -Suite Relevant -Configuration Release -Platform x64 .\scripts\winterm\build-unpackaged.ps1 -Configuration Release -Platform x64 -.\scripts\winterm\build-installer.ps1 -Version 1.4.0-beta -Platform x64 -.\scripts\winterm\build-portable.ps1 -Version 1.4.0-beta -Platform x64 +.\scripts\winterm\build-installer.ps1 -Version 1.4.1 -Platform x64 +.\scripts\winterm\build-portable.ps1 -Version 1.4.1 -Platform x64 ``` The unpackaged generator uses an unsigned MSIX only as an upstream build diff --git a/docs/current-progress.md b/docs/current-progress.md index f4f431acc..ca495d451 100644 --- a/docs/current-progress.md +++ b/docs/current-progress.md @@ -1,209 +1,135 @@ # Current development progress -Last updated: 2026-08-13 +Last updated: 2026-08-25 ## Repository state -- Branch: `release/v1.4.0-beta`, based on `main` at - `45cf23975` (1.4.0-alpha release metadata, pull request #47) -- Microsoft Terminal upstream revision: - `1cea42d433253d95c4487a3037db48197b5e72f4` -- Application version: `1.4.0-beta` -- Package/file version: `1.4.0.1` -- PowerShell module version: `1.4.0` with prerelease suffix `beta` -- Release channel: `beta` (published as a GitHub prerelease) -- Release tag: `v1.4.0-beta`, running the full guarded release pipeline; - the checkpoint-tag allowlist is unchanged. -- Current public Latest: `v1.3.0`, the stable Command Timeline release - (moved from v1.2.0, the Visual Progress stable, on 2026-08-13) -- Newest published prerelease: `v1.4.0-beta` (replacing `v1.4.0-alpha`) +- Working branch: `codex/release-v1.4.1` +- Branch base: `63c52f538abc29d434077c60acfc5e4aafe95d1b` (`origin/main` at + release preparation start) +- Application version: `1.4.1` +- Package/file version: `1.4.1.0` +- PowerShell module version: `1.4.1` with an empty prerelease suffix +- Release channel: `stable` +- Intended tag: `v1.4.1` - Supported target: Windows 11 x64 -`v1.4.0-beta` is the beta of the Pane Search release. Manual validation of -`v1.4.0-alpha` passed — live search under sustained Kafka/Spring Boot log -output, navigation, the counter, and the scrollbar overview all behaved — -so the alpha content is promoted unchanged with beta metadata. -`/releases/latest` resolves to v1.3.0, and WinGet is not updated for -prereleases. - -## Pane Search status (v1.4 roadmap) - -- Phase 1 — active-pane search — complete at `1.3.1`. -- Phase 2 — search UX and scrollbar overview — complete at `1.3.2`. -- Phase 3 — performance and edge-case hardening — complete at `1.3.3`. -- Final integration — `1.4.0-alpha` published 2026-08-13; manual user - validation **passed**. -- Beta — `1.4.0-beta` published for wider testing. Beta feedback decides - the stable `1.4.0` promotion; only the user authorizes it. - -Phase 1 made `Ctrl+F` (with the retained `Ctrl+Shift+F` alias) open the -existing Microsoft Terminal search box inside the focused pane only, -reusing the mature upstream pipeline end to end — `SearchBoxControl`, -`ControlCore::Search`, `Search`/`TextBuffer::SearchText`, and renderer -search highlights — with no second search engine, no index, and no buffer -duplication. - -Phase 2 turned that functional search into the winTerm search experience. -The search box now follows the winTerm compact overlay language (search -glyph, input, `current / total` counter, case/regex toggles, previous/next, -close, at chrome density with theme-aware system brushes) and degrades -gracefully in narrow panes through width-driven layout states that always -keep the input and close button usable. The scrollbar shows a search -overview while search is open: one right-aligned marker per matching buffer -row across the full scrollback, deduplicating same-row occurrences, with -the current match's row drawn at double width. Search overview markers are -independent of the `ShowMarks` setting (which keeps gating generic shell -marks, default off), coexist with generic marks when both are enabled, -disappear on close, respect a deliberately hidden scrollbar, and refresh -only through the existing throttled scrollbar update path — no timers, no -polling. Split panes keep fully isolated search state, including their -overview markers. - -Phase 3 hardened that experience for real terminal workloads without adding -any new search engine, index, or persistent state. Live typing is coalesced -per pane (50 ms, leading immediate + trailing latest, reading the search -box's state at fire time so the latest query always wins and navigation can -never act on a stale query). An open search now converges during sustained -output: the debounced OutputIdle refresh is complemented by a non-debounced -500 ms cap armed only while a search is active, so `tail -f`-style streams -no longer freeze the counter, highlights, or overview — and search closed -still means zero recurring search work. The scrollbar mark bitmap repaints -only when its inputs change, so plain scrolling with large result sets no -longer re-enumerates occurrences. Edge cases were fixed deterministically: -pane resize no longer converts pre-reflow spans into a stray selection, -main/alt buffer switches drop the other buffer's highlight spans immediately -(search keeps following the active buffer), and closing search releases the -terminal-side span copy. Regression tests cover mutation invalidation, -match-anchor stability during appended output, scrollback eviction, reflow, -alternate-screen transitions, generation/arming semantics, repaint-signature -contracts, wide-character spans (CJK, Korean, accented Latin, emoji), and a -log-only scan benchmark (`WINTERM_SEARCH_BENCH_LINES` scales it locally). - -## Command Timeline status - -The Command Timeline is feature-complete for its in-memory surface. Engineering -checkpoints v1.2.1 through v1.2.4 are all merged to `main`: - -| Checkpoint | Scope | -| --- | --- | -| `v1.2.1` | Pane-owned OSC 133 index, stable command IDs, bounded command-text cache | -| `v1.2.2` | Overlay, deterministic navigation, wheel accumulation, accessibility | -| `v1.2.3` | Load without executing, copy command/output, jump to output, context menu | -| `v1.2.4` | Pane-local search, public settings, shell degradation, bounded history | - -There is no persistent history, no output cache, no output search, and no -telemetry. - -## Release channel handling - -`verify-version.ps1` is now channel-aware rather than stable-only. It accepts -`stable`, `alpha`, and `beta`, and enforces that the channel, the module -prerelease suffix, and the application-version suffix agree with each other, so -a prerelease can never publish as Latest and a stable release can never carry a -prerelease suffix. The package version stays four-part numeric for MSIX and the -Win32 resource fields, and the PowerShell module version stays numeric with the -suffix carried in `PrivateData.PSData.Prerelease`. - -## Beta2 findings - -Field testing the published beta2 build with long-running processes surfaced -one defect, fixed through pull request #41 and carried by this release -branch: - -1. The Shell Integration fallback animated forever for any command that - intentionally stays running (Alternate Screen TUIs, development servers, - `tail -f`). The fallback is now a bounded one-shot launch indication: the - comet plays one 1,800 ms traversal driven by a one-shot compositor batch - (no timer, no polling loop), then the overlay hides with a silent - Hidden/Running snapshot. The expiration is command-generation scoped, so - stale completions cannot affect a newer command, alternate-screen churn - cannot replay a consumed launch, and an expired fallback cannot resurrect - after a provider or explicit OSC 9;4 owner clears. Explicit progress, - recognized providers, short commands, and the result presentations are - unchanged. - -## Beta1 findings - -Producing the sanitized website screenshots against the published beta1 -portable build surfaced one defect, fixed on `main` through pull request #39: - -1. Every Command Timeline entry reported `✓ Succeeded`, including commands - that failed. The prompt wrapper executed `Get-Module` before the prompt - function read `$?`, so the finished mark always carried exit code 0. The - wrapper now captures `$?` first and passes it through, and the shell - integration suite drives the installed wrapper end to end (first prompt, - success, cmdlet failure, native exit code, recovery). - -## Alpha3 field reports - -Local testing of `v1.3.0-alpha3` surfaced three issues, all fixed on `main` -through pull request #36: - -1. An antivirus alert on every new PowerShell tab: the `touch` command's raw - `File::Open` write plus same-file command dispatch read as a - write-then-execute pattern; creation now uses `New-Item` and the module - loads clean. -2. Every completed command displayed `? Unknown`: the Enter keypress - heuristic never notified the lifecycle, so the capability chain never read - Full. Enter now reports the executed transition for shell-established - marks with non-empty input, restoring Running and the ✓/✕ results. -3. Long commands were unreadable: rows now carry a full-command tooltip, and - the Unknown status explains itself on hover. - -## Alpha2 field reports - -Local testing of `v1.3.0-alpha2` surfaced five issues, all fixed on `main` -through pull request #34: - -1. Stray `\` characters before every prompt: the module's OSC terminator was - a two-character PowerShell string; it is now a single backslash. -2. The Timeline captured the whole prompt line and Load inserted the prompt - path: the marks were console side effects; they are now embedded in the - returned prompt string in FinalTerm order. -3. The Visual Progress bar animated at an idle integrated prompt: `133;B` - (composing input) no longer shows a bar; only `133;C` does. -4. A phantom `Command text unavailable / Running` row for the active prompt: - a `133;B`-only mark no longer creates a Timeline entry. -5. Rows jumped on arrow keys and hover: selection-only updates now reuse the - existing rows. - -An antivirus-blocked module component (seen with `Compatibility.ps1`) is now -skipped silently and reported through diagnostics. - -## Alpha1 field reports - -Local testing of `v1.3.0-alpha1` surfaced four issues, all fixed on `main` -through pull request #32: - -1. The Timeline handle covered terminal content. It is now a thin auto-hiding - strip on the terminal's left edge that widens on hover, focus, or while the - overlay is open. -2. Clicking the terminal area did not close an open Timeline. The overlay now - light-dismisses on a terminal press, which still reaches the terminal. -3. A `dir` listing left the Visual Progress bar animating indefinitely. The - recognition engine no longer claims ownership from a bare product-name - mention, no longer rematches arbitrary records under an established claim, - structurally clears a still-running bar after two consecutive ordinary - records, and no longer reads slashed dates as meters. -4. Typed commands never appeared in the Timeline because nothing imported the - packaged `winTerm.Shell` module. Bare PowerShell profile commandlines are - now rewritten at connection creation to import it, gated by the new - per-profile setting `"shellIntegration.autoInject"` (default `true`). +The source remains based on the repository's pinned Microsoft Terminal +baseline `release-1.25@1cea42d433253d95c4487a3037db48197b5e72f4`. +Microsoft Terminal `upstream/main` was separately fetched through +`86d15aef08e500be34497ff4e3a6f0d099ffb067` (2026-08-24) for a targeted +cursor-positioning audit; no broad upstream merge was performed. + +## 1.4.1 release scope + +winTerm 1.4.1 promotes the complete Pane Search development line and adds +Click to position cursor as a stable, default-enabled winTerm feature. It is +the combined application delta since stable v1.3.0; the earlier 1.4.0 alpha +and beta entries remain historical records only. + +### Pane Search + +- `Ctrl+F` opens the focused pane's search overlay; `Ctrl+Shift+F` remains a + compatibility alias. +- Full-scrollback live matching, all-match highlighting, forward/backward + navigation, case-sensitive and regular-expression modes, and the compact + `current / total` counter are complete. +- The pane-local scrollbar overview is complete and remains independent of + the generic `ShowMarks` setting. +- Typing coalescing, sustained-output convergence, repaint signatures, + reflow, scrollback eviction, alternate-buffer changes, wide characters, + and invalid regex handling have regression coverage. + +### Click to position cursor + +- A plain single click is recorded on mouse-down and acted on only at + mouse-up. Crossing the existing drag threshold cancels positioning and + preserves normal selection without first moving the shell cursor. +- Ctrl+Click hyperlinks, VT mouse applications, double/triple click, + Shift+Click, drag selection, and copy-on-select retain their established + precedence. +- Positioning requires the unfinished final OSC 133 shell mark and accepts + only the current editable command. Historical commands, output, scrollback, + and untrusted locations safely do nothing. +- Coordinate translation validates viewport, buffer, inclusive TextBuffer + bounds, padding, overflow, resize-era points, and malformed input before any + buffer iterator is obtained. +- Full-width glyph trailing cells are excluded from LEFT/RIGHT event counts, + and each split pane sends input only through its own connection. +- The winTerm default is enabled; an explicit profile value of `false` is + preserved. The compatible internal JSON key remains + `experimental.repositionCursorWithMouse`, while localized UI describes the + stable feature as **Click to position cursor**. + +### Visual Progress and Command Timeline + +- Visual Progress remains stable in 1.4.1, including its determinate and + indeterminate renderer, per-pane state, accessibility path, and local-only + recognition controls. +- Command Timeline remains stable and pane-local, with OSC 133-backed command + boundaries, filtering, load-without-executing, copy, jump, and status. +- The 1.4.1 cursor work does not change either feature's settings, protocol, + persistence, or privacy boundaries. + +## Upstream cursor audit + +- Inspected Microsoft Terminal PR #20442 and merged commit + `de3fc87d186e5da1d5ccd8731412905f5e2aba30`, which clamps click coordinates + before TextBuffer access. +- Searched subsequent upstream history for changes involving + `RepositionCursorWithMouse`, `_repositionCursorWithMouse`, and related click + behavior. No later directly applicable cursor-safety fix was found. +- Also reviewed earlier related cursor-selection commits + `4995af3dc1cc600b57fcdd953734a13b9b1a425f` and + `d14ff939dc418fa04401304fdef539424dbb5bd5`; their relevant behavior was + already present in winTerm. +- The backport follows current inclusive coordinate contracts rather than + copying the upstream clamp literally, and adds stricter editable-mark, + overflow, vertical-padding, glyph, and release-time interaction guards. + +## Release channel and artifacts + +The guarded tag workflow must confirm an exact tag/version match, absence of +an existing Release, a clean checkout, version/branding/security/privacy +gates, an x64 Release build with compiled tests, artifact generation, and a +public asset re-download before publication is considered complete. + +The expected public Release assets are: + +- `winTerm-1.4.1-setup-x64.exe` +- `winTerm-1.4.1-portable-x64.zip` +- `SHA256SUMS.txt` +- `THIRD_PARTY_NOTICES.md` +- `SBOM.spdx.json` +- `SBOM.cyclonedx.json` +- `release-metadata.json` +- `winTerm-1.4.1-release-notes.md` + +The installer is currently not Authenticode-signed, so Unknown Publisher or +SmartScreen warnings remain possible. The Release notes disclose this and +direct users to verify `SHA256SUMS.txt`. -## Next steps +## Validation state -1. Collect Pane Search feedback on the published `v1.4.0-beta` - prerelease; promote a stable `1.4.0` only after beta testing. Only the - user authorizes the promotion, which is the point at which Latest, - WinGet, and the website stable slot move to 1.4.0. -2. Keep the winTerm website's stable slot (v1.3.0) and prerelease slot - (v1.4.0-beta) in sync with GitHub Releases. +Local stable-candidate validation completed on 2026-08-25: -## Validation state +- Debug x64 package and all three unit-test projects built successfully. +- Smoke and Relevant suites passed. +- Compiled Settings Model, Terminal App, and Control suites passed with + 381/381, 51/51, and 93/93 tests respectively. +- Version, branding, privacy, release-workflow, and PowerShell syntax gates + passed as part of those suites. + +The formal Release workflow and public asset verification remain authoritative +for the published installer and Portable ZIP. + +## Next steps -Publication is gated by the tag-triggered release workflow: exact tag/version -match, release absence, clean checkout, version and branding verification, -static/security/privacy/workflow gates, an x64 Release build with compiled -tests, artifact generation, Draft asset re-download testing, and only then -publication. Record results from those actual runs; do not treat this document -as evidence for a command that did not run. +1. Merge through the application pull request, synchronize the Wiki ledger, + tag the exact merged `main` commit, and monitor the formal Release workflow + through public asset validation. +2. Update and deploy winterm.dev from the real `published_at` timestamp and + public v1.4.1 asset URLs, then verify English/Japanese production pages. +3. Only after both the stable Release and website are verified, remove the + published v1.4.0-beta GitHub prerelease entry while retaining its historical + git tag. diff --git a/docs/releases/1.4.1.md b/docs/releases/1.4.1.md new file mode 100644 index 000000000..fb21698f0 --- /dev/null +++ b/docs/releases/1.4.1.md @@ -0,0 +1,107 @@ +# winTerm 1.4.1 + +**winTerm 1.4.1 is a stable production release.** GitHub Latest points to +`v1.4.1`; there is no alpha, beta, preview, or prerelease suffix in the +application, package, PowerShell module, or Release metadata. + +This release contains everything changed since stable v1.3.0: the complete +Pane Search feature line, Click to position cursor, cursor-coordinate and +interaction hardening, and the associated tests and documentation. + +## New: Pane Search + +Press `Ctrl+F` to search the **focused pane only**. `Ctrl+Shift+F` remains a +remappable compatibility alias. The implementation reuses the terminal's +existing search pipeline rather than maintaining a second index or buffer. + +- Search follows typing across the pane's entire active scrollback and + highlights all matches at once. +- `Enter` and `Shift+Enter` move forward and backward with wrap-around. +- Case-sensitive and regular-expression modes are available from the compact + search overlay, alongside the `current / total` match counter. +- The scrollbar shows one marker per matching buffer row and emphasizes the + current match's row. This overview is pane-local and does not require generic + shell marks to be enabled. +- Typing is coalesced, live results converge during sustained streaming output, + and unchanged scrollbar state is not needlessly repainted. +- Resize/reflow, scrollback eviction, active-buffer changes, invalid regular + expressions, CJK, and emoji search spans have deterministic regression + coverage. + +## New: Click to position cursor + +A plain single left click inside the current editable command moves the shell +cursor to that location. winTerm calculates the cell distance and sends LEFT +or RIGHT input events, keeping PowerShell, Readline, and cooked-input state +authoritative. It never moves only the renderer cursor, edits the command +buffer directly, inserts or deletes text, or executes the command. + +The feature is enabled by default for winTerm profiles. It can be disabled in +**Profile > Advanced > Click to position cursor**, and an explicit existing +`"experimental.repositionCursorWithMouse": false` setting remains respected. +The internal JSON property name is retained for configuration compatibility; +the user-facing feature is not described as experimental. + +Shell integration supplies the safety boundary. Positioning is allowed only +when the last shell mark identifies a current, unfinished editable command. +Clicks on previous commands, previous output, unrelated scrollback, completed +or executing commands, and any position that cannot be validated are safe +no-ops. + +## Mouse behavior and compatibility + +Cursor positioning occurs on left-button release only when the existing drag +threshold was not crossed. Starting a drag never moves the command cursor +first. Existing interaction precedence remains: + +1. Ctrl+Click hyperlink activation. +2. VT mouse reporting to the child terminal application. +3. Double-click word and triple-click line selection. +4. Shift+Click and drag selection. +5. Plain single click positioning. + +Mouse-aware applications such as Vim, Less, top, htop, k9s, and tmux retain +mouse ownership when VT mouse reporting is active. Each split pane owns its +own terminal connection, marks, selection state, and cursor-position input. + +## Stability fixes + +- Integrated the safety intent of Microsoft Terminal PR #20442, merged as + `de3fc87d186e5da1d5ccd8731412905f5e2aba30`. Mouse coordinates are translated + at widened precision, vertical padding and out-of-viewport rows are rejected, + horizontal padding is clamped to rendered buffer cells, and every point is + checked against the TextBuffer's inclusive coordinate contract before use. +- Negative, overflow, beyond-width, beyond-height, padding, empty-command, + historical-output, wrapped-command, resize-era, and pane-isolation cases + fail safely and have automated regression coverage. +- Full-width glyph trailing cells do not produce duplicate LEFT/RIGHT events. +- Pane Search reflow no longer creates a stray selection, active-buffer changes + discard stale highlights, and closing search releases retained search-span + storage. + +## Downloads + +- `winTerm-1.4.1-setup-x64.exe` — current-user or all-users installation. +- `winTerm-1.4.1-portable-x64.zip` — extract and run without installation; + `portable.marker` keeps data beside the executable. +- `SHA256SUMS.txt` — checksums for the application downloads and published + release artifacts. +- `THIRD_PARTY_NOTICES.md` — dependency and attribution notices. +- `SBOM.spdx.json` and `SBOM.cyclonedx.json` — software bills of materials. +- `release-metadata.json` — immutable build and source provenance. +- `winTerm-1.4.1-release-notes.md` — the packaged copy of these notes. + +Upgrading keeps settings, themes, workspaces, and snapshots under +`%LOCALAPPDATA%\winTerm`, or under the adjacent `data` directory in portable +mode. + +## Signing notice + +The installer is **not Authenticode-signed**. Windows may display Unknown +Publisher or a Microsoft Defender SmartScreen warning. Download only from the +official GitHub Release and verify `SHA256SUMS.txt` from that same Release +before running the installer. + +Requires Windows 11 x64. winTerm is an independent open-source project based +on Microsoft Terminal source code; it is not a Microsoft product and is not +affiliated with or endorsed by Microsoft. diff --git a/docs/user/keyboard-shortcuts.md b/docs/user/keyboard-shortcuts.md index 2ddf62ecb..61c9c1097 100644 --- a/docs/user/keyboard-shortcuts.md +++ b/docs/user/keyboard-shortcuts.md @@ -53,6 +53,17 @@ that needs a literal `Ctrl+F` keystroke can reclaim it by unbinding the default (`{ "command": "unbound", "keys": "ctrl+f" }` in settings); Find stays reachable through `Ctrl+Shift+F` or the Command Palette. +## Click to position cursor + +With shell integration active, a plain single left click inside the current +editable command moves the shell cursor on button release. Dragging past the +existing selection threshold selects text without moving the command cursor +first. Ctrl+Click hyperlinks, VT mouse reporting, double/triple click, +Shift+Click, and drag selection keep their normal precedence. The feature is +enabled by default for winTerm and can be disabled under Profile > Advanced. +If an editable shell range cannot be established safely, the click does +nothing. + ## Command Timeline | Shortcut | Behavior | diff --git a/scripts/winterm/package-shell-assets.ps1 b/scripts/winterm/package-shell-assets.ps1 index 2fa5ec83e..81c1ed1d4 100644 --- a/scripts/winterm/package-shell-assets.ps1 +++ b/scripts/winterm/package-shell-assets.ps1 @@ -32,9 +32,9 @@ foreach ($relativePath in $sourceAssets) } $version = Get-Content -LiteralPath (Join-Path $repositoryRoot 'shell\shared\version.json') -Raw | ConvertFrom-Json -if ($version.moduleVersion -ne '1.4.0' -or - $version.modulePrerelease -ne 'beta' -or - $version.applicationVersion -ne '1.4.0-beta' -or +if ($version.moduleVersion -ne '1.4.1' -or + $version.modulePrerelease -ne '' -or + $version.applicationVersion -ne '1.4.1' -or $version.protocolVersion -ne 1) { throw 'The winTerm Shell asset version metadata is invalid.' diff --git a/scripts/winterm/test-visual-progress.ps1 b/scripts/winterm/test-visual-progress.ps1 index 665d525c0..5e2b98ad8 100644 --- a/scripts/winterm/test-visual-progress.ps1 +++ b/scripts/winterm/test-visual-progress.ps1 @@ -1411,9 +1411,10 @@ try Assert-Matches $releaseNotes $requiredPattern 'winTerm 1.2.0 Visual Progress release notes' } Assert-Matches $source.Changelog '(?m)^##\s+.*1\.2\.0' 'Changelog 1.2.0 entry' - Assert-Matches $source.CurrentProgress '(?i)1\.2\.0.*Visual Progress|Visual Progress.*1\.2\.0' 'Current progress v1.2.0 milestone' + Assert-Matches $source.CurrentProgress '(?i)1\.4\.1' 'Current progress v1.4.1 milestone' + Assert-Matches $source.CurrentProgress '(?i)Visual Progress' 'Current progress Visual Progress coverage' Assert-Contains $source.Readme '/releases/latest' 'Stable latest-release README download route' - Assert-Matches $source.Readme '(?i)1\.2\.0' 'README v1.2.0 surface' + Assert-Matches $source.Readme '(?i)1\.4\.1' 'README v1.4.1 surface' foreach ($requiredPattern in @( '(?is)recognition.*runs locally|locally.*recognition', '(?is)bounded.*newly\s+arriving\s+output|newly\s+arriving.*bounded', @@ -1431,12 +1432,12 @@ try $version = $source.VersionMetadata | ConvertFrom-Json $expectedVersionValues = [ordered]@{ - applicationVersion = '1.4.0-beta' - packageVersion = '1.4.0.1' - moduleVersion = '1.4.0' - modulePrerelease = 'beta' - channel = 'beta' - tag = 'v1.4.0-beta' + applicationVersion = '1.4.1' + packageVersion = '1.4.1.0' + moduleVersion = '1.4.1' + modulePrerelease = '' + channel = 'stable' + tag = 'v1.4.1' workspaceSchemaVersion = 2 dockingModelVersion = 1 shellProtocolVersion = 1 @@ -1451,39 +1452,39 @@ try } } $shellVersion = $source.ShellVersion | ConvertFrom-Json - if ($shellVersion.applicationVersion -ne '1.4.0-beta' -or $shellVersion.moduleVersion -ne '1.4.0' -or $shellVersion.protocolVersion -ne 1) + if ($shellVersion.applicationVersion -ne '1.4.1' -or $shellVersion.moduleVersion -ne '1.4.1' -or $shellVersion.protocolVersion -ne 1) { - throw 'Shell version metadata does not match winTerm version 1.4.0-beta with protocol version 1.' + throw 'Shell version metadata does not match winTerm version 1.4.1 with protocol version 1.' } foreach ($surface in @( - @{ Content = $source.ReleaseMetadata; Value = 'ApplicationVersion{ L"1.4.0-beta" }'; Description = 'About release metadata' }, - @{ Content = $source.PackageManifest; Value = 'Version="1.4.0.1"'; Description = 'MSIX package manifest' }, - @{ Content = $source.HostResource; Value = 'FILEVERSION 1,4,0,1'; Description = 'Terminal host file version' }, - @{ Content = $source.HostResource; Value = '"ProductVersion", "1.4.0-beta\0"'; Description = 'Terminal host display version' }, - @{ Content = $source.ShimResource; Value = 'FILEVERSION 1,4,0,1'; Description = 'Shim file version' }, - @{ Content = $source.ShimResource; Value = '"ProductVersion", "1.4.0-beta\0"'; Description = 'Shim display version' }, + @{ Content = $source.ReleaseMetadata; Value = 'ApplicationVersion{ L"1.4.1" }'; Description = 'About release metadata' }, + @{ Content = $source.PackageManifest; Value = 'Version="1.4.1.0"'; Description = 'MSIX package manifest' }, + @{ Content = $source.HostResource; Value = 'FILEVERSION 1,4,1,0'; Description = 'Terminal host file version' }, + @{ Content = $source.HostResource; Value = '"ProductVersion", "1.4.1\0"'; Description = 'Terminal host display version' }, + @{ Content = $source.ShimResource; Value = 'FILEVERSION 1,4,1,0'; Description = 'Shim file version' }, + @{ Content = $source.ShimResource; Value = '"ProductVersion", "1.4.1\0"'; Description = 'Shim display version' }, @{ Content = $source.CustomProps; Value = '1'; Description = 'Executable major version' }, @{ Content = $source.CustomProps; Value = '4'; Description = 'Executable minor version' }, - @{ Content = $source.ShellModuleManifest; Value = "ModuleVersion = '1.4.0'"; Description = 'PowerShell module manifest' }, - @{ Content = $source.ShellModule; Value = "`$script:WinTermModuleVersion = '1.4.0'"; Description = 'PowerShell module runtime' }, + @{ Content = $source.ShellModuleManifest; Value = "ModuleVersion = '1.4.1'"; Description = 'PowerShell module manifest' }, + @{ Content = $source.ShellModule; Value = "`$script:WinTermModuleVersion = '1.4.1'"; Description = 'PowerShell module runtime' }, @{ Content = $source.PackageShellAssets; Value = "'shell\shared\version.json'"; Description = 'Canonical shell version metadata packaging' }, - @{ Content = $source.WorkspaceSerializer; Value = '"1.4.0-beta"'; Description = 'Workspace application-version fallback' } + @{ Content = $source.WorkspaceSerializer; Value = '"1.4.1"'; Description = 'Workspace application-version fallback' } )) { Assert-Contains $surface.Content $surface.Value $surface.Description } foreach ($required in @( - "applicationVersion -eq '1.4.0-beta'", - "packageVersion -eq '1.4.0.1'", - "moduleVersion -eq '1.4.0'", - "tag -eq 'v1.4.0-beta'", + "applicationVersion -eq '1.4.1'", + "packageVersion -eq '1.4.1.0'", + "moduleVersion -eq '1.4.1'", + "tag -eq 'v1.4.1'", "Workspace Schema version remains 2", "Docking Model version remains 1", "Shell Protocol version remains 1", "Theme Schema remains at version 1" )) { - Assert-Contains $source.VerifyVersion $required 'Authoritative v1.4.0-beta version validation surface' + Assert-Contains $source.VerifyVersion $required 'Authoritative v1.4.1 version validation surface' } $testBinary = Join-Path $root "bin\$Platform\$Configuration\UnitTests_SettingsModel\SettingsModel.Unit.Tests.dll" diff --git a/scripts/winterm/test.ps1 b/scripts/winterm/test.ps1 index 30af8d165..73b9bd7d4 100644 --- a/scripts/winterm/test.ps1 +++ b/scripts/winterm/test.ps1 @@ -292,8 +292,8 @@ function Test-ShellExperienceFoundations } $manifest = Import-PowerShellDataFile -LiteralPath $moduleManifest - if ($manifest.ModuleVersion -ne '1.4.0' -or - $manifest.PrivateData.PSData.Prerelease -ne 'beta' -or + if ($manifest.ModuleVersion -ne '1.4.1' -or + $manifest.PrivateData.PSData.Prerelease -ne '' -or $manifest.PowerShellVersion -ne '5.1') { throw 'The winTerm PowerShell module manifest does not declare the supported version boundary.' diff --git a/scripts/winterm/verify-branding.ps1 b/scripts/winterm/verify-branding.ps1 index 831698d34..346d1671d 100644 --- a/scripts/winterm/verify-branding.ps1 +++ b/scripts/winterm/verify-branding.ps1 @@ -126,7 +126,7 @@ function Test-Manifest Test-Requirement -Condition ($null -ne $identity -and $identity.Name -eq 'HelloThisWorld.winTerm') -Message "$Path uses package identity HelloThisWorld.winTerm" Test-Requirement -Condition ($null -ne $identity -and $identity.Name -notmatch '^Microsoft\.') -Message "$Path does not use a Microsoft package name" Test-Requirement -Condition ($null -ne $identity -and $identity.Publisher -ceq $ExpectedPublisher) -Message "$Path uses the expected non-Microsoft publisher" - Test-Requirement -Condition ($null -ne $identity -and $identity.Version -eq '1.4.0.1') -Message "$Path uses package version 1.4.0.1" + Test-Requirement -Condition ($null -ne $identity -and $identity.Version -eq '1.4.1.0') -Message "$Path uses package version 1.4.1.0" Test-Requirement -Condition ($null -ne $properties -and $properties.DisplayName -eq 'winTerm') -Message "$Path package display name is winTerm" Test-Requirement -Condition ($null -ne $application -and $application.Id -eq 'winTerm') -Message "$Path application ID is winTerm" Test-Requirement -Condition ($null -ne $visualElements -and $visualElements.DisplayName -eq 'winTerm') -Message "$Path application display name is winTerm" diff --git a/scripts/winterm/verify-version.ps1 b/scripts/winterm/verify-version.ps1 index 27f3aeb55..33e97f45b 100644 --- a/scripts/winterm/verify-version.ps1 +++ b/scripts/winterm/verify-version.ps1 @@ -49,10 +49,10 @@ try $versionPath = Join-Path $repositoryRoot 'src\winterm\Branding\version.json' $version = Get-Content -LiteralPath $versionPath -Raw | ConvertFrom-Json - Assert-Condition ($version.applicationVersion -eq '1.4.0-beta') 'Application version is 1.4.0-beta' - Assert-Condition ($version.packageVersion -eq '1.4.0.1') 'Package version is 1.4.0.1' - Assert-Condition ($version.moduleVersion -eq '1.4.0') 'PowerShell module version is 1.4.0' - Assert-Condition ($version.modulePrerelease -eq 'beta') 'PowerShell module prerelease suffix is beta' + Assert-Condition ($version.applicationVersion -eq '1.4.1') 'Application version is 1.4.1' + Assert-Condition ($version.packageVersion -eq '1.4.1.0') 'Package version is 1.4.1.0' + Assert-Condition ($version.moduleVersion -eq '1.4.1') 'PowerShell module version is 1.4.1' + Assert-Condition ($version.modulePrerelease -eq '') 'PowerShell module prerelease suffix is empty for stable' # The release workflow treats any channel other than 'stable' as a # prerelease: it marks the GitHub Release --prerelease and --latest=false. @@ -66,7 +66,7 @@ try Assert-Condition ($version.packageVersion -match '^\d+\.\d+\.\d+\.\d+$') 'Package version stays a four-part numeric version' Assert-Condition ($version.moduleVersion -match '^\d+\.\d+\.\d+$') 'PowerShell module version stays numeric' - Assert-Condition ($version.tag -eq 'v1.4.0-beta') 'Prerelease tag is v1.4.0-beta' + Assert-Condition ($version.tag -eq 'v1.4.1') 'Stable tag is v1.4.1' Assert-Condition ($version.workspaceSchemaVersion -eq 2) 'Workspace Schema version remains 2' Assert-Condition ($version.dockingModelVersion -eq 1) 'Docking Model version remains 1' Assert-Condition ($version.shellProtocolVersion -eq 1) 'Shell Protocol version remains 1' @@ -90,7 +90,7 @@ try $moduleManifest = Import-PowerShellDataFile -LiteralPath (Join-Path $repositoryRoot 'shell\powershell\winTerm.Shell\winTerm.Shell.psd1') Assert-Condition ($moduleManifest.ModuleVersion.ToString() -eq $version.moduleVersion) 'PowerShell manifest version matches release metadata' Assert-Condition ($moduleManifest.PrivateData.PSData.Prerelease -eq $version.modulePrerelease) 'PowerShell manifest prerelease matches release metadata' - Assert-Condition ((Get-Text 'shell\powershell\winTerm.Shell\winTerm.Shell.psm1').Contains("`$script:WinTermModuleVersion = '1.4.0'")) 'PowerShell module runtime version matches release metadata' + Assert-Condition ((Get-Text 'shell\powershell\winTerm.Shell\winTerm.Shell.psm1').Contains("`$script:WinTermModuleVersion = '1.4.1'")) 'PowerShell module runtime version matches release metadata' $shellVersion = Get-Text 'shell\shared\version.json' | ConvertFrom-Json Assert-Condition ($shellVersion.applicationVersion -eq $version.applicationVersion) 'Shell asset application version matches release metadata' @@ -98,8 +98,8 @@ try Assert-Condition ($shellVersion.protocolVersion -eq $version.shellProtocolVersion) 'Shell asset protocol version matches release metadata' $releaseHeader = Get-Text 'src\winterm\Branding\ReleaseMetadata.h' - Assert-Condition ($releaseHeader.Contains('ApplicationVersion{ L"1.4.0-beta" }')) 'About metadata application version is 1.4.0-beta' - Assert-Condition ($releaseHeader.Contains('ReleaseChannel{ L"Beta" }')) 'About metadata channel is Beta' + Assert-Condition ($releaseHeader.Contains('ApplicationVersion{ L"1.4.1" }')) 'About metadata application version is 1.4.1' + Assert-Condition ($releaseHeader.Contains('ReleaseChannel{ L"Stable" }')) 'About metadata channel is Stable' Assert-Condition ($releaseHeader.Contains($version.microsoftTerminalUpstreamRevision)) 'About metadata contains the Microsoft Terminal upstream revision' Assert-Condition ($releaseHeader.Contains('WorkspaceSchemaVersion{ 2 }')) 'About metadata contains Workspace Schema version 2' Assert-Condition ($releaseHeader.Contains('DockingModelVersion{ 1 }')) 'About metadata contains Docking Model version 1' @@ -129,10 +129,10 @@ try Assert-Condition ((Get-Text 'src\winterm\Workspaces\Model\WorkspaceDescriptor.h').Contains('WorkspaceSchemaVersion{ 2 }')) 'Workspace model remains at Schema version 2' Assert-Condition ((Get-Text 'src\winterm\Workspaces\Model\WorkspaceDescriptor.h').Contains('DockingModelVersion{ 1 }')) 'Workspace model remains at Docking version 1' - Assert-Condition ((Get-Text 'src\winterm\Workspaces\Model\WorkspaceDescriptor.h').Contains('applicationVersion{ "1.4.0-beta" }')) 'Workspace model application-version fallback is 1.4.0-beta' + Assert-Condition ((Get-Text 'src\winterm\Workspaces\Model\WorkspaceDescriptor.h').Contains('applicationVersion{ "1.4.1" }')) 'Workspace model application-version fallback is 1.4.1' Assert-Condition ((Get-Text 'src\winterm\Shell\Protocol\ShellIntegrationProtocol.h').Contains('ShellProtocolVersion{ 1 }')) 'Shell protocol remains at version 1' Assert-Condition ((Get-Text 'src\winterm\Appearance\Themes\ThemeDescriptor.h').Contains('CurrentThemeSchemaVersion{ 1 }')) 'Theme Schema remains at version 1' - Assert-Condition ((Get-Text 'src\winterm\Workspaces\Persistence\WorkspaceSerializer.cpp').Contains('"1.4.0-beta"')) 'Workspace serializer application-version fallback is 1.4.0-beta' + Assert-Condition ((Get-Text 'src\winterm\Workspaces\Persistence\WorkspaceSerializer.cpp').Contains('"1.4.1"')) 'Workspace serializer application-version fallback is 1.4.1' $releaseWorkflow = Get-Text '.github\workflows\release.yml' Assert-Condition ($releaseWorkflow.Contains("- 'v*'")) 'Release workflow accepts version tags through a generic guarded trigger' @@ -141,9 +141,9 @@ try Assert-Condition ($releaseWorkflow.Contains("`$expectedTag = `"v`$(`$metadata.applicationVersion)`"")) 'Release workflow derives the expected tag from version.json' Assert-Condition ($releaseWorkflow.Contains("`$metadata.tag -cne `$expectedTag")) 'Release workflow rejects a version metadata tag mismatch' - Assert-Condition ((Get-Text 'CHANGELOG.md').Contains('## 1.3.0 - 2026-08-13')) 'Changelog contains the 1.3.0 stable entry' + Assert-Condition ((Get-Text 'CHANGELOG.md').Contains('## 1.4.1 - 2026-08-25')) 'Changelog contains the 1.4.1 stable entry' Assert-Condition ((Get-Text 'README.md').Contains("current source version is ``$($version.applicationVersion)``")) 'README source version matches release metadata' - Assert-Condition ((Get-Text 'README.md').Contains('latest stable release is `1.3.0`')) 'README stable pointer is 1.3.0' + Assert-Condition ((Get-Text 'README.md').Contains('latest stable release is `1.4.1`')) 'README stable pointer is 1.4.1' # The Japanese README is a maintained counterpart, not a marketing summary: # both files must link to each other, the translation must point at the @@ -162,7 +162,7 @@ try Assert-Condition ($readmeJa.Contains('[English](README.md)')) 'Japanese README links back to the English README' Assert-Condition ($readmeJa.Contains('https://winterm.dev/ja/')) 'Japanese README links to the Japanese website' Assert-Condition ($readmeJa.Contains("``$($version.applicationVersion)``")) 'Japanese README source version matches release metadata' - Assert-Condition ($readmeJa.Contains('`1.3.0`')) 'Japanese README stable pointer is 1.3.0' + Assert-Condition ($readmeJa.Contains('`1.4.1`')) 'Japanese README stable pointer is 1.4.1' Assert-Condition ($readmeJa.Contains('winTerm--setup-x64.exe')) 'Japanese README retains the installer asset pattern' Assert-Condition ($readmeJa.Contains('winTerm--portable-x64.zip')) 'Japanese README retains the portable asset pattern' Assert-Condition ($readmeJa.Contains('SHA256SUMS.txt')) 'Japanese README retains the checksum filename' @@ -187,7 +187,7 @@ try if ($RequireTag) { $tag = (& git describe --tags --exact-match 2>$null).Trim() - Assert-Condition ($LASTEXITCODE -eq 0 -and $tag -eq $version.tag) 'Checked-out commit is exactly tagged v1.4.0-beta' + Assert-Condition ($LASTEXITCODE -eq 0 -and $tag -eq $version.tag) 'Checked-out commit is exactly tagged v1.4.1' } Write-Host 'winTerm version consistency verification passed.' -ForegroundColor Green diff --git a/shell/powershell/winTerm.Shell/winTerm.Shell.psd1 b/shell/powershell/winTerm.Shell/winTerm.Shell.psd1 index 001c3352c..202f4e623 100644 --- a/shell/powershell/winTerm.Shell/winTerm.Shell.psd1 +++ b/shell/powershell/winTerm.Shell/winTerm.Shell.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'winTerm.Shell.psm1' - ModuleVersion = '1.4.0' + ModuleVersion = '1.4.1' GUID = 'f65cd8f4-5d25-4a2a-a0d4-58df1ab3dc5a' Author = 'winTerm contributors' CompanyName = 'winTerm' @@ -12,7 +12,7 @@ AliasesToExport = @() PrivateData = @{ PSData = @{ - Prerelease = 'beta' + Prerelease = '' Tags = @('winTerm', 'terminal', 'shell-integration') ProjectUri = 'https://github.com/HelloThisWorld/winTerm' LicenseUri = 'https://github.com/HelloThisWorld/winTerm/blob/main/LICENSE' diff --git a/shell/powershell/winTerm.Shell/winTerm.Shell.psm1 b/shell/powershell/winTerm.Shell/winTerm.Shell.psm1 index 94b5e9d6e..1029b9d09 100644 --- a/shell/powershell/winTerm.Shell/winTerm.Shell.psm1 +++ b/shell/powershell/winTerm.Shell/winTerm.Shell.psm1 @@ -3,7 +3,7 @@ Set-StrictMode -Version Latest -$script:WinTermModuleVersion = '1.4.0' +$script:WinTermModuleVersion = '1.4.1' $script:WinTermProtocolVersion = 1 $script:WinTermIntegrationEnabled = $false $script:WinTermPromptWrapped = $false diff --git a/shell/shared/version.json b/shell/shared/version.json index 9e7310ba2..3a429ec44 100644 --- a/shell/shared/version.json +++ b/shell/shared/version.json @@ -1,6 +1,6 @@ { - "applicationVersion": "1.4.0-beta", - "moduleVersion": "1.4.0", - "modulePrerelease": "beta", + "applicationVersion": "1.4.1", + "moduleVersion": "1.4.1", + "modulePrerelease": "", "protocolVersion": 1 } diff --git a/src/cascadia/CascadiaPackage/Package-winTerm.appxmanifest b/src/cascadia/CascadiaPackage/Package-winTerm.appxmanifest index 457c7bf87..1de1e4d91 100644 --- a/src/cascadia/CascadiaPackage/Package-winTerm.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-winTerm.appxmanifest @@ -18,7 +18,7 @@ + Version="1.4.1.0" /> winTerm diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 23fb8e31a..7b1508cf1 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -2661,14 +2661,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->MultiClickSelection(terminalPosition, mode); selectionNeedsToBeCopied = true; } - else if (_settings.RepositionCursorWithMouse() && !selectionNeedsToBeCopied) // Don't reposition cursor if this is part of a selection operation - { - _repositionCursorWithMouse(terminalPosition); - } - _updateSelectionUI(); - } - - void ControlCore::_repositionCursorWithMouse(const til::point terminalPosition) + _updateSelectionUI(); + } + + void ControlCore::RepositionCursorWithMouse(const til::point terminalPosition) + { + const auto lock = _terminal->LockForWriting(); + + // This is deliberately a release-time action. Selection has already + // had the opportunity to claim the gesture using its established drag + // threshold, and VT mouse mode remains authoritative. + if (!_settings.RepositionCursorWithMouse() || + _terminal->IsSelectionActive() || + _terminal->IsTrackingMouseInput()) + { + return; + } + + _repositionCursorWithMouse(terminalPosition); + } + + void ControlCore::_repositionCursorWithMouse(const til::point terminalPosition) { // If we're handling a single left click, without shift pressed, and // outside mouse mode, AND the user has RepositionCursorWithMouse turned @@ -2681,19 +2694,74 @@ namespace winrt::Microsoft::Terminal::Control::implementation // As noted in GH #8573, there's plenty of edge cases with this // approach, but it's good enough to bring value to 90% of use cases. - // Does the current buffer line have a mark on it? - const auto& marks{ _terminal->GetMarkExtents() }; - if (!marks.empty()) - { - const auto& last{ marks.back() }; - const auto [start, end] = last.GetExtent(); - const auto& buffer = _terminal->GetTextBuffer(); - const auto cursorPos = buffer.GetCursor().GetPosition(); - const auto bufferSize = buffer.GetSize(); - auto lastNonSpace = buffer.GetLastNonSpaceCharacter(); - bufferSize.IncrementInBounds(lastNonSpace, true); - - // If the user clicked off to the right side of the prompt, we + // Does the current editable prompt have a trustworthy shell mark? + const auto& marks{ _terminal->GetMarkExtents() }; + if (!marks.empty()) + { + const auto& last{ marks.back() }; + // Command text itself gives the live mark a commandEnd extent as + // the user types, so commandEnd is not evidence of execution. + // The shell-integration lifecycle is authoritative: only the + // unfinished CommandStart state is editable. Executing, finished, + // historic-output, and prompt-only states are safe no-ops. + const auto shellIntegrationState = static_cast(_terminal->GetShellIntegrationState()); + if (shellIntegrationState != ShellIntegrationMarkKind::CommandStart || + last.outputEnd.has_value() || + last.data.exitCode.has_value()) + { + return; + } + + const auto& buffer = _terminal->GetTextBuffer(); + const auto cursorPos = buffer.GetCursor().GetPosition(); + const auto bufferSize = buffer.GetSize(); + const auto viewport = _terminal->GetViewport(); + + if (bufferSize.Width() <= 0 || + bufferSize.Height() <= 0 || + terminalPosition.y < 0 || + terminalPosition.y >= viewport.Height() || + !bufferSize.IsInBounds(last.end) || + !bufferSize.IsInBounds(cursorPos)) + { + return; + } + + // terminalPosition is viewport-relative. Do the addition at a + // wider precision so even malformed coordinates cannot overflow, + // then clamp horizontal padding to the nearest rendered cell. + // Vertical padding is not an editable row and remains a no-op. + const auto viewportOrigin = viewport.Origin(); + const auto rawX = static_cast(viewportOrigin.x) + terminalPosition.x; + const auto rawY = static_cast(viewportOrigin.y) + terminalPosition.y; + if (rawY < bufferSize.Top() || rawY > bufferSize.BottomInclusive()) + { + return; + } + + const til::point bufferPos{ + gsl::narrow_cast(std::clamp(rawX, bufferSize.Left(), bufferSize.RightInclusive())), + gsl::narrow_cast(rawY), + }; + + auto lastNonSpace = buffer.GetLastNonSpaceCharacter(); + if (!bufferSize.IsInBounds(lastNonSpace)) + { + return; + } + bufferSize.IncrementInBounds(lastNonSpace); + + // The cursor can sit after trailing spaces, which are intentionally + // absent from GetLastNonSpaceCharacter(). Preserve that valid + // position while keeping every point passed to TextBuffer APIs in + // inclusive buffer bounds. + const auto editableEnd = std::max(std::max(last.end, lastNonSpace), cursorPos); + if (!bufferSize.IsInBounds(editableEnd) || cursorPos < last.end || cursorPos > editableEnd) + { + return; + } + + // If the user clicked off to the right side of the prompt, we // want to send keystrokes to the last character in the prompt +1. // // We don't want to send too many here. In CMD, if the user's @@ -2705,18 +2773,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation // should leave the cursor at the very end of the prompt, // without adding any characters from a previous command. - // terminalPosition is viewport-relative. - const auto bufferPos = _terminal->GetViewport().Origin() + terminalPosition; - if (bufferPos.y > lastNonSpace.y) - { - // Clicked under the prompt. Bail. - return; - } - - // Limit the click to 1 past the last character on the last line. - const auto clampedClick = std::min(bufferPos, lastNonSpace); - - if (clampedClick >= last.end) + if (bufferPos < last.end || bufferPos.y > editableEnd.y) + { + // Clicked before the current command or under it. Bail. + return; + } + + // Limit horizontal padding on the final command row to the + // current editable end. Positions beyond another row are not part + // of the command and are rejected above. + const auto clampedClick = bufferPos > editableEnd ? editableEnd : bufferPos; + + if (clampedClick >= last.end) { // Get the distance between the cursor and the click, in cells. @@ -2727,8 +2795,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto startPoint = goRight ? cursorPos : clampedClick; const auto endPoint = goRight ? clampedClick : cursorPos; - const auto delta = _terminal->GetTextBuffer().GetCellDistance(startPoint, endPoint); - const WORD key = goRight ? VK_RIGHT : VK_LEFT; + // Count rendered glyphs, not trailing halves of wide cells. + // One LEFT/RIGHT event should cross one shell character, so + // CJK and other double-cell glyphs must not produce two keys. + size_t delta = 0; + auto cell = buffer.GetCellDataAt(startPoint); + const auto endCell = buffer.GetCellDataAt(endPoint); + while (cell != endCell) + { + if (cell->DbcsAttr() != DbcsAttribute::Trailing) + { + ++delta; + } + ++cell; + } + const WORD key = goRight ? VK_RIGHT : VK_LEFT; std::wstring buffer; const auto append = [&](TerminalInput::OutputType&& out) { @@ -2748,9 +2829,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation append(_terminal->SendKeyEvent(key, 0, {}, false)); } - { - // Sending input requires that we're unlocked, because - // writing the input pipe may block indefinitely. + if (!buffer.empty()) + { + // Sending input requires that we're unlocked, because + // writing the input pipe may block indefinitely. const auto suspension = _terminal->SuspendLock(); SendInput(buffer); } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 39210a7be..fbd258a93 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -260,12 +260,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation uint64_t BufferMutationId() const; void ClearSearch(); - void LeftClickOnTerminal(const til::point terminalPosition, - const int numberOfClicks, - const bool altEnabled, - const bool shiftEnabled, - const bool isOnOriginalPosition, - bool& selectionNeedsToBeCopied); + void LeftClickOnTerminal(const til::point terminalPosition, + const int numberOfClicks, + const bool altEnabled, + const bool shiftEnabled, + const bool isOnOriginalPosition, + bool& selectionNeedsToBeCopied); + void RepositionCursorWithMouse(const til::point terminalPosition); void AttachUiaEngine(::Microsoft::Console::Render::UiaEngine* const pEngine); void DetachUiaEngine(::Microsoft::Console::Render::UiaEngine* const pEngine); diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index 065cc700f..2b9be4047 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -257,9 +257,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Un-rounded coordinates; we only round when selecting text const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false); - const auto altEnabled = modifiers.IsAltPressed(); - const auto shiftEnabled = modifiers.IsShiftPressed(); - const auto ctrlEnabled = modifiers.IsCtrlPressed(); + const auto altEnabled = modifiers.IsAltPressed(); + const auto shiftEnabled = modifiers.IsShiftPressed(); + const auto ctrlEnabled = modifiers.IsCtrlPressed(); + _cursorRepositionPending = false; // GH#9396: we prioritize hyper-link over VT mouse events auto hyperlink = _core->GetHyperlink(terminalPosition.to_core_point()); @@ -285,7 +286,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // between single-, double-, and triple-click. To increase the // number of acceptable click states, simply increment // MAX_CLICK_COUNT and add another if-statement - const auto multiClickMapper = clickCount > MAX_CLICK_COUNT ? ((clickCount + MAX_CLICK_COUNT - 1) % MAX_CLICK_COUNT) + 1 : clickCount; + const auto multiClickMapper = clickCount > MAX_CLICK_COUNT ? ((clickCount + MAX_CLICK_COUNT - 1) % MAX_CLICK_COUNT) + 1 : clickCount; + _cursorRepositionPending = multiClickMapper == 1 && !altEnabled && !shiftEnabled && !ctrlEnabled; // Capture the position of the first click when no selection is active if (multiClickMapper == 1) @@ -397,8 +399,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto distanceSquared = dx * dx + dy * dy; const auto maxDistanceSquared = w * w / 16; // (w / 4)^2 - if (distanceSquared >= maxDistanceSquared) - { + if (distanceSquared >= maxDistanceSquared) + { + // Selection owns this gesture. Never move the shell cursor + // before establishing the drag selection. + _cursorRepositionPending = false; + // GH#9955.c: Make sure to use the terminal location of the // _touchdown_ point here. We want to start the selection // from where the user initially clicked, not where they are @@ -470,9 +476,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation const unsigned int pointerUpdateKind, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const Core::Point pixelPosition) - { - const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false); - // Short-circuit isReadOnly check to avoid warning dialog + { + const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false); + const auto repositionPending = _cursorRepositionPending; + _cursorRepositionPending = false; + // Short-circuit isReadOnly check to avoid warning dialog if (!_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers)) { _sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); @@ -483,17 +491,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Right clicks and middle clicks should not need to do anything when released. const auto isLeftMouseRelease = pointerUpdateKind == WM_LBUTTONUP; - if (_core->CopyOnSelect() && - isLeftMouseRelease && - _selectionNeedsToBeCopied) + if (_core->CopyOnSelect() && + isLeftMouseRelease && + _selectionNeedsToBeCopied) { // IMPORTANT! // DO NOT clear the selection here! // Otherwise, the selection will be cleared immediately after you make it. - CopySelectionToClipboard(false, false, _core->Settings().CopyFormatting()); - } - - _singleClickTouchdownPos = std::nullopt; + CopySelectionToClipboard(false, false, _core->Settings().CopyFormatting()); + } + + if (isLeftMouseRelease && + repositionPending && + !_selectionNeedsToBeCopied && + !modifiers.IsAltPressed() && + !modifiers.IsShiftPressed() && + !modifiers.IsCtrlPressed()) + { + _core->RepositionCursorWithMouse(_getTerminalPosition(til::point{ pixelPosition }, true)); + } + + _singleClickTouchdownPos = std::nullopt; } void ControlInteractivity::TouchReleased() diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index b800fbb25..e51a7ec66 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -125,7 +125,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation unsigned int _multiClickCounter; Timestamp _lastMouseClickTimestamp; std::optional _lastMouseClickPos; - std::optional _singleClickTouchdownPos; + std::optional _singleClickTouchdownPos; + bool _cursorRepositionPending{ false }; std::optional _lastMouseClickPosNoSelection; // This field tracks whether the selection has changed meaningfully // since it was last copied. It's generally used to prevent copyOnSelect diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 00f52e940..c01ab31bd 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -1365,14 +1365,14 @@ When enabled, the Terminal automatically add a mark indicating the position of the end of the command when you press enter. A description for what the "Automatically mark prompts on pressing Enter" setting does. Presented near "Profile_AutoMarkPrompts". - - Experimental: Reposition the cursor with mouse clicks - This allows the user to move the text cursor just by clicking with the mouse. - - - When enabled, clicking inside the prompt will move the cursor to that position. This requires shell integration to be enabled in your shell to work as expected. - A description for what the "Reload environment variables" setting does. Presented near "Profile_RepositionCursorWithMouse". - + + Click to position cursor + This allows the user to move the text cursor just by clicking with the mouse. + + + Click inside the current command line to move the text cursor to that position. Shell integration is required to identify the editable command safely. + A description for what the "Click to position cursor" setting does. Presented near "Profile_RepositionCursorWithMouse". + Audible An option to choose from for the "bell style" setting. When selected, an audible cue is used to notify the user. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw index e5fec2ee4..d9ea258d5 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw @@ -1365,14 +1365,14 @@ 有効にすると、ターミナルは Enter キーを押したときにコマンドの終了位置を示すマークを自動的に追加します。 A description for what the "Automatically mark prompts on pressing Enter" setting does. Presented near "Profile_AutoMarkPrompts". - - 試験段階: マウスのクリックでカーソルの位置を変更する - This allows the user to move the text cursor just by clicking with the mouse. - - - 有効にすると、プロンプト内をクリック時にカーソルがその位置に移動します。これを行うには、シェルでシェル統合を有効にし、期待どおりに動作させる必要があります。 - A description for what the "Reload environment variables" setting does. Presented near "Profile_RepositionCursorWithMouse". - + + クリックした位置にカーソルを移動 + This allows the user to move the text cursor just by clicking with the mouse. + + + 現在編集中のコマンドライン内をクリックすると、テキストカーソルがその位置へ移動します。編集可能な範囲を安全に判定するため、シェル統合が必要です。 + A description for what the "Click to position cursor" setting does. Presented near "Profile_RepositionCursorWithMouse". + 音によるチャイム An option to choose from for the "bell style" setting. When selected, an audible cue is used to notify the user. @@ -2752,4 +2752,4 @@ 入力してアイコンをフィルター処理します Placeholder text for a text box to filter and select an icon. - \ No newline at end of file + diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 0376bc989..7bff455e2 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -20,8 +20,10 @@ Author(s): #if defined(WT_BRANDING_WINTERM) #define MTSM_FIRST_WINDOW_PREFERENCE_DEFAULT FirstWindowPreference::PersistedLayout +#define MTSM_REPOSITION_CURSOR_WITH_MOUSE_DEFAULT true #else #define MTSM_FIRST_WINDOW_PREFERENCE_DEFAULT FirstWindowPreference::DefaultProfile +#define MTSM_REPOSITION_CURSOR_WITH_MOUSE_DEFAULT false #endif #define MTSM_GLOBAL_SETTINGS(X) \ @@ -124,7 +126,7 @@ Author(s): X(bool, AutoMarkPrompts, "autoMarkPrompts", true) \ X(bool, AutoInjectShellIntegration, "shellIntegration.autoInject", true) \ X(bool, ShowMarks, "showMarksOnScrollbar", false) \ - X(bool, RepositionCursorWithMouse, "experimental.repositionCursorWithMouse", false) \ + X(bool, RepositionCursorWithMouse, "experimental.repositionCursorWithMouse", MTSM_REPOSITION_CURSOR_WITH_MOUSE_DEFAULT) \ X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) \ X(bool, RainbowSuggestions, "experimental.rainbowSuggestions", false) \ X(bool, ForceVTInput, "compatibility.input.forceVT", false) \ diff --git a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp index 9a5601810..cb57d2588 100644 --- a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp @@ -68,7 +68,9 @@ namespace ControlUnitTests TEST_METHOD(TestSelectOutputScrolling); TEST_METHOD(TestSelectOutputExactWrap); - TEST_METHOD(TestSimpleClickSelection); + TEST_METHOD(TestSimpleClickSelection); + TEST_METHOD(TestCursorRepositionSafetyAndMovement); + TEST_METHOD(TestCursorRepositionWrappedWideAndPaneIsolation); TEST_CLASS_SETUP(ModuleSetup) { @@ -1534,7 +1536,7 @@ namespace ControlUnitTests } } - void ControlCoreTests::TestSimpleClickSelection() + void ControlCoreTests::TestSimpleClickSelection() { // Create a simple selection with the mouse, then click somewhere else, // and confirm the selection got updated. @@ -1595,6 +1597,162 @@ namespace ControlUnitTests VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } - VERIFY_IS_TRUE(gotSelectionUpdate); - } -} + VERIFY_IS_TRUE(gotSelectionUpdate); + } + + void ControlCoreTests::TestCursorRepositionSafetyAndMovement() + { + const auto arrows = [](const wchar_t direction, const size_t count) { + std::wstring value; + for (size_t i = 0; i < count; ++i) + { + value.append(L"\x1b["); + value.push_back(direction); + } + return value; + }; + + const auto createPrompt = [&](const bool enabled, const std::wstring_view command) { + auto [settings, conn] = _createSettingsAndConnection(); + settings->RepositionCursorWithMouse(enabled); + auto core = createCore(*settings, *conn); + _standardInit(core); + + std::wstring prompt{ L"\x1b]133;A\aPS> \x1b]133;B\a" }; + prompt.append(command); + conn->WriteInput(winrt_wstring_to_array_view(prompt)); + return std::make_tuple(settings, conn, core); + }; + + { + auto [settings, conn, core] = createPrompt(false, L"abcdef"); + std::wstring sent; + conn->TerminalOutput([&](const winrt::array_view value) { + sent.append(winrt_array_to_wstring_view(value)); + }); + + core->RepositionCursorWithMouse({ 7, 0 }); + VERIFY_IS_TRUE(sent.empty(), L"An explicit false setting must disable cursor repositioning"); + } + + { + auto [settings, conn, core] = createPrompt(true, L"abcdef"); + std::wstring sent; + conn->TerminalOutput([&](const winrt::array_view value) { + sent.append(winrt_array_to_wstring_view(value)); + }); + + // microsoft/terminal#20442: padding and malformed coordinates + // must not reach TextBuffer iterators out of bounds. + core->RepositionCursorWithMouse({ -1, 0 }); + core->RepositionCursorWithMouse({ 5, -1 }); + core->RepositionCursorWithMouse({ 5, 20 }); + core->RepositionCursorWithMouse({ til::CoordTypeMax, til::CoordTypeMax }); + VERIFY_IS_TRUE(sent.empty()); + + core->RepositionCursorWithMouse({ 7, 0 }); + VERIFY_ARE_EQUAL(arrows(L'D', 3), sent, L"A click before the cursor sends LEFT events"); + + sent.clear(); + core->RepositionCursorWithMouse({ 10, 0 }); + VERIFY_ARE_EQUAL(arrows(L'C', 3), sent, L"A click after the cursor sends RIGHT events"); + + sent.clear(); + core->RepositionCursorWithMouse({ 4, 0 }); + VERIFY_ARE_EQUAL(arrows(L'D', 6), sent, L"The command start is a valid destination"); + + sent.clear(); + core->RepositionCursorWithMouse({ 200, 0 }); + VERIFY_ARE_EQUAL(arrows(L'C', 6), sent, L"Right padding clamps to the editable command end"); + } + + { + auto [settings, conn, core] = createPrompt(true, L""); + std::wstring sent; + conn->TerminalOutput([&](const winrt::array_view value) { + sent.append(winrt_array_to_wstring_view(value)); + }); + core->RepositionCursorWithMouse({ 4, 0 }); + VERIFY_IS_TRUE(sent.empty(), L"An empty command is a safe no-op"); + } + + { + auto [settings, conn] = _createSettingsAndConnection(); + settings->RepositionCursorWithMouse(true); + auto core = createCore(*settings, *conn); + _standardInit(core); + conn->WriteInput(winrt_wstring_to_array_view( + L"\x1b]133;A\aPS> \x1b]133;B\aold\x1b]133;C\a\r\noutput\r\n" + L"\x1b]133;D;0\a\x1b]133;A\aPS> \x1b]133;B\anew")); + + std::wstring sent; + conn->TerminalOutput([&](const winrt::array_view value) { + sent.append(winrt_array_to_wstring_view(value)); + }); + core->RepositionCursorWithMouse({ 5, 0 }); + VERIFY_IS_TRUE(sent.empty(), L"Historical command output is never an editable destination"); + } + } + + void ControlCoreTests::TestCursorRepositionWrappedWideAndPaneIsolation() + { + const auto arrows = [](const wchar_t direction, const size_t count) { + std::wstring value; + for (size_t i = 0; i < count; ++i) + { + value.append(L"\x1b["); + value.push_back(direction); + } + return value; + }; + + const auto createPrompt = [&](const std::wstring_view command) { + auto [settings, conn] = _createSettingsAndConnection(); + settings->RepositionCursorWithMouse(true); + auto core = createCore(*settings, *conn); + _standardInit(core); + std::wstring prompt{ L"\x1b]133;A\aPS> \x1b]133;B\a" }; + prompt.append(command); + conn->WriteInput(winrt_wstring_to_array_view(prompt)); + return std::make_tuple(settings, conn, core); + }; + + { + auto [settings, conn, core] = createPrompt(L"\u4f60a"); + std::wstring sent; + conn->TerminalOutput([&](const winrt::array_view value) { + sent.append(winrt_array_to_wstring_view(value)); + }); + core->RepositionCursorWithMouse({ 4, 0 }); + VERIFY_ARE_EQUAL(arrows(L'D', 2), sent, L"A double-cell CJK glyph produces one LEFT event"); + } + + { + const std::wstring wrappedCommand(40, L'a'); + auto [settings, conn, core] = createPrompt(wrappedCommand); + std::wstring sent; + conn->TerminalOutput([&](const winrt::array_view value) { + sent.append(winrt_array_to_wstring_view(value)); + }); + core->RepositionCursorWithMouse({ 4, 0 }); + VERIFY_ARE_EQUAL(arrows(L'D', wrappedCommand.size()), sent, L"Wrapped commands remain navigable"); + } + + { + auto [settingsA, connA, coreA] = createPrompt(L"pane-a"); + auto [settingsB, connB, coreB] = createPrompt(L"pane-b"); + std::wstring sentA; + std::wstring sentB; + connA->TerminalOutput([&](const winrt::array_view value) { + sentA.append(winrt_array_to_wstring_view(value)); + }); + connB->TerminalOutput([&](const winrt::array_view value) { + sentB.append(winrt_array_to_wstring_view(value)); + }); + + coreA->RepositionCursorWithMouse({ 4, 0 }); + VERIFY_ARE_EQUAL(arrows(L'D', 6), sentA); + VERIFY_IS_TRUE(sentB.empty(), L"A click can only write to its owning pane connection"); + } + } +} diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp index ee020bb3c..8ea34c02d 100644 --- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp @@ -43,6 +43,8 @@ namespace ControlUnitTests TEST_METHOD(GetMouseEventsInTest); TEST_METHOD(AltBufferClampMouse); TEST_METHOD(RightClickCopiesSelectionThenPastes); + TEST_METHOD(CursorRepositionOccursOnReleaseOnly); + TEST_METHOD(CursorRepositionDragAndSelectionPriority); TEST_CLASS_SETUP(ClassSetup) { @@ -973,7 +975,7 @@ namespace ControlUnitTests VERIFY_ARE_EQUAL(1u, pasteCount); } - void ControlInteractivityTests::AltBufferClampMouse() + void ControlInteractivityTests::AltBufferClampMouse() { // This is a test for // * GH#10642 @@ -1081,6 +1083,90 @@ namespace ControlUnitTests 0, // timestamp modifiers, cursorPosition1.to_core_point()); - VERIFY_ARE_EQUAL(0u, expectedOutput.size(), L"Validate we drained all the expected output"); - } -} + VERIFY_ARE_EQUAL(0u, expectedOutput.size(), L"Validate we drained all the expected output"); + } + + void ControlInteractivityTests::CursorRepositionOccursOnReleaseOnly() + { + auto [settings, conn] = _createSettingsAndConnection(); + settings->RepositionCursorWithMouse(true); + auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); + _standardInit(core, interactivity); + conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;A\aPS> \x1b]133;B\aabcdef")); + + std::wstring sent; + conn->TerminalOutput([&](const winrt::array_view value) { + sent.append(winrt_array_to_wstring_view(value)); + }); + + const auto font = core->FontSizeInDips(); + const Core::Point click{ static_cast(font.Width * 5.5f), static_cast(font.Height * 0.5f) }; + const auto modifiers = ControlKeyStates{}; + const auto leftMouseDown = Control::MouseButtonState::IsLeftButtonDown; + const Control::MouseButtonState noMouseDown{}; + + interactivity->PointerPressed(leftMouseDown, WM_LBUTTONDOWN, 1, modifiers, click); + VERIFY_IS_TRUE(sent.empty(), L"Mouse-down must not reposition the shell cursor"); + VERIFY_IS_TRUE(interactivity->_cursorRepositionPending); + + interactivity->PointerReleased(noMouseDown, WM_LBUTTONUP, modifiers, click); + VERIFY_ARE_EQUAL(std::wstring{ L"\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D" }, sent); + VERIFY_IS_FALSE(interactivity->_cursorRepositionPending); + } + + void ControlInteractivityTests::CursorRepositionDragAndSelectionPriority() + { + auto [settings, conn] = _createSettingsAndConnection(); + settings->RepositionCursorWithMouse(true); + auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); + _standardInit(core, interactivity); + conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;A\aPS> \x1b]133;B\aabcdef")); + + std::wstring sent; + conn->TerminalOutput([&](const winrt::array_view value) { + sent.append(winrt_array_to_wstring_view(value)); + }); + + const auto font = core->FontSizeInDips(); + const Core::Point start{ static_cast(font.Width * 5.5f), static_cast(font.Height * 0.5f) }; + const Core::Point end{ static_cast(font.Width * 8.5f), static_cast(font.Height * 0.5f) }; + const auto modifiers = ControlKeyStates{}; + const auto leftMouseDown = Control::MouseButtonState::IsLeftButtonDown; + const Control::MouseButtonState noMouseDown{}; + + interactivity->PointerPressed(leftMouseDown, WM_LBUTTONDOWN, 1, modifiers, start); + interactivity->PointerMoved(leftMouseDown, WM_LBUTTONDOWN, modifiers, true, end, true); + VERIFY_IS_TRUE(core->HasSelection()); + VERIFY_IS_FALSE(interactivity->_cursorRepositionPending); + interactivity->PointerReleased(noMouseDown, WM_LBUTTONUP, modifiers, end); + VERIFY_IS_TRUE(sent.empty(), L"A drag selection must never move the command-line cursor first"); + + core->ClearSelection(); + const auto shift = ControlKeyStates{ SHIFT_PRESSED }; + const auto shiftClickTime = interactivity->_multiClickTimer + 10; + interactivity->PointerPressed(leftMouseDown, WM_LBUTTONDOWN, shiftClickTime, shift, start); + VERIFY_IS_TRUE(core->HasSelection(), L"Shift+Click retains selection semantics"); + interactivity->PointerReleased(noMouseDown, WM_LBUTTONUP, shift, start); + VERIFY_IS_TRUE(sent.empty()); + + // Establish the first click of a fresh multi-click sequence. A first + // click may reposition normally; the second and third clicks must be + // owned exclusively by word/line selection. + core->ClearSelection(); + const auto firstClickTime = shiftClickTime + interactivity->_multiClickTimer + 10; + interactivity->PointerPressed(leftMouseDown, WM_LBUTTONDOWN, firstClickTime, modifiers, start); + interactivity->PointerReleased(noMouseDown, WM_LBUTTONUP, modifiers, start); + sent.clear(); + + interactivity->PointerPressed(leftMouseDown, WM_LBUTTONDOWN, firstClickTime + 1, modifiers, start); + VERIFY_IS_TRUE(core->HasSelection(), L"The second click retains word-selection semantics"); + interactivity->PointerReleased(noMouseDown, WM_LBUTTONUP, modifiers, start); + VERIFY_IS_TRUE(sent.empty()); + + core->ClearSelection(); + interactivity->PointerPressed(leftMouseDown, WM_LBUTTONDOWN, firstClickTime + 2, modifiers, start); + VERIFY_IS_TRUE(core->HasSelection(), L"The third click retains line-selection semantics"); + interactivity->PointerReleased(noMouseDown, WM_LBUTTONUP, modifiers, start); + VERIFY_IS_TRUE(sent.empty()); + } +} diff --git a/src/cascadia/UnitTests_SettingsModel/TerminalSettingsTests.cpp b/src/cascadia/UnitTests_SettingsModel/TerminalSettingsTests.cpp index 90aba8669..c160a4991 100644 --- a/src/cascadia/UnitTests_SettingsModel/TerminalSettingsTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/TerminalSettingsTests.cpp @@ -53,10 +53,40 @@ namespace SettingsModelUnitTests TEST_METHOD(MakeSettingsForDefaultProfileThatDoesntExist); TEST_METHOD(TestLayerProfileOnColorScheme); TEST_METHOD(TestCommandlineToTitlePromotion); - TEST_METHOD(TestInitialPositionParsing); - }; - - // CascadiaSettings::_normalizeCommandLine abuses some aspects from CommandLineToArgvW + TEST_METHOD(TestInitialPositionParsing); + TEST_METHOD(TestCursorRepositionDefaultAndOverride); + }; + + void TerminalSettingsTests::TestCursorRepositionDefaultAndOverride() + { + static constexpr std::string_view defaultJson{ R"({ + "profiles": [ + { "name": "default", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" } + ] + })" }; + static constexpr std::string_view disabledJson{ R"({ + "profiles": [ + { + "name": "disabled", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "experimental.repositionCursorWithMouse": false + } + ] + })" }; + + const auto defaults = winrt::make_self(defaultJson); +#if defined(WT_BRANDING_WINTERM) + VERIFY_IS_TRUE(defaults->AllProfiles().GetAt(0).RepositionCursorWithMouse()); +#else + VERIFY_IS_FALSE(defaults->AllProfiles().GetAt(0).RepositionCursorWithMouse()); +#endif + + const auto disabled = winrt::make_self(disabledJson); + VERIFY_IS_FALSE(disabled->AllProfiles().GetAt(0).RepositionCursorWithMouse(), + L"An explicit user false must override the winTerm default"); + } + + // CascadiaSettings::_normalizeCommandLine abuses some aspects from CommandLineToArgvW // to simplify the implementation. It assumes that all arguments returned by // CommandLineToArgvW are returned back to back in memory as "arg1\0arg2\0arg3\0...". // This test ensures CommandLineToArgvW doesn't change just to be sure. diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.rc b/src/cascadia/WindowsTerminal/WindowsTerminal.rc index bccda1209..7cad9ea00 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.rc +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.rc @@ -83,8 +83,8 @@ IDI_APPICON_HC_WHITE ICON "..\\..\\..\\res\\terminal\\imag #if defined(WT_BRANDING_WINTERM) 1 VERSIONINFO - FILEVERSION 1,4,0,1 - PRODUCTVERSION 1,4,0,1 + FILEVERSION 1,4,1,0 + PRODUCTVERSION 1,4,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG @@ -101,12 +101,12 @@ BEGIN BEGIN VALUE "CompanyName", "helloThisWorld\0" VALUE "FileDescription", "winTerm Terminal Host\0" - VALUE "FileVersion", "1.4.0.1\0" + VALUE "FileVersion", "1.4.1.0\0" VALUE "InternalName", "WindowsTerminal\0" VALUE "LegalCopyright", "Copyright (c) winTerm contributors. Portions copyright Microsoft Corporation.\0" VALUE "OriginalFilename", "WindowsTerminal.exe\0" VALUE "ProductName", "winTerm\0" - VALUE "ProductVersion", "1.4.0-beta\0" + VALUE "ProductVersion", "1.4.1\0" END END BLOCK "VarFileInfo" diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h index c9bab8cac..9f7c6ab09 100644 --- a/src/cascadia/inc/ControlProperties.h +++ b/src/cascadia/inc/ControlProperties.h @@ -1,7 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#pragma once +#pragma once + +#if defined(WT_BRANDING_WINTERM) +#define CONTROL_REPOSITION_CURSOR_WITH_MOUSE_DEFAULT true +#else +#define CONTROL_REPOSITION_CURSOR_WITH_MOUSE_DEFAULT false +#endif // --------------------------- Core Appearance --------------------------- // All of these settings are defined in ICoreAppearance. @@ -55,7 +61,7 @@ X(bool, CommandTimelineEnabled, true) \ X(int32_t, CommandTimelineHistoryLimit, 500) \ X(bool, AutoMarkPrompts) \ - X(bool, RepositionCursorWithMouse, false) \ + X(bool, RepositionCursorWithMouse, CONTROL_REPOSITION_CURSOR_WITH_MOUSE_DEFAULT) \ X(bool, RainbowSuggestions) \ X(bool, AllowVtChecksumReport) \ X(bool, AllowVtClipboardWrite, true) diff --git a/src/cascadia/wt/wt.rc b/src/cascadia/wt/wt.rc index 7a812666e..3b6812955 100644 --- a/src/cascadia/wt/wt.rc +++ b/src/cascadia/wt/wt.rc @@ -58,8 +58,8 @@ IDI_APPICON ICON "..\\..\\..\\res\\terminal.ico" #if defined(WT_BRANDING_WINTERM) 1 VERSIONINFO - FILEVERSION 1,4,0,1 - PRODUCTVERSION 1,4,0,1 + FILEVERSION 1,4,1,0 + PRODUCTVERSION 1,4,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG @@ -76,12 +76,12 @@ BEGIN BEGIN VALUE "CompanyName", "helloThisWorld\0" VALUE "FileDescription", "winTerm Launcher\0" - VALUE "FileVersion", "1.4.0.1\0" + VALUE "FileVersion", "1.4.1.0\0" VALUE "InternalName", "winTerm\0" VALUE "LegalCopyright", "Copyright (c) winTerm contributors. Portions copyright Microsoft Corporation.\0" VALUE "OriginalFilename", "winTerm.exe\0" VALUE "ProductName", "winTerm\0" - VALUE "ProductVersion", "1.4.0-beta\0" + VALUE "ProductVersion", "1.4.1\0" END END BLOCK "VarFileInfo" diff --git a/src/winterm-tools/winterm-shim/winterm-shim.rc b/src/winterm-tools/winterm-shim/winterm-shim.rc index befa6a8e6..3ee9d994f 100644 --- a/src/winterm-tools/winterm-shim/winterm-shim.rc +++ b/src/winterm-tools/winterm-shim/winterm-shim.rc @@ -4,8 +4,8 @@ #include 1 VERSIONINFO - FILEVERSION 1,4,0,1 - PRODUCTVERSION 1,4,0,1 + FILEVERSION 1,4,1,0 + PRODUCTVERSION 1,4,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG @@ -22,12 +22,12 @@ BEGIN BEGIN VALUE "CompanyName", "helloThisWorld\0" VALUE "FileDescription", "winTerm Shell Integration Helper\0" - VALUE "FileVersion", "1.4.0.1\0" + VALUE "FileVersion", "1.4.1.0\0" VALUE "InternalName", "winterm-shim\0" VALUE "LegalCopyright", "Copyright (c) winTerm contributors.\0" VALUE "OriginalFilename", "winterm-shim.exe\0" VALUE "ProductName", "winTerm\0" - VALUE "ProductVersion", "1.4.0-beta\0" + VALUE "ProductVersion", "1.4.1\0" END END BLOCK "VarFileInfo" diff --git a/src/winterm/Branding/ReleaseMetadata.h b/src/winterm/Branding/ReleaseMetadata.h index bcb615f96..2356e932b 100644 --- a/src/winterm/Branding/ReleaseMetadata.h +++ b/src/winterm/Branding/ReleaseMetadata.h @@ -25,8 +25,8 @@ namespace winTerm::Branding { inline constexpr std::wstring_view Publisher{ L"helloThisWorld" }; - inline constexpr std::wstring_view ApplicationVersion{ L"1.4.0-beta" }; - inline constexpr std::wstring_view ReleaseChannel{ L"Beta" }; + inline constexpr std::wstring_view ApplicationVersion{ L"1.4.1" }; + inline constexpr std::wstring_view ReleaseChannel{ L"Stable" }; inline constexpr std::wstring_view CommitSha{ WINTERM_BUILD_COMMIT_SHA }; inline constexpr std::wstring_view BuildTimestamp{ WINTERM_BUILD_TIMESTAMP }; inline constexpr std::wstring_view WorkflowRunId{ WINTERM_BUILD_WORKFLOW_RUN_ID }; diff --git a/src/winterm/Branding/version.json b/src/winterm/Branding/version.json index 195a855d3..c10eb084a 100644 --- a/src/winterm/Branding/version.json +++ b/src/winterm/Branding/version.json @@ -1,10 +1,10 @@ { - "applicationVersion": "1.4.0-beta", - "packageVersion": "1.4.0.1", - "moduleVersion": "1.4.0", - "modulePrerelease": "beta", - "channel": "beta", - "tag": "v1.4.0-beta", + "applicationVersion": "1.4.1", + "packageVersion": "1.4.1.0", + "moduleVersion": "1.4.1", + "modulePrerelease": "", + "channel": "stable", + "tag": "v1.4.1", "workspaceSchemaVersion": 2, "dockingModelVersion": 1, "shellProtocolVersion": 1, diff --git a/src/winterm/Workspaces/Model/WorkspaceDescriptor.h b/src/winterm/Workspaces/Model/WorkspaceDescriptor.h index 55829fa25..12a99a934 100644 --- a/src/winterm/Workspaces/Model/WorkspaceDescriptor.h +++ b/src/winterm/Workspaces/Model/WorkspaceDescriptor.h @@ -219,7 +219,7 @@ namespace winTerm::Workspaces std::string createdAt; std::string updatedAt; WorkspaceSource source{ WorkspaceSource::User }; - std::string applicationVersion{ "1.4.0-beta" }; + std::string applicationVersion{ "1.4.1" }; uint32_t protocolVersion{ 1 }; uint32_t dockingModelVersion{ DockingModelVersion }; WorkspaceStartupBehavior startupBehavior; diff --git a/src/winterm/Workspaces/Persistence/WorkspaceSerializer.cpp b/src/winterm/Workspaces/Persistence/WorkspaceSerializer.cpp index 7f6fd2633..053577454 100644 --- a/src/winterm/Workspaces/Persistence/WorkspaceSerializer.cpp +++ b/src/winterm/Workspaces/Persistence/WorkspaceSerializer.cpp @@ -618,7 +618,7 @@ WorkspaceDescriptor WorkspaceSerializer::FromJson(const Json::Value& json, const throw std::runtime_error("The workspace source is not supported."); } workspace.source = *source; - workspace.applicationVersion = StringOrDefault(json, "applicationVersion", "1.4.0-beta"); + workspace.applicationVersion = StringOrDefault(json, "applicationVersion", "1.4.1"); workspace.protocolVersion = UIntOrDefault(json, "protocolVersion", 1); workspace.dockingModelVersion = UIntOrDefault(json, "dockingModelVersion", DockingModelVersion); if (const auto& startup = json["startupBehavior"]; !startup.isNull())