Major updates to barnard-ui. Fixed several bugs, changed server file format. Old ~/.config/barnard/servers.conf should be removed before running barnard-ui after this change.

This commit is contained in:
Storm Dragon
2026-07-14 19:31:56 -04:00
parent 7fed25e8d0
commit 30a02eba14
2 changed files with 437 additions and 104 deletions
+4
View File
@@ -118,6 +118,10 @@ An Ncurses interface has been created by members of the [F123 Group](https://git
Make sure the folder in which you store the barnard binary is in your path. This should be the default for any f123 user. Make sure the folder in which you store the barnard binary is in your path. This should be the default for any f123 user.
Then just run ./barnard-ui from this folder, and follow the instructions. Then just run ./barnard-ui from this folder, and follow the instructions.
You can add barnard-ui to your path as well, and access it from anywhere. You can add barnard-ui to your path as well, and access it from anywhere.
New installs start with an empty server list. Saved servers are stored in
`~/.config/barnard/servers.conf` as INI-style `[server]` entries. If you save a
server password, it is written to that file so Barnard can use it when
connecting; the UI writes the file with mode `0600`.
## Modifications ## Modifications
+433 -104
View File
@@ -22,28 +22,69 @@
# 02110-1301, USA. # 02110-1301, USA.
# #
#--code-- #--code--
# the gettext essentials # the gettext essentials
export TEXTDOMAIN=barnard-ui export TEXTDOMAIN=barnard-ui
export TEXTDOMAINDIR=/usr/share/locale export TEXTDOMAINDIR=/usr/share/locale
source gettext.sh # shellcheck disable=SC1091
if ! source gettext.sh 2> /dev/null; then
gettext() {
printf '%s\n' "$1"
}
fi
cacheDir="${XDG_CACHE_HOME:-$HOME/.cache}"
configDir="$HOME/.config/barnard"
serverFile="$configDir/servers.conf"
certFile="$configDir/barnard.pem"
logFile="$cacheDir/${0##*/}.log"
if ! mkdir -p "$cacheDir" "$configDir"; then
printf 'Could not create Barnard configuration directories.\n' >&2
exit 1
fi
if ! : > "$logFile"; then
printf 'Could not write log file: %s\n' "$logFile" >&2
exit 1
fi
# Settings to improve accessibility of dialog.
export DIALOGOPTS='--insecure --no-lines --visit-items'
declare -Ag mumbleServerList=()
declare -Ag serverAddresses=()
declare -Ag serverPorts=()
declare -Ag serverPasswords=()
declare -Ag serverInsecure=()
# Log writing function # Log writing function
log() { log() {
# Usage: command | log for just stdout. # Usage: command | log for just stdout.
# Or command |& log for stderr and stdout. # Or command |& log for stderr and stdout.
while read -r line ; do local line
echo "$line" | tee -a "$logFile" &> /dev/null while IFS= read -r line ; do
printf '%s\n' "$line" >> "$logFile"
done done
} }
# Log file name is ~/.cache/scriptname.log
logFile="$HOME/.cache/${0##*/}.log"
# Clear previous logs
echo -n | tee "$logFile" &> /dev/null
# Settings to improve accessibility of dialog. fatal() {
export DIALOGOPTS='--insecure --no-lines --visit-items' local message="$*"
printf '%s\n' "$message" | log
if command -v dialog > /dev/null 2>&1; then
dialog --clear --msgbox "$message" 10 72
else
printf '%s\n' "$message" >&2
fi
exit 1
}
require_command() {
local commandName="$1"
local displayName="${2:-$1}"
if ! command -v "$commandName" > /dev/null 2>&1; then
fatal "$(gettext "Required command not found:") $displayName"
fi
}
inputbox() { inputbox() {
# Returns: text entered by the user # Returns: text entered by the user
@@ -62,9 +103,9 @@ passwordbox() {
} }
msgbox() { msgbox() {
# Returns: None # Returns: None
# Shows the provided message on the screen with an ok button. # Shows the provided message on the screen with an ok button.
dialog --clear --msgbox "$*" 10 72 dialog --clear --msgbox "$*" 10 72
} }
yesno() { yesno() {
@@ -72,8 +113,7 @@ yesno() {
# Args: Question to user. # Args: Question to user.
# Called in if $(yesno) == "Yes" # Called in if $(yesno) == "Yes"
# Or variable=$(yesno) # Or variable=$(yesno)
dialog --clear --backtitle "$(gettext "Press 'Enter' for \"yes\" or 'Escape' for \"no\".")" --yesno "$*" 10 80 --stdout if dialog --clear --backtitle "$(gettext "Press 'Enter' for \"yes\" or 'Escape' for \"no\".")" --yesno "$*" 10 80 --stdout; then
if [[ $? -eq 0 ]]; then
echo "Yes" echo "Yes"
else else
echo "No" echo "No"
@@ -84,7 +124,7 @@ menulist() {
# Args: menu options. # Args: menu options.
# returns: selected tag # returns: selected tag
local i local i
local menuList local -a menuList=()
for i in "$@" ; do for i in "$@" ; do
menuList+=("$i" "$i") menuList+=("$i" "$i")
done done
@@ -94,93 +134,371 @@ menulist() {
--menu "$(gettext "Please select one")" 0 0 0 "${menuList[@]}" --stdout --menu "$(gettext "Please select one")" 0 0 0 "${menuList[@]}" --stdout
} }
[[ -d ~/.config/barnard ]] || mkdir ~/.config/barnard trim() {
if [[ ! -r ~/.config/barnard/servers.conf ]]; then local value="$1"
echo "Adding default mumble server." | log value="${value#"${value%%[![:space:]]*}"}"
echo "declare -Ag mumbleServerList=(" > ~/.config/barnard/servers.conf value="${value%"${value##*[![:space:]]}"}"
echo "[Slint]=\"slint.fr:64738 -insecure\"" >> ~/.config/barnard/servers.conf printf '%s' "$value"
echo ")" >> ~/.config/barnard/servers.conf }
fi
source ~/.config/barnard/servers.conf
function add-server() { field_is_valid() {
local serverName="$(inputbox "$(gettext "Enter a name for the new server:")")" local value="$1"
[[ $? -ne 0 ]] && return [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]]
local serverAddress="$(inputbox "$(gettext "Enter the address of the server. If there is a password, do it in the form, password@address, if the port is not standard, add it after a :, address:port:")")" }
[[ $? -ne 0 ]] && return
local serverPassword="${serverAddress%@*}" port_is_valid() {
local serverAddress="${serverAddress#*@}" local port="$1"
local serverPort="${serverAddress##*:}" [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 ))
local serverAddress="${serverAddress%:*}" }
if ! [[ "$serverPort" =~ ^[0-9]+ ]]; then
serverPort=64738 parse_host_port() {
local hostPort
hostPort="$(trim "$1")"
parsedAddress=""
parsedPort="64738"
if [[ -z "$hostPort" ]]; then
return 1
fi fi
mumbleServerList[$serverName]="${serverAddress}:${serverPort}${serverPassword:+ -password ${serverPassword}}"
echo "declare -Ag mumbleServerList=(" > ~/.config/barnard/servers.conf if [[ "$hostPort" =~ ^\[([^]]+)\](:([0-9]+))?$ ]]; then
for i in "${!mumbleServerList[@]}" ; do parsedAddress="${BASH_REMATCH[1]}"
echo "[${i}]=\"${mumbleServerList[$i]}\"" >> ~/.config/barnard/servers.conf parsedPort="${BASH_REMATCH[3]:-64738}"
done elif [[ "$hostPort" =~ ^(.+):([0-9]+)$ ]]; then
echo ")" >> ~/.config/barnard/servers.conf parsedAddress="${BASH_REMATCH[1]}"
echo "Added server $serverName ${serverAddress}:${serverPort}" | log parsedPort="${BASH_REMATCH[2]}"
elif [[ "$hostPort" =~ ^(.+):([^:]+)$ ]]; then
return 1
else
parsedAddress="$hostPort"
fi
parsedAddress="$(trim "$parsedAddress")"
if [[ -z "$parsedAddress" ]] || ! port_is_valid "$parsedPort"; then
return 1
fi
field_is_valid "$parsedAddress"
}
parse_server_input() {
local raw="$1"
local hostPort
raw="$(trim "$raw")"
parsedPassword=""
if [[ -z "$raw" ]]; then
return 1
fi
if [[ "$raw" == *@* ]]; then
parsedPassword="${raw%%@*}"
hostPort="${raw#*@}"
else
hostPort="$raw"
fi
field_is_valid "$parsedPassword" && parse_host_port "$hostPort"
}
add_server_record() {
local serverName
local serverAddress="$2"
local serverPort="$3"
local serverPassword="$4"
local insecure="${5:-0}"
serverName="$(trim "$1")"
if [[ -z "$serverName" ]]; then
return 1
fi
if ! field_is_valid "$serverName" || ! field_is_valid "$serverAddress" || ! field_is_valid "$serverPassword"; then
return 1
fi
if ! port_is_valid "$serverPort"; then
return 1
fi
insecure="${insecure,,}"
if [[ "$insecure" == "true" || "$insecure" == "yes" || "$insecure" == "on" ]]; then
insecure="1"
fi
if [[ "$insecure" != "1" ]]; then
insecure="0"
fi
serverAddresses["$serverName"]="$serverAddress"
serverPorts["$serverName"]="$serverPort"
serverPasswords["$serverName"]="$serverPassword"
serverInsecure["$serverName"]="$insecure"
mumbleServerList["$serverName"]="$serverAddress:$serverPort"
}
server_names() {
printf '%s\n' "${!mumbleServerList[@]}" | LC_ALL=C sort
}
server_list_is_empty() {
(( ${#mumbleServerList[@]} == 0 ))
}
save_servers() {
local tmpFile="$serverFile.tmp"
local name
local insecure
if ! {
printf '# barnard-ui server list\n'
printf '# Passwords are stored only when provided; this file is written with mode 0600.\n\n'
while IFS= read -r name; do
[[ -z "$name" ]] && continue
if [[ "${serverInsecure[$name]}" == "1" ]]; then
insecure="true"
else
insecure="false"
fi
printf '[server]\n'
printf 'name = %s\n' "$name"
printf 'address = %s\n' "${serverAddresses[$name]}"
printf 'port = %s\n' "${serverPorts[$name]}"
printf 'password = %s\n' "${serverPasswords[$name]}"
printf 'insecure = %s\n\n' "$insecure"
done < <(server_names)
} > "$tmpFile"; then
rm -f "$tmpFile"
msgbox "$(gettext "Could not save server list.")"
return 1
fi
chmod 600 "$tmpFile" 2> /dev/null || true
if ! mv "$tmpFile" "$serverFile"; then
rm -f "$tmpFile"
msgbox "$(gettext "Could not save server list.")"
return 1
fi
}
load_servers() {
local line
local name
local address
local port
local password
local insecure
local key
local value
local inServerSection=0
local needsRewrite=0
[[ -r "$serverFile" ]] || return 0
flush_server_section() {
if (( inServerSection )); then
if [[ -n "$name" || -n "$address" || -n "$password" ]]; then
if ! add_server_record "$name" "$address" "$port" "$password" "$insecure"; then
printf 'Ignored invalid server entry from %s\n' "$serverFile" | log
needsRewrite=1
fi
fi
fi
name=""
address=""
port="64738"
password=""
insecure="0"
inServerSection=0
}
flush_server_section
while IFS= read -r line || [[ -n "$line" ]]; do
line="$(trim "$line")"
[[ -z "$line" || "$line" == \#* ]] && continue
if [[ "$line" =~ ^\[([^]]+)\]$ ]]; then
flush_server_section
if [[ "${BASH_REMATCH[1],,}" == "server" ]]; then
inServerSection=1
else
needsRewrite=1
fi
continue
fi
if (( ! inServerSection )); then
needsRewrite=1
continue
fi
if [[ "$line" == *=* ]]; then
key="${line%%=*}"
value="${line#*=}"
key="$(trim "$key")"
key="${key,,}"
value="$(trim "$value")"
case "$key" in
name) name="$value" ;;
address|host) address="$value" ;;
port) port="$value" ;;
password) password="$value" ;;
insecure) insecure="$value" ;;
*) needsRewrite=1 ;;
esac
else
needsRewrite=1
fi
done < "$serverFile"
flush_server_section
if (( needsRewrite )); then
save_servers
fi
}
config_has_nonempty_value() {
local key="$1"
local configFile="${2:-$HOME/.barnard.toml}"
local line
local currentKey
local value
key="${key,,}"
[[ -r "$configFile" ]] || return 1
while IFS= read -r line || [[ -n "$line" ]]; do
line="$(trim "$line")"
[[ -z "$line" || "$line" == \#* || "$line" != *=* ]] && continue
currentKey="${line%%=*}"
currentKey="$(trim "$currentKey")"
currentKey="${currentKey,,}"
[[ "$currentKey" == "$key" ]] || continue
value="${line#*=}"
value="$(trim "$value")"
[[ -z "$value" || "$value" == '""' || "$value" == "''" ]] && return 1
return 0
done < "$configFile"
return 1
}
add-server() {
local serverName
local serverAddress
local serverPassword
local insecure="0"
serverName="$(inputbox "$(gettext "Enter a name for the new server:")")" || return
serverName="$(trim "$serverName")"
if [[ -z "$serverName" ]]; then
msgbox "$(gettext "Server name cannot be empty.")"
return
fi
if ! field_is_valid "$serverName"; then
msgbox "$(gettext "Server name cannot contain line breaks.")"
return
fi
serverAddress="$(inputbox "$(gettext "Enter the address of the server. If the port is not standard, add it after a colon, like address:port.")")" || return
if ! parse_server_input "$serverAddress"; then
msgbox "$(gettext "Invalid server address or port.")"
return
fi
serverPassword="$(passwordbox "$(gettext "Enter the server password, or leave it blank if there is no password:")")" || return
if [[ -n "$serverPassword" ]]; then
if ! field_is_valid "$serverPassword"; then
msgbox "$(gettext "Server password cannot contain line breaks.")"
return
fi
parsedPassword="$serverPassword"
fi
if [[ "$(yesno "$(gettext "Skip server certificate verification for this server?")")" == "Yes" ]]; then
insecure="1"
fi
if ! add_server_record "$serverName" "$parsedAddress" "$parsedPort" "$parsedPassword" "$insecure"; then
msgbox "$(gettext "Could not add server. Check the server name, address, and password.")"
return
fi
save_servers || return
printf 'Added server %s %s:%s\n' "$serverName" "$parsedAddress" "$parsedPort" | log
msgbox "$(gettext "Added server") $serverName" msgbox "$(gettext "Added server") $serverName"
} }
connect() { connect() {
ifs="$IFS"
IFS=$'\n'
local serverName local serverName
serverName="$(menulist "${!mumbleServerList[@]}" "Go Back")" local barnardStatus
[[ $? -eq 1 ]] && exit 0 local -a names=()
IFS="$ifs" local -a barnardArgs=()
if [[ -z "$serverName" || "$serverName" == "Go Back" ]]; then
if server_list_is_empty; then
msgbox "$(gettext "No saved servers. Add a server first.")"
return return
fi fi
local username
username="$(grep -m 1 '^Username = ' ~/.barnard.toml 2> /dev/null | cut -d '=' -f2- | sed "s/^[[:space:]]*//;s/[[:space:]]*$//;s/'//g")" mapfile -t names < <(server_names)
username="${username//[[:space:]]/_}" serverName="$(menulist "${names[@]}" "$(gettext "Go Back")")" || return
username="${username:-${USER}-${HOSTNAME}}" if [[ -z "$serverName" || "$serverName" == "$(gettext "Go Back")" ]]; then
local certArgs=() return
if [[ -f "$certFile" ]]; then fi
certArgs=(-certificate "$certFile")
require_command barnard barnard
barnardArgs=(-server "${serverAddresses[$serverName]}:${serverPorts[$serverName]}")
if [[ -n "${serverPasswords[$serverName]}" ]]; then
barnardArgs+=(-password "${serverPasswords[$serverName]}")
fi
if [[ "${serverInsecure[$serverName]}" == "1" ]]; then
barnardArgs+=(-insecure)
fi
if ! config_has_nonempty_value username; then
barnardArgs+=(-username "${USER}-${HOSTNAME}")
fi
if [[ -f "$certFile" ]] && ! config_has_nonempty_value certificate; then
barnardArgs+=(-certificate "$certFile")
fi
command barnard "${barnardArgs[@]}" --fifo "$configDir/cmd" --buffers 16 |& log
barnardStatus=${PIPESTATUS[0]}
if (( barnardStatus != 0 )); then
msgbox "$(gettext "Barnard exited with status") $barnardStatus. $(gettext "See log:") $logFile"
fi fi
# shellcheck disable=SC2086
command barnard -username "$username" -server ${mumbleServerList[$serverName]} "${certArgs[@]}" --fifo ~/.config/barnard/cmd --buffers 16 |& log
} }
remove-server() { remove-server() {
ifs="$IFS" local serverName
IFS=$'\n' local -a names=()
local serverName="$(menulist "${!mumbleServerList[@]}" "Go Back")"
IFS="$ifs" if server_list_is_empty; then
if [[ -z "$serverName" || "$serverName" == "Go Back" ]]; then msgbox "$(gettext "No saved servers to remove.")"
return return
fi fi
unset "mumbleServerList[$serverName]"
echo "declare -Ag mumbleServerList=(" > ~/.config/barnard/servers.conf
for i in "${!mumbleServerList[@]}" ; do
echo "[${i}]=\"${mumbleServerList[$i]}\"" >> ~/.config/barnard/servers.conf
done
echo ")" >> ~/.config/barnard/servers.conf
echo "Removed server $serverName ${serverAddress}:${serverPort}" | log
msgbox "$(gettext "Removed server") $serverName"
}
# Certificate configuration mapfile -t names < <(server_names)
certDir="$HOME/.config/barnard" serverName="$(menulist "${names[@]}" "$(gettext "Go Back")")" || return
certFile="$certDir/barnard.pem" if [[ -z "$serverName" || "$serverName" == "$(gettext "Go Back")" ]]; then
return
fi
unset "mumbleServerList[$serverName]"
unset "serverAddresses[$serverName]"
unset "serverPorts[$serverName]"
unset "serverPasswords[$serverName]"
unset "serverInsecure[$serverName]"
save_servers || return
printf 'Removed server %s\n' "$serverName" | log
msgbox "$(gettext "Removed server") $serverName"
}
generate-certificate() { generate-certificate() {
local commonName
require_command openssl openssl
if [[ -f "$certFile" ]]; then if [[ -f "$certFile" ]]; then
if [[ "$(yesno "$(gettext "A certificate already exists. Do you want to replace it? This may affect your registered identity on servers.")")" != "Yes" ]]; then if [[ "$(yesno "$(gettext "A certificate already exists. Do you want to replace it? This may affect your registered identity on servers.")")" != "Yes" ]]; then
return return
fi fi
fi fi
local commonName commonName="$(inputbox "$(gettext "Enter a name for your certificate (e.g., your username):")" "barnard")" || return
commonName="$(inputbox "$(gettext "Enter a name for your certificate (e.g., your username):")" "barnard")"
[[ $? -ne 0 ]] && return
[[ -z "$commonName" ]] && commonName="barnard" [[ -z "$commonName" ]] && commonName="barnard"
if openssl req -x509 -newkey rsa:2048 -keyout "$certFile" -out "$certFile" -days 3650 -nodes -subj "/CN=$commonName" 2>/dev/null; then if openssl req -x509 -newkey rsa:2048 -keyout "$certFile" -out "$certFile" -days 3650 -nodes -subj "/CN=$commonName" 2> /dev/null; then
chmod 600 "$certFile" chmod 600 "$certFile"
msgbox "$(gettext "Certificate generated successfully.")" msgbox "$(gettext "Certificate generated successfully.")"
else else
@@ -189,12 +507,14 @@ generate-certificate() {
} }
view-certificate() { view-certificate() {
local certInfo
require_command openssl openssl
if [[ ! -f "$certFile" ]]; then if [[ ! -f "$certFile" ]]; then
msgbox "$(gettext "No certificate found.") $certFile" msgbox "$(gettext "No certificate found.") $certFile"
return return
fi fi
local certInfo certInfo=$(openssl x509 -in "$certFile" -noout -subject -dates -fingerprint 2> /dev/null)
certInfo=$(openssl x509 -in "$certFile" -noout -subject -dates -fingerprint 2>/dev/null)
if [[ -n "$certInfo" ]]; then if [[ -n "$certInfo" ]]; then
msgbox "$certInfo" msgbox "$certInfo"
else else
@@ -204,8 +524,9 @@ view-certificate() {
import-certificate() { import-certificate() {
local importPath local importPath
importPath="$(inputbox "$(gettext "Enter the full path to your certificate file (PEM format with certificate and private key):")")" require_command openssl openssl
[[ $? -ne 0 ]] && return
importPath="$(inputbox "$(gettext "Enter the full path to your certificate file (PEM format with certificate and private key):")")" || return
[[ -z "$importPath" ]] && return [[ -z "$importPath" ]] && return
# Expand ~ if present # Expand ~ if present
@@ -217,13 +538,13 @@ import-certificate() {
fi fi
# Verify it's a valid certificate # Verify it's a valid certificate
if ! openssl x509 -in "$importPath" -noout 2>/dev/null; then if ! openssl x509 -in "$importPath" -noout 2> /dev/null; then
msgbox "$(gettext "The file does not appear to be a valid PEM certificate.")" msgbox "$(gettext "The file does not appear to be a valid PEM certificate.")"
return return
fi fi
# Verify it contains a private key # Verify it contains a private key
if ! openssl rsa -in "$importPath" -check -noout 2>/dev/null && ! openssl ec -in "$importPath" -check -noout 2>/dev/null; then if ! openssl rsa -in "$importPath" -check -noout 2> /dev/null && ! openssl ec -in "$importPath" -check -noout 2> /dev/null; then
msgbox "$(gettext "The file does not appear to contain a valid private key. The certificate file must contain both the certificate and private key.")" msgbox "$(gettext "The file does not appear to contain a valid private key. The certificate file must contain both the certificate and private key.")"
return return
fi fi
@@ -242,29 +563,37 @@ import-certificate() {
} }
manage-certificate() { manage-certificate() {
local certAction
while : ; do while : ; do
local certAction certAction="$(menulist "$(gettext "Generate")" "$(gettext "View")" "$(gettext "Import")" "$(gettext "Go Back")")" || return
certAction="$(menulist "Generate" "View" "Import" "Go_Back")"
[[ $? -eq 1 ]] && return
case "$certAction" in case "$certAction" in
"Generate") generate-certificate ;; "$(gettext "Generate")") generate-certificate ;;
"View") view-certificate ;; "$(gettext "View")") view-certificate ;;
"Import") import-certificate ;; "$(gettext "Import")") import-certificate ;;
"Go_Back"|"") return ;; "$(gettext "Go Back")"|"") return ;;
esac esac
done done
} }
# main menu main() {
while : ; do local action
action="$(menulist "Connect" "Add_server" "Remove_server" "Manage_Certificate")"
[[ $? -eq 1 ]] && exit 0
action="${action,,}"
action="${action//_/-}"
if [[ "$action" == "exit" ]]; then
exit 0
fi
eval "$action" require_command dialog dialog
load_servers
done while : ; do
action="$(menulist "$(gettext "Connect")" "$(gettext "Add server")" "$(gettext "Remove server")" "$(gettext "Manage Certificate")" "$(gettext "Exit")")" || exit 0
case "$action" in
"$(gettext "Connect")") connect ;;
"$(gettext "Add server")") add-server ;;
"$(gettext "Remove server")") remove-server ;;
"$(gettext "Manage Certificate")") manage-certificate ;;
"$(gettext "Exit")"|"") exit 0 ;;
esac
done
}
if [[ "${BARNARD_UI_TESTING:-0}" != "1" ]]; then
main "$@"
fi