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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Interactive tool pickers use Quiet Accent semantic colors when a compatible console is available,
while preserving ASCII/plain output for redirected sessions and `NO_COLOR`. (#507)

### Fixed

### Removed
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ Picker behavior is consistent across platforms:
- Confirming installs the checked tools through the same ordered selection engine used by flags.
- Cancelling exits successfully without installing anything and prints `Install cancelled.`
- Confirming with every item unchecked exits successfully and prints `Nothing selected, exiting.`
- In supported interactive consoles, Quiet Accent uses cyan for headings and focus, green for
checked markers and confirmations, and yellow for opt-ins, cancellations, and warnings.
The ASCII markers and labels remain the source of meaning. Styling is suppressed for redirected
output, `NO_COLOR`, and unsupported terminals (including Bash with `TERM=dumb`).

Controls depend on the shell:

Expand Down
64 changes: 57 additions & 7 deletions scripts/linux/lib/tui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,40 @@
# show_tool_menu <default_tools_array_name> <available_tools_array_name>
# Sets globals: _MENU_SELECTION (CSV), _MENU_CANCELLED (0|1)
#
# Test seam: set _TUI_ARROW_NAV_OVERRIDE=0|1 to force a specific nav path.
# Test seams: set _TUI_ARROW_NAV_OVERRIDE=0|1 to force a specific nav path,
# and _TUI_COLOR_OVERRIDE=0|1 to force plain or colored rendering.
# ponytail: eval for bash 3.2 nameref compat; upgrade to local -n when 4.3 is minimum.

# ---------------------------------------------------------------------------
# _tui_color_enabled -- true when menu styling is safe, or test-forced.
# NO_COLOR always wins, including over the force-color test seam.
# ---------------------------------------------------------------------------
_tui_color_enabled() {
[[ "${_TUI_COLOR_OVERRIDE:-}" == "0" ]] && return 1
[[ -n "${NO_COLOR:-}" ]] && return 1
[[ "${_TUI_COLOR_OVERRIDE:-}" == "1" ]] && return 0
[[ -t 1 ]] || return 1
[[ "${TERM:-}" != "dumb" ]] || return 1
return 0
}

# Fixed native SGR styles. Each function emits plain ASCII text when disabled.
_tui_style_heading() {
if _tui_color_enabled; then printf '\033[1;36m%s\033[0m' "$1"; else printf '%s' "$1"; fi
}
_tui_style_checked() {
if _tui_color_enabled; then printf '\033[32m%s\033[0m' "$1"; else printf '%s' "$1"; fi
}
_tui_style_optin() {
if _tui_color_enabled; then printf '\033[33m%s\033[0m' "$1"; else printf '%s' "$1"; fi
}
_tui_style_confirmation() {
if _tui_color_enabled; then printf '\033[32m%s\033[0m' "$1"; else printf '%s' "$1"; fi
}
_tui_style_warning() {
if _tui_color_enabled; then printf '\033[33m%s\033[0m' "$1"; else printf '%s' "$1"; fi
}

# ---------------------------------------------------------------------------
# _tui_render_list -- draw the full menu list to stdout
# $1: cursor index (-1 = no cursor indicator)
Expand All @@ -36,7 +67,12 @@ _tui_render_list() {
if [[ "$checked" == "1" ]]; then check_str="[x]"; else check_str="[ ]"; fi
if [[ "$is_def" == "1" ]]; then label="(default)"; else label="(opt-in)"; fi
if [[ "$cursor" -eq "$i" ]]; then pointer=">"; else pointer=" "; fi
printf '%s %s %s %s\n' "$pointer" "$check_str" "$tool" "$label"
if [[ "$pointer" == ">" ]]; then _tui_style_heading "$pointer"; else printf '%s' "$pointer"; fi
printf ' '
if [[ "$checked" == "1" ]]; then _tui_style_checked "$check_str"; else printf '%s' "$check_str"; fi
printf ' %s ' "$tool"
if [[ "$is_def" == "1" ]]; then printf '%s' "$label"; else _tui_style_optin "$label"; fi
printf '\n'
i=$((i+1))
done <<< "$tools_nl"
}
Expand Down Expand Up @@ -188,7 +224,8 @@ _tui_arrow_mode() {
IFS=' ' read -ra checked_arr <<< "$checked_sp"

printf '\n'
printf 'Arrow keys to move, Space to toggle, Enter to confirm, q/ESC to cancel.\n\n'
_tui_style_heading 'Arrow keys to move, Space to toggle, Enter to confirm, q/ESC to cancel.'
printf '\n\n'
_tui_render_list "$cursor" "$tools_nl" "${checked_arr[*]}" "$is_def_sp"

local done_=0 k1 k2 k3 key
Expand All @@ -212,7 +249,9 @@ _tui_arrow_mode() {

if [[ "$_kd_cancelled" == "1" ]]; then
_MENU_CANCELLED=1
printf '\nInstall cancelled.\n'
printf '\n'
_tui_style_warning 'Install cancelled.'
printf '\n'
return 0
fi
[[ "$_kd_done" == "1" ]] && done_=1
Expand All @@ -229,6 +268,8 @@ _tui_arrow_mode() {
i=$((i+1))
done <<< "$tools_nl"

_tui_style_confirmation 'Selection confirmed.'
printf '\n'
_MENU_SELECTION="$sel"
}

Expand All @@ -254,7 +295,8 @@ _tui_numbered_mode() {
;;
q|Q)
_MENU_CANCELLED=1
printf 'Install cancelled.\n'
_tui_style_warning 'Install cancelled.'
printf '\n'
return 0
;;
a|A)
Expand All @@ -281,6 +323,8 @@ _tui_numbered_mode() {
for i in $(seq 0 $((count-1))); do
[[ "${checked_arr[$i]}" == "1" ]] && sel="${sel:+${sel},}${tool_arr[$i]}"
done
_tui_style_confirmation 'Selection confirmed.'
printf '\n'
_MENU_SELECTION="$sel"
}

Expand All @@ -290,14 +334,20 @@ _tui_numbered_mode() {
# ---------------------------------------------------------------------------
_tui_numbered_render() {
local _tname="$1" _cname="$2" _dname="$3" _cnt="$4"
printf '\nArrow keys unavailable (bash 3.2). Number to toggle, a=all, Enter=confirm, q=cancel.\n'
printf '\n'
_tui_style_heading 'Arrow keys unavailable (bash 3.2). Number to toggle, a=all, Enter=confirm, q=cancel.'
printf '\n'
local i check_str label _t _c _d
for i in $(seq 0 $((_cnt-1))); do
# shellcheck disable=SC2086
eval "_t=\"\${${_tname}[$i]}\"; _c=\"\${${_cname}[$i]}\"; _d=\"\${${_dname}[$i]}\""
if [[ "$_c" == "1" ]]; then check_str="[x]"; else check_str="[ ]"; fi
if [[ "$_d" == "1" ]]; then label="(default)"; else label="(opt-in)"; fi
printf '%d. %s %s %s\n' "$((i+1))" "$check_str" "$_t" "$label"
printf '%d. ' "$((i+1))"
if [[ "$_c" == "1" ]]; then _tui_style_checked "$check_str"; else printf '%s' "$check_str"; fi
printf ' %s ' "$_t"
if [[ "$_d" == "1" ]]; then printf '%s' "$label"; else _tui_style_optin "$label"; fi
printf '\n'
done
printf '\n> '
}
122 changes: 118 additions & 4 deletions scripts/windows/lib/tui.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,99 @@
# PS 5.1 ASCII-only: no smart quotes, em-dashes, arrows, or non-ASCII characters.
# No Write-Host. Console output via [Console]::Write / [Console]::WriteLine only.

# ---------------------------------------------------------------------------
# Quiet Accent console styling. This remains deliberately local to the TUI:
# redirected/plain output is unchanged, and colors always restore immediately.
# Test seam: _PS_TUI_COLOR_OVERRIDE=0|1 forces plain or colored rendering.
# ---------------------------------------------------------------------------
$script:_TuiWriteOverride = $null

function Test-TuiColorEnabled {
if ($env:_PS_TUI_COLOR_OVERRIDE -eq '0') { return $false }
if (-not [string]::IsNullOrEmpty($env:NO_COLOR)) { return $false }

try {
if ($null -eq $Host.UI.RawUI) { return $false }
$null = [Console]::ForegroundColor
}
catch {
return $false
}

if ($env:_PS_TUI_COLOR_OVERRIDE -eq '1') { return $true }
return -not [Console]::IsOutputRedirected
}

function Get-TuiRoleColor {
param(
[ValidateSet('Heading', 'Focus', 'Checked', 'OptIn', 'Confirmation', 'Warning', 'Cancel')]
[string]$Role
)

switch ($Role) {
'Heading' { return [ConsoleColor]::Cyan }
'Focus' { return [ConsoleColor]::Cyan }
'Checked' { return [ConsoleColor]::Green }
'OptIn' { return [ConsoleColor]::Yellow }
'Confirmation' { return [ConsoleColor]::Green }
'Warning' { return [ConsoleColor]::Yellow }
'Cancel' { return [ConsoleColor]::Yellow }
}
}

function Write-TuiRaw {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSAvoidUsingWriteHost', '',
Justification = 'The interactive TUI must write directly to the console without Write-Host.')]
param(
[string]$Text,
[bool]$ErrorOutput = $false
)

if ($null -ne $script:_TuiWriteOverride) {
& $script:_TuiWriteOverride $Text $ErrorOutput
return
}
if ($ErrorOutput) {
[Console]::Error.Write($Text)
} else {
[Console]::Write($Text)
}
}

function Write-TuiStyled {
param(
[string]$Text,
[ConsoleColor]$Color,
[bool]$ErrorOutput = $false
)

if (-not (Test-TuiColorEnabled)) {
Write-TuiRaw -Text $Text -ErrorOutput $ErrorOutput
return
}

$previousColor = [Console]::ForegroundColor
try {
[Console]::ForegroundColor = $Color
Write-TuiRaw -Text $Text -ErrorOutput $ErrorOutput
}
finally {
[Console]::ForegroundColor = $previousColor
}
}

function Write-TuiLine {
param(
[string]$Text,
[ConsoleColor]$Color,
[bool]$ErrorOutput = $false
)

Write-TuiStyled -Text $Text -Color $Color -ErrorOutput $ErrorOutput
Write-TuiRaw -Text ([Environment]::NewLine) -ErrorOutput $ErrorOutput
}

# ---------------------------------------------------------------------------
# Resolve-FinalToolset: canonical ordered tool resolver.
# Pure function -- no exit, no console output, no validation.
Expand Down Expand Up @@ -81,13 +174,32 @@ function Show-ToolMenu {

# Render-Menu as scriptblock (closure over $items, $checked, $cursor, $nDef)
$renderMenu = {
[Console]::WriteLine('Select tools to install (defaults pre-checked):')
Write-TuiLine -Text 'Select tools to install (defaults pre-checked):' `
-Color (Get-TuiRoleColor -Role 'Heading')
[Console]::WriteLine('')
for ($i = 0; $i -lt $items.Count; $i++) {
$mark = if ($checked[$i]) { '[x]' } else { '[ ]' }
$label = if ($i -lt $nDef) { '(default)' } else { '(opt-in)' }
$arrow = if ($i -eq $cursor) { '>' } else { ' ' }
[Console]::WriteLine(" $arrow $mark $($items[$i]) $label")
Write-TuiRaw -Text ' '
if ($arrow -eq '>') {
Write-TuiStyled -Text $arrow -Color (Get-TuiRoleColor -Role 'Focus')
} else {
Write-TuiRaw -Text $arrow
}
Write-TuiRaw -Text ' '
if ($checked[$i]) {
Write-TuiStyled -Text $mark -Color (Get-TuiRoleColor -Role 'Checked')
} else {
Write-TuiRaw -Text $mark
}
Write-TuiRaw -Text " $($items[$i]) "
if ($i -lt $nDef) {
[Console]::WriteLine($label)
} else {
Write-TuiStyled -Text $label -Color (Get-TuiRoleColor -Role 'OptIn')
[Console]::WriteLine()
}
}
[Console]::WriteLine('')
[Console]::WriteLine(' Up/Down=move Space=toggle A=all Enter=confirm Esc/Q=cancel')
Expand Down Expand Up @@ -118,8 +230,9 @@ function Show-ToolMenu {
}
} catch {
# ReadKey failure in a nominally interactive host: one clear warning, install cancelled
[Console]::Error.WriteLine(
"WARNING: Interactive menu failed ($($_.Exception.Message)). Installation is being cancelled.")
Write-TuiLine -Text (
"WARNING: Interactive menu failed ($($_.Exception.Message)). Installation is being cancelled.") `
-Color (Get-TuiRoleColor -Role 'Warning') -ErrorOutput $true
return $null
}

Expand All @@ -129,5 +242,6 @@ function Show-ToolMenu {
for ($i = 0; $i -lt $items.Count; $i++) {
if ($checked[$i]) { $selected += $items[$i] }
}
Write-TuiLine -Text 'Selection confirmed.' -Color (Get-TuiRoleColor -Role 'Confirmation')
return ,$selected
}
2 changes: 1 addition & 1 deletion scripts/windows/setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ if (Test-ShouldShowMenu `
}

if ($null -eq $selectedNames) {
Write-Output 'Install cancelled.'
Write-TuiLine -Text 'Install cancelled.' -Color (Get-TuiRoleColor -Role 'Cancel')
exit 0
}
if ($selectedNames.Count -eq 0) {
Expand Down
Loading
Loading