Skip to content

Instantly share code, notes, and snippets.

@birgersp
Last active June 8, 2026 07:41
Show Gist options
  • Select an option

  • Save birgersp/c7d84daca89d68daa9d68a65ec67e0f0 to your computer and use it in GitHub Desktop.

Select an option

Save birgersp/c7d84daca89d68daa9d68a65ec67e0f0 to your computer and use it in GitHub Desktop.
fedora-silverblue-setup.py
import argparse
from os import environ, path
import os
import shutil
import subprocess
from typing import Literal, TypeGuard, get_args
FEDORA_VERSION = 44
RPM_PACKAGES = [
"akmods",
"rpmdevtools",
"code",
"dbus-tools",
"git",
"insync",
"mosh",
"pipx",
"postgresql",
"yaru-theme",
"zsh",
"https://desktop.docker.com/linux/main/amd64/docker-desktop-x86_64.rpm",
"https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm",
f"https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-{FEDORA_VERSION}.noarch.rpm",
f"https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-{FEDORA_VERSION}.noarch.rpm",
]
FLATPAK_APPS = [
"com.github.git_cola.git-cola",
"org.pgadmin.pgadmin4",
"com.spotify.Client",
"com.discordapp.Discord",
"io.github.Faugus.faugus-launcher",
"org.sqlitebrowser.sqlitebrowser",
"com.valvesoftware.Steam",
"page.tesk.Refine",
"com.mattjakeman.ExtensionManager",
"io.github.kolunmi.Bazaar",
"io.github.pwr_solaar.solaar",
]
GNOME_EXTENSIONS = [
"quicksettings-audio-devices-hider@marcinjahn.com",
"appindicatorsupport@rgcjonas.gmail.com",
"clipboard-indicator@tudmotu.com",
"dash-to-panel@jderose9.github.com",
"emoji-copy@felipeftn",
"gnome-extensions-cli install",
"switcher@landau.fi",
"system-monitor@gnome-shell-extensions.gcampax.github.com",
"window-calls@domandoman.xyz",
]
HOME = os.environ["HOME"]
CHROME_PROFILE_DIRECTORY = "Default"
CHROME_DESKTOP_ENTRY_FILES = [
"google-chrome.desktop",
"com.google.Chrome.desktop",
]
#
#
#
# repo files, installation urls, etc
DOCKER_REPO_PATH = "/etc/yum.repos.d/docker-ce.repo"
DOCKER_REPO_URL = "https://download.docker.com/linux/fedora/docker-ce.repo"
VSCODE_REPO_PATH = "/etc/yum.repos.d/vscode.repo"
VSCODE_REPO_FILE = """[vscode-yum]
name=vscode-yum
baseurl=https://packages.microsoft.com/yumrepos/vscode/
repo_gpgcheck=0
gpgcheck=0
enabled=1
gpgkey=https://packages.microsoft.com/yumrepos/vscode/repodata/repomd.xml.key
"""
NAUTILUS_TYPEAHEAD_REPO_PATH = f"/etc/yum.repos.d/nelsonaloysio-nautilus-typeahead-fedora-{FEDORA_VERSION}.repo"
NAUTILUS_TYPEAHEAD_REPO_URL = f"https://copr.fedorainfracloud.org/coprs/nelsonaloysio/nautilus-typeahead/repo/fedora-44/nelsonaloysio-nautilus-typeahead-fedora-{FEDORA_VERSION}.repo"
INSYNC_REPO_PATH = "/etc/yum.repos.d/insync.repo"
INSYNC_REPO_FILE = """[insync]
name=insync repo
baseurl=http://yum.insync.io/fedora/$releasever/
gpgcheck=1
gpgkey=https://d2t3ff60b2tol4.cloudfront.net/repomd.xml.key
enabled=1
metadata_expire=120m
"""
NVM_INSTALL_URL = "https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh"
#
#
#
# main
ActionKey = Literal["all", "configure-gnome", "configure-chrome", "flatpaks"]
def main() -> None:
alternatives = get_args(ActionKey)
alternativesStr = ", ".join(alternatives)
parser = argparse.ArgumentParser(description="Setup tool",)
parser.add_argument("action", nargs="?", default="all", help=f"command to run (default: all), one of these: {alternativesStr}")
args = parser.parse_args()
action = args.action
if not isAction(action):
print(f"\"{action}\" is an invalid action, use one of these: {alternativesStr}")
exit(1)
action: ActionKey
if action == "all":
doAllActions()
elif action == "configure-gnome":
installGnomeExtensions()
configureGnome()
configureChromeDesktopEntries()
elif action == "configure-chrome":
configureChromeDesktopEntries()
elif action == "flatpaks":
installFlatpaks()
def doAllActions() -> None:
appState = AppState()
if appState.state == "":
appState.setState("1")
if appState.state == "1":
installSystemPackages()
installNautilusTypeahead()
appState.setState("2")
print("you'll need to reboot to proceed. reboot and re-run this script")
exit(0)
if appState.state == "2":
installGnomeExtensions()
configureGnome()
configureChromeDesktopEntries()
setDefaultShellToZsh()
appState.setState("3")
print("you'll need to log out")
exit(0)
#
#
#
# helpers
def assertCommandExists(command: str) -> None:
if not checkCommandExists(command):
print(f"command \"{command}\" not found. perhaps you need to reboot?")
exit(1)
def writeStateFile(fileName: str, content: str) -> None:
stateFilePath = getStateFilePath(fileName)
stateFileDir = os.path.dirname(stateFilePath)
if not os.path.exists(stateFileDir):
os.mkdir(stateFileDir)
with open(stateFilePath, 'w', encoding='utf-8') as file:
file.write(content)
def readStateFile(fileName: str) -> str:
try:
with open(getStateFilePath(fileName)) as file:
return file.read().strip()
except:
return ""
def isAction(value: str) -> TypeGuard[ActionKey]:
return value in get_args(ActionKey)
def optionalBool() -> bool | None:
return None
def executeReturnOutput(command: str) -> str:
print(f"> {command}")
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
if process.stdout is None:
raise Exception()
buffer = []
for line in process.stdout:
buffer.append(line)
process.wait()
output = "".join(buffer).strip()
if process.returncode != 0:
raise subprocess.CalledProcessError(returncode=process.returncode, cmd=command, output=output)
return output
def executePrintOutput(command: str) -> None:
print(f"> {command}")
if subprocess.run(command, shell=True).returncode != 0:
print("Command failed")
exit(1)
def checkInstallVscodeExtension(id: str) -> None:
targetFolder = id.lower().replace("@", "-")
for file in os.listdir(path.join(environ["HOME"], ".vscode", "extensions")):
if file.startswith(targetFolder):
# target folder already present
return
executePrintOutput(f"code --install-extension {id}")
def checkCommandExists(cmd: str) -> bool:
return shutil.which(cmd) != None
def getStateFilePath(fileName: str) -> str:
return path.join(environ["HOME"], ".local", "state", "birgersp-setup", fileName)
def checkSystemIsFedora() -> bool:
return executeReturnOutput("grep '^ID=fedora' /etc/os-release") == "ID=fedora"
def writeFileAsSudo(filePath: str, content: str) -> None:
if not content.endswith("\n"):
content = content + "\n"
subprocess.run(["sudo", "mkdir", "-p", os.path.dirname(filePath)])
subprocess.run(["sudo", "tee", filePath], input=content, text=True, capture_output=True)
def checkFileExists(filePath: str) -> bool:
return os.path.exists(filePath)
class AppState:
state = ""
def __init__(self):
self.state = readStateFile("state")
def setState(self, newState: str) -> None:
writeStateFile("state", newState)
self.state = newState
#
#
#
# setup action functions
def installSystemPackages() -> None:
print("Installing system packages...")
if not checkFileExists(VSCODE_REPO_PATH):
writeFileAsSudo(VSCODE_REPO_PATH, VSCODE_REPO_FILE)
if not checkFileExists(INSYNC_REPO_PATH):
writeFileAsSudo(INSYNC_REPO_PATH, INSYNC_REPO_FILE)
if not checkFileExists(DOCKER_REPO_PATH):
executePrintOutput(f"sudo wget https://download.docker.com/linux/fedora/docker-ce.repo -O \"{DOCKER_REPO_PATH}\"")
rpmPackagesStr = " ".join(RPM_PACKAGES)
executePrintOutput(f"sudo rpm-ostree install -y {rpmPackagesStr}")
executePrintOutput(f"sudo rpm-ostree override remove nautilus nautilus-extensions papers-nautilus --install nautilus-typeahead")
def installNautilusTypeahead() -> None:
if not checkFileExists(NAUTILUS_TYPEAHEAD_REPO_PATH):
executePrintOutput(f"sudo wget \"{NAUTILUS_TYPEAHEAD_REPO_URL}\" -O \"{NAUTILUS_TYPEAHEAD_REPO_PATH}\"")
executePrintOutput(f"sudo rpm-ostree override remove nautilus nautilus-extensions papers-nautilus --install nautilus-typeahead")
def installFlatpaks() -> None:
flatpaksStr = " ".join(FLATPAK_APPS)
executePrintOutput(f"flatpak install -y flathub {flatpaksStr}")
def installNvm() -> None:
if checkFileExists(path.join(HOME, ".nvm")):
return
executePrintOutput(f"wget -qO- {NVM_INSTALL_URL} | bash")
def installDeno() -> None:
if checkCommandExists("deno"):
return
executePrintOutput("curl -fsSL https://deno.land/install.sh | sh")
def installGnomeExtensions() -> None:
executePrintOutput("pipx install gnome-extensions-cli")
executePrintOutput("pipx ensurepath")
if not checkCommandExists("gnome-extensions-cli"):
print("Command \"install gnome-extensions-cli\" not found. Try re-running in a new shell (where PATH is loaded)")
exit(1)
extensionsStr = " ".join(GNOME_EXTENSIONS)
executePrintOutput(f"gnome-extensions-cli install {extensionsStr}")
def configureGnome() -> None:
commands = [
"dconf write \"/org/gnome/desktop/interface/icon-theme\" \"'Yaru-blue'\"",
"dconf write \"/org/gnome/desktop/wm/keybindings/switch-applications-backward\" \"@as []\"",
"dconf write \"/org/gnome/desktop/wm/keybindings/switch-applications\" \"@as []\"",
"dconf write \"/org/gnome/desktop/wm/keybindings/switch-windows-backward\" \"['<Shift><Alt>Tab']\"",
"dconf write \"/org/gnome/desktop/wm/keybindings/switch-windows\" \"['<Alt>Tab']\"",
"dconf write \"/org/gnome/desktop/wm/preferences/button-layout\" \"':minimize,maximize,close'\"",
"dconf write \"/org/gnome/desktop/wm/preferences/resize-with-right-button\" \"true\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/binding\" \"'<Control><Alt>t'\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/command\" \"'bash /var/home/birger/repo/tools/linux/hotkeys.sh --ctrl-alt-t'\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/name\" \"'ctrl+alt+t'\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/binding\" \"'<Super>m'\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/command\" \"'bash /var/home/birger/repo/tools/linux/hotkeys.sh --super-m'\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/name\" \"'super+m'\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom2/binding\" \"'<Super>g'\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom2/command\" \"'bash /var/home/birger/repo/tools/linux/hotkeys.sh --super-g'\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom2/name\" \"'super+g'\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings\" \"['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/', '/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/', '/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom2/']\"",
"dconf write \"/org/gnome/settings-daemon/plugins/media-keys/terminal\" \"@as []\"",
"dconf write \"/org/gnome/shell/extensions/clipboard-indicator/cache-size\" \"20\"",
"dconf write \"/org/gnome/shell/extensions/clipboard-indicator/history-size\" \"100\"",
"dconf write \"/org/gnome/shell/extensions/clipboard-indicator/toggle-menu\" \"['<Control><Alt>h']\"",
"dconf write \"/org/gnome/shell/extensions/dash-to-panel/dot-size\" \"0\"",
"dconf write \"/org/gnome/shell/extensions/dash-to-panel/group-apps\" \"false\"",
"dconf write \"/org/gnome/shell/extensions/dash-to-panel/trans-bg-color\" \"'#3d3846'\"",
"dconf write \"/org/gnome/shell/extensions/dash-to-panel/trans-panel-opacity\" \"1.0\"",
"dconf write \"/org/gnome/shell/extensions/dash-to-panel/trans-use-custom-bg\" \"true\"",
"dconf write \"/org/gnome/shell/extensions/dash-to-panel/trans-use-custom-opacity\" \"false\"",
"dconf write \"/org/gnome/shell/extensions/emoji-copy/always-show\" \"false\"",
"dconf write \"/org/gnome/shell/extensions/emoji-copy/emoji-keybind\" \"['<Ctrl><Alt>period']\"",
"dconf write \"/org/gnome/shell/extensions/switcher/fade-enable\" \"true\"",
"dconf write \"/org/gnome/shell/extensions/switcher/font-size\" \"uint32 24\"",
"dconf write \"/org/gnome/shell/extensions/switcher/max-width-percentage\" \"uint32 50\"",
"dconf write \"/org/gnome/shell/extensions/system-monitor/show-cpu\" \"false\"",
"dconf write \"/org/gnome/shell/extensions/system-monitor/show-download\" \"false\"",
"dconf write \"/org/gnome/shell/extensions/system-monitor/show-swap\" \"false\"",
"dconf write \"/org/gnome/shell/extensions/system-monitor/show-upload\" \"false\"",
"dconf write \"/org/gnome/shell/keybindings/toggle-message-tray\" \"@as []\"",
]
for command in commands:
executePrintOutput(command)
def configureChromeDesktopEntries() -> None:
applicationsDir = path.join(HOME, ".local", "share", "applications")
os.makedirs(applicationsDir, exist_ok=True)
for desktopEntryFile in CHROME_DESKTOP_ENTRY_FILES:
srcPath = path.join("/usr/share/applications", desktopEntryFile)
destPath = path.join(applicationsDir, desktopEntryFile)
if not checkFileExists(srcPath):
print(f"Skipping {desktopEntryFile}: {srcPath} does not exist")
continue
shutil.copyfile(srcPath, destPath)
with open(destPath, encoding="utf-8") as file:
lines = file.read().splitlines()
replacements = {
"Exec=/usr/bin/google-chrome-stable %U": f"Exec=/usr/bin/google-chrome-stable --profile-directory={CHROME_PROFILE_DIRECTORY} %U",
"Exec=/usr/bin/google-chrome-stable": f"Exec=/usr/bin/google-chrome-stable --profile-directory={CHROME_PROFILE_DIRECTORY}",
"Exec=/usr/bin/google-chrome-stable --incognito": f"Exec=/usr/bin/google-chrome-stable --profile-directory={CHROME_PROFILE_DIRECTORY} --incognito",
}
with open(destPath, "w", encoding="utf-8") as file:
file.write("\n".join([replacements.get(line, line) for line in lines]) + "\n")
if checkCommandExists("update-desktop-database"):
executePrintOutput(f"update-desktop-database \"{applicationsDir}\"")
def setDefaultShellToZsh() -> None:
zshPath = shutil.which("zsh")
if zshPath is None:
print("zsh is not found(?). you might wanna reboot or something")
exit(1)
if os.environ["SHELL"] == zshPath:
# shell is zsh
return
executePrintOutput(f"chsh -s {zshPath}")
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment