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'
if [ "$(echo $@ | grep '^root@' > /dev/null)" ]
then
AS_ROOT='true'
fi
class RgbColor():
red: int
green: int
blue: int
def __init__(self, red: int, green: int, blue: int):
self.red = red
self.green = green
self.blue = blue
function getHostRealFqdn()
{
local strHostname="$1"
UserId = str # eg 'root'
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'
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}'
def __str__(self) -> str:
target_as_str = f'{self.host_id}'
if self.user_id:
target_as_str = f'{self.user_id}@{target_as_str}'
return target_as_str
# '''
# function getHostRealFqdn()
# {
# local strHostname="$1"
local strHostFqdn="$(host $strHostname | tail -1 | awk '{print $1}')"
echo "$strHostFqdn"
}
# local strHostFqdn="$(host $strHostname | tail -1 | awk '{print $1}')"
# echo "$strHostFqdn"
# }
HOST_FQDN=$(getHostRealFqdn $HOSTNAME)
# HOST_FQDN=$(getHostRealFqdn $HOSTNAME)
SOLARIZED_BACKGROUND='{0, 8256, 10368}' #002b36 (solarized dark base03)
SAILOR_BLUE='{8832, 14208, 18816}' # 2e4a62
PRUNE='{15360, 10944, 14592}' #50394c
TAUPE='{19968, 18816, 16512}' #686256
DARK_ORANGE='{27456, 12864, 0}' #8f4300
DARK_OLIVE='{8832, 11904, 5568}' #2e3e1d
# SOLARIZED_BACKGROUND='{0, 8256, 10368}' #002b36 (solarized dark base03)
# SAILOR_BLUE='{8832, 14208, 18816}' # 2e4a62
# PRUNE='{15360, 10944, 14592}' #50394c
# TAUPE='{19968, 18816, 16512}' #686256
# DARK_ORANGE='{27456, 12864, 0}' #8f4300
# DARK_OLIVE='{8832, 11904, 5568}' #2e3e1d
function get_current_terminal_window_id()
{
local terminal_windows_id=''
terminal_windows_id=$(osascript -e "tell application \"Terminal\" to get id of window 1")
echo "$terminal_windows_id"
}
# function get_current_terminal_window_id()
# {
# local terminal_windows_id=''
# terminal_windows_id=$(osascript -e "tell application \"Terminal\" to get id of window 1")
# echo "$terminal_windows_id"
# }
function set_bg ()
{
local strOsxTermWindowId="$1"
local strColor="$2" # eg {45000, 0, 0, 50000}
# function set_bg ()
# {
# 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"
}
# 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
DCONF_PROFILES_PATH='/org/gnome/terminal/legacy/profiles:'
create_new_profile()
{
# 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 ^: |\
sed 's/\///g' | sed 's/://g'))
local profile_name="$1"
local profile_ids_old="$(dconf read "$DCONF_PROFILES_PATH"/list | tr -d "]")"
local profile_id="$(uuidgen)"
[ -z "$profile_ids_old" ] && local lb="[" # if there's no `list` key
[ ${#profile_ids[@]} -gt 0 ] && local delimiter=, # if the list is empty
dconf write $DCONF_PROFILES_PATH/list \
"${profile_ids_old}${delimiter} '$profile_id']"
dconf write "$DCONF_PROFILES_PATH/:$profile_id"/visible-name "'$profile_name'"
echo $profile_id
}
# function on_exit ()
# {
# set_bg "$THIS_OSX_TERM_WINDOW_ID" "$SOLARIZED_BACKGROUND"
# }
function get_terminal_profile_uuid()
{
# 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
# # 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
local profile=''
# #if [ "$AS_ROOT" = 'true' ]
# #then
# # BG_COLOR="$DARK_ORANGE"
# #fi
local this_profile=''
local profile_uuid=''
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
for this_profile in $(dconf list "$DCONF_PROFILES_PATH/")
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
GSettingsKeyPath = str # eg org.gnome.Terminal.ProfilesList
# the profile named $profile_name doesn't exist... create it then
profile_uuid=$(create_new_profile $profile_name)
echo "$profile_uuid"
}
class GnomeSettings():
function set_terminal_profile_bg_color()
{
local profile_name="$1" # eg physix.ipr.univ-rennes1.fr
local bg_color="$2" # eg rgb(17, 25, 24)
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))
local profile_uuid=''
profile_uuid=$(get_terminal_profile_uuid $profile_name)
echo "profile_name=$profile_name profile_uuid=$profile_uuid"
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
dconf write $DCONF_PROFILES_PATH/:$profile_uuid/use-theme-colors 'false'
dconf write $DCONF_PROFILES_PATH/:$profile_uuid/background-color "'rgb$bg_color'"
}
def set_list(self, key_id: GSettingsKeyPath, value: List[str]):
COLOR_SATURATION='0.3'
COLOR_VALUE='0.2'
OS_NAME=$(uname)
case "$OS_NAME" in
'Darwin')
BG_COLOR=$(make_color.py $HOST_FQDN $COLOR_VALUE $COLOR_SATURATION osx)
THIS_OSX_TERM_WINDOW_ID=$(get_current_terminal_window_id)
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"
# 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)
gnome-terminal --window-with-profile=$HOST_FQDN --title=$HOST_FQDN -- /usr/bin/ssh "$@"
;;
*)
echo "error : unexpeced os name : $OS_NAME"
exit 1
esac
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:'
@staticmethod
def create_new_profile(profile_name: TermProfileName) -> TermProfileUuid:
'''
copied from https://askubuntu.com/questions/270469/how-can-i-create-a-new-profile-for-gnome-terminal-via- command-line
'''
# 20260616-16:24:12 graffy@graffy-ws2:~$ dconf list '/org/gnome/terminal/legacy/profiles:/'
# :2a05665f-baa7-484e-b179-4e94f3814238/
# :2e0dec33-5ec2-4355-b671-7922010cee9f/
# last command status : [0]
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
@staticmethod
def get_term_profile_name(profile_uuid: TermProfileUuid) -> Optional[TermProfileName]:
profile_name = None
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
'''
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
logging.info('the profile named %s doesnt exist... create it then', profile_name)
profile_uuid = GnomeTerminal.create_new_profile(profile_name)
return profile_uuid
@staticmethod
def set_terminal_profile_bg_color(profile_name: TermProfileName, bg_color: RgbColor):
# function set_terminal_profile_bg_color()
# profile_name # eg physix.ipr.univ-rennes1.fr
# bg_color # eg RgbColor(17, 25, 24)
profile_uuid = GnomeTerminal.get_terminal_profile_uuid(profile_name)
logging.debug("profile_name=%s profile_uuid=%s", profile_name, profile_uuid)
subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/use-theme-colors", 'false'], check=True, stdout=subprocess.PIPE)
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)
def main():
logging.basicConfig(level=logging.DEBUG)
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')
arg_parser.add_argument('ssh_target', help='eg root@alambix50.ipr.univ-rennes.fr')
args = arg_parser.parse_args()
logging.debug(args)
ssh_target = SshTarget(args.ssh_target) # eg 'root@alambix50.ipr.univ-rennes.fr'
# 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()