在此之前每个输入框只有一个"是否全选"的 bool,光标恒画在文本末尾—— 模型里根本没有"光标在哪"这个概念,所以点不了、拖不了、方向键无处可去。 这类能力是输入框的地板不是天花板,因此落在 `ui/text_field.rs`:命中 换算与渲染共用同一个起点和格宽,两边各算一次必然在全角字符和居中字段 上漂移,而漂移几像素恰恰最难被报告、也最伤手感。 配色这轮追出两个根因,都不是参数写错: - `push_stroke` 画的是**实心**圆角矩形,靠调用方随后的填充盖住中心才 形成"环"。`panel`/`input` 不透明盖得住,而 `card`/`surface` 只有 4~8% 不透明度盖不住——描边色渗满整块,把卡片和按钮压深一整档。实测 Nebula 深色的分组卡片因此成了 (62,65,74),原型是 (56,61,73)。新增 `surface::over` 把叠加色与底色预合成,`push_group` 一个 quad 画完。 - 深色浮层底改从 `shell_bg` 提亮,不再从 `term_bg`。旧注释的理由是 "浮层要比内容层亮",方向对、起点错:Nebula 外壳比终端亮 20,从终端 起跳的浮层落在 (29,31,40),比它实际漂浮的那张外壳还暗 5 级。 分段选中片改回两个主题共用 `panel`。前两轮按"选中片必须比轨道亮"去分 主题,那条规则本身是错的——原型深色下选中片比轨道暗 18 级(按进去), 浅色下比轨道亮(浮起来),方向相反但对比都成立。同一个语义就该同一个 token。 顺带修掉一个多实例竞态:`ai_hook` 的原子写用固定临时文件名,两个 Nebula 同时自愈 `settings.json` 时后者 rename 扑空,报出一句"系统找不到 指定的文件"——指的却是那个临时文件,读起来像 settings.json 不见了。 `display/mod.rs` 8562 → 7974 行,SSH 交互层按 Warp 的 model/view/input 三分拆出 `ssh_editor_input.rs`。 506 passed。
172 lines
7.1 KiB
PowerShell
172 lines
7.1 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[ValidatePattern('^[0-9A-Za-z][0-9A-Za-z.-]*$')]
|
|
[string] $Version = 'unreleased',
|
|
|
|
[ValidateSet('debug', 'release')]
|
|
[string] $Configuration = 'release',
|
|
|
|
[switch] $SkipBuild,
|
|
[switch] $Force,
|
|
[string] $OutputDirectory,
|
|
|
|
# Cargo target directory to build into and package from. Defaults to the
|
|
# repo's own `target/`.
|
|
#
|
|
# Windows refuses to overwrite a running exe, so packaging while a Nebula
|
|
# instance is live out of `target/release/nebula.exe` dies with a bare
|
|
# "拒绝访问 (os error 5)" that names the linker, not the real cause. Point
|
|
# this at a separate directory to build a package without touching — let
|
|
# alone killing — the instance the user is working in.
|
|
[string] $TargetDirectory
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Set-StrictMode -Version Latest
|
|
|
|
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
|
if ([string]::IsNullOrWhiteSpace($OutputDirectory)) {
|
|
$OutputDirectory = Join-Path $repo 'dist'
|
|
} elseif (-not [System.IO.Path]::IsPathRooted($OutputDirectory)) {
|
|
$OutputDirectory = Join-Path $repo $OutputDirectory
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($TargetDirectory)) {
|
|
$TargetDirectory = Join-Path $repo 'target'
|
|
} elseif (-not [System.IO.Path]::IsPathRooted($TargetDirectory)) {
|
|
$TargetDirectory = Join-Path $repo $TargetDirectory
|
|
}
|
|
$cargoTargetRoot = [System.IO.Path]::GetFullPath($TargetDirectory)
|
|
$outputRoot = [System.IO.Path]::GetFullPath($OutputDirectory)
|
|
$targetRoot = Join-Path $cargoTargetRoot $Configuration
|
|
$stage = Join-Path $outputRoot ".stage-$Version-$PID"
|
|
$zipPath = Join-Path $outputRoot "NebulaTerminal-v$Version-windows-x64.zip"
|
|
$temporaryZip = Join-Path $outputRoot ".NebulaTerminal-v$Version-windows-x64-$PID.tmp.zip"
|
|
|
|
$manifest = [ordered]@{
|
|
'nebula.exe' = Join-Path $targetRoot 'nebula.exe'
|
|
'README.md' = Join-Path $repo 'README.md'
|
|
'runtime/nebula-hook.exe' = Join-Path $targetRoot 'nebula-hook.exe'
|
|
'runtime/conpty.dll' = Join-Path $targetRoot 'conpty.dll'
|
|
'runtime/OpenConsole.exe' = Join-Path $targetRoot 'OpenConsole.exe'
|
|
'fonts/MapleMonoNormal-NF-CN-Regular.ttf' = Join-Path $repo 'assets\fonts\MapleMonoNormal-NF-CN-Regular.ttf'
|
|
'docs/CHANGELOG.md' = Join-Path $repo 'CHANGELOG.md'
|
|
'docs/INSTALL.md' = Join-Path $repo 'INSTALL.md'
|
|
'docs/lua-configuration.md' = Join-Path $repo 'docs\lua-configuration.md'
|
|
'licenses/LICENSE' = Join-Path $repo 'LICENSE'
|
|
'licenses/LICENSE-LUA' = Join-Path $repo 'licenses\LICENSE-LUA'
|
|
'licenses/LICENSE-MLUA' = Join-Path $repo 'licenses\LICENSE-MLUA'
|
|
'licenses/THIRD-PARTY-NOTICES' = Join-Path $repo 'THIRD-PARTY-NOTICES'
|
|
}
|
|
|
|
function Assert-Manifest([string] $Root, [string[]] $Expected) {
|
|
$rootPrefix = [System.IO.Path]::GetFullPath($Root).TrimEnd('\') + '\'
|
|
$actual = @(Get-ChildItem -LiteralPath $Root -Recurse -File | ForEach-Object {
|
|
$fullPath = [System.IO.Path]::GetFullPath($_.FullName)
|
|
if (-not $fullPath.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Package entry escaped the staging directory: $fullPath"
|
|
}
|
|
$fullPath.Substring($rootPrefix.Length).Replace('\', '/')
|
|
} | Sort-Object)
|
|
$difference = @(Compare-Object -ReferenceObject @($Expected | Sort-Object) -DifferenceObject $actual)
|
|
if ($difference.Count -ne 0) {
|
|
throw "Package manifest differs from the required layout:`n$($difference | Out-String)"
|
|
}
|
|
}
|
|
|
|
function Remove-StageSafely([string] $Path) {
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
return
|
|
}
|
|
$resolved = (Resolve-Path -LiteralPath $Path).Path
|
|
$prefix = $outputRoot.TrimEnd('\') + '\.stage-'
|
|
if (-not $resolved.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Refusing to remove path outside the package staging boundary: $resolved"
|
|
}
|
|
Remove-Item -LiteralPath $resolved -Recurse -Force
|
|
}
|
|
|
|
if (-not $SkipBuild) {
|
|
Push-Location $repo
|
|
$previousTargetDirectory = $env:CARGO_TARGET_DIR
|
|
try {
|
|
$env:CARGO_TARGET_DIR = $cargoTargetRoot
|
|
if ($Configuration -eq 'release') {
|
|
& cargo build --workspace --release
|
|
} else {
|
|
& cargo build --workspace
|
|
}
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Cargo build failed with exit code $LASTEXITCODE"
|
|
}
|
|
} finally {
|
|
$env:CARGO_TARGET_DIR = $previousTargetDirectory
|
|
Pop-Location
|
|
}
|
|
}
|
|
|
|
$missing = @($manifest.GetEnumerator() | Where-Object {
|
|
-not (Test-Path -LiteralPath $_.Value -PathType Leaf)
|
|
} | ForEach-Object { "$($_.Key) <- $($_.Value)" })
|
|
if ($missing.Count -ne 0) {
|
|
throw "Required package files are missing:`n$($missing -join "`n")"
|
|
}
|
|
|
|
New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null
|
|
if (Test-Path -LiteralPath $zipPath) {
|
|
if (-not $Force) {
|
|
throw "Package already exists: $zipPath (pass -Force to replace it)"
|
|
}
|
|
Remove-Item -LiteralPath $zipPath -Force
|
|
}
|
|
if (Test-Path -LiteralPath $temporaryZip) {
|
|
Remove-Item -LiteralPath $temporaryZip -Force
|
|
}
|
|
|
|
try {
|
|
Remove-StageSafely $stage
|
|
New-Item -ItemType Directory -Path $stage | Out-Null
|
|
foreach ($entry in $manifest.GetEnumerator()) {
|
|
$destination = Join-Path $stage $entry.Key.Replace('/', '\')
|
|
$parent = Split-Path -Parent $destination
|
|
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
|
Copy-Item -LiteralPath $entry.Value -Destination $destination
|
|
}
|
|
|
|
Assert-Manifest $stage @($manifest.Keys)
|
|
Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $temporaryZip -CompressionLevel Optimal
|
|
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
$archive = [System.IO.Compression.ZipFile]::OpenRead($temporaryZip)
|
|
try {
|
|
$zipManifest = @($archive.Entries |
|
|
Where-Object { -not $_.FullName.EndsWith('/') } |
|
|
ForEach-Object { $_.FullName.Replace('\', '/') } |
|
|
Sort-Object)
|
|
$difference = @(
|
|
Compare-Object -ReferenceObject @($manifest.Keys | Sort-Object) -DifferenceObject $zipManifest
|
|
)
|
|
if ($difference.Count -ne 0) {
|
|
throw "ZIP manifest differs from the required layout:`n$($difference | Out-String)"
|
|
}
|
|
$unpackedSize = ($archive.Entries | Measure-Object -Property Length -Sum).Sum
|
|
} finally {
|
|
$archive.Dispose()
|
|
}
|
|
|
|
Move-Item -LiteralPath $temporaryZip -Destination $zipPath
|
|
$zip = Get-Item -LiteralPath $zipPath
|
|
$sha256 = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash
|
|
[PSCustomObject]@{
|
|
Path = $zip.FullName
|
|
Files = $manifest.Count
|
|
Size = $zip.Length
|
|
UnpackedSize = $unpackedSize
|
|
SHA256 = $sha256
|
|
} | Format-List
|
|
} finally {
|
|
Remove-StageSafely $stage
|
|
if (Test-Path -LiteralPath $temporaryZip) {
|
|
Remove-Item -LiteralPath $temporaryZip -Force
|
|
}
|
|
}
|