adapted cssh to ubuntu 26.04

because gnome changed:
```md
1. What Changed Between Ubuntu 20.04 and 26.04?
Ubuntu 20.04 (GNOME 3.36)

In older versions of gnome-terminal (e.g., 3.36.x), profiles were automatically registered in the org.gnome.Terminal.ProfilesList schema when they were created via dconf or the GUI.
The list key in org.gnome.Terminal.ProfilesList was dynamically updated to include any profile that existed in /org/gnome/terminal/legacy/profiles:/.
Result: You could create a profile (e.g., via dconf write), and gnome-terminal would automatically recognize it, even if you didn’t explicitly add it to the list key.
Ubuntu 26.04 (GNOME 46+)

In newer versions of gnome-terminal (e.g., 3.58.x or later), the list key in org.gnome.Terminal.ProfilesList is now strictly enforced.
Profiles must be explicitly added to the list key to be recognized by gnome-terminal.
Result: If you create a profile via dconf but don’t add it to the list key, gnome-terminal will ignore it and fall back to the default profile.
```

- took this opportunity to rewrite cssh in python for clarity
This commit is contained in:
Guillaume Raffy 2026-06-17 00:25:04 +02:00
parent 9f4f301ba8
commit 0e7309d3b6
1 changed files with 291 additions and 141 deletions

View File

