# 小飞燕输入法 · 安装 / 卸载 # # irm https://writing.freecoding.shop | iex # # 上下键选安装还是卸载。装和卸是同一份脚本 —— 分成两份,两边的逻辑迟早各自漂移。 # 也可以跳过菜单直接指定: # & ([scriptblock]::Create((irm https://writing.freecoding.shop))) -Install # & ([scriptblock]::Create((irm https://writing.freecoding.shop))) -Uninstall # ... -Uninstall -PurgeUserData 连用户配置一起清掉 param( [switch]$Install, [switch]$Uninstall, [switch]$PurgeUserData ) $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $Base = 'https://writing.freecoding.shop' $PayloadUrl = "$Base/dl/swallow-payload.zip" $PayloadSha = 'd771e7ebff12b0e6312e4b1528d6d0226b1d069f15ff4f095b6a7a6b655b2d01' $Version = '0.17.4' $InstallDir = Join-Path $env:ProgramFiles "Rime\swallow-$Version" function Show-Banner { # 燕子来自 resource/swallow-512.png:原图是描边线条,用 9px 椭圆核做闭运算把 # 笔画缝隙桥接上,再从画布外洪水填充补实内部,最后按控制台 2:1 的字符 # 高宽比降采样。 $rows = @( ,@('┌──────────────────────────────────────────────────────────────────────────────────────────┐', '') ,@('│ .:*##########**::. ', ' │') ,@('│ #####################*:. ', ' │') ,@('│ :#####*. .########*.. ...:::::::::. ', ' │') ,@('│ *################. ', ' │') ,@('│ .########: .:::. ', ' │') ,@('│ *#########. ', ' │') ,@('│ *###########: ', ' _____ _____ ___ __ __ _ _ _ │') ,@('│ .*###########################***::::... ', ' | ___| ____|_ _| \ \ / // \ | \ | | │') ,@('│ ########..**##########: . ', ' | |_ | _| | | \ V // _ \ | \| | │') ,@('│ ########* .:*###*: ', ' | _| | |___ | | | |/ ___ \| |\ | │') ,@('│ :#####*:. .:*##*:. ', ' |_| |_____|___| |_/_/ \_\_| \_| │') ,@('│ ###* ..::::.... ', ' │') ,@('│ ### ', ' │') ,@('│ *#* ', ' │') ,@('│ :#: ', ' │') ,@('│ .# ', ' │') ,@('│ * ', ' │') ,@('│ . ', ' │') ,@('└──────────────────────────────────────────────────────────────────────────────────────────┘', '') ) $narrowRows = @( ,@('┌──────────────────────────────────────────────────────┐', '') ,@('│ .:*###########**::.. ', ' │') ,@('│ .#######################*:. ', ' │') ,@('│ :******. .##########*::.:::***********. ', ' │') ,@('│ .#######::*#########. ', ' │') ,@('│ ##########:**####*. ', ' │') ,@('│ *#########*. ', ' │') ,@('│ ###########*. ', ' │') ,@('│ *##############:............. ', ' │') ,@('│ .#############################********::::... ', ' │') ,@('│ ######### .:*############. . ', ' │') ,@('│ #########* .:*####*: ', ' │') ,@('│ :#######*: .:**##*:. ', ' │') ,@('│ ####. ..:***::.. ', ' │') ,@('│ ###: .... ', ' │') ,@('│ *## ', ' │') ,@('│ :#* ', ' │') ,@('│ .#: ', ' │') ,@('│ #. ', ' │') ,@('│ * ', ' │') ,@('│ . ', ' │') ,@('│ ', '│') ,@('│ ', ' _____ _____ ___ __ __ _ _ _ │') ,@('│ ', ' | ___| ____|_ _| \ \ / // \ | \ | | │') ,@('│ ', ' | |_ | _| | | \ V // _ \ | \| | │') ,@('│ ', ' | _| | |___ | | | |/ ___ \| |\ | │') ,@('│ ', ' |_| |_____|___| |_/_/ \_\_| \_| │') ,@('└──────────────────────────────────────────────────────┘', '') ) $w = 999 try { $w = $Host.UI.RawUI.WindowSize.Width } catch {} $use = if ($w -ge 92) { $rows } else { $narrowRows } Write-Host '' foreach ($r in $use) { if ($r[1] -eq '') { Write-Host $r[0] -ForegroundColor DarkCyan } else { Write-Host $r[0].Substring(0,1) -NoNewline -ForegroundColor DarkCyan Write-Host $r[0].Substring(1) -NoNewline -ForegroundColor Cyan Write-Host $r[1].Substring(0, $r[1].Length - 1) -NoNewline -ForegroundColor Blue Write-Host '│' -ForegroundColor DarkCyan } } Write-Host '' Write-Host ' 小 飞 燕 输 入 法' -ForegroundColor White -NoNewline Write-Host " Swallow IME $Version 三色三选,一按即中" -ForegroundColor DarkGray Write-Host '' } # --- 控制台输出 ----------------------------------------------------------- # 步骤行先原地打出来,做完再在同一行补上结果,避免每步占两行。 $script:StepNo = 0 $script:StepTotal = 7 $script:StepSw = $null function Step([string]$text) { $script:StepNo++ $script:StepSw = [Diagnostics.Stopwatch]::StartNew() Write-Host (' [{0}/{1}] ' -f $script:StepNo, $script:StepTotal) -NoNewline -ForegroundColor DarkGray Write-Host $text.PadRight(34) -NoNewline -ForegroundColor Gray } function StepDone([string]$note = '') { $ms = if ($script:StepSw) { $script:StepSw.ElapsedMilliseconds } else { 0 } $t = if ($ms -ge 1000) { '{0:N1}s' -f ($ms / 1000) } else { "${ms}ms" } Write-Host '✓ ' -NoNewline -ForegroundColor Green Write-Host $t.PadRight(8) -NoNewline -ForegroundColor DarkGray if ($note) { Write-Host $note -ForegroundColor DarkGray } else { Write-Host '' } } function StepWarn([string]$note) { Write-Host '! ' -NoNewline -ForegroundColor Yellow Write-Host $note -ForegroundColor Yellow } function Die([string]$text) { Write-Host '' Write-Host ' × ' -NoNewline -ForegroundColor Red Write-Host $text -ForegroundColor Red Write-Host '' exit 1 } function Show-Progress([long]$done, [long]$len, [double]$secs) { $w = 28 $frac = if ($len -gt 0) { [double]$done / $len } else { 0 } $fill = [int][Math]::Round($frac * $w) $bar = ('█' * $fill) + ('░' * ($w - $fill)) $mb = '{0,5:N1}/{1:N1} MB' -f ($done / 1MB), ($len / 1MB) $rate = if ($secs -gt 0.3) { '{0,5:N1} MB/s' -f ($done / 1MB / $secs) } else { ' … ' } # `r 回到行首原地重画。用 [Console]::Write 而不是 Write-Host: # Write-Host 每次都补一个换行,进度条会变成刷屏。 [Console]::Write("`r ") Write-Host $bar -NoNewline -ForegroundColor Cyan [Console]::Write((' {0,4:N0}% {1} {2}' -f ($frac * 100), $mb, $rate)) } # --- 已经装在哪 ----------------------------------------------------------- function Find-Installed { try { $d = (Get-ItemProperty 'HKLM:\SOFTWARE\Rime\Swallow' -Name InstallDir -EA Stop).InstallDir if ($d -and (Test-Path (Join-Path $d 'WeaselSetup.exe'))) { return $d } } catch {} # 注册表被清过、或者装到了别的盘:按目录结构找回来 foreach ($root in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, 'D:\Program Files', 'E:\Program Files')) { if (-not $root) { continue } $p = Join-Path $root 'Rime' if (-not (Test-Path $p)) { continue } $hit = @(Get-ChildItem $p -Directory -EA SilentlyContinue | Where-Object { Test-Path (Join-Path $_.FullName 'WeaselSetup.exe') }) if ($hit.Count) { return $hit[0].FullName } } return $null } function Get-InstalledVersion { try { return (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Swallow' ` -Name DisplayVersion -EA Stop).DisplayVersion } catch { return $null } } # --- 上下键菜单 ----------------------------------------------------------- # 原地重画:把光标挪回菜单第一行再写一遍,而不是每按一次键就往下刷一屏。 function Read-Choice([string[]]$items, [int]$default = 0) { # 输入被重定向(管道、计划任务、CI)时没有真键盘,卡在 ReadKey 上就成了假死。 try { if ([Console]::IsInputRedirected) { return $default } } catch { return $default } $i = $default $drawn = $false while ($true) { if ($drawn) { try { [Console]::SetCursorPosition(0, [Console]::CursorTop - $items.Count) } catch { } # 菜单正好在窗口底部滚过,回不去就顺着往下画 } $drawn = $true for ($k = 0; $k -lt $items.Count; $k++) { if ($k -eq $i) { Write-Host ' > ' -NoNewline -ForegroundColor Cyan Write-Host $items[$k].PadRight(52) -ForegroundColor Cyan } else { Write-Host ' ' -NoNewline Write-Host $items[$k].PadRight(52) -ForegroundColor DarkGray } } $key = [Console]::ReadKey($true) if ($key.Key -eq 'UpArrow') { $i = ($i - 1 + $items.Count) % $items.Count; continue } if ($key.Key -eq 'DownArrow') { $i = ($i + 1) % $items.Count; continue } if ($key.Key -eq 'Enter') { return $i } if ($key.Key -eq 'Escape') { return -1 } # 数字键直选,不想按方向键的人更快 $n = [int]$key.KeyChar - 48 if ($n -ge 1 -and $n -le $items.Count) { return $n - 1 } } } # --- 安装 ----------------------------------------------------------------- function Invoke-Install { $script:StepNo = 0 $script:StepTotal = 7 Step '停止正在运行的服务' $was = @(Get-Process WeaselServer -ErrorAction SilentlyContinue) # WaitForExit 有返回值,不吞掉会被当成输出打到控制台,还把后面的 ✓ 挤到下一行 $was | ForEach-Object { try { $_.Kill(); [void]$_.WaitForExit(5000) } catch {} } if ($was.Count) { Start-Sleep -Milliseconds 800 } StepDone $(if ($was.Count) { '已停止旧进程' } else { '没有在运行' }) Step '下载程序包' Write-Host '' $tmp = Join-Path $env:TEMP "swallow-$Version.zip" $sw = [Diagnostics.Stopwatch]::StartNew() $resp = $null; $in = $null; $out = $null try { $req = [Net.HttpWebRequest]::Create($PayloadUrl) $req.UserAgent = "swallow-installer/$Version" $resp = $req.GetResponse() $len = $resp.ContentLength $in = $resp.GetResponseStream() $out = [IO.File]::Create($tmp) $buf = New-Object byte[] 262144 $done = 0L $lastDraw = -1000 while (($n = $in.Read($buf, 0, $buf.Length)) -gt 0) { $out.Write($buf, 0, $n) $done += $n # 限流重画:每收到 256KB 就画一次的话,画进度条本身成了瓶颈。 if ($sw.ElapsedMilliseconds - $lastDraw -ge 120) { Show-Progress $done $len $sw.Elapsed.TotalSeconds $lastDraw = $sw.ElapsedMilliseconds } } Show-Progress $done $len $sw.Elapsed.TotalSeconds Write-Host '' } catch { Write-Host '' Die "下载失败:$($_.Exception.Message)" } finally { if ($out) { $out.Close() } if ($in) { $in.Close() } if ($resp) { $resp.Close()} } Write-Host (' [{0}/{1}] ' -f $script:StepNo, $script:StepTotal) -NoNewline -ForegroundColor DarkGray Write-Host '下载完成'.PadRight(34) -NoNewline -ForegroundColor Gray StepDone ('{0:N1} MB' -f ((Get-Item $tmp).Length / 1MB)) Step '校验完整性' $got = (Get-FileHash -Path $tmp -Algorithm SHA256).Hash.ToLower() if ($got -ne $PayloadSha) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue Die "SHA256 不符,文件可能损坏或被篡改,已删除。`n 期望 $PayloadSha`n 实际 $got" } StepDone 'SHA256 一致' Step '解压程序文件' if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null } Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [IO.Compression.ZipFile]::OpenRead($tmp) $count = 0 try { # 逐条覆盖而不是先删目录:用户可能把自己的词库放在了 data 下面。 foreach ($e in $zip.Entries) { $dest = Join-Path $InstallDir $e.FullName if ([string]::IsNullOrEmpty($e.Name)) { New-Item -ItemType Directory -Path $dest -Force | Out-Null continue } $dir = Split-Path $dest -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } try { [IO.Compression.ZipFileExtensions]::ExtractToFile($e, $dest, $true) } catch { # 占用中的 DLL 覆盖不掉:改名让路,新文件照样就位。这正是「不用重启 # 电脑」的由来 —— 旧的那份留给下次开机清理。 $old = "$dest.old." + [Guid]::NewGuid().ToString('N').Substring(0, 6) try { Move-Item $dest $old -Force [IO.Compression.ZipFileExtensions]::ExtractToFile($e, $dest, $true) } catch { Die "无法写入 $dest:$($_.Exception.Message)" } } $count++ } } finally { $zip.Dispose() } Remove-Item $tmp -Force -ErrorAction SilentlyContinue StepDone "$count 个文件" Step '注册输入法' New-Item -Path 'HKLM:\SOFTWARE\Rime\Swallow' -Force | Out-Null Set-ItemProperty -Path 'HKLM:\SOFTWARE\Rime\Swallow' -Name 'InstallDir' -Value $InstallDir # 这里**不能**加 -WindowStyle Hidden。之前加了,而 WeaselSetup 首次安装会弹 # 「安装选项」对话框,被 Hidden 变成看不见也点不掉的模态框,安装就永远停在 # 这一步。那个对话框现在已经从 WeaselSetup 里整个移除,但这条规矩要留着 —— # 隐藏一个可能弹 UI 的进程,出问题的表现是死锁,而不是报错。 $setup = Start-Process -FilePath (Join-Path $InstallDir 'WeaselSetup.exe') ` -ArgumentList '/s' -Wait -PassThru if ($setup.ExitCode -ne 0) { Die "WeaselSetup 返回 $($setup.ExitCode),注册失败。" } StepDone 'TSF 已注册' Step '部署输入方案' $userDir = Join-Path $env:APPDATA 'Rime' if (-not (Test-Path $userDir)) { New-Item -ItemType Directory -Path $userDir -Force | Out-Null } # librime-predict 只在用户目录找 predict.db,不回退共享目录。不放这一份, # 顺序联想会静默失效。已存在就不动 —— 用户可能换过版本。 $predictSrc = Join-Path $InstallDir 'data\predict.db' $predictDst = Join-Path $userDir 'predict.db' if ((Test-Path $predictSrc) -and -not (Test-Path $predictDst)) { Copy-Item $predictSrc $predictDst } # 先起服务再部署:UpdateWorkspace 要跟服务端通信才能进维护模式。 Start-Process -FilePath (Join-Path $InstallDir 'WeaselServer.exe') | Out-Null Start-Sleep -Seconds 2 # 不隐藏窗口,也不无限等。 # # 隐藏窗口会把「意外弹出的对话框」变成死锁 —— 今晚在 WeaselSetup 和 # WeaselDeployer 上各栽了一次。窗口露出来,至少能看见它在等什么。 # 再加超时:部署本身是编译词库,冷启动几十秒正常,但超过 3 分钟就不该 # 继续干等 —— 输入法这时已经能用了,部署随时可以从托盘菜单重做。 $dep = Start-Process -FilePath (Join-Path $InstallDir 'WeaselDeployer.exe') ` -ArgumentList '/deploy' -PassThru if ($dep.WaitForExit(180000)) { if ($dep.ExitCode -ne 0) { StepWarn "部署返回 $($dep.ExitCode),输入法仍可用,可在托盘菜单里「重新部署」" } else { StepDone '词库已编译' } } else { StepWarn '部署超过 3 分钟仍未结束,先跳过;输入法已可用,可稍后在托盘菜单里「重新部署」' } Step '登记自启与卸载' Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' ` -Name 'WeaselServer' -Value (Join-Path $InstallDir 'WeaselServer.exe') $unkey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Swallow' New-Item -Path $unkey -Force | Out-Null Set-ItemProperty -Path $unkey -Name 'DisplayName' -Value '小飞燕输入法' Set-ItemProperty -Path $unkey -Name 'DisplayVersion' -Value $Version Set-ItemProperty -Path $unkey -Name 'DisplayIcon' -Value (Join-Path $InstallDir 'WeaselServer.exe') Set-ItemProperty -Path $unkey -Name 'InstallLocation' -Value $InstallDir Set-ItemProperty -Path $unkey -Name 'Publisher' -Value '小飞燕' # 控制面板的卸载入口指回同一个脚本,不再单独维护一份本地卸载器 —— # 两份卸载逻辑早晚会不一致。 Set-ItemProperty -Path $unkey -Name 'UninstallString' ` -Value ('powershell -NoProfile -ExecutionPolicy Bypass -Command "& ([scriptblock]::Create((irm ' + $Base + '))) -Uninstall"') Set-ItemProperty -Path $unkey -Name 'NoModify' -Value 1 -Type DWord Set-ItemProperty -Path $unkey -Name 'NoRepair' -Value 1 -Type DWord StepDone '完成' Write-Host '' Write-Host ' ✓ 装好了,不需要重启电脑。' -ForegroundColor Green Write-Host '' foreach ($r in @( @('切换输入法', 'Win + 空格,选「小飞燕」'), @('三色分组', '打字时按 ~ 开启,再按 1 / 2 / 3 选组'), @('面板', 'Ctrl + ; 联想词 / 剪贴板 / 预设值'), @('设置', '右键托盘里的小飞燕图标'), @('卸载', "irm $Base | iex 再选「卸载」"))) { Write-Host ' ' -NoNewline Write-Host $r[0].PadRight(12) -NoNewline -ForegroundColor White Write-Host $r[1] -ForegroundColor Gray } Write-Host '' Write-Host ' 安装时已经开着的程序还握着旧的一份,重开一次才会用上新版。' -ForegroundColor DarkGray Write-Host '' } # --- 卸载 ----------------------------------------------------------------- function Invoke-Uninstall([string]$dir) { $script:StepNo = 0 $script:StepTotal = 4 if (-not $dir) { Write-Host ' 没找到安装目录,可能已经卸载过了。' -ForegroundColor Yellow Write-Host '' return } Write-Host " 安装目录 $dir" -ForegroundColor DarkGray Write-Host '' Step '停止服务' Get-Process WeaselServer -EA SilentlyContinue | ForEach-Object { try { $_.Kill(); [void]$_.WaitForExit(5000) } catch {} } Start-Sleep -Milliseconds 800 StepDone # 必须在删文件之前:WeaselSetup.exe 就在要删的那个目录里。 Step '注销文字服务' $setup = Join-Path $dir 'WeaselSetup.exe' if (Test-Path $setup) { # 同样不隐藏、带超时:卸载卡在一个看不见的窗口上,比卸不干净更糟。 try { $u = Start-Process $setup -ArgumentList '/u' -PassThru if (-not $u.WaitForExit(30000)) { $u.Kill() } } catch {} } StepDone Step '清理注册表' Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' ` -Name 'WeaselServer' -EA SilentlyContinue foreach ($k in @('HKLM:\SOFTWARE\Rime\Swallow', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Swallow', 'HKCU:\Software\Rime\Swallow')) { Remove-Item -Path $k -Recurse -Force -EA SilentlyContinue } # HKLM\SOFTWARE\Rime 是 RIME 系输入法共用的父项,只在它已经空了的时候才删, # 否则会顺手把同一台机器上的小狼毫一起弄坏。 try { $rime = Get-Item 'HKLM:\SOFTWARE\Rime' -EA Stop if ($rime.SubKeyCount -eq 0 -and $rime.ValueCount -eq 0) { Remove-Item 'HKLM:\SOFTWARE\Rime' -Force -EA SilentlyContinue } } catch {} StepDone Step '删除程序文件' $pending = $false try { Remove-Item -Path $dir -Recurse -Force -EA Stop } catch { # 还有 DLL 被别的进程占着。安排开机时再删,不要求用户现在重启。 $pending = $true try { Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; public class Mv { [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern bool MoveFileExW(string a, string b, int f); } "@ -EA SilentlyContinue Get-ChildItem $dir -Recurse -File -EA SilentlyContinue | ForEach-Object { [void][Mv]::MoveFileExW($_.FullName, $null, 4) # DELAY_UNTIL_REBOOT } [void][Mv]::MoveFileExW($dir, $null, 4) } catch {} } if ($pending) { StepWarn '部分文件仍被占用,已安排下次开机时删除' } else { StepDone } if ($PurgeUserData) { $userDir = Join-Path $env:APPDATA 'Rime' if (Test-Path $userDir) { Remove-Item $userDir -Recurse -Force -EA SilentlyContinue } } Write-Host '' Write-Host ' ✓ 小飞燕已卸载。' -ForegroundColor Green Write-Host '' if (-not $PurgeUserData) { Write-Host ' 用户配置保留在 %APPDATA%\Rime(自造词、预设短语、输入习惯)。' -ForegroundColor DarkGray Write-Host " 要一并清掉: & ([scriptblock]::Create((irm $Base))) -Uninstall -PurgeUserData" -ForegroundColor DarkGray } Write-Host ' 输入法列表里的残留项会在注销或重启后消失。' -ForegroundColor DarkGray Write-Host '' } # ========================================================================== Show-Banner if ([Environment]::OSVersion.Version.Major -lt 10) { Die '需要 Windows 10 或更新的系统。' } if (-not [Environment]::Is64BitOperatingSystem) { Die '这个程序包只含 64 位版本。32 位系统请从源码构建。' } $found = Find-Installed $curVer = Get-InstalledVersion # 显式开关优先;没装过就没什么可选的,直接装;其余情况让用户上下键选。 $action = '' if ($Uninstall) { $action = 'uninstall' } elseif ($Install) { $action = 'install' } elseif (-not $found) { $action = 'install' } else { $verNote = if ($curVer -and $curVer -ne $Version) { "$curVer -> $Version" } elseif ($curVer) { "覆盖安装 $curVer" } else { $Version } Write-Host " 检测到已安装:$found" -ForegroundColor Gray Write-Host '' Write-Host ' 上下键选择 Enter 确认 Esc 退出' -ForegroundColor DarkGray Write-Host '' $pick = Read-Choice @("重新安装 / 更新 $verNote", '卸载', '退出') 0 Write-Host '' if ($pick -eq 0) { $action = 'install' } elseif ($pick -eq 1) { $action = 'uninstall' } else { Write-Host ' 已取消。' -ForegroundColor DarkGray; Write-Host ''; return } } # 提权。选择结果要一起带过去,否则提权后的那个窗口会再问一遍。 $admin = ([Security.Principal.WindowsPrincipal] ` [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $admin) { Write-Host ' 需要管理员权限,正在请求提权…' -ForegroundColor Yellow Write-Host '' $flag = if ($action -eq 'uninstall') { '-Uninstall' } else { '-Install' } if ($PurgeUserData) { $flag += ' -PurgeUserData' } $cmd = "& ([scriptblock]::Create((irm $Base))) $flag" try { Start-Process powershell -Verb RunAs -ArgumentList ` '-NoProfile', '-ExecutionPolicy', 'Bypass', '-NoExit', '-Command', $cmd } catch { Die '提权被拒绝。右键「以管理员身份运行 PowerShell」后再执行同一条命令。' } return } if ($action -eq 'uninstall') { Invoke-Uninstall $found } else { Invoke-Install }