mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat: notify-only startup update check
Ask the GitHub releases API once, in the background, on GUI startup whether a newer version has shipped and — if so — surface a "Download" prompt in Settings → About that links to the Releases page. tty7 never downloads or self-updates; the user installs by hand, exactly as the README's Install section describes. - core::update: UpdateStatus global, version parse/compare, and the GitHub fetch via reqwest_client. That reqwest+rustls stack is already compiled into the tree through gpui-component-assets, so this pins no new native code — it only exposes what we already build. - config: check_for_updates toggle (default true) to skip the network call entirely for offline / privacy. - Settings -> About: an "Updates" section with the version prompt, a Download button (default style, matching the sibling actions), and the toggle. - Fails soft: no network, a rate-limit, a private/renamed repo, or an unparseable tag all collapse to "no prompt", logged at debug and never surfaced. The terminal opens the same whether or not GitHub is reachable.
This commit is contained in:
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Startup update check: tty7 asks GitHub once, in the background, whether a
|
||||
newer release has shipped and — if so — shows a "Download" prompt in
|
||||
Settings → About that opens the Releases page. It never downloads or updates
|
||||
itself; you still install by hand. Turn the check off with `check_for_updates`
|
||||
in `config.json` or the "Check for updates on launch" toggle in About. A
|
||||
failed or offline check is silent.
|
||||
- ⌘K (Ctrl+K on Windows/Linux) clears the screen and scrollback — the same
|
||||
"Clear" the right-click menu already offered, now on the keyboard shortcut
|
||||
Terminal.app, iTerm2, and Ghostty users expect. Also available from the
|
||||
|
||||
Generated
+226
-14
@@ -645,6 +645,29 @@ dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
@@ -681,7 +704,7 @@ dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.11.0",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
@@ -950,6 +973,12 @@ dependencies = [
|
||||
"shlex 2.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
@@ -1016,6 +1045,15 @@ dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.25.0"
|
||||
@@ -1103,6 +1141,16 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "command-fds"
|
||||
version = "0.3.3"
|
||||
@@ -1512,7 +1560,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1712,7 +1760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1985,6 +2033,12 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@@ -2302,7 +2356,7 @@ dependencies = [
|
||||
"log",
|
||||
"presser",
|
||||
"thiserror 2.0.18",
|
||||
"windows 0.62.2",
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2934,6 +2988,15 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http_client_tls"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
@@ -3009,7 +3072,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.62.2",
|
||||
"windows-core 0.58.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3350,6 +3413,22 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys 0.3.1",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
@@ -3878,7 +3957,7 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4105,7 +4184,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5506,6 +5585,24 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"futures",
|
||||
"gpui_util",
|
||||
"http_client",
|
||||
"http_client_tls",
|
||||
"log",
|
||||
"regex",
|
||||
"serde",
|
||||
"tokio",
|
||||
"zed-reqwest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resvg"
|
||||
version = "0.45.1"
|
||||
@@ -5682,7 +5779,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5695,7 +5792,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5715,6 +5812,8 @@ version = "0.23.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -5754,12 +5853,40 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs 0.26.11",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@@ -6317,7 +6444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6363,7 +6490,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6710,7 +6837,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7185,6 +7312,7 @@ dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"portable-pty",
|
||||
"reqwest_client",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
@@ -7213,7 +7341,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset 0.9.1",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7765,6 +7893,24 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
|
||||
dependencies = [
|
||||
"webpki-root-certs 1.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
@@ -7965,7 +8111,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8316,6 +8462,15 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -8352,6 +8507,21 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
@@ -8412,6 +8582,12 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -8424,6 +8600,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -8436,6 +8618,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -8460,6 +8648,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -8472,6 +8666,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -8484,6 +8684,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -8496,6 +8702,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
|
||||
@@ -26,6 +26,15 @@ smallvec.workspace = true
|
||||
serde = { workspace = true }
|
||||
serde_json.workspace = true
|
||||
|
||||
# HTTP client for the startup update check (`core::update`): one GET to the
|
||||
# GitHub releases API to see if a newer version has shipped. `reqwest_client`
|
||||
# wraps Zed's `zed-reqwest` fork behind gpui's `http_client` trait (re-exported
|
||||
# as `gpui::http_client`) and manages its own tokio runtime. That reqwest+rustls
|
||||
# stack is *already* compiled into the tree via `gpui-component-assets`, so this
|
||||
# pins no new native code — it only exposes what we're already building. Pinned
|
||||
# to gpui's rev so the shared `http_client`/`zed-reqwest` versions stay aligned.
|
||||
reqwest_client = { git = "https://github.com/zed-industries/zed", rev = "1d217ee39d381ac101b7cf49d3d22451ac1093fe" }
|
||||
|
||||
# Zed's fork of alacritty_terminal — same rev Zed pins. Used by the *client*
|
||||
# (`terminal::remote`) for the VT parser + grid (`Term`/`ansi::Processor`) that
|
||||
# renders the mirror. The daemon's PTY itself is driven by `portable-pty` below.
|
||||
|
||||
@@ -68,6 +68,11 @@ pub struct Config {
|
||||
/// finishes.
|
||||
#[serde(default, deserialize_with = "de_lenient")]
|
||||
pub notify_on_command_finish: NotifyMode,
|
||||
/// On startup, ask GitHub whether a newer release has shipped and, if so,
|
||||
/// surface a "download" prompt in Settings → About. Never downloads or
|
||||
/// self-updates — it only links to the Releases page. On by default; set to
|
||||
/// `false` to skip the network call entirely (offline / privacy).
|
||||
pub check_for_updates: bool,
|
||||
|
||||
// ── Appearance ──────────────────────────────────────────────────────────
|
||||
/// The shape drawn for the terminal cursor.
|
||||
@@ -234,6 +239,9 @@ impl Default for Config {
|
||||
scrollback_limit: 10_000,
|
||||
new_tab_position: NewTabPosition::AfterCurrent,
|
||||
notify_on_command_finish: NotifyMode::Unfocused,
|
||||
// Opt-out, not opt-in: a stale terminal that never tells you it's
|
||||
// outdated is the status quo we're fixing. One cheap GET at startup.
|
||||
check_for_updates: true,
|
||||
cursor_style: CursorStyle::Block,
|
||||
// Input/mouse defaults preserve today's behavior: GPUI already hides
|
||||
// the pointer while typing (its `CursorHideMode` default), so this
|
||||
|
||||
@@ -12,3 +12,4 @@ pub mod config;
|
||||
pub mod osc;
|
||||
pub mod session;
|
||||
pub mod threads;
|
||||
pub mod update;
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
//! Notify-only update check.
|
||||
//!
|
||||
//! On GUI startup (unless `config.check_for_updates` is off) we make one GET to
|
||||
//! the GitHub releases API, compare the latest published version against the
|
||||
//! running binary, and — if it's newer — stash an [`UpdateStatus`] global that
|
||||
//! Settings → About reads to show a "download" prompt linking to the Releases
|
||||
//! page. That's the whole feature: we never download, replace, or restart
|
||||
//! anything. The user updates by hand (drag the new `.app`, unzip, …), exactly
|
||||
//! as the README's Install section describes.
|
||||
//!
|
||||
//! Everything here fails soft: no network, a rate-limit, a private/renamed repo,
|
||||
//! an unparseable tag — all collapse to "no prompt", logged at `debug` and never
|
||||
//! surfaced. A terminal must open the same whether or not GitHub is reachable.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::http_client::{AsyncBody, HttpClient as _, HttpRequestExt as _, RedirectPolicy};
|
||||
use gpui::{App, Global, http_client};
|
||||
use reqwest_client::ReqwestClient;
|
||||
use smol::io::AsyncReadExt as _;
|
||||
|
||||
use crate::core::config::Config;
|
||||
|
||||
/// `owner/repo` the release check queries — matches the repository the binary is
|
||||
/// published from (see `Cargo.toml`'s `repository`).
|
||||
const REPO: &str = "l0ng-ai/tty7";
|
||||
|
||||
/// Where the "Download" prompt points. GitHub's `/releases/latest` alias always
|
||||
/// resolves to the newest published (non-prerelease) build, so it never goes
|
||||
/// stale as versions roll — no need to embed a specific tag.
|
||||
pub const RELEASES_URL: &str = "https://github.com/l0ng-ai/tty7/releases/latest";
|
||||
|
||||
/// A newer release than the one currently running.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AvailableUpdate {
|
||||
/// The newer version, normalized without a leading `v` (e.g. `"0.3.1"`), for
|
||||
/// display in the About panel.
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
/// The result of the startup update check, stored as a GPUI global so the
|
||||
/// Settings view can read it. Absent until the check completes; `available` is
|
||||
/// `None` when we're already current (or the check failed / was skipped).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct UpdateStatus {
|
||||
pub available: Option<AvailableUpdate>,
|
||||
}
|
||||
|
||||
impl Global for UpdateStatus {}
|
||||
|
||||
/// Kick off the background update check. Returns immediately; the network work
|
||||
/// runs on a detached task and, if a newer version exists, writes the
|
||||
/// [`UpdateStatus`] global and repaints so an already-open About panel updates.
|
||||
///
|
||||
/// Honors `config.check_for_updates`: when off, we make no network call at all.
|
||||
pub fn spawn_check(cx: &mut App) {
|
||||
if !cx.global::<Config>().check_for_updates {
|
||||
return;
|
||||
}
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let current = env!("CARGO_PKG_VERSION");
|
||||
let latest = match fetch_latest_version().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
// `{e:#}` includes the anyhow context chain; kept at debug so a
|
||||
// routine offline start doesn't spam the log.
|
||||
log::debug!("update check skipped: {e:#}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !is_update_available(&latest, current) {
|
||||
log::debug!("update check: up to date (latest {latest}, running {current})");
|
||||
return;
|
||||
}
|
||||
|
||||
let version = latest.trim_start_matches('v').to_string();
|
||||
log::info!("update available: {version} (running {current})");
|
||||
let _ = cx.update(|cx| {
|
||||
cx.set_global(UpdateStatus {
|
||||
available: Some(AvailableUpdate { version }),
|
||||
});
|
||||
// Repaint so a Settings → About that's already open shows the prompt
|
||||
// now rather than only on the next interaction.
|
||||
cx.refresh_windows();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// The `tag_name` field of GitHub's release payload — the only piece we read.
|
||||
#[derive(serde::Deserialize)]
|
||||
struct LatestRelease {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
/// GET the repo's latest release and return its raw tag (e.g. `"v0.3.1"`).
|
||||
async fn fetch_latest_version() -> Result<String> {
|
||||
// GitHub rejects requests without a User-Agent; identify ourselves. The
|
||||
// reqwest+rustls stack this rides on is already compiled into the app via
|
||||
// `gpui-component-assets`, so constructing a client here is cheap.
|
||||
let client = ReqwestClient::user_agent(concat!("tty7/", env!("CARGO_PKG_VERSION")))
|
||||
.context("building HTTP client")?;
|
||||
|
||||
let url = format!("https://api.github.com/repos/{REPO}/releases/latest");
|
||||
let request = http_client::Request::get(&url)
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.header("X-GitHub-Api-Version", "2022-11-28")
|
||||
.follow_redirects(RedirectPolicy::FollowAll)
|
||||
.body(AsyncBody::default())
|
||||
.context("building request")?;
|
||||
|
||||
let mut response = client
|
||||
.send(request)
|
||||
.await
|
||||
.context("requesting latest release")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("GitHub API returned HTTP {}", response.status().as_u16());
|
||||
}
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("reading response body")?;
|
||||
|
||||
let release: LatestRelease =
|
||||
serde_json::from_slice(&body).context("parsing release JSON")?;
|
||||
Ok(release.tag_name)
|
||||
}
|
||||
|
||||
/// Parse a version string into a `(major, minor, patch)` triple, tolerating a
|
||||
/// leading `v` and ignoring any pre-release / build suffix (`-rc.1`, `+build`).
|
||||
/// Missing minor/patch components read as `0`. Returns `None` if the numeric
|
||||
/// core doesn't parse — the caller treats that as "don't prompt".
|
||||
fn parse_version(s: &str) -> Option<(u64, u64, u64)> {
|
||||
let core = s.trim().trim_start_matches('v');
|
||||
// A pre-release/build tag (`0.4.0-rc.1`, `0.4.0+ci`) compares by its release
|
||||
// core here; we don't ship pre-releases, so finer ordering isn't worth it.
|
||||
let core = core.split(['-', '+']).next().unwrap_or(core);
|
||||
let mut parts = core.split('.');
|
||||
let major = parts.next()?.parse().ok()?;
|
||||
let minor = parts.next().unwrap_or("0").parse().ok()?;
|
||||
let patch = parts.next().unwrap_or("0").parse().ok()?;
|
||||
Some((major, minor, patch))
|
||||
}
|
||||
|
||||
/// Whether `latest` names a strictly newer version than `current`. If either
|
||||
/// side fails to parse we return `false`: an unrecognizable tag should never
|
||||
/// nag the user to "update" to something we can't even order.
|
||||
fn is_update_available(latest: &str, current: &str) -> bool {
|
||||
match (parse_version(latest), parse_version(current)) {
|
||||
(Some(latest), Some(current)) => latest > current,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_versions_with_and_without_prefix() {
|
||||
assert_eq!(parse_version("v0.3.1"), Some((0, 3, 1)));
|
||||
assert_eq!(parse_version("0.3.1"), Some((0, 3, 1)));
|
||||
assert_eq!(parse_version(" 1.2.0 "), Some((1, 2, 0)));
|
||||
// Missing components default to zero.
|
||||
assert_eq!(parse_version("v2"), Some((2, 0, 0)));
|
||||
assert_eq!(parse_version("v2.5"), Some((2, 5, 0)));
|
||||
// Pre-release / build metadata is ignored down to the release core.
|
||||
assert_eq!(parse_version("v0.4.0-rc.1"), Some((0, 4, 0)));
|
||||
assert_eq!(parse_version("0.4.0+ci.7"), Some((0, 4, 0)));
|
||||
// Garbage yields None.
|
||||
assert_eq!(parse_version("nightly"), None);
|
||||
assert_eq!(parse_version(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_newer_versions() {
|
||||
assert!(is_update_available("v0.3.1", "0.3.0"));
|
||||
assert!(is_update_available("v1.0.0", "0.9.9"));
|
||||
assert!(is_update_available("0.4.0", "0.3.99"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_same_or_older_versions() {
|
||||
assert!(!is_update_available("v0.3.0", "0.3.0"));
|
||||
assert!(!is_update_available("v0.2.9", "0.3.0"));
|
||||
assert!(!is_update_available("0.3.0", "0.3.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unparseable_tag_never_prompts() {
|
||||
assert!(!is_update_available("garbage", "0.3.0"));
|
||||
assert!(!is_update_available("v0.3.1", "garbage"));
|
||||
}
|
||||
}
|
||||
@@ -261,6 +261,10 @@ fn main() {
|
||||
crate::ui::theme::apply_cursor_hide_mode(cx);
|
||||
// Start watching `config.json` so edits hot-reload theme/colors live.
|
||||
spawn_config_watcher(cx);
|
||||
// Ask GitHub (once, in the background) whether a newer release exists;
|
||||
// if so, Settings → About surfaces a download prompt. Fails soft and
|
||||
// is a no-op when `check_for_updates` is disabled.
|
||||
crate::core::update::spawn_check(cx);
|
||||
keymap::init(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
|
||||
@@ -559,6 +559,13 @@ impl Tty7App {
|
||||
self.update_config(cx, |cfg| cfg.link_url = on);
|
||||
}
|
||||
|
||||
/// Toggle the startup update check (Settings → About). Takes effect on the
|
||||
/// next launch — this only persists the preference; it doesn't run or cancel
|
||||
/// an in-flight check.
|
||||
pub(crate) fn set_check_for_updates(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
self.update_config(cx, |cfg| cfg.check_for_updates = on);
|
||||
}
|
||||
|
||||
pub(crate) fn set_cursor_blink(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
self.update_config(cx, |cfg| cfg.cursor_blink = on);
|
||||
// Turning blink off mid-cycle could leave the cursor in its hidden phase;
|
||||
@@ -1634,6 +1641,25 @@ impl Tty7App {
|
||||
log::warn!("failed to open {}: {e}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the GitHub Releases page in the browser — the "Download" action of
|
||||
/// the Settings → About update prompt. Deliberately hand-off, not
|
||||
/// self-update: the newest build is one click away on the web.
|
||||
pub(crate) fn open_releases_page(&self) {
|
||||
let opener = if cfg!(target_os = "macos") {
|
||||
"open"
|
||||
} else if cfg!(windows) {
|
||||
"explorer"
|
||||
} else {
|
||||
"xdg-open"
|
||||
};
|
||||
if let Err(e) = std::process::Command::new(opener)
|
||||
.arg(crate::core::update::RELEASES_URL)
|
||||
.spawn()
|
||||
{
|
||||
log::warn!("failed to open releases page: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Tty7App {
|
||||
|
||||
@@ -1265,6 +1265,13 @@ impl Tty7App {
|
||||
let theme = cx.theme();
|
||||
let (foreground, muted_fg) = (theme.foreground, theme.muted_foreground);
|
||||
|
||||
// Startup update check (see `core::update`): a newer release, if one was
|
||||
// found, plus the toggle that controls whether we look at all.
|
||||
let update = cx
|
||||
.try_global::<crate::core::update::UpdateStatus>()
|
||||
.and_then(|s| s.available.clone());
|
||||
let check_for_updates = cx.global::<Config>().check_for_updates;
|
||||
|
||||
let logo = Arc::new(Image::from_bytes(
|
||||
ImageFormat::Png,
|
||||
include_bytes!("../../assets/logo@256.png").to_vec(),
|
||||
@@ -1313,6 +1320,65 @@ impl Tty7App {
|
||||
.child("GPU-rendered · daemon-backed · shell-aware"),
|
||||
),
|
||||
)
|
||||
// Updates: the startup check drops a newer version here if it found
|
||||
// one. We never self-update — "Download" just opens the Releases
|
||||
// page; the toggle turns the check off (see `core::update`).
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_6()
|
||||
.gap_2()
|
||||
.child(self.section_rule(cx))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(foreground)
|
||||
.child("Updates"),
|
||||
)
|
||||
.when_some(update, |this, upd| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.items_center()
|
||||
.child(div().text_sm().text_color(foreground).child(
|
||||
format!("Version {} is available.", upd.version),
|
||||
))
|
||||
.child(
|
||||
// Match the sibling "Restart Background
|
||||
// Service…" button (default style, not the
|
||||
// dark `.primary()` fill) so About reads as
|
||||
// one panel.
|
||||
Button::new("download-update")
|
||||
.label("Download")
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, _w, _cx| {
|
||||
this.open_releases_page()
|
||||
})),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(div().text_sm().text_color(muted_fg).child(
|
||||
"Check GitHub for a newer release on launch and show a download link here. tty7 never updates itself; the button opens the Releases page.",
|
||||
))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.child(
|
||||
Switch::new("check-updates")
|
||||
.checked(check_for_updates)
|
||||
.on_click(cx.listener(|this, on: &bool, _w, cx| {
|
||||
this.set_check_for_updates(*on, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(foreground)
|
||||
.child("Check for updates on launch"),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Manage that daemon. A fresh process is the only way to pick up a
|
||||
// macOS permission granted after it started (e.g. Full Disk Access),
|
||||
// to recover if it wedges, or to start clean — quitting/reopening the
|
||||
|
||||
Reference in New Issue
Block a user