@ -1,163 +1,313 @@
#!/bin/bash #!/usr/bin/env python3
import logging
from typing import Optional, List
import argparse
import re
import subprocess
HOSTNAME=`echo $@ | sed s/.*@//`
AS_ROOT='false' class RgbColor():
if [ "$(echo $@ | grep '^root@' > /dev/null)" ] red: int
then green: int
AS_ROOT='true' blue: int
fi def __init__(self, red: int, green: int, blue: int):
self.red = red
self.green = green
self.blue = blue
function getHostRealFqdn()
{
local strHostname="$1"
local strHostFqdn="$(host $strHostname | tail -1 | awk '{print $1}')" UserId = str # eg 'root'
echo "$strHostFqdn" HostId = str # eg alambix50.ipr.univ-rennes.fr or alambix50
} TermProfileName = str # terminal profile name eg 'alambix50.ipr.univ-rennes.fr'
TermProfileUuid = str # eg '2e0dec33-5ec2-4355-b671-7922010cee9f'
HOST_FQDN=$(getHostRealFqdn $HOSTNAME) class SshTarget():
user_id: Optional[UserId]
host_id: HostId
# eg root@alambix50.ipr.univ-rennes.fr
def __init__(self, ssh_target_as_str: str):
parts = ssh_target_as_str.split('@')
assert len(parts) > 0 and len(parts) <= 2, "malformed ssh target : {ssh_target}. It is expected to be of the form [<user_id>@]<host_id>"
self.host_id = parts[-1] # eg 'alambix50.ipr.univ-rennes.fr'
self.user_id = None
if len(parts) > 1:
self.user_id = parts[0]
assert re.match(r'^[a-z]+$', self.user_id), f'malformed user id : {self.user_id}'
SOLARIZED_BACKGROUND='{0, 8256, 10368}' #002b36 (solarized dark base03) def __str__(self) -> str:
SAILOR_BLUE='{8832, 14208, 18816}' # 2e4a62 target_as_str = f'{self.host_id}'
PRUNE='{15360, 10944, 14592}' #50394c if self.user_id:
TAUPE='{19968, 18816, 16512}' #686256 target_as_str = f'{self.user_id}@{target_as_str}'
DARK_ORANGE='{27456, 12864, 0}' #8f4300 return target_as_str
DARK_OLIVE='{8832, 11904, 5568}' #2e3e1d
function get_current_terminal_window_id() # '''
{ # function getHostRealFqdn()
local terminal_windows_id='' # {
terminal_windows_id=$(osascript -e "tell application \"Terminal\" to get id of window 1") # local strHostname="$1"
echo "$terminal_windows_id"
}
function set_bg () # local strHostFqdn="$(host $strHostname | tail -1 | awk '{print $1}')"
{ # echo "$strHostFqdn"
local strOsxTermWindowId="$1" # }
local strColor="$2" # eg {45000, 0, 0, 50000}
osascript -e "tell application \"Terminal\" to set background color of (every window whose id is $strOsxTermWindowId) to $strColor" # HOST_FQDN=$(getHostRealFqdn $HOSTNAME)
}
function on_exit () # SOLARIZED_BACKGROUND='{0, 8256, 10368}' #002b36 (solarized dark base03)
{ # SAILOR_BLUE='{8832, 14208, 18816}' # 2e4a62
set_bg "$THIS_OSX_TERM_WINDOW_ID" "$SOLARIZED_BACKGROUND" # PRUNE='{15360, 10944, 14592}' #50394c
} # TAUPE='{19968, 18816, 16512}' #686256
# DARK_ORANGE='{27456, 12864, 0}' #8f4300
# DARK_OLIVE='{8832, 11904, 5568}' #2e3e1d
# if [ $(echo $HOST_FQDN | grep '^simpatix') ] # function get_current_terminal_window_id()
# then # {
# BG_COLOR="$SAILOR_BLUE" # local terminal_windows_id=''
# elif [ $(echo $HOST_FQDN | grep '^physix') ] # terminal_windows_id=$(osascript -e "tell application \"Terminal\" to get id of window 1")
# then # echo "$terminal_windows_id"
# BG_COLOR="$TAUPE" # }
# else
# case $HOST_FQDN in
# 'pr079234.spm.univ-rennes1.fr')
# BG_COLOR="$SOLARIZED_BACKGROUND"
# ;;
# 'puppet3.ipr.univ-rennes1.fr')
# BG_COLOR="$PRUNE"
# ;;
# *)
# BG_COLOR="$DARK_OLIVE"
# ;;
# esac
# fi
#if [ "$AS_ROOT" = 'true' ] # function set_bg ()
#then # {
# BG_COLOR="$DARK_ORANGE" # local strOsxTermWindowId="$1"
#fi # local strColor="$2" # eg {45000, 0, 0, 50000}
# osascript -e "tell application \"Terminal\" to set background color of (every window whose id is $strOsxTermWindowId) to $strColor"
# }
# function on_exit ()
# {
# set_bg "$THIS_OSX_TERM_WINDOW_ID" "$SOLARIZED_BACKGROUND"
# }
# # if [ $(echo $HOST_FQDN | grep '^simpatix') ]
# # then
# # BG_COLOR="$SAILOR_BLUE"
# # elif [ $(echo $HOST_FQDN | grep '^physix') ]
# # then
# # BG_COLOR="$TAUPE"
# # else
# # case $HOST_FQDN in
# # 'pr079234.spm.univ-rennes1.fr')
# # BG_COLOR="$SOLARIZED_BACKGROUND"
# # ;;
# # 'puppet3.ipr.univ-rennes1.fr')
# # BG_COLOR="$PRUNE"
# # ;;
# # *)
# # BG_COLOR="$DARK_OLIVE"
# # ;;
# # esac
# # fi
# #if [ "$AS_ROOT" = 'true' ]
# #then
# # BG_COLOR="$DARK_ORANGE"
# #fi
def is_valid_uuid(uuid: str) -> bool:
'''
uuid: eg 'b1dcc9dd-5262-4d8d-a863-c897e6d979b9'
'''
m = re.match(r'^[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]$', uuid)
return m is not None
GSettingsKeyPath = str # eg org.gnome.Terminal.ProfilesList
class GnomeSettings():
def get_list(self, key_id: GSettingsKeyPath) -> List[str]:
# 20260616-17:34:08 graffy@graffy-ws2:~$ gsettings get org.gnome.Terminal.ProfilesList list
# ['b1dcc9dd-5262-4d8d-a863-c897e6d979b9', '2e0dec33-5ec2-4355-b671-7922010cee9f']
proc = subprocess.run(['gsettings', 'get', key_id, 'list'], check=True, stdout=subprocess.PIPE)
lines = proc.stdout.decode('utf8').split('\n')
values = []
assert len(lines) == 2
m = re.match(r'^\[(?P<series_of_strings>[^]]*)\]$', lines[0])
assert m is not None
series_of_strings = m['series_of_strings']
logging.debug('get_list: series_of_strings = %s', str(series_of_strings))
strings = series_of_strings.split(', ')
for s in strings:
m = re.match(r"'(?P<unquoted_string>[^']*)'", s)
assert m is not None
values.append(m['unquoted_string'])
logging.debug('get_list: values = %s', str(values))
return values
def set_list(self, key_id: GSettingsKeyPath, value: List[str]):
# gsettings set org.gnome.Terminal.ProfilesList list "$(echo "$CURRENT_LIST" | tr -d "[]" | sed "s/$/,$PROFILE_ID/")"
value_as_str = ','.join([f"'{unq_str}'" for unq_str in value])
logging.debug('value_as_str = "%s"', value_as_str)
subprocess.run(['gsettings', 'set', key_id, 'list', f"[{value_as_str}]"], check=True)
def register_profile_uuid(self, profile_uuid: TermProfileUuid):
'''
profile_uuid: eg '2e0dec33-5ec2-4355-b671-7922010cee9f'
'''
assert is_valid_uuid(profile_uuid), f'invalid uuid: "{profile_uuid}"'
profiles_key_id = 'org.gnome.Terminal.ProfilesList'
profile_uuids = self.get_list(profiles_key_id)
assert profile_uuid not in profile_uuids
profile_uuids.append(profile_uuid)
self.set_list(profiles_key_id, profile_uuids)
class GnomeTerminal():
DCONF_PROFILES_PATH = '/org/gnome/terminal/legacy/profiles:' DCONF_PROFILES_PATH = '/org/gnome/terminal/legacy/profiles:'
create_new_profile() # DCONF_PROFILES_PATH='/org/gnome/terminal/legacy/profiles:'
{
# copied from https://askubuntu.com/questions/270469/how-can-i-create-a-new-profile-for-gnome-terminal-via- command-line
local profile_ids=($(dconf list $DCONF_PROFILES_PATH/ | grep ^: |\ @staticmethod
sed 's/\///g' | sed 's/://g')) def create_new_profile(profile_name: TermProfileName) -> TermProfileUuid:
local profile_name="$1" '''
local profile_ids_old="$(dconf read "$DCONF_PROFILES_PATH"/list | tr -d "]")" copied from https://askubuntu.com/questions/270469/how-can-i-create-a-new-profile-for-gnome-terminal-via- command-line
local profile_id="$(uuidgen)" '''
[ -z "$profile_ids_old" ] && local lb="[" # if there's no `list` key # 20260616-16:24:12 graffy@graffy-ws2:~$ dconf list '/org/gnome/terminal/legacy/profiles:/'
[ ${#profile_ids[@]} -gt 0 ] && local delimiter=, # if the list is empty # :2a05665f-baa7-484e-b179-4e94f3814238/
dconf write $DCONF_PROFILES_PATH/list \ # :2e0dec33-5ec2-4355-b671-7922010cee9f/
"${profile_ids_old}${delimiter} '$profile_id']" # last command status : [0]
dconf write "$DCONF_PROFILES_PATH/:$profile_id"/visible-name "'$profile_name'"
echo $profile_id old_profile_uuids = GnomeTerminal.get_term_profiles()
} # local profile_ids=($(dconf list $DCONF_PROFILES_PATH/ | grep ^: |\
# sed 's/\///g' | sed 's/://g'))
# local profile_name="$1"
# local profile_ids_old="$(dconf read "$DCONF_PROFILES_PATH"/list | tr -d "]")"
proc = subprocess.run(['uuidgen'], check=True, stdout=subprocess.PIPE)
lines = proc.stdout.decode('utf8').split('\n')
assert len(lines) == 2
profile_uuid = lines[0]
m = re.match(r'^[a-z0-9\-]+$', profile_uuid)
logging.info("creating profile profile_uuid=%s with profile_name=%s", profile_uuid, profile_name)
# echo "new_profiles_list=$new_profiles_list"
# dconf write $DCONF_PROFILES_PATH/list "$new_profiles_list"
proc = subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/visible-name", f"'{profile_name}'"], check=True)
assert GnomeTerminal.get_term_profile_name(profile_name) == profile_uuid
# dconf write /org/gnome/terminal/legacy/profiles:/:dc932a58-6a38-4511-876a-d453b5e33a26/visible-name "'toto'"
gsettings = GnomeSettings()
gsettings.register_profile_uuid(profile_uuid)
return profile_uuid
@staticmethod
def get_term_profiles() -> List[TermProfileUuid]:
proc = subprocess.run(['dconf', 'list', f"{GnomeTerminal.DCONF_PROFILES_PATH}/"], check=True, stdout=subprocess.PIPE)
profile_uuids = []
for line in proc.stdout.decode('utf8').split('\n'):
print(line)
# eg ':b1dcc9dd-5262-4d8d-a863-c897e6d979b9/'
m = re.match(r'^:?(?P<uuid>[0-9a-f\-]+)/$', line)
if m is None:
assert line in ['list', ''], f'unexpected format for line: {line}'
else:
profile_uuids.append(m['uuid'])
return profile_uuids
function get_terminal_profile_uuid() @staticmethod
{ def get_term_profile_name(profile_uuid: TermProfileUuid) -> Optional[TermProfileName]:
# returns the id of the profile which has the given name. profile_name = None
# if the named profile doesn't exist, it creates it first proc = subprocess.run(['dconf', 'read', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/visible-name"], check=True, stdout=subprocess.PIPE)
lines = proc.stdout.decode('utf8').split('\n')
if (len(lines) >= 2):
assert len(lines) == 2, lines
assert lines[1] == ''
m = re.match(r"^\'(?P<profile_name>[^']+)'$", lines[0])
assert m, f'unexpected line : {line}'
profile_name = m['profile_name']
else:
pass # (the default profile has no name)
#assert False, f'unexpected case: the terminal profile profile_uuid={profile_uuid} has no name!!!'
return profile_name
@staticmethod
def get_terminal_profile_uuid(profile_name: TermProfileName) -> TermProfileUuid:
'''
returns the id of the profile which has the given name.
if the named profile doesn't exist, it creates it first
local profile_name="$1" # eg physix.ipr.univ-rennes1.fr local profile_name="$1" # eg physix.ipr.univ-rennes1.fr
'''
local profile='' profile_uuid = None
for profile_uuid in GnomeTerminal.get_term_profiles():
logging.debug(f'processing profile_uuid={profile_uuid}')
prof_name = GnomeTerminal.get_term_profile_name(profile_uuid)
logging.debug('prof_name = %s', prof_name)
if profile_name == prof_name:
return profile_uuid
local this_profile='' logging.info('the profile named %s doesnt exist... create it then', profile_name)
local profile_uuid='' profile_uuid = GnomeTerminal.create_new_profile(profile_name)
for this_profile in $(dconf list "$DCONF_PROFILES_PATH/") return profile_uuid
do
# eg value for profile : ':b1dcc9dd-5262-4d8d-a863-c897e6d979b9/'
local this_profile_name=''
this_profile_name=$(dconf read $DCONF_PROFILES_PATH/${this_profile}visible-name | tr -d "'" )
# echo "this_profile_name=$this_profile_name"
if [ "$this_profile_name" = "$profile_name" ]
then
profile_uuid=$(echo "$this_profile" | sed 's/^://' | sed 's|/||')
echo "$profile_uuid"
return 0
fi
done
# the profile named $profile_name doesn't exist... create it then @staticmethod
profile_uuid=$(create_new_profile $profile_name) def set_terminal_profile_bg_color(profile_name: TermProfileName, bg_color: RgbColor):
echo "$profile_uuid" # function set_terminal_profile_bg_color()
} # profile_name # eg physix.ipr.univ-rennes1.fr
# bg_color # eg RgbColor(17, 25, 24)
function set_terminal_profile_bg_color() profile_uuid = GnomeTerminal.get_terminal_profile_uuid(profile_name)
{ logging.debug("profile_name=%s profile_uuid=%s", profile_name, profile_uuid)
local profile_name="$1" # eg physix.ipr.univ-rennes1.fr
local bg_color="$2" # eg rgb(17, 25, 24)
local profile_uuid='' subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/use-theme-colors", 'false'], check=True, stdout=subprocess.PIPE)
profile_uuid=$(get_terminal_profile_uuid $profile_name) subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/background-color", f'({bg_color.red}, {bg_color.green}, {bg_color.blue})'], check=True, stdout=subprocess.PIPE)
echo "profile_name=$profile_name profile_uuid=$profile_uuid"
dconf write $DCONF_PROFILES_PATH/:$profile_uuid/use-theme-colors 'false'
dconf write $DCONF_PROFILES_PATH/:$profile_uuid/background-color "'rgb$bg_color'"
}
COLOR_SATURATION='0.3' def main():
COLOR_VALUE='0.2' logging.basicConfig(level=logging.DEBUG)
OS_NAME=$(uname) arg_parser = argparse.ArgumentParser('cssh (colored ssh) opens a terminal window with a background color that depends on the fully qualified domain name of the target machine')
case "$OS_NAME" in arg_parser.add_argument('ssh_target', help='eg root@alambix50.ipr.univ-rennes.fr')
'Darwin') args = arg_parser.parse_args()
BG_COLOR=$(make_color.py $HOST_FQDN $COLOR_VALUE $COLOR_SATURATION osx) logging.debug(args)
THIS_OSX_TERM_WINDOW_ID=$(get_current_terminal_window_id) ssh_target = SshTarget(args.ssh_target) # eg 'root@alambix50.ipr.univ-rennes.fr'
set_bg "$THIS_OSX_TERM_WINDOW_ID" "$BG_COLOR"
trap on_exit EXIT
/usr/bin/ssh "$@"
;;
'Linux')
BG_COLOR=$(make_color.py $HOST_FQDN $COLOR_VALUE $COLOR_SATURATION linux)
if [ $? != 0 ]
then
echo "error : make_color.py failed"
exit 1
fi
set_terminal_profile_bg_color "$HOST_FQDN" "$BG_COLOR"
gnome-terminal --window-with-profile=$HOST_FQDN --title=$HOST_FQDN -- /usr/bin/ssh "$@"
;;
*)
echo "error : unexpeced os name : $OS_NAME"
exit 1
esac
# AS_ROOT='false'
# if [ "$(echo $@ | grep '^root@' > /dev/null)" ]
# then
# AS_ROOT='true'
# fi
color_saturation = 0.3
color_value = 0.2
proc = subprocess.run(['uname'], check=True, stdout=subprocess.PIPE)
os_name = proc.stdout.decode('utf8').strip()
if os_name == 'Darwin':
raise NotImplementedError()
# 'Darwin')
# BG_COLOR=$(make_color.py $HOST_FQDN $COLOR_VALUE $COLOR_SATURATION osx)
# THIS_OSX_TERM_WINDOW_ID=$(get_current_terminal_window_id)
# echo "THIS_OSX_TERM_WINDOW_ID=$THIS_OSX_TERM_WINDOW_ID"
# set_bg "$THIS_OSX_TERM_WINDOW_ID" "$BG_COLOR"
# trap on_exit EXIT
# /usr/bin/ssh "$@"
# ;;
elif os_name == 'Linux':
proc = subprocess.run(['make_color.py', ssh_target.host_id, str(color_value), str(color_saturation), 'linux'], check=True, stdout=subprocess.PIPE)
bg_color_as_str = proc.stdout.decode('utf8').strip()
match = re.match(r'\((?P<red>[0-9]+), (?P<green>[0-9]+), (?P<blue>[0-9]+)\)', bg_color_as_str)
assert match, f'unexpected color as string: {bg_color_as_str}'
bg_color = RgbColor(match['red'], match['green'], match['blue'])
logging.debug('bg_color = %s', str(bg_color)) # eg (35, 43, 51)
GnomeTerminal.set_terminal_profile_bg_color(ssh_target.host_id, bg_color)
profile_uuid = GnomeTerminal.get_terminal_profile_uuid(ssh_target.host_id)
launch_term_command = ['gnome-terminal', f'--window-with-profile={profile_uuid}', f'--title={ssh_target.host_id}', '--', '/usr/bin/ssh', f'{str(ssh_target)}']
logging.debug('launch_term_command = %s', launch_term_command)
subprocess.run(launch_term_command, check=True)
else:
raise NotImplementedError(f'unhandled os : "{os_name}"')
main()