├── .starship.deno ├── .gitignore ├── .dockerignore ├── TODO.md ├── src ├── model │ ├── cloneable.ts │ ├── dconf-dump-to-lines.ts │ ├── dependency.ts │ └── command.ts ├── commands │ ├── meld.ts │ ├── files │ │ ├── open-mux │ │ ├── m-temp │ │ ├── tmuxinator-files.ts │ │ ├── .bash_aliases │ │ └── starship.toml │ ├── gedit.ts │ ├── common │ │ ├── noop.ts │ │ ├── gsettings-to-cmds.ts │ │ ├── group.ts │ │ ├── dconf-load.ts │ │ ├── file-commands.ts │ │ └── os-package.ts │ ├── yubikey.ts │ ├── all-6-gaming.ts │ ├── minecraft.ts │ ├── chrome.ts │ ├── eog.ts │ ├── all-4-developer-java.ts │ ├── mullvad.ts │ ├── desktop-is-home.ts │ ├── downloads-is-tmp.ts │ ├── signal-desktop-via-docker.ts │ ├── git.ts │ ├── network-utils.ts │ ├── add-node_modules-bin-to-path.ts │ ├── java.ts │ ├── alacritty.ts │ ├── dmenu.ts │ ├── save-bash-history.ts │ ├── mdr.ts │ ├── add-home-bin-to-path.ts │ ├── bash.ts │ ├── downloads-is-cleaned-on-boot.ts │ ├── git-completion.ts │ ├── sudo-no-password.ts │ ├── m-temp.ts │ ├── webstorm.ts │ ├── sdkman.ts │ ├── gnome-shell-extension-installer.ts │ ├── gitk.ts │ ├── vim.ts │ ├── bash-git-prompt.ts │ ├── all-3-developer-web.ts │ ├── android.ts │ ├── virtualbox.ts │ ├── refresh-os-packages.ts │ ├── open-mux.ts │ ├── insync.ts │ ├── fzf.ts │ ├── keybase.ts │ ├── bash-aliases.ts │ ├── idea.ts │ ├── pass.ts │ ├── starship.ts │ ├── all-2-developer-base.ts │ ├── deno.ts │ ├── all-5-personal.ts │ ├── gnome-custom-keybindings-backup.ts │ ├── isolate-in-docker.ts │ ├── wm-utils.ts │ ├── all-1-minimal-sanity.ts │ ├── gnome-disable-wayland.ts │ ├── toggle-terminal.ts │ ├── brave.ts │ ├── docker.ts │ ├── tmuxinator_byobu_bash_aliases.ts │ ├── firefox-local.ts │ ├── signal-desktop.ts │ ├── rust.ts │ ├── exec.ts │ ├── node.ts │ ├── index.ts │ ├── gnome-shell-extensions.ts │ └── gsettings.ts ├── os │ ├── resolve-path.ts │ ├── read-relative-file.ts │ ├── require-env.ts │ ├── user │ │ ├── is-running-as-root.ts │ │ ├── sudo-keepalive.ts │ │ └── target-user.ts │ ├── file-ends-with-newline.ts │ ├── read-from-url.ts │ ├── create-temp-dir.ts │ ├── defer.ts │ └── exec.ts ├── config.ts ├── usage.ts ├── fn.ts ├── deps.ts ├── run.ts └── cli.ts ├── test ├── docker-run └── docker-build ├── dev-fmt.sh ├── dev-debug.sh ├── dev-serve.sh ├── .editorconfig ├── dev-docker.sh ├── dev-Dockerfile ├── README.md ├── LICENSE └── settings_diff /.starship.deno: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.*/ 2 | .*.swp 3 | /node_modules/ 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.* 2 | test 3 | README.md 4 | .*.swp 5 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## ui 4 | 5 | git@github.com:alexlafroscia/d_ui.git 6 | -------------------------------------------------------------------------------- /src/model/cloneable.ts: -------------------------------------------------------------------------------- 1 | export interface Cloneable { 2 | clone(): T; 3 | } 4 | -------------------------------------------------------------------------------- /test/docker-run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | exec docker run --rm -it hugojosefson/ubuntu-install-scripts "$@" 3 | -------------------------------------------------------------------------------- /dev-fmt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | exec deno fmt -- lib src *.md *.ts 6 | -------------------------------------------------------------------------------- /src/commands/meld.ts: -------------------------------------------------------------------------------- 1 | import { gsettingsMeld } from "./gsettings.ts"; 2 | 3 | export const meld = gsettingsMeld; 4 | -------------------------------------------------------------------------------- /src/commands/files/open-mux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | while true; do 3 | tmuxinator start base 4 | sleep .1 5 | done 6 | -------------------------------------------------------------------------------- /src/commands/gedit.ts: -------------------------------------------------------------------------------- 1 | import { gsettingsGedit } from "./gsettings.ts"; 2 | 3 | export const gedit = gsettingsGedit; 4 | -------------------------------------------------------------------------------- /src/commands/common/noop.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../model/command.ts"; 2 | 3 | export const NOOP = () => (new Command()); 4 | -------------------------------------------------------------------------------- /dev-debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | exec deno run -A --unstable --inspect-brk src/cli.ts "$@" 6 | -------------------------------------------------------------------------------- /src/commands/yubikey.ts: -------------------------------------------------------------------------------- 1 | import { InstallOsPackage } from "./common/os-package.ts"; 2 | 3 | export const yubikey = InstallOsPackage.of("yubioath-desktop"); 4 | -------------------------------------------------------------------------------- /dev-serve.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | exec deno run --allow-read=. --allow-net https://deno.land/std/http/file_server.ts 6 | -------------------------------------------------------------------------------- /test/docker-build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | cd "$(dirname "$(readlink -f "${0}")")" 6 | exec docker build -t hugojosefson/ubuntu-install-scripts -f Dockerfile .. 7 | -------------------------------------------------------------------------------- /src/os/resolve-path.ts: -------------------------------------------------------------------------------- 1 | import { PasswdEntry } from "../deps.ts"; 2 | 3 | export function resolvePath( 4 | user: PasswdEntry, 5 | path: string, 6 | ): string { 7 | return path.replace("~", user.homedir); 8 | } 9 | -------------------------------------------------------------------------------- /src/commands/all-6-gaming.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { minecraft } from "./minecraft.ts"; 3 | 4 | export const all6Gaming = Command.custom() 5 | .withDependencies([ 6 | minecraft, 7 | ]); 8 | -------------------------------------------------------------------------------- /src/commands/minecraft.ts: -------------------------------------------------------------------------------- 1 | import { installOsPackageFromUrl } from "./common/os-package.ts"; 2 | 3 | export const minecraft = installOsPackageFromUrl( 4 | "minecraft-launcher", 5 | "https://launcher.mojang.com/download/Minecraft.deb", 6 | ); 7 | -------------------------------------------------------------------------------- /src/os/read-relative-file.ts: -------------------------------------------------------------------------------- 1 | import { readFromUrl } from "./read-from-url.ts"; 2 | 3 | export const readRelativeFile = async ( 4 | relativeFilePath: string, 5 | base: string | URL, 6 | ) => await readFromUrl(new URL(relativeFilePath, base)); 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /src/commands/chrome.ts: -------------------------------------------------------------------------------- 1 | import { installOsPackageFromUrl } from "./common/os-package.ts"; 2 | 3 | export const chrome = installOsPackageFromUrl( 4 | "google-chrome-stable", 5 | "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb", 6 | ); 7 | -------------------------------------------------------------------------------- /src/commands/eog.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { InstallOsPackage } from "./common/os-package.ts"; 3 | 4 | export const eog = Command.custom().withDependencies([ 5 | InstallOsPackage.of("eog"), 6 | InstallOsPackage.of("eog-plugins"), 7 | ]); 8 | -------------------------------------------------------------------------------- /src/os/require-env.ts: -------------------------------------------------------------------------------- 1 | export const requireEnv = (name: string): string => { 2 | const maybeValue: string | undefined = Deno.env.get(name); 3 | 4 | if (!maybeValue) { 5 | throw new Error(`Missing env variable "${name}".`); 6 | } 7 | 8 | return maybeValue; 9 | }; 10 | -------------------------------------------------------------------------------- /src/commands/all-4-developer-java.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { idea } from "./idea.ts"; 3 | import { java } from "./java.ts"; 4 | 5 | export const all4DeveloperJava = Command.custom() 6 | .withDependencies([ 7 | java, 8 | idea, 9 | ]); 10 | -------------------------------------------------------------------------------- /src/commands/mullvad.ts: -------------------------------------------------------------------------------- 1 | import { installOsPackageFromUrl } from "./common/os-package.ts"; 2 | import { isDocker } from "../deps.ts"; 3 | 4 | export const mullvad = installOsPackageFromUrl( 5 | "mullvad-vpn", 6 | "https://mullvad.net/download/app/deb/latest", 7 | ) 8 | .withSkipIfAny([await isDocker()]); 9 | -------------------------------------------------------------------------------- /dev-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | REPO="hugojosefson/ubuntu-install-scripts" 6 | TAG="ubuntu-22.04" 7 | 8 | docker build -f dev-Dockerfile -t "${REPO}:${TAG}" . 9 | 10 | exec docker run --net=host --rm -it -v "$(pwd)":"$(pwd)":ro -w "$(pwd)" "${REPO}:${TAG}" "$@" 11 | -------------------------------------------------------------------------------- /src/commands/desktop-is-home.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemPath } from "../model/dependency.ts"; 2 | import { targetUser } from "../os/user/target-user.ts"; 3 | import { Symlink } from "./common/file-commands.ts"; 4 | 5 | export const desktopIsHome = new Symlink( 6 | targetUser, 7 | ".", 8 | FileSystemPath.of(targetUser, `~/Desktop`), 9 | ); 10 | -------------------------------------------------------------------------------- /src/commands/downloads-is-tmp.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemPath } from "../model/dependency.ts"; 2 | import { targetUser } from "../os/user/target-user.ts"; 3 | import { Symlink } from "./common/file-commands.ts"; 4 | 5 | export const downloadsIsTmp = new Symlink( 6 | targetUser, 7 | "/tmp", 8 | FileSystemPath.of(targetUser, `~/Downloads`), 9 | ); 10 | -------------------------------------------------------------------------------- /src/commands/signal-desktop-via-docker.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { 3 | isolateInDocker, 4 | symlinkToIsolateInDocker, 5 | } from "./isolate-in-docker.ts"; 6 | 7 | export const signalDesktopViaDocker = Command.custom().withDependencies([ 8 | isolateInDocker, 9 | symlinkToIsolateInDocker("signal-desktop"), 10 | ]); 11 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export type Config = { 2 | ANDROID_HOSTNAME: string; 3 | NON_INTERACTIVE: boolean; 4 | VERBOSE: boolean; 5 | }; 6 | export const config: Config = { 7 | ANDROID_HOSTNAME: Deno.env.get("ANDROID_HOSTNAME") || "my-android-device", 8 | NON_INTERACTIVE: Deno.env.get("NON_INTERACTIVE") !== "false", 9 | VERBOSE: Deno.env.get("VERBOSE") !== "false", 10 | }; 11 | -------------------------------------------------------------------------------- /src/commands/git.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { bashAliases } from "./bash-aliases.ts"; 3 | import { InstallOsPackage } from "./common/os-package.ts"; 4 | import { gitCompletion } from "./git-completion.ts"; 5 | 6 | export const git = Command.custom().withDependencies([ 7 | InstallOsPackage.of("git"), 8 | bashAliases, 9 | gitCompletion, 10 | ]); 11 | -------------------------------------------------------------------------------- /src/commands/network-utils.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { InstallOsPackage } from "./common/os-package.ts"; 3 | 4 | export const networkUtils: Command = Command.custom() 5 | .withDependencies( 6 | [ 7 | "corkscrew", 8 | "mtr", 9 | "netcat-openbsd", 10 | "nmap", 11 | "whois", 12 | ].map(InstallOsPackage.of), 13 | ); 14 | -------------------------------------------------------------------------------- /src/os/user/is-running-as-root.ts: -------------------------------------------------------------------------------- 1 | export async function isRunningAsRoot(): Promise { 2 | const runOptions: Deno.RunOptions = { 3 | cmd: ["bash", "-c", "echo $EUID"], 4 | stdout: "piped", 5 | }; 6 | const outputBytes: Uint8Array = await Deno.run(runOptions).output(); 7 | const outputString = new TextDecoder().decode(outputBytes); 8 | return outputString.trim() === "0"; 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/add-node_modules-bin-to-path.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemPath } from "../model/dependency.ts"; 2 | import { targetUser } from "../os/user/target-user.ts"; 3 | import { LineInFile } from "./common/file-commands.ts"; 4 | 5 | export const addNodeModulesBinToPath = new LineInFile( 6 | targetUser, 7 | FileSystemPath.of(targetUser, "~/.bashrc"), 8 | "export PATH=$PATH:./node_modules/.bin", 9 | ); 10 | -------------------------------------------------------------------------------- /src/os/file-ends-with-newline.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemPath } from "../model/dependency.ts"; 2 | 3 | export async function fileEndsWithNewline( 4 | file: FileSystemPath, 5 | ): Promise { 6 | try { 7 | const data = await Deno.readFile(file.path); 8 | return data.slice(-1)[0] === 10; 9 | } catch (e) { 10 | if (e instanceof Deno.errors.NotFound) { 11 | return true; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/java.ts: -------------------------------------------------------------------------------- 1 | import { targetUser } from "../os/user/target-user.ts"; 2 | import { InstallOsPackage } from "./common/os-package.ts"; 3 | import { Exec } from "./exec.ts"; 4 | import { sdkman, sdkmanSourceLine } from "./sdkman.ts"; 5 | 6 | export const java = InstallOsPackage.of("openjdk-18-jdk"); 7 | 8 | export const sdkmanJava = new Exec([sdkman], [], targetUser, {}, [ 9 | "bash", 10 | "-c", 11 | `${sdkmanSourceLine} && sdk install java`, 12 | ]); 13 | -------------------------------------------------------------------------------- /src/os/user/sudo-keepalive.ts: -------------------------------------------------------------------------------- 1 | import { PasswdEntry } from "../../deps.ts"; 2 | 3 | export function sudoKeepalive(asUser: PasswdEntry): () => void { 4 | const process: Deno.Process = Deno.run({ 5 | cmd: [ 6 | "sh", 7 | "-c", 8 | `while true; do sudo --user="${asUser.username}" sudo -v; sleep 10; done;`, 9 | ], 10 | stdin: "null", 11 | stdout: "null", 12 | stderr: "null", 13 | }); 14 | return () => process.kill("SIGTERM"); 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/alacritty.ts: -------------------------------------------------------------------------------- 1 | import { InstallRustPackage } from "./rust.ts"; 2 | import { InstallOsPackage } from "./common/os-package.ts"; 3 | 4 | export const alacritty = InstallRustPackage 5 | .of("alacritty") 6 | .withDependencies( 7 | [ 8 | "cmake", 9 | "pkg-config", 10 | "libfreetype6-dev", 11 | "libfontconfig1-dev", 12 | "libxcb-xfixes0-dev", 13 | "libxkbcommon-dev", 14 | "python3", 15 | ].map(InstallOsPackage.of), 16 | ); 17 | -------------------------------------------------------------------------------- /src/os/read-from-url.ts: -------------------------------------------------------------------------------- 1 | import { fetchFile } from "../deps.ts"; 2 | 3 | export async function readFromUrl(url: string | URL): Promise { 4 | const response: Response = await fetchFile(url); 5 | return await response.text(); 6 | } 7 | 8 | export async function readFromUrlBytes(url: string | URL): Promise { 9 | const response: Response = await fetchFile(url); 10 | const arrayBuffer = await response.arrayBuffer(); 11 | return new Uint8Array(arrayBuffer); 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/dmenu.ts: -------------------------------------------------------------------------------- 1 | import { Symlink } from "./common/file-commands.ts"; 2 | import { ROOT } from "../os/user/target-user.ts"; 3 | import { FileSystemPath } from "../model/dependency.ts"; 4 | import { InstallBrewPackage } from "./common/os-package.ts"; 5 | 6 | export const dmenu = new Symlink( 7 | ROOT, 8 | "/home/linuxbrew/.linuxbrew/bin/dmenu", 9 | FileSystemPath.of(ROOT, "/usr/local/bin/dmenu"), 10 | ) 11 | .withDependencies([ 12 | InstallBrewPackage.of("dmenu"), 13 | ]); 14 | -------------------------------------------------------------------------------- /src/commands/save-bash-history.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { LineInFile } from "./common/file-commands.ts"; 5 | 6 | export const saveBashHistory: Command = new LineInFile( 7 | targetUser, 8 | FileSystemPath.of(targetUser, "~/.bashrc"), 9 | '[[ "${PROMPT_COMMAND}" == *"history -a $HOME/.bash_history"* ]] || export PROMPT_COMMAND="${PROMPT_COMMAND:-:}; history -a $HOME/.bash_history"', 10 | ); 11 | -------------------------------------------------------------------------------- /src/commands/mdr.ts: -------------------------------------------------------------------------------- 1 | import { CreateFile, MODE_EXECUTABLE_775 } from "./common/file-commands.ts"; 2 | import { ROOT } from "../os/user/target-user.ts"; 3 | import { FileSystemPath } from "../model/dependency.ts"; 4 | import { readFromUrlBytes } from "../os/read-from-url.ts"; 5 | 6 | export const mdr = new CreateFile( 7 | ROOT, 8 | FileSystemPath.of(ROOT, "/usr/local/bin/mdr"), 9 | await readFromUrlBytes( 10 | "https://github.com/MichaelMure/mdr/releases/download/v0.2.5/mdr_linux_amd64", 11 | ), 12 | false, 13 | MODE_EXECUTABLE_775, 14 | ); 15 | -------------------------------------------------------------------------------- /src/commands/add-home-bin-to-path.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { CreateDir, LineInFile } from "./common/file-commands.ts"; 5 | 6 | const HOME_BIN = FileSystemPath.of(targetUser, "~/bin"); 7 | 8 | export const addHomeBinToPath = Command.custom() 9 | .withDependencies([ 10 | new CreateDir(targetUser, HOME_BIN), 11 | new LineInFile( 12 | targetUser, 13 | FileSystemPath.of(targetUser, "~/.bashrc"), 14 | `export PATH="${HOME_BIN.path}:$PATH"`, 15 | ), 16 | ]); 17 | -------------------------------------------------------------------------------- /src/commands/bash.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { ensureSuccessful } from "../os/exec.ts"; 3 | import { ROOT, targetUser } from "../os/user/target-user.ts"; 4 | import { InstallOsPackage } from "./common/os-package.ts"; 5 | 6 | export const bash = Command.custom() 7 | .withDependencies([ 8 | InstallOsPackage.of("bash"), 9 | InstallOsPackage.of("bash-completion"), 10 | InstallOsPackage.of("util-linux"), 11 | ]) 12 | .withRun(async () => { 13 | await ensureSuccessful(ROOT, [ 14 | "chsh", 15 | "--shell", 16 | "/bin/bash", 17 | targetUser.username, 18 | ]); 19 | }); 20 | -------------------------------------------------------------------------------- /src/os/create-temp-dir.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../config.ts"; 2 | import { colorlog, PasswdEntry } from "../deps.ts"; 3 | import { FileSystemPath } from "../model/dependency.ts"; 4 | 5 | export async function createTempDir( 6 | asUser: PasswdEntry, 7 | ): Promise { 8 | const path = await Deno.makeTempDir(); 9 | await Deno.chown(path, asUser.uid, asUser.gid); 10 | const fileSystemPath = FileSystemPath.of(asUser, path); 11 | config.VERBOSE && console.warn( 12 | colorlog.warning( 13 | `createTempDir: fileSystemPath: ${JSON.stringify(fileSystemPath)}`, 14 | ), 15 | ); 16 | return fileSystemPath; 17 | } 18 | -------------------------------------------------------------------------------- /dev-Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | RUN apt-get update && apt-get full-upgrade -y --auto-remove --purge 4 | RUN apt-get install -y sudo 5 | RUN echo '%adm ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/adm-nopasswd 6 | 7 | RUN useradd --create-home --user-group --groups adm user 8 | USER user 9 | RUN id 10 | RUN sudo id 11 | 12 | WORKDIR /home/user 13 | COPY src/cli.ts . 14 | RUN head -n 2 cli.ts > download-deno.ts 15 | RUN echo 'console.log(`Deno version ${Deno.version.deno} downloaded successfully.`)' >> download-deno.ts 16 | RUN tail -n 2 cli.ts | head -n 1 >> download-deno.ts 17 | RUN rm cli.ts 18 | RUN chmod +x download-deno.ts 19 | RUN ./download-deno.ts 20 | -------------------------------------------------------------------------------- /src/commands/downloads-is-cleaned-on-boot.ts: -------------------------------------------------------------------------------- 1 | import { CreateFile } from "./common/file-commands.ts"; 2 | import { ROOT, targetUser } from "../os/user/target-user.ts"; 3 | import { FileSystemPath } from "../model/dependency.ts"; 4 | 5 | const contents = ` 6 | #Type Path Mode User Group Age Argument 7 | d! ${targetUser.homedir}/Downloads 0770 ${targetUser.uid} ${targetUser.gid} - 8 | `.trimStart(); 9 | 10 | export const downloadsIsCleanedOnBoot = new CreateFile( 11 | ROOT, 12 | FileSystemPath.of( 13 | ROOT, 14 | `/etc/tmpfiles.d/${targetUser.username}-downloads.conf`, 15 | ), 16 | contents, 17 | false, 18 | 0o0644, 19 | ); 20 | -------------------------------------------------------------------------------- /src/model/dconf-dump-to-lines.ts: -------------------------------------------------------------------------------- 1 | import { decodeToml, readAll } from "../deps.ts"; 2 | 3 | type Schema = string; 4 | type Key = string; 5 | type Value = unknown; 6 | 7 | const dump: string = new TextDecoder().decode(await readAll(Deno.stdin)); 8 | const asToml: Record> = decodeToml(dump); 9 | 10 | const rows = Object.entries(asToml) 11 | .map( 12 | ([schema, stuff]) => { 13 | return Object.entries(stuff) 14 | .map(([key, value]) => { 15 | return [schema, key, JSON.stringify(value)].join(" "); 16 | }); 17 | }, 18 | ).flat().sort(); 19 | 20 | const output: string = rows.join("\n"); 21 | console.log(output); 22 | -------------------------------------------------------------------------------- /src/commands/git-completion.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { readFromUrl } from "../os/read-from-url.ts"; 4 | import { targetUser } from "../os/user/target-user.ts"; 5 | import { CreateFile } from "./common/file-commands.ts"; 6 | 7 | export const gitCompletion: Command = Command.custom() 8 | .withDependencies([ 9 | new CreateFile( 10 | targetUser, 11 | FileSystemPath.of(targetUser, "~/.git-completion"), 12 | await readFromUrl( 13 | "https://raw.githubusercontent.com/git/git/master/contrib/completion/git-completion.bash", 14 | ), 15 | false, 16 | ), 17 | ]); 18 | -------------------------------------------------------------------------------- /src/commands/sudo-no-password.ts: -------------------------------------------------------------------------------- 1 | import { CreateFile } from "./common/file-commands.ts"; 2 | import { ROOT, targetUser } from "../os/user/target-user.ts"; 3 | import { FileSystemPath } from "../model/dependency.ts"; 4 | import { InstallOsPackage } from "./common/os-package.ts"; 5 | import { ensureUserInSystemGroup } from "./common/group.ts"; 6 | 7 | const GROUP_NAME = "sudo-no-password"; 8 | 9 | export const sudoNoPassword = new CreateFile( 10 | ROOT, 11 | FileSystemPath.of(ROOT, `/etc/sudoers.d/${GROUP_NAME}`), 12 | `%${GROUP_NAME} ALL=(ALL) NOPASSWD: ALL`, 13 | ) 14 | .withDependencies([ 15 | ensureUserInSystemGroup(targetUser, GROUP_NAME), 16 | InstallOsPackage.of("sudo"), 17 | ]); 18 | -------------------------------------------------------------------------------- /src/commands/common/gsettings-to-cmds.ts: -------------------------------------------------------------------------------- 1 | export function gsettingsToCmds(gsettings: string): string[][] { 2 | const trim = (line: string) => line.trim(); 3 | const isNotEmpty = (line: string): boolean => line.length > 0; 4 | const isNotComment = (line: string): boolean => /^[a-z0-9_]/i.test(line); 5 | 6 | const split = (line: string) => 7 | line.match(/^([^ ]+) ([^ ]+) (.+)/) as [string, string, string, string]; 8 | 9 | return gsettings 10 | .split("\n") 11 | .map(trim) 12 | .filter(isNotEmpty) 13 | .filter(isNotComment) 14 | .map(split) 15 | .map(([_line, schema, key, value]) => [schema, key, value]) 16 | .map(([schema, key, value]) => ["gsettings", "set", schema, key, value]); 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/m-temp.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { readRelativeFile } from "../os/read-relative-file.ts"; 4 | import { targetUser } from "../os/user/target-user.ts"; 5 | import { CreateFile, MODE_EXECUTABLE_775 } from "./common/file-commands.ts"; 6 | import { InstallOsPackage } from "./common/os-package.ts"; 7 | 8 | export const mTemp = Command.custom() 9 | .withDependencies([ 10 | InstallOsPackage.of("gettext"), 11 | new CreateFile( 12 | targetUser, 13 | FileSystemPath.of(targetUser, "~/bin/m-temp"), 14 | await readRelativeFile("./files/m-temp", import.meta.url), 15 | false, 16 | MODE_EXECUTABLE_775, 17 | ), 18 | ]); 19 | -------------------------------------------------------------------------------- /src/commands/webstorm.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { addHomeBinToPath } from "./add-home-bin-to-path.ts"; 5 | import { CreateFile, MODE_EXECUTABLE_775 } from "./common/file-commands.ts"; 6 | import { InstallSnapPackage } from "./common/os-package.ts"; 7 | 8 | const contents = `#!/usr/bin/env bash 9 | arg=\${1:-.} 10 | webstorm "\${arg}" &>/dev/null & 11 | `; 12 | 13 | export const webstorm: Command = Command 14 | .custom() 15 | .withDependencies([ 16 | InstallSnapPackage.ofClassic("webstorm"), 17 | addHomeBinToPath, 18 | new CreateFile( 19 | targetUser, 20 | FileSystemPath.of(targetUser, "~/bin/ws"), 21 | contents, 22 | false, 23 | MODE_EXECUTABLE_775, 24 | ), 25 | ]); 26 | -------------------------------------------------------------------------------- /src/commands/sdkman.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { LineInFile } from "./common/file-commands.ts"; 5 | import { InstallOsPackage } from "./common/os-package.ts"; 6 | import { Exec } from "./exec.ts"; 7 | 8 | export const sdkmanSourceLine = ". ~/.sdkman/bin/sdkman-init.sh"; 9 | 10 | export const sdkman = Command.custom() 11 | .withDependencies([ 12 | new Exec([], [], targetUser, {}, [ 13 | "bash", 14 | "-c", 15 | "curl -fsSL https://get.sdkman.io/ | bash", 16 | ]) 17 | .withDependencies(["unzip", "zip", "curl"].map(InstallOsPackage.of)), 18 | new LineInFile( 19 | targetUser, 20 | FileSystemPath.of(targetUser, "~/.bashrc"), 21 | sdkmanSourceLine, 22 | ), 23 | ]); 24 | -------------------------------------------------------------------------------- /src/commands/gnome-shell-extension-installer.ts: -------------------------------------------------------------------------------- 1 | import { CreateFile, MODE_EXECUTABLE_775 } from "./common/file-commands.ts"; 2 | import { targetUser } from "../os/user/target-user.ts"; 3 | import { FileSystemPath } from "../model/dependency.ts"; 4 | import { addHomeBinToPath } from "./add-home-bin-to-path.ts"; 5 | import { readFromUrl } from "../os/read-from-url.ts"; 6 | 7 | export const gnomeShellExtensionInstallerFile = FileSystemPath.of( 8 | targetUser, 9 | "~/bin/gnome-shell-extension-installer", 10 | ); 11 | 12 | export const gnomeShellExtensionInstaller = new CreateFile( 13 | targetUser, 14 | gnomeShellExtensionInstallerFile, 15 | await readFromUrl( 16 | "https://github.com/brunelli/gnome-shell-extension-installer/raw/master/gnome-shell-extension-installer", 17 | ), 18 | false, 19 | MODE_EXECUTABLE_775, 20 | ) 21 | .withDependencies([addHomeBinToPath]); 22 | -------------------------------------------------------------------------------- /src/commands/gitk.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { addHomeBinToPath } from "./add-home-bin-to-path.ts"; 5 | import { CreateFile, MODE_EXECUTABLE_775 } from "./common/file-commands.ts"; 6 | import { InstallOsPackage } from "./common/os-package.ts"; 7 | import { git } from "./git.ts"; 8 | 9 | const contents = `#!/usr/bin/env bash 10 | arg=\${1:---all} 11 | gitk "\${arg}" &>/dev/null & 12 | `; 13 | 14 | export const gitk: Command = Command.custom() 15 | .withDependencies([ 16 | addHomeBinToPath, 17 | git, 18 | InstallOsPackage.of("gitk"), 19 | new CreateFile( 20 | targetUser, 21 | FileSystemPath.of(targetUser, "~/bin/gk"), 22 | contents, 23 | false, 24 | MODE_EXECUTABLE_775, 25 | ), 26 | ]); 27 | -------------------------------------------------------------------------------- /src/commands/vim.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { ROOT } from "../os/user/target-user.ts"; 4 | import { LineInFile } from "./common/file-commands.ts"; 5 | import { ReplaceOsPackage } from "./common/os-package.ts"; 6 | import { Exec } from "./exec.ts"; 7 | 8 | export const vim: Command = Command.custom() 9 | .withDependencies([ 10 | new Exec( 11 | [ReplaceOsPackage.of2("vim-runtime", "neovim")], 12 | [FileSystemPath.of(ROOT, "/etc/alternatives")], 13 | ROOT, 14 | {}, 15 | [ 16 | "bash", 17 | "-c", 18 | 'sudo update-alternatives --set editor "$(readlink -f "$(command -v vim)")"', 19 | ], 20 | ), 21 | new LineInFile( 22 | ROOT, 23 | FileSystemPath.of(ROOT, "/etc/environment"), 24 | "EDITOR=vim", 25 | ), 26 | ]); 27 | -------------------------------------------------------------------------------- /src/commands/bash-git-prompt.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { LineInFile } from "./common/file-commands.ts"; 5 | import { InstallBrewPackage, InstallOsPackage } from "./common/os-package.ts"; 6 | 7 | export const bashGitPrompt: Command = Command.custom() 8 | .withDependencies([ 9 | InstallOsPackage.of("git"), 10 | InstallBrewPackage.of("bash-git-prompt"), 11 | new LineInFile( 12 | targetUser, 13 | FileSystemPath.of(targetUser, "~/.bashrc"), 14 | `if [ -f "/home/linuxbrew/.linuxbrew/opt/bash-git-prompt/share/gitprompt.sh" ]; then 15 | __GIT_PROMPT_DIR="/home/linuxbrew/.linuxbrew/opt/bash-git-prompt/share" 16 | source "/home/linuxbrew/.linuxbrew/opt/bash-git-prompt/share/gitprompt.sh" 17 | fi`, 18 | ), 19 | ]); 20 | -------------------------------------------------------------------------------- /src/commands/all-3-developer-web.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { all2DeveloperBase } from "./all-2-developer-base.ts"; 3 | import { InstallOsPackage, InstallSnapPackage } from "./common/os-package.ts"; 4 | import { eog } from "./eog.ts"; 5 | import { networkUtils } from "./network-utils.ts"; 6 | import { rust } from "./rust.ts"; 7 | import { webstorm } from "./webstorm.ts"; 8 | import { deno } from "./deno.ts"; 9 | import { node } from "./node.ts"; 10 | import { brave } from "./brave.ts"; 11 | import { chrome } from "./chrome.ts"; 12 | 13 | export const all3DeveloperWeb = Command.custom() 14 | .withDependencies([ 15 | all2DeveloperBase, 16 | brave, 17 | chrome, 18 | deno, 19 | eog, 20 | networkUtils, 21 | node, 22 | rust, 23 | webstorm, 24 | InstallSnapPackage.of("chromium"), 25 | InstallSnapPackage.ofClassic("code"), 26 | ]); 27 | -------------------------------------------------------------------------------- /src/commands/android.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../config.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { addHomeBinToPath } from "./add-home-bin-to-path.ts"; 5 | import { CreateFile, MODE_EXECUTABLE_775 } from "./common/file-commands.ts"; 6 | import { InstallOsPackage } from "./common/os-package.ts"; 7 | 8 | export const android = new CreateFile( 9 | targetUser, 10 | FileSystemPath.of(targetUser, "~/bin/android-remote-screen"), 11 | `#!/usr/bin/env bash 12 | 13 | set -euo pipefail 14 | IFS=$'\\t\\n' 15 | 16 | adb connect ${config.ANDROID_HOSTNAME}:\${1:-5555} 17 | scrcpy --prefer-text 18 | `, 19 | false, 20 | MODE_EXECUTABLE_775, 21 | ) 22 | .withDependencies([ 23 | addHomeBinToPath, 24 | InstallOsPackage.of("scrcpy") 25 | .withDependencies([InstallOsPackage.of("android-sdk")]), 26 | ]); 27 | -------------------------------------------------------------------------------- /src/commands/virtualbox.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { InstallOsPackage } from "./common/os-package.ts"; 3 | import { Exec } from "./exec.ts"; 4 | import { ROOT } from "../os/user/target-user.ts"; 5 | 6 | /** pre-register accept license for deb package virtualbox-ext-pack */ 7 | const acceptLicense = new Exec( 8 | [], 9 | [], 10 | ROOT, 11 | {}, 12 | [ 13 | "sh", 14 | "-c", 15 | "echo virtualbox-ext-pack virtualbox-ext-pack/license select true | debconf-set-selections", 16 | ], 17 | ); 18 | 19 | export const virtualbox: Command = Command 20 | .custom() 21 | .withDependencies([ 22 | InstallOsPackage.of("virtualbox"), 23 | InstallOsPackage.of("virtualbox-guest-additions-iso"), 24 | InstallOsPackage.of("virtualbox-ext-pack").withDependencies([ 25 | acceptLicense, 26 | ]), 27 | InstallOsPackage.of("virtualbox-qt"), 28 | ]); 29 | -------------------------------------------------------------------------------- /src/commands/refresh-os-packages.ts: -------------------------------------------------------------------------------- 1 | import { Command, RunResult } from "../model/command.ts"; 2 | import { OS_PACKAGE_SYSTEM } from "../model/dependency.ts"; 3 | import { ensureSuccessful } from "../os/exec.ts"; 4 | import { ROOT } from "../os/user/target-user.ts"; 5 | 6 | export const REFRESH_OS_PACKAGES = (new Command()) 7 | .withLocks([OS_PACKAGE_SYSTEM]) 8 | .withRun(async (): Promise => { 9 | await ensureSuccessful(ROOT, [ 10 | "apt", 11 | "update", 12 | ]); 13 | return `Refreshed list of OS packages.`; 14 | }); 15 | 16 | export const UPGRADE_OS_PACKAGES = (new Command()) 17 | .withLocks([OS_PACKAGE_SYSTEM]) 18 | .withRun(async (): Promise => { 19 | await ensureSuccessful(ROOT, [ 20 | "apt", 21 | "full-upgrade", 22 | "-y", 23 | "--purge", 24 | "--auto-remove", 25 | ]); 26 | return `Upgraded OS packages.`; 27 | }); 28 | -------------------------------------------------------------------------------- /src/commands/open-mux.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { readRelativeFile } from "../os/read-relative-file.ts"; 4 | import { targetUser } from "../os/user/target-user.ts"; 5 | import { 6 | CreateFile, 7 | LineInFile, 8 | MODE_EXECUTABLE_775, 9 | } from "./common/file-commands.ts"; 10 | import { addHomeBinToPath } from "./add-home-bin-to-path.ts"; 11 | 12 | export const openMux = Command.custom() 13 | .withDependencies([ 14 | addHomeBinToPath, 15 | new CreateFile( 16 | targetUser, 17 | FileSystemPath.of(targetUser, "~/bin/open-mux"), 18 | await readRelativeFile("./files/open-mux", import.meta.url), 19 | false, 20 | MODE_EXECUTABLE_775, 21 | ), 22 | new LineInFile( 23 | targetUser, 24 | FileSystemPath.of(targetUser, "~/.bash_history"), 25 | "open-mux", 26 | ), 27 | ]); 28 | -------------------------------------------------------------------------------- /src/commands/common/group.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../model/command.ts"; 2 | import { Exec } from "../exec.ts"; 3 | import { FileSystemPath } from "../../model/dependency.ts"; 4 | import { ROOT } from "../../os/user/target-user.ts"; 5 | import { PasswdEntry } from "../../deps.ts"; 6 | 7 | export function ensureSystemGroup(name: string): Command { 8 | return new Exec( 9 | [], 10 | [FileSystemPath.of(ROOT, "/etc/group")], 11 | ROOT, 12 | {}, 13 | ["groupadd", "--force", "--system", name], 14 | ); 15 | } 16 | 17 | export function ensureUserInSystemGroup( 18 | user: PasswdEntry, 19 | group: string, 20 | ): Command { 21 | return new Exec( 22 | [ensureSystemGroup(group)], 23 | [ 24 | FileSystemPath.of(ROOT, "/etc/group"), 25 | FileSystemPath.of(ROOT, "/etc/passwd"), 26 | ], 27 | ROOT, 28 | {}, 29 | ["usermod", "--append", "--groups", group, user.username], 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/files/m-temp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Creates and opens a new temp session 4 | 5 | export SESSION_DIR="$(readlink -f "${1:-"$(mktemp -d)"}")" 6 | export SESSION_NAME="$(echo "${SESSION_DIR}" \ 7 | | sed -E 's/^[\./]//g' \ 8 | | sed -E 's/[\./]+/-/g' \ 9 | | sed -E 's/[åä]/a/g' \ 10 | | sed -E 's/[ÅÄ]/A/g' \ 11 | | sed -E 's/[ö]/o/g' \ 12 | | sed -E 's/[Ö]/O/g' \ 13 | | sed -E 's/^Users-/home-/' \ 14 | | sed -E 's/^home-[^-]+-//' \ 15 | | sed -E 's/^surviving-data-//' \ 16 | | sed -E 's/^code-//g')" 17 | FILE_NAME="${SESSION_NAME}.yml" 18 | 19 | cd ~/.tmuxinator 20 | envsubst < temp.TEMPLATE > "${FILE_NAME}" 21 | 22 | mkdir -p "${SESSION_DIR}" 23 | tmuxinator start "${SESSION_NAME}" 24 | sleep 1 25 | rm "${FILE_NAME}" 26 | -------------------------------------------------------------------------------- /src/commands/insync.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { 3 | InstallOsPackage, 4 | installOsPackageFromUrl, 5 | } from "./common/os-package.ts"; 6 | import { targetUser } from "../os/user/target-user.ts"; 7 | import { ensureSuccessfulStdOut } from "../os/exec.ts"; 8 | 9 | export const insync: Command = installOsPackageFromUrl( 10 | "insync", 11 | () => 12 | ensureSuccessfulStdOut(targetUser, [ 13 | "bash", 14 | "-c", 15 | ` 16 | set -euo pipefail 17 | IFS=$'\n\t' 18 | 19 | codename="$(lsb_release -cs)" 20 | base_url="http://apt.insync.io/ubuntu" 21 | deb_path="$(curl -sfL "$base_url/dists/$codename/non-free/binary-amd64/Packages.gz" | gunzip | awk '/^Package: insync$/,/^Filename: /{print $2}' | tail -n1)" 22 | deb_url="$base_url/$deb_path" 23 | echo "$deb_url" 24 | `, 25 | ]), 26 | ) 27 | .withDependencies([ 28 | InstallOsPackage.of("lsb-release"), 29 | InstallOsPackage.of("curl"), 30 | ]); 31 | -------------------------------------------------------------------------------- /src/commands/common/dconf-load.ts: -------------------------------------------------------------------------------- 1 | import { Exec } from "../exec.ts"; 2 | import { InstallOsPackage } from "./os-package.ts"; 3 | import { Command } from "../../model/command.ts"; 4 | import { FileSystemPath } from "../../model/dependency.ts"; 5 | import { targetUser } from "../../os/user/target-user.ts"; 6 | import { startsAndEndsWith } from "../../fn.ts"; 7 | 8 | const startsAndEndsWithSlash = startsAndEndsWith("/"); 9 | 10 | export function dconfLoadRoot(dconfDump: string): Command { 11 | return dconfLoad("/", dconfDump); 12 | } 13 | 14 | export function dconfLoad(directoryPath: string, dconfDump: string): Command { 15 | if (!startsAndEndsWithSlash(directoryPath)) { 16 | throw new Error("directoryPath must start and end with a slash"); 17 | } 18 | return new Exec( 19 | [ 20 | InstallOsPackage.of("dconf-cli"), 21 | ], 22 | [FileSystemPath.of(targetUser, `${targetUser.homedir}/.config/dconf`)], 23 | targetUser, 24 | { stdin: dconfDump.trim() }, 25 | ["dconf", "load", directoryPath], 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/fzf.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemPath } from "../model/dependency.ts"; 2 | import { targetUser } from "../os/user/target-user.ts"; 3 | import { CreateDir } from "./common/file-commands.ts"; 4 | import { Exec } from "./exec.ts"; 5 | import { git } from "./git.ts"; 6 | 7 | export const fzf = () => { 8 | const installDir = FileSystemPath.of(targetUser, "~/.fzf"); 9 | 10 | const deleteDir = new Exec( 11 | [new CreateDir(targetUser, installDir)], 12 | [installDir], 13 | targetUser, 14 | {}, 15 | [ 16 | "rm", 17 | "-rf", 18 | "--", 19 | installDir.path, 20 | ], 21 | ); 22 | const gitClone = new Exec( 23 | [git, deleteDir], 24 | [installDir], 25 | targetUser, 26 | {}, 27 | [ 28 | "git", 29 | "clone", 30 | "https://github.com/junegunn/fzf", 31 | installDir.path, 32 | ], 33 | ); 34 | 35 | return new Exec( 36 | [gitClone], 37 | [installDir], 38 | targetUser, 39 | { cwd: installDir.path }, 40 | ["./install", "--all"], 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/commands/keybase.ts: -------------------------------------------------------------------------------- 1 | import { ROOT, targetUser } from "../os/user/target-user.ts"; 2 | import { installOsPackageFromUrl } from "./common/os-package.ts"; 3 | import { Exec } from "./exec.ts"; 4 | import { Command } from "../model/command.ts"; 5 | import { FileSystemPath } from "../model/dependency.ts"; 6 | import { isDocker } from "../deps.ts"; 7 | 8 | export const installKeybase = new Exec( 9 | [ 10 | installOsPackageFromUrl( 11 | "keybase", 12 | "https://prerelease.keybase.io/keybase_amd64.deb", 13 | ), 14 | ], 15 | [FileSystemPath.of(ROOT, "/etc/apt/trusted.gpg.d/keybase.gpg")], 16 | ROOT, 17 | {}, 18 | [ 19 | "bash", 20 | "-c", 21 | "apt-key export 656D16C7 | gpg --dearmour --output - | tee /etc/apt/trusted.gpg.d/keybase.gpg >/dev/null", 22 | ], 23 | ); 24 | 25 | const runKeybase = new Exec( 26 | [installKeybase], 27 | [], 28 | targetUser, 29 | {}, 30 | ["run_keybase"], 31 | ) 32 | .withSkipIfAny([await isDocker()]); 33 | 34 | export const keybase = Command.custom().withDependencies([ 35 | installKeybase, 36 | runKeybase, 37 | ]); 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Install scripts, for Ubuntu 2 | 3 | _Install scripts for various things I like to install on a fresh Ubuntu virtual 4 | machine._ 5 | 6 | Do note, that most settings are for how I like them inside virtual machines, so 7 | not likely to be appropriate for your main install! Happy testing! 8 | 9 | ## Download and run 10 | 11 | This will download into `./ubuntu-install-scripts-ubuntu-22.04/`, and show you 12 | the usage/help text: 13 | 14 | ```bash 15 | wget -qO- https://github.com/hugojosefson/ubuntu-install-scripts/archive/refs/heads/ubuntu-22.04.tar.gz | tar xz 16 | sudo ./ubuntu-install-scripts-ubuntu-22.04/src/cli.ts 17 | ``` 18 | 19 | > Do note that usually, during the first run, you will be asked to try again 20 | > after having restarted Gnome. 21 | > 22 | > If so, please **log out** and **log in**, or restart the VM. Then run the same 23 | > command again. 24 | > 25 | > This is because Gnome doesn't pick up installed extensions when installing 26 | > them via `gnome-extensions install` until the next time it starts. After that, 27 | > we can apply settings for the newly installed extensions. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014-2022 Hugo Josefson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/commands/bash-aliases.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { readRelativeFile } from "../os/read-relative-file.ts"; 4 | import { targetUser } from "../os/user/target-user.ts"; 5 | import { bash } from "./bash.ts"; 6 | import { CreateFile, LineInFile } from "./common/file-commands.ts"; 7 | import { InstallOsPackage } from "./common/os-package.ts"; 8 | import { gitCompletion } from "./git-completion.ts"; 9 | import { vim } from "./vim.ts"; 10 | 11 | export const bashAliases = Command.custom() 12 | .withDependencies([ 13 | bash, 14 | gitCompletion, 15 | vim, 16 | InstallOsPackage.of("exa"), 17 | InstallOsPackage.of("jq"), 18 | InstallOsPackage.of("xsel"), 19 | new LineInFile( 20 | targetUser, 21 | FileSystemPath.of(targetUser, "~/.bashrc"), 22 | ". ~/.bash_aliases", 23 | ), 24 | new CreateFile( 25 | targetUser, 26 | FileSystemPath.of(targetUser, "~/.bash_aliases"), 27 | await readRelativeFile("./files/.bash_aliases", import.meta.url), 28 | true, 29 | ), 30 | ]); 31 | -------------------------------------------------------------------------------- /src/commands/idea.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { ROOT, targetUser } from "../os/user/target-user.ts"; 4 | import { addHomeBinToPath } from "./add-home-bin-to-path.ts"; 5 | import { CreateFile, MODE_EXECUTABLE_775 } from "./common/file-commands.ts"; 6 | import { InstallSnapPackage } from "./common/os-package.ts"; 7 | 8 | const iContents = `#!/usr/bin/env bash 9 | arg=\${1:-.} 10 | idea "\${arg}" &>/dev/null & 11 | `; 12 | 13 | const ideaContents = `#!/usr/bin/env bash 14 | exec intellij-idea-community "\${@}" 15 | `; 16 | 17 | export const idea: Command = Command.custom() 18 | .withDependencies([ 19 | InstallSnapPackage.ofClassic("intellij-idea-community"), 20 | new CreateFile( 21 | ROOT, 22 | FileSystemPath.of(ROOT, "/usr/local/bin/idea"), 23 | ideaContents, 24 | true, 25 | MODE_EXECUTABLE_775, 26 | ), 27 | new CreateFile( 28 | targetUser, 29 | FileSystemPath.of(targetUser, "~/bin/i"), 30 | iContents, 31 | false, 32 | MODE_EXECUTABLE_775, 33 | ).withDependencies([addHomeBinToPath]), 34 | ]); 35 | -------------------------------------------------------------------------------- /src/commands/pass.ts: -------------------------------------------------------------------------------- 1 | import { InstallOsPackage } from "./common/os-package.ts"; 2 | import { Symlink } from "./common/file-commands.ts"; 3 | import { ROOT } from "../os/user/target-user.ts"; 4 | import { FileSystemPath } from "../model/dependency.ts"; 5 | import { Exec } from "./exec.ts"; 6 | import { readFromUrl } from "../os/read-from-url.ts"; 7 | import { dmenu } from "./dmenu.ts"; 8 | import { firefoxLocal } from "./firefox-local.ts"; 9 | 10 | const passFirefoxHost = new Exec( 11 | [firefoxLocal], 12 | [], 13 | ROOT, 14 | { 15 | stdin: await readFromUrl( 16 | "https://github.com/passff/passff-host/releases/latest/download/install_host_app.sh", 17 | ), 18 | }, 19 | ["bash", "-s", "--", "firefox"], 20 | ); 21 | 22 | const passPackage = InstallOsPackage.of("pass"); 23 | 24 | export const pass = new Symlink( 25 | ROOT, 26 | "/usr/share/doc/pass/examples/dmenu/passmenu", 27 | FileSystemPath.of(ROOT, "/usr/local/bin/passmenu"), 28 | ).withDependencies([ 29 | InstallOsPackage.of("pass-extension-otp") 30 | .withDependencies([ 31 | passPackage, 32 | dmenu, 33 | ]), 34 | passPackage, 35 | passFirefoxHost, 36 | ]); 37 | -------------------------------------------------------------------------------- /src/commands/starship.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { readRelativeFile } from "../os/read-relative-file.ts"; 4 | import { targetUser } from "../os/user/target-user.ts"; 5 | import { CreateFile, LineInFile } from "./common/file-commands.ts"; 6 | import { InstallOsPackage } from "./common/os-package.ts"; 7 | import { vim } from "./vim.ts"; 8 | import { InstallRustPackage } from "./rust.ts"; 9 | 10 | const installStarship = InstallRustPackage.of("starship") 11 | .withDependencies([ 12 | InstallOsPackage.of("fonts-noto-color-emoji"), 13 | InstallOsPackage.of("fonts-powerline"), 14 | vim, 15 | ]); 16 | 17 | const activateStarship = new LineInFile( 18 | targetUser, 19 | FileSystemPath.of(targetUser, "~/.bashrc"), 20 | 'eval "$(starship init bash)"', 21 | ); 22 | 23 | const configureStarship = new CreateFile( 24 | targetUser, 25 | FileSystemPath.of(targetUser, "~/.config/starship.toml"), 26 | await readRelativeFile("./files/starship.toml", import.meta.url), 27 | ); 28 | 29 | export const starship = Command.custom() 30 | .withDependencies([ 31 | installStarship, 32 | activateStarship, 33 | configureStarship, 34 | ]); 35 | -------------------------------------------------------------------------------- /src/commands/all-2-developer-base.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { all1MinimalSanity } from "./all-1-minimal-sanity.ts"; 3 | import { InstallBrewPackage, InstallOsPackage } from "./common/os-package.ts"; 4 | import { docker } from "./docker.ts"; 5 | import { fzf } from "./fzf.ts"; 6 | import { gitk } from "./gitk.ts"; 7 | import { meld } from "./meld.ts"; 8 | import { starship } from "./starship.ts"; 9 | import { InstallRustPackage } from "./rust.ts"; 10 | import { 11 | isolateInDocker, 12 | symlinkToIsolateInDocker, 13 | } from "./isolate-in-docker.ts"; 14 | 15 | export const all2DeveloperBase = Command.custom() 16 | .withDependencies([ 17 | all1MinimalSanity, 18 | ...[ 19 | "git-absorb", 20 | "moreutils", 21 | "bind9-dnsutils", 22 | "stunnel4", 23 | "tig", 24 | ].map(InstallOsPackage.of), 25 | ...[ 26 | "gh", 27 | "git-revise", 28 | ].map(InstallBrewPackage.of), 29 | ...[ 30 | "bat", 31 | "exa", 32 | "fd-find", 33 | "ripgrep", 34 | ].map(InstallRustPackage.of), 35 | docker, 36 | gitk, 37 | isolateInDocker, 38 | symlinkToIsolateInDocker(`firefox40`), 39 | meld, 40 | starship, 41 | await fzf(), 42 | ]); 43 | -------------------------------------------------------------------------------- /src/commands/deno.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemPath } from "../model/dependency.ts"; 2 | import { isSuccessful } from "../os/exec.ts"; 3 | import { ROOT, targetUser } from "../os/user/target-user.ts"; 4 | import { LineInFile } from "./common/file-commands.ts"; 5 | import { InstallOsPackage } from "./common/os-package.ts"; 6 | import { Exec } from "./exec.ts"; 7 | import { readFromUrl } from "../os/read-from-url.ts"; 8 | import { Command } from "../model/command.ts"; 9 | 10 | const downloadDenoToUsrLocalBin = new Exec( 11 | [ 12 | InstallOsPackage.of("curl"), 13 | InstallOsPackage.of("unzip"), 14 | ], 15 | [FileSystemPath.of(ROOT, "/usr/local/bin/deno")], 16 | ROOT, 17 | { 18 | env: { DENO_INSTALL: "/usr/local" }, 19 | stdin: await readFromUrl("https://deno.land/install.sh"), 20 | }, 21 | ["sh"], 22 | ) 23 | .withSkipIfAll([ 24 | () => isSuccessful(targetUser, "command -v deno".split(" "), {}), 25 | () => isSuccessful(ROOT, "command -v deno".split(" "), {}), 26 | ]); 27 | 28 | const setDenoInstallEnv = new LineInFile( 29 | ROOT, 30 | FileSystemPath.of(targetUser, "/etc/environment"), 31 | `DENO_INSTALL=/usr/local`, 32 | ); 33 | 34 | export const deno = Command.custom().withDependencies([ 35 | setDenoInstallEnv, 36 | downloadDenoToUsrLocalBin, 37 | ]); 38 | -------------------------------------------------------------------------------- /src/os/defer.ts: -------------------------------------------------------------------------------- 1 | export interface ResolveFn { 2 | (value: PromiseLike | T): void; 3 | } 4 | 5 | export interface RejectFn { 6 | // deno-lint-ignore no-explicit-any : because Promise defines it as ?any 7 | (reason?: any): void; 8 | } 9 | 10 | export interface Deferred { 11 | promise: Promise; 12 | resolve: ResolveFn; 13 | reject: RejectFn; 14 | isDone: boolean; 15 | } 16 | 17 | export const defer = (): Deferred => { 18 | let resolve: ResolveFn; 19 | let reject: RejectFn; 20 | 21 | const promise: Promise = new Promise( 22 | (resolveFn, rejectFn) => { 23 | resolve = (value) => { 24 | deferred.isDone = true; 25 | return resolveFn(value); 26 | }; 27 | reject = (reason) => { 28 | deferred.isDone = true; 29 | return rejectFn(reason); 30 | }; 31 | }, 32 | ); 33 | 34 | // @ts-ignore: Promise constructor argument is called immediately, so our resolve and reject variables have been initialised by the time we get here. 35 | const deferred = { promise, resolve, reject, isDone: false }; 36 | return deferred; 37 | }; 38 | 39 | export function deferAlreadyResolvedVoid(): Deferred { 40 | const deferred: Deferred = defer(); 41 | deferred.resolve(); 42 | return deferred; 43 | } 44 | -------------------------------------------------------------------------------- /src/usage.ts: -------------------------------------------------------------------------------- 1 | import { availableCommands } from "./commands/index.ts"; 2 | import { colorlog } from "./deps.ts"; 3 | 4 | export function usageAndExit( 5 | code = 1, 6 | message?: string, 7 | ): never { 8 | if (message) { 9 | console.error(colorlog.error(message)); 10 | } 11 | console.error(` 12 | Usage, if you only have curl, unzip and sh: 13 | 14 | curl -fsSL https://raw.githubusercontent.com/hugojosefson/ubuntu-install-scripts/ubuntu-22.04/src/cli.ts \\ 15 | | sudo sh -s 16 | 17 | 18 | Usage, if you have deno: 19 | 20 | sudo deno -A --unstable https://raw.githubusercontent.com/hugojosefson/ubuntu-install-scripts/ubuntu-22.04/src/cli.ts \\ 21 | 22 | 23 | 24 | Usage, if you have deno, and have cloned this git repo: 25 | 26 | sudo deno -A --unstable ./src/cli.ts 27 | 28 | 29 | Available commands: 30 | ${ 31 | availableCommands.map((name) => ` ${name}`) 32 | .join("\n") 33 | } 34 | 35 | ...or any valid OS-level package. 36 | `); 37 | return Deno.exit(code); 38 | } 39 | 40 | export function errorAndExit( 41 | code = 1, 42 | message?: string, 43 | ): never { 44 | if (message) { 45 | console.error(colorlog.error(message)); 46 | } 47 | return Deno.exit(code); 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/all-5-personal.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { InstallOsPackage, InstallSnapPackage } from "./common/os-package.ts"; 3 | import { gsettingsAll } from "./gsettings.ts"; 4 | import { gnomeShellExtensions } from "./gnome-shell-extensions.ts"; 5 | import { keybase } from "./keybase.ts"; 6 | import { signalDesktop } from "./signal-desktop.ts"; 7 | import { yubikey } from "./yubikey.ts"; 8 | import { mullvad } from "./mullvad.ts"; 9 | import { pass } from "./pass.ts"; 10 | 11 | export const all5Personal = Command.custom() 12 | .withDependencies([ 13 | ...[ 14 | "fonts-noto", 15 | "ttf-bitstream-vera", 16 | "fonts-croscore", 17 | "fonts-dejavu", 18 | "fonts-droid-fallback", 19 | "fonts-ibm-plex", 20 | "fonts-liberation2", 21 | "arandr", 22 | "baobab", 23 | "cheese", 24 | "docx2txt", 25 | "dosbox", 26 | "efibootmgr", 27 | "ffmpeg", 28 | "gimp", 29 | "gnome-tweaks", 30 | "gpick", 31 | "imagemagick", 32 | "inkscape", 33 | "neofetch", 34 | ] 35 | .map(InstallOsPackage.of), 36 | ...[ 37 | "spotify", 38 | "slack", 39 | "teams", 40 | ] 41 | .map(InstallSnapPackage.of), 42 | pass, 43 | yubikey, 44 | keybase, 45 | signalDesktop, 46 | gnomeShellExtensions, 47 | mullvad, 48 | gsettingsAll, 49 | ]); 50 | -------------------------------------------------------------------------------- /src/os/user/target-user.ts: -------------------------------------------------------------------------------- 1 | import { parsePasswd, PasswdEntry } from "../../deps.ts"; 2 | import { ensureSuccessfulStdOut } from "../exec.ts"; 3 | import { FileSystemPath } from "../../model/dependency.ts"; 4 | 5 | const byUid = (a: PasswdEntry, b: PasswdEntry) => a?.uid - b?.uid; 6 | 7 | const getUsers = async () => 8 | parsePasswd(await ensureSuccessfulStdOut(ROOT, ["getent", "passwd"])) 9 | .sort(byUid); 10 | 11 | const SUDO_USER = "SUDO_USER"; 12 | 13 | export const ROOT: PasswdEntry = { 14 | uid: 0, 15 | gid: 0, 16 | username: "root", 17 | homedir: "/root", 18 | }; 19 | 20 | const getTargetUser = async (): Promise => { 21 | const users: Array = await getUsers(); 22 | const sudoUser: string | undefined = Deno.env.get( 23 | SUDO_USER, 24 | ); 25 | if (sudoUser) { 26 | const targetUser: PasswdEntry | undefined = users.find(( 27 | { username }, 28 | ) => username === sudoUser); 29 | 30 | if (targetUser) { 31 | return targetUser; 32 | } 33 | throw new Error( 34 | `ERROR: Could not find requested ${SUDO_USER} "${sudoUser}".`, 35 | ); 36 | } 37 | 38 | throw new Error( 39 | `ERROR: No target user found. Log in graphically as the target user. Then use sudo.`, 40 | ); 41 | }; 42 | 43 | export const targetUser = await getTargetUser(); 44 | 45 | export const DBUS_SESSION_BUS_ADDRESS = 46 | `unix:path=/run/user/${targetUser.uid}/bus`; 47 | -------------------------------------------------------------------------------- /src/commands/gnome-custom-keybindings-backup.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { ensureSuccessfulStdOut } from "../os/exec.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { InstallOsPackage } from "./common/os-package.ts"; 5 | 6 | const dconfPath = 7 | "/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/"; 8 | 9 | const gsettingsSchema = "org.gnome.settings-daemon.plugins.media-keys"; 10 | const gsettingsKey = "custom-keybindings"; 11 | 12 | function getGsettingsValue(): Promise { 13 | return ensureSuccessfulStdOut(targetUser, [ 14 | "gsettings", 15 | "get", 16 | gsettingsSchema, 17 | gsettingsKey, 18 | ]); 19 | } 20 | function getDconfData(): Promise { 21 | return ensureSuccessfulStdOut(targetUser, ["dconf", "dump", dconfPath]); 22 | } 23 | 24 | export const gnomeCustomKeybindingsBackup = Command.custom() 25 | .withDependencies([ 26 | InstallOsPackage.of("gnome-settings-daemon"), 27 | InstallOsPackage.of("dconf-cli"), 28 | InstallOsPackage.of("libglib2.0-bin"), 29 | ]) 30 | .withRun(async () => { 31 | const backupScript = `#!/usr/bin/env bash 32 | 33 | set -euo pipefail 34 | IFS=$'\n\t' 35 | 36 | dconf load ${dconfPath} <<'EOF' 37 | ${await getDconfData()} 38 | EOF 39 | 40 | gsettings set ${gsettingsSchema} ${gsettingsKey} "${await getGsettingsValue()}" 41 | `; 42 | console.log(backupScript); 43 | }); 44 | -------------------------------------------------------------------------------- /src/commands/isolate-in-docker.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { readFromUrl } from "../os/read-from-url.ts"; 4 | import { ROOT } from "../os/user/target-user.ts"; 5 | import { 6 | CreateFile, 7 | MODE_EXECUTABLE_775, 8 | Symlink, 9 | } from "./common/file-commands.ts"; 10 | import { docker } from "./docker.ts"; 11 | 12 | export function symlinkToIsolateInDocker(name: string) { 13 | return new Symlink( 14 | ROOT, 15 | "isolate-in-docker", 16 | FileSystemPath.of(ROOT, `/usr/local/bin/${name}`), 17 | ) 18 | .withDependencies([isolateInDocker]); 19 | } 20 | 21 | export const isolateInDocker = Command.custom() 22 | .withDependencies([ 23 | docker, 24 | new CreateFile( 25 | ROOT, 26 | FileSystemPath.of(ROOT, "/usr/local/bin/isolate-in-docker"), 27 | await readFromUrl( 28 | "https://raw.githubusercontent.com/hugojosefson/isolate-in-docker/master/isolate-in-docker", 29 | ), 30 | false, 31 | MODE_EXECUTABLE_775, 32 | ), 33 | ]); 34 | 35 | export const isolateInDockerAll = Command.custom() 36 | .withDependencies([ 37 | `node`, 38 | `npm`, 39 | `npx`, 40 | `yarn`, 41 | `heroku`, 42 | `webstorm`, 43 | `webstorm-install-rust`, 44 | `goland`, 45 | `clion`, 46 | `jetbrains-toolbox`, 47 | `aws`, 48 | `firefox40`, 49 | ].map(symlinkToIsolateInDocker)); 50 | -------------------------------------------------------------------------------- /src/commands/wm-utils.ts: -------------------------------------------------------------------------------- 1 | import { Exec } from "./exec.ts"; 2 | import { InstallOsPackage } from "./common/os-package.ts"; 3 | import { ROOT } from "../os/user/target-user.ts"; 4 | import { FileSystemPath, OS_PACKAGE_SYSTEM } from "../model/dependency.ts"; 5 | 6 | const spvkgnPpa = new Exec( 7 | [InstallOsPackage.of("software-properties-common")], 8 | [ 9 | OS_PACKAGE_SYSTEM, 10 | ...[ 11 | "/etc/apt/sources.list.d/spvkgn-ubuntu-ppa-jammy.list", 12 | "/etc/apt/sources.list.d/spvkgn-ubuntu-ppa-focal.list", 13 | "/etc/apt/preferences.d/99spvkgn-repository", 14 | ].map((path) => FileSystemPath.of(ROOT, path)), 15 | ], 16 | ROOT, 17 | {}, 18 | [ 19 | "bash", 20 | "-c", 21 | ` 22 | set -euo pipefail 23 | IFS=$'\\t\\n' 24 | 25 | add-apt-repository ppa:spvkgn/ppa -y --no-update 26 | sed -E s/jammy/focal/g -i /etc/apt/sources.list.d/spvkgn-ubuntu-ppa-jammy.list 27 | mv /etc/apt/sources.list.d/spvkgn-ubuntu-ppa-{jammy,focal}.list 28 | 29 | cat > /etc/apt/preferences.d/99spvkgn-repository <<'EOF' 30 | # Allow upgrading only wmutils-core from spvkgn repository 31 | Package: wmutils-core 32 | Pin: release n=spvkgn 33 | Pin-Priority: 500 34 | 35 | # Never prefer other packages from the spvkgn repository 36 | Package: * 37 | Pin: release n=spvkgn 38 | Pin-Priority: 1 39 | EOF 40 | 41 | apt update 42 | `, 43 | ], 44 | ); 45 | 46 | export const wmUtils = InstallOsPackage.of("wmutils-core") 47 | .withDependencies([spvkgnPpa]); 48 | -------------------------------------------------------------------------------- /src/commands/all-1-minimal-sanity.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { addHomeBinToPath } from "./add-home-bin-to-path.ts"; 3 | import { bash } from "./bash.ts"; 4 | import { InstallOsPackage } from "./common/os-package.ts"; 5 | import { desktopIsHome } from "./desktop-is-home.ts"; 6 | import { downloadsIsCleanedOnBoot } from "./downloads-is-cleaned-on-boot.ts"; 7 | import { gnomeDisableWayland } from "./gnome-disable-wayland.ts"; 8 | import { saveBashHistory } from "./save-bash-history.ts"; 9 | import { tmuxinatorByobuBashAliases } from "./tmuxinator_byobu_bash_aliases.ts"; 10 | import { vim } from "./vim.ts"; 11 | import { starship } from "./starship.ts"; 12 | import { UPGRADE_OS_PACKAGES } from "./refresh-os-packages.ts"; 13 | import { sudoNoPassword } from "./sudo-no-password.ts"; 14 | import { mdr } from "./mdr.ts"; 15 | 16 | export const all1MinimalSanity = Command.custom() 17 | .withDependencies([ 18 | sudoNoPassword, 19 | UPGRADE_OS_PACKAGES, 20 | ...[ 21 | "bash-completion", 22 | "command-not-found", 23 | "dos2unix", 24 | "jq", 25 | "man-db", 26 | "manpages", 27 | "ncdu", 28 | "network-manager-openvpn-gnome", 29 | "qemu-guest-agent", 30 | "ssh", 31 | "tree", 32 | ].map(InstallOsPackage.of), 33 | bash, 34 | vim, 35 | starship, 36 | mdr, 37 | saveBashHistory, 38 | desktopIsHome, 39 | downloadsIsCleanedOnBoot, 40 | addHomeBinToPath, 41 | tmuxinatorByobuBashAliases, 42 | gnomeDisableWayland, 43 | ]); 44 | -------------------------------------------------------------------------------- /src/commands/gnome-disable-wayland.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { ROOT } from "../os/user/target-user.ts"; 4 | import { decodeToml, encodeToml } from "../deps.ts"; 5 | import { CreateFile } from "./common/file-commands.ts"; 6 | import { InstallOsPackage } from "./common/os-package.ts"; 7 | 8 | const CONFIG_FILE_PATH = FileSystemPath.of(ROOT, "/etc/gdm3/custom.conf"); 9 | type Toml = Record; 10 | 11 | async function getExistingToml(path: FileSystemPath): Promise { 12 | try { 13 | const text: string = await Deno.readTextFile(path.path); 14 | return decodeToml(text); 15 | } catch (e) { 16 | if (e.code === "ENOENT") { // NotFound 17 | return {}; 18 | } 19 | throw e; 20 | } 21 | } 22 | 23 | async function writeToml(path: FileSystemPath, toml: Toml): Promise { 24 | const text = encodeToml(toml, { whitespace: true }); 25 | await (new CreateFile(ROOT, path, text, false, 0o644)).run(); 26 | } 27 | 28 | export const gnomeDisableWayland = Command.custom() 29 | .withDependencies([InstallOsPackage.of("gdm3")]) 30 | .withLocks([CONFIG_FILE_PATH]) 31 | .withRun(async function () { 32 | const originalToml: Toml = await getExistingToml(CONFIG_FILE_PATH); 33 | const newToml: Toml = { 34 | ...originalToml, 35 | daemon: { 36 | ...originalToml.daemon, 37 | DefaultSession: "ubuntu-xorg.desktop", 38 | WaylandEnable: false, 39 | }, 40 | }; 41 | await writeToml(CONFIG_FILE_PATH, newToml); 42 | }); 43 | -------------------------------------------------------------------------------- /src/commands/toggle-terminal.ts: -------------------------------------------------------------------------------- 1 | import { wmUtils } from "./wm-utils.ts"; 2 | import { ROOT } from "../os/user/target-user.ts"; 3 | import { CreateFile, MODE_EXECUTABLE_775 } from "./common/file-commands.ts"; 4 | import { FileSystemPath } from "../model/dependency.ts"; 5 | import { readFromUrl } from "../os/read-from-url.ts"; 6 | import { InstallOsPackage } from "./common/os-package.ts"; 7 | import { dconfLoad } from "./common/dconf-load.ts"; 8 | import { Command } from "../model/command.ts"; 9 | import { deno } from "./deno.ts"; 10 | 11 | const installToggleTerminal = new CreateFile( 12 | ROOT, 13 | FileSystemPath.of(ROOT, "/usr/local/bin/toggle-terminal.ts"), 14 | await readFromUrl( 15 | "https://raw.githubusercontent.com/hugojosefson/toggle-terminal/main/toggle-terminal.ts", 16 | ), 17 | false, 18 | MODE_EXECUTABLE_775, 19 | ) 20 | .withDependencies([ 21 | wmUtils, 22 | deno, 23 | ...[ 24 | "coreutils", 25 | "xdotool", 26 | "curl", 27 | "unzip", 28 | ].map(InstallOsPackage.of), 29 | ]); 30 | 31 | const hotkey = dconfLoad( 32 | "/org/gnome/settings-daemon/plugins/media-keys/", 33 | ` 34 | [/] 35 | custom-keybindings=['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/'] 36 | 37 | [custom-keybindings/custom0] 38 | name='toggle-terminal' 39 | command='toggle-terminal.ts' 40 | binding='F1' 41 | `, 42 | ) 43 | .withDependencies([ 44 | InstallOsPackage.of("gnome-settings-daemon"), 45 | ]); 46 | 47 | export const toggleTerminal = Command.custom().withDependencies([ 48 | installToggleTerminal, 49 | hotkey, 50 | ]); 51 | -------------------------------------------------------------------------------- /src/commands/brave.ts: -------------------------------------------------------------------------------- 1 | import { Exec } from "./exec.ts"; 2 | import { InstallOsPackage } from "./common/os-package.ts"; 3 | import { ROOT } from "../os/user/target-user.ts"; 4 | import { FileSystemPath, OS_PACKAGE_SYSTEM } from "../model/dependency.ts"; 5 | import { CreateFile } from "./common/file-commands.ts"; 6 | import { readFromUrlBytes } from "../os/read-from-url.ts"; 7 | 8 | const keyringPath = "/usr/share/keyrings/brave-browser-archive-keyring.gpg"; 9 | const repoUrl = "https://brave-browser-apt-release.s3.brave.com"; 10 | 11 | const braveKeyring = new CreateFile( 12 | ROOT, 13 | FileSystemPath.of( 14 | ROOT, 15 | keyringPath, 16 | ), 17 | await readFromUrlBytes( 18 | `${repoUrl}/brave-browser-archive-keyring.gpg`, 19 | ), 20 | ); 21 | 22 | const braveAptSources = new CreateFile( 23 | ROOT, 24 | FileSystemPath.of(ROOT, "/etc/apt/sources.list.d/brave-browser-release.list"), 25 | `deb [signed-by=${keyringPath} arch=amd64] ${repoUrl}/ stable main`, 26 | ) 27 | .withDependencies([InstallOsPackage.of("apt-transport-https")]); 28 | 29 | const bravePin = new CreateFile( 30 | ROOT, 31 | FileSystemPath.of(ROOT, "/etc/apt/preferences.d/99brave-repository"), 32 | `# Allow upgrading only brave-browser, brave-keyring from brave repository 33 | Package: brave-browser brave-keyring 34 | Pin: release n=brave 35 | Pin-Priority: 500 36 | 37 | # Never prefer other packages from the brave repository 38 | Package: * 39 | Pin: release n=brave 40 | Pin-Priority: 1 41 | `, 42 | ); 43 | 44 | export const brave = InstallOsPackage.of("brave-browser") 45 | .withDependencies([ 46 | new Exec( 47 | [braveKeyring, braveAptSources, bravePin], 48 | [OS_PACKAGE_SYSTEM], 49 | ROOT, 50 | {}, 51 | ["apt", "update"], 52 | ), 53 | ]); 54 | -------------------------------------------------------------------------------- /src/commands/docker.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { ensureSuccessful, isSuccessful } from "../os/exec.ts"; 3 | import { ROOT, targetUser } from "../os/user/target-user.ts"; 4 | import { UserInGroup } from "./common/file-commands.ts"; 5 | import { InstallOsPackage } from "./common/os-package.ts"; 6 | import { Exec } from "./exec.ts"; 7 | import { OS_PACKAGE_SYSTEM } from "../model/dependency.ts"; 8 | import { isDocker } from "../deps.ts"; 9 | 10 | const installDocker = new Exec( 11 | [InstallOsPackage.of("curl")], 12 | [OS_PACKAGE_SYSTEM], 13 | ROOT, 14 | {}, 15 | ["bash", "-c", "curl -fsSL https://get.docker.com | sh"], 16 | ) 17 | .withSkipIfAll([ 18 | () => isSuccessful(ROOT, ["docker", "--version"]), 19 | ]); 20 | 21 | const startDocker = new Exec( 22 | [installDocker], 23 | [], 24 | ROOT, 25 | {}, 26 | ["systemctl", "start", "docker"], 27 | ) 28 | .withSkipIfAll([() => isSuccessful(ROOT, ["docker", "ps"])]); 29 | 30 | const addUserToDockerGroup = new UserInGroup(targetUser, "docker") 31 | .withSkipIfAll([ 32 | () => isSuccessful(targetUser, ["docker", "--version"]), 33 | () => isSuccessful(targetUser, ["docker", "ps"]), 34 | ]) 35 | .withDependencies([installDocker, startDocker]); 36 | 37 | const testDocker = Command 38 | .custom() 39 | .withDependencies([ 40 | installDocker, 41 | addUserToDockerGroup, 42 | startDocker, 43 | ]) 44 | .withRun(() => 45 | ensureSuccessful(targetUser, [ 46 | "docker", 47 | "run", 48 | "--rm", 49 | "hello-world", 50 | ]) 51 | ); 52 | 53 | export const docker = Command 54 | .custom() 55 | .withDependencies( 56 | await isDocker() 57 | ? [installDocker] 58 | : [installDocker, addUserToDockerGroup, testDocker], 59 | ); 60 | -------------------------------------------------------------------------------- /src/commands/tmuxinator_byobu_bash_aliases.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { bashAliases } from "./bash-aliases.ts"; 5 | import { CreateDir, CreateFile } from "./common/file-commands.ts"; 6 | import { InstallOsPackage, RemoveOsPackage } from "./common/os-package.ts"; 7 | import { 8 | tmuxinatorBaseYml, 9 | tmuxinatorMuxYml, 10 | tmuxinatorTempTemplate, 11 | } from "./files/tmuxinator-files.ts"; 12 | import { mTemp } from "./m-temp.ts"; 13 | import { toggleTerminal } from "./toggle-terminal.ts"; 14 | import { openMux } from "./open-mux.ts"; 15 | 16 | const files: Array<[string, string]> = [ 17 | ["base.yml", tmuxinatorBaseYml], 18 | ["temp.TEMPLATE", tmuxinatorTempTemplate], 19 | ["mux.yml", tmuxinatorMuxYml], 20 | ]; 21 | 22 | export const createTmuxinatorFiles: Array = files.map(( 23 | [filename, contents], 24 | ) => 25 | new CreateFile( 26 | targetUser, 27 | FileSystemPath.of(targetUser, `~/.tmuxinator/${filename}`), 28 | contents, 29 | true, 30 | ) 31 | ); 32 | 33 | const createCodeDir = new CreateDir( 34 | targetUser, 35 | FileSystemPath.of(targetUser, "~/code"), 36 | ); 37 | 38 | const installTmuxinator = RemoveOsPackage.of("screen") 39 | .withDependencies([ 40 | ...([ 41 | "byobu", 42 | "gnome-terminal", 43 | "tmux", 44 | "tmuxinator", 45 | "xsel", 46 | ].map(InstallOsPackage.of)), 47 | ]); 48 | 49 | export const tmuxinatorByobuBashAliases = Command.custom() 50 | .withDependencies([ 51 | toggleTerminal, 52 | installTmuxinator, 53 | ...createTmuxinatorFiles, 54 | createCodeDir, 55 | mTemp, 56 | openMux, 57 | bashAliases, 58 | ]); 59 | -------------------------------------------------------------------------------- /src/commands/firefox-local.ts: -------------------------------------------------------------------------------- 1 | import { ROOT, targetUser } from "../os/user/target-user.ts"; 2 | import { FileSystemPath } from "../model/dependency.ts"; 3 | import { CreateFile, Symlink } from "./common/file-commands.ts"; 4 | import { readFromUrlBytes } from "../os/read-from-url.ts"; 5 | import { Exec } from "./exec.ts"; 6 | import { RemoveSnapPackage } from "./common/os-package.ts"; 7 | import { isSuccessful } from "../os/exec.ts"; 8 | 9 | const installDir = FileSystemPath.of(targetUser, "~/.local/share/firefox"); 10 | const desktopDir = FileSystemPath.of(targetUser, "~/.local/share/applications"); 11 | const desktopUrl = 12 | "https://hg.mozilla.org/mozilla-central/raw-file/tip/browser/components/shell/search-provider-files/firefox.desktop"; 13 | 14 | const symlink = new Symlink( 15 | ROOT, 16 | installDir.slash("firefox"), 17 | FileSystemPath.of(ROOT, "/usr/local/bin/firefox"), 18 | ); 19 | 20 | const desktopFile = new CreateFile( 21 | targetUser, 22 | desktopDir.slash("firefox.desktop"), 23 | await readFromUrlBytes(desktopUrl), 24 | true, 25 | ); 26 | 27 | const downloadAndUnpack = new Exec( 28 | [], 29 | [installDir], 30 | targetUser, 31 | { 32 | cwd: installDir.slash(".."), 33 | }, 34 | [ 35 | "bash", 36 | "-c", 37 | "curl -fL 'https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US' | tar xj", 38 | ], 39 | ) 40 | .withSkipIfAny([ 41 | () => 42 | isSuccessful(targetUser, [installDir.slash("firefox").path, "--version"]), 43 | ]); 44 | 45 | const removeSnap = RemoveSnapPackage.of("firefox"); 46 | 47 | const updateDesktopDatabase = new Exec( 48 | [ 49 | removeSnap, 50 | downloadAndUnpack, 51 | desktopFile, 52 | symlink, 53 | ], 54 | [desktopDir], 55 | targetUser, 56 | {}, 57 | ["update-desktop-database", desktopDir.path], 58 | ); 59 | 60 | export const firefoxLocal = updateDesktopDatabase; 61 | -------------------------------------------------------------------------------- /src/commands/signal-desktop.ts: -------------------------------------------------------------------------------- 1 | import { ROOT } from "../os/user/target-user.ts"; 2 | import { InstallOsPackage } from "./common/os-package.ts"; 3 | import { Exec } from "./exec.ts"; 4 | import { FileSystemPath, OS_PACKAGE_SYSTEM } from "../model/dependency.ts"; 5 | import { CreateFile } from "./common/file-commands.ts"; 6 | import { readFromUrlBytes } from "../os/read-from-url.ts"; 7 | 8 | export const signalDesktop = InstallOsPackage.of("signal-desktop") 9 | .withDependencies([ 10 | new Exec( 11 | [ 12 | new CreateFile( 13 | ROOT, 14 | FileSystemPath.of(ROOT, "/etc/apt/sources.list.d/signal-xenial.list"), 15 | "deb [arch=amd64 signed-by=/usr/share/keyrings/signal-desktop-keyring.gpg] https://updates.signal.org/desktop/apt xenial main", 16 | ), 17 | new CreateFile( 18 | ROOT, 19 | FileSystemPath.of( 20 | ROOT, 21 | "/etc/apt/preferences.d/99signal-desktop-repository", 22 | ), 23 | ` 24 | # Allow upgrading only signal-desktop from signal-desktop repository 25 | Package: signal-desktop 26 | Pin: release n=signal-desktop 27 | Pin-Priority: 500 28 | 29 | # Never prefer other packages from the signal-desktop repository 30 | Package: * 31 | Pin: release n=signal-desktop 32 | Pin-Priority: 1 33 | 34 | `.trim(), 35 | ), 36 | 37 | new Exec( 38 | [], 39 | [FileSystemPath.of( 40 | ROOT, 41 | "/usr/share/keyrings/signal-desktop-keyring.gpg", 42 | )], 43 | ROOT, 44 | { 45 | stdin: await readFromUrlBytes( 46 | "https://updates.signal.org/desktop/apt/keys.asc", 47 | ), 48 | }, 49 | [ 50 | "bash", 51 | "-c", 52 | "gpg --dearmor > /usr/share/keyrings/signal-desktop-keyring.gpg", 53 | ], 54 | ), 55 | ], 56 | [OS_PACKAGE_SYSTEM], 57 | ROOT, 58 | {}, 59 | ["apt", "update"], 60 | ), 61 | ]); 62 | -------------------------------------------------------------------------------- /settings_diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | IFS=$'\t\n' 5 | 6 | script_dir="$(cd "$(dirname "${0}")" && pwd)" 7 | 8 | get_gsettings() { 9 | ( 10 | for schema in $(gsettings list-schemas); do 11 | gsettings list-recursively "${schema}" 12 | done 13 | ) | sort -u 14 | } 15 | 16 | get_dconf() { 17 | dconf dump / | deno run --quiet "${script_dir}/src/model/dconf-dump-to-lines.ts" 18 | } 19 | 20 | skip_first_n_lines() { 21 | local n 22 | n="${1}" 23 | tail -n +"$((n + 1))" 24 | } 25 | 26 | # output only lines starting with '+', removing the initial '+' 27 | only_new_lines() { 28 | awk '/^\+/ { print }' | sed 's/^+//' 29 | } 30 | 31 | simple_diff() { 32 | diff --unified="0" --minimal --suppress-common-lines --color="never" "${@}" | skip_first_n_lines 3 | only_new_lines || true 33 | } 34 | 35 | show_changes_loop() { 36 | local database 37 | local fn 38 | local previous 39 | local current 40 | local the_diff 41 | 42 | database="${1}" 43 | fn="get_${database}" 44 | 45 | current="$("${fn}")" 46 | echo "Make a config change, then press ENTER to show changes to ${database} (or Ctrl+C to exit) 47 | -----------------------------------------------------------------------------------------------" >&2 48 | while true; do 49 | previous="${current}" 50 | read -r 51 | current="$("${fn}")" 52 | the_diff="$(simple_diff <(echo "${previous}") <(echo "${current}"))" 53 | # if the_diff is empty or only contains whitespace, continue the loop 54 | if [[ -z "${the_diff}" ]] || [[ "${the_diff}" =~ ^[[:space:]]*$ ]]; then 55 | continue 56 | fi 57 | if [[ "${database}" = "dconf" ]]; then 58 | for dir in $(echo "${the_diff}" | awk '{print $1}' | sort -u); do 59 | dconf dump "/${dir}/" | sed -E "s|^\[/]|[${dir}]|g" 60 | done 61 | else 62 | echo "${the_diff}" 63 | fi 64 | done 65 | } 66 | 67 | DATABASE="${1:-""}" 68 | if [[ -z "${DATABASE}" ]]; then 69 | echo "Usage: ${0} " >&2 70 | exit 2 71 | fi 72 | 73 | show_changes_loop "${DATABASE}" 74 | -------------------------------------------------------------------------------- /src/commands/rust.ts: -------------------------------------------------------------------------------- 1 | import { targetUser } from "../os/user/target-user.ts"; 2 | import { 3 | AbstractPackageCommand, 4 | InstallOsPackage, 5 | RustPackageName, 6 | } from "./common/os-package.ts"; 7 | import { Exec } from "./exec.ts"; 8 | import { RunResult } from "../model/command.ts"; 9 | import { ensureSuccessful, isSuccessful } from "../os/exec.ts"; 10 | import { memoize } from "../deps.ts"; 11 | import { FileSystemPath } from "../model/dependency.ts"; 12 | 13 | export const rust = new Exec( 14 | [ 15 | InstallOsPackage.of("build-essential"), 16 | InstallOsPackage.of("cmake"), 17 | ], 18 | [], 19 | targetUser, 20 | {}, 21 | ["sh", "-c", "curl -fsSL https://sh.rustup.rs | sh -s -- -y"], 22 | ); 23 | 24 | export class InstallRustPackage 25 | extends AbstractPackageCommand { 26 | private constructor(packageName: RustPackageName) { 27 | super(packageName); 28 | this.locks.push(FileSystemPath.of(targetUser, "~/.cargo")); 29 | this.dependencies.push(rust); 30 | this.skipIfAll.push(() => isInstalledRustPackage(packageName)); 31 | } 32 | 33 | async run(): Promise { 34 | await ensureSuccessful(targetUser, [ 35 | "bash", 36 | "-c", 37 | `. <(grep cargo/ "${ 38 | FileSystemPath.of(targetUser, "~/.bashrc").path 39 | }") && cargo install --locked ${this.packageName}`, 40 | ]); 41 | 42 | return `Installed Rust package ${this.packageName}.`; 43 | } 44 | 45 | static of: (packageName: RustPackageName) => InstallRustPackage = memoize( 46 | (packageName: RustPackageName): InstallRustPackage => 47 | new InstallRustPackage(packageName), 48 | ); 49 | } 50 | function isInstalledRustPackage( 51 | packageName: RustPackageName, 52 | ): Promise { 53 | return isSuccessful(targetUser, [ 54 | "bash", 55 | "-c", 56 | `. <(grep cargo/ "${ 57 | FileSystemPath.of(targetUser, "~/.bashrc").path 58 | }") && command -v ${packageName}`, // not really accurate, but cargo install checks if it's installed too. 59 | ], { verbose: false }); 60 | } 61 | -------------------------------------------------------------------------------- /src/fn.ts: -------------------------------------------------------------------------------- 1 | export const complement = 2 | (fn: (t: T) => boolean): (t: T) => boolean => (t: T) => !fn(t); 3 | 4 | export const toObject = () => 5 | ( 6 | acc: Record, 7 | [key, value]: [K, V], 8 | ): Record => { 9 | acc[key] = value; 10 | return acc; 11 | }; 12 | 13 | export type SimpleValue = string | number | boolean; 14 | 15 | export async function filterAsync( 16 | predicate: (t: T) => Promise, 17 | array: T[], 18 | ): Promise { 19 | return (await Promise.all( 20 | array 21 | .map((t) => [t, predicate(t)] as [T, Promise]) 22 | .map( 23 | async ([t, shouldIncludePromise]) => 24 | [t, await shouldIncludePromise] as [T, boolean], 25 | ), 26 | )) 27 | .filter(([_t, shouldInclude]) => shouldInclude) 28 | .map(([t, _shouldInclude]) => t); 29 | } 30 | 31 | export type Getter = () => T | Promise; 32 | export type Ish = T | Promise | Getter; 33 | 34 | export async function resolveValue(x: Ish): Promise { 35 | if (typeof x === "function") { 36 | const x_ = x as Getter; 37 | return await resolveValue(await x_()); 38 | } 39 | return x; 40 | } 41 | 42 | export function startsAndEndsWith( 43 | start: string, 44 | end: string = start, 45 | ): (s: string) => boolean { 46 | return function (s: string): boolean { 47 | return s.startsWith(start) && s.endsWith(end); 48 | }; 49 | } 50 | 51 | export async function loopUntil( 52 | predicate: () => Promise, 53 | delayMs = 100, 54 | timeoutMs = 10_000, 55 | ): Promise { 56 | const expires = Date.now() + timeoutMs; 57 | while (true) { 58 | if (await predicate()) { 59 | break; 60 | } 61 | if (Date.now() > expires) { 62 | const predicateSourceCode: string = predicate.toString(); 63 | throw new Error(`Timeout from ${predicateSourceCode}`); 64 | } 65 | await sleep(delayMs); 66 | } 67 | return; 68 | } 69 | 70 | export function sleep(ms: number): Promise { 71 | return new Promise((resolve) => setTimeout(resolve, ms)); 72 | } 73 | -------------------------------------------------------------------------------- /src/commands/exec.ts: -------------------------------------------------------------------------------- 1 | import { PasswdEntry } from "../deps.ts"; 2 | import { Command, CommandResult, RunResult } from "../model/command.ts"; 3 | import { Lock } from "../model/dependency.ts"; 4 | import { ensureSuccessful, ExecOptions } from "../os/exec.ts"; 5 | import { Ish, resolveValue, SimpleValue } from "../fn.ts"; 6 | 7 | export class Exec extends Command { 8 | private readonly asUser: PasswdEntry; 9 | private readonly cmd: Ish; 10 | private readonly options?: ExecOptions; 11 | 12 | constructor( 13 | dependencies: Array, 14 | locks: Array, 15 | asUser: PasswdEntry, 16 | options: ExecOptions = {}, 17 | cmd: Ish, 18 | ) { 19 | super(); 20 | this.asUser = asUser; 21 | this.cmd = cmd; 22 | this.options = options; 23 | this.dependencies.push(...dependencies); 24 | this.locks.push(...locks); 25 | } 26 | 27 | async run(): Promise { 28 | return ensureSuccessful( 29 | this.asUser, 30 | await resolveValue(this.cmd), 31 | this.options, 32 | ); 33 | } 34 | 35 | static sequentialExec( 36 | asUser: PasswdEntry, 37 | options: ExecOptions, 38 | cmds: Array>, 39 | ): Command { 40 | return (new Command()) 41 | .withRun(async () => { 42 | const results: Array = []; 43 | for (const cmd of cmds) { 44 | results.push(await ensureSuccessful(asUser, cmd, options)); 45 | } 46 | 47 | const success: boolean = results 48 | .map((result) => result.status.success) 49 | .reduce((acc, curr) => acc && curr, true); 50 | 51 | const stdout: string = results 52 | .map(({ stdout }) => stdout.trim()) 53 | .filter((s) => s.length > 0) 54 | .join("\n"); 55 | 56 | const stderr: string = results 57 | .map(({ stderr }) => stderr.trim()) 58 | .filter((s) => s.length > 0) 59 | .join("\n"); 60 | 61 | const result: CommandResult = { 62 | stdout, 63 | stderr, 64 | status: success ? { success, code: 0 } : { success, code: 1 }, 65 | }; 66 | return result; 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/commands/node.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemPath } from "../model/dependency.ts"; 2 | import { isSuccessful } from "../os/exec.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { LineInFile } from "./common/file-commands.ts"; 5 | import { InstallOsPackage } from "./common/os-package.ts"; 6 | import { Exec } from "./exec.ts"; 7 | import { Command } from "../model/command.ts"; 8 | 9 | export const nvmBashRc = Command.sequential( 10 | ` 11 | export NVM_DIR="$HOME/.nvm" 12 | [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm 13 | [ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion" # This loads nvm bash_completion 14 | ` 15 | .split("\n").map((line) => line.trim()).filter((line) => line.length > 0) 16 | .map((line) => 17 | new LineInFile( 18 | targetUser, 19 | FileSystemPath.of(targetUser, "~/.bashrc"), 20 | line, 21 | ) 22 | ), 23 | ); 24 | 25 | export const nvm = new Exec( 26 | [ 27 | InstallOsPackage.of("curl"), 28 | InstallOsPackage.of("git"), 29 | nvmBashRc, 30 | ], 31 | [FileSystemPath.of(targetUser, "~/.nvm")], 32 | targetUser, 33 | {}, 34 | [ 35 | "sh", 36 | "-c", 37 | "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash", 38 | ], 39 | ) 40 | .withSkipIfAll([ 41 | () => 42 | isSuccessful(targetUser, [ 43 | "bash", 44 | "-c", 45 | `. <(grep nvm "${ 46 | FileSystemPath.of(targetUser, "~/.bashrc").path 47 | }") && command -v nvm`, 48 | ], {}), 49 | ]); 50 | 51 | export const node = new Exec( 52 | [nvm], 53 | [FileSystemPath.of(targetUser, "~/.nvm")], 54 | targetUser, 55 | {}, 56 | [ 57 | "bash", 58 | "-c", 59 | ` 60 | set -euo pipefail 61 | IFS=$'\n' 62 | . <(grep nvm "${FileSystemPath.of(targetUser, "~/.bashrc").path}") 63 | 64 | nvm install --lts --latest-npm 65 | nvm exec --lts npm install --location=global yarn@latest 66 | nvm install node --latest-npm 67 | nvm exec node npm install --location=global yarn@latest 68 | 69 | nvm use node 70 | nvm alias default node 71 | `, 72 | ], 73 | ) 74 | .withSkipIfAll([ 75 | () => 76 | isSuccessful(targetUser, [ 77 | "bash", 78 | "-c", 79 | `. <(grep nvm "${ 80 | FileSystemPath.of(targetUser, "~/.bashrc").path 81 | }") && command -v node`, 82 | ], {}), 83 | ]); 84 | -------------------------------------------------------------------------------- /src/commands/files/tmuxinator-files.ts: -------------------------------------------------------------------------------- 1 | import { stringifyYaml } from "../../deps.ts"; 2 | 3 | export const tmuxinatorBaseYml = stringifyYaml({ 4 | "name": "base", 5 | "tmux_command": "byobu-tmux", 6 | "windows": [ 7 | { 8 | "code": "cd ~/code && ll -laF", 9 | }, 10 | { 11 | "top": "top", 12 | }, 13 | { 14 | "": ` 15 | ( 16 | set -euo pipefail; IFS=$'\\t\\n'; 17 | 18 | hr() { 19 | printf '\\n---------------------------------------------------------------------\\n' 20 | }; 21 | 22 | _if() { 23 | local command 24 | command="\${1}" 25 | if [[ "\${command}" = "sudo" ]]; then 26 | command="\${2}" 27 | fi 28 | if command -v "\${command}" >/dev/null; then 29 | "\${@}" 30 | fi 31 | }; 32 | 33 | _if_hr() { 34 | local command 35 | command="\${1}" 36 | if [[ "\${command}" = "sudo" ]]; then 37 | command="\${2}" 38 | fi 39 | if command -v "\${command}" >/dev/null; then 40 | hr 41 | "\${@}" 42 | fi 43 | }; 44 | 45 | main() { 46 | clear 47 | _if_hr sudo apt update 48 | _if_hr sudo apt full-upgrade -y --purge --auto-remove 49 | _if_hr sudo snap refresh 50 | _if_hr sudo flatpak update -y 51 | if [[ -w "$(command -v deno)" ]]; then 52 | _if_hr deno upgrade 53 | elif sudo [ -w "$(command -v deno)" ]; then 54 | _if_hr sudo deno upgrade 55 | fi 56 | _if_hr nvm install --lts --latest-npm 57 | _if_hr nvm exec --lts npm install --location=global yarn@latest 58 | _if_hr nvm install node --latest-npm 59 | _if_hr nvm exec node npm install --location=global yarn@latest 60 | _if_hr brew upgrade 61 | _if_hr rustup update 62 | _if_hr cargo install --locked -- $(_if cargo install --list | awk '/^[^ ]/{print $1}') 63 | }; 64 | 65 | main 66 | )`.trim(), 67 | }, 68 | ], 69 | }); 70 | 71 | export const tmuxinatorTempTemplate = stringifyYaml({ 72 | "name": "${SESSION_NAME}", 73 | "root": "${SESSION_DIR}", 74 | "tmux_command": "byobu-tmux", 75 | "windows": [ 76 | { 77 | "": "", 78 | }, 79 | ], 80 | }); 81 | 82 | export const tmuxinatorMuxYml = stringifyYaml({ 83 | "name": "mux", 84 | "root": "~/.tmuxinator", 85 | "tmux_command": "byobu-tmux", 86 | "windows": [ 87 | { 88 | "mux": "ls -lF", 89 | }, 90 | { 91 | "bin": "cd ~/bin && ls -lFtr", 92 | }, 93 | { 94 | "dotfiles": "cd ~/dotfiles && ls -lF", 95 | }, 96 | { 97 | "mover": "byobu select-window -t :0; exit", 98 | }, 99 | ], 100 | }); 101 | -------------------------------------------------------------------------------- /src/model/dependency.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../config.ts"; 2 | import { memoize, normalizePath, PasswdEntry } from "../deps.ts"; 3 | import { defer, deferAlreadyResolvedVoid, Deferred } from "../os/defer.ts"; 4 | import { resolvePath } from "../os/resolve-path.ts"; 5 | import { ROOT } from "../os/user/target-user.ts"; 6 | 7 | export type LockReleaser = () => void; 8 | 9 | export class Lock { 10 | private currentLock: Deferred = deferAlreadyResolvedVoid(); 11 | 12 | async take(): Promise { 13 | const previousLock = this.currentLock.promise; 14 | this.currentLock = defer(); 15 | await previousLock; 16 | return this.currentLock.resolve; 17 | } 18 | } 19 | 20 | export function asStringPath(path: string | FileSystemPath): string { 21 | if (path instanceof FileSystemPath) { 22 | return path.path; 23 | } 24 | return path; 25 | } 26 | 27 | export class FileSystemPath extends Lock { 28 | readonly path: string; 29 | 30 | private constructor(path: string) { 31 | super(); 32 | this.path = path; 33 | } 34 | 35 | toString(): string { 36 | return `${this.constructor.name}(${this.path})`; 37 | } 38 | 39 | slash(relativePath: string): FileSystemPath { 40 | const resolvedPath: string = normalizePath(this.path + "/" + relativePath); 41 | return FileSystemPath.ofAbsolutePath(resolvedPath); 42 | } 43 | 44 | private static ofAbsolutePath(absolutePath: string): FileSystemPath { 45 | config.VERBOSE && console.warn( 46 | `ofAbsolutePath(absolutePath: ${JSON.stringify(absolutePath)})`, 47 | ); 48 | if (!absolutePath) { 49 | throw new Error( 50 | `ofAbsolutePath(absolutePath: ${ 51 | JSON.stringify(absolutePath) 52 | }): absolutePath is not.`, 53 | ); 54 | } 55 | const fileSystemPath = new FileSystemPath(absolutePath); 56 | config.VERBOSE && console.warn( 57 | `ofAbsolutePath(absolutePath: ${ 58 | JSON.stringify(absolutePath) 59 | }): fileSystemPath is: ${JSON.stringify(fileSystemPath)}`, 60 | ); 61 | return fileSystemPath; 62 | } 63 | 64 | private static ofAbsolutePathMemoized: (path: string) => FileSystemPath = 65 | memoize( 66 | FileSystemPath.ofAbsolutePath, 67 | ); 68 | 69 | static of(user: PasswdEntry, path: string): FileSystemPath { 70 | config.VERBOSE && console.warn( 71 | `of(user: ${JSON.stringify(user)}, path: ${JSON.stringify(path)})`, 72 | ); 73 | const resolvedPath: string = resolvePath(user, path); 74 | if (!resolvedPath) { 75 | throw new Error( 76 | `of(user: ${JSON.stringify(user)}, path: ${ 77 | JSON.stringify(path) 78 | }): resolvedPath is not.`, 79 | ); 80 | } 81 | config.VERBOSE && console.warn( 82 | `of(user: ${JSON.stringify(user)}, path: ${ 83 | JSON.stringify(path) 84 | }): resolvedPath is: ${resolvedPath}`, 85 | ); 86 | const fileSystemPath = FileSystemPath.ofAbsolutePathMemoized(resolvedPath); 87 | config.VERBOSE && console.warn( 88 | `of(user: ${JSON.stringify(user)}, path: ${ 89 | JSON.stringify(path) 90 | }): fileSystemPath is: ${fileSystemPath}`, 91 | ); 92 | return fileSystemPath; 93 | } 94 | } 95 | 96 | export const OS_PACKAGE_SYSTEM: Lock = FileSystemPath.of(ROOT, "/var/lib/apt"); 97 | export const FLATPAK: Lock = FileSystemPath.of(ROOT, "/var/lib/flatpak"); 98 | export const SNAP = FileSystemPath.of(ROOT, "/snap"); 99 | -------------------------------------------------------------------------------- /src/commands/files/.bash_aliases: -------------------------------------------------------------------------------- 1 | # vi:syntax=bash 2 | 3 | # .bash_aliases 4 | alias ba='vim ~/.bash_aliases' 5 | alias .ba='. ~/.bash_aliases' 6 | 7 | # ls 8 | alias ls='exa -F' 9 | alias l='exa -F' 10 | alias ll='exa -lF' 11 | alias la='exa -aF' 12 | alias lla='exa -Fla' 13 | 14 | # clear 15 | alias cc='clear && printf '\''\e[3J'\' 16 | 17 | # ssh TERM 18 | function ssh { 19 | if [[ "${TERM}" = alacritty ]]; then 20 | env TERM=xterm-256color /usr/bin/ssh "$@" 21 | else 22 | /usr/bin/ssh "$@" 23 | fi 24 | } 25 | 26 | # deno 27 | command -v deno > /dev/null && . <(deno completions bash) 28 | 29 | # node 30 | alias nn='nvm install $(cat package.json | jq -r .engines.node)' 31 | 32 | # tmuxinator 33 | alias mux=tmuxinator 34 | alias m=mux 35 | alias mm='mux mux' 36 | _tmuxinator() { 37 | COMPREPLY=() 38 | local word 39 | word="${COMP_WORDS[COMP_CWORD]}" 40 | 41 | if [ "$COMP_CWORD" -eq 1 ]; then 42 | local commands="$(compgen -W "$(tmuxinator commands)" -- "$word")" 43 | local projects="$(compgen -W "$(tmuxinator completions start)" -- "$word")" 44 | 45 | COMPREPLY=( $commands $projects ) 46 | elif [ "$COMP_CWORD" -eq 2 ]; then 47 | local words 48 | words=("${COMP_WORDS[@]}") 49 | unset words[0] 50 | unset words[$COMP_CWORD] 51 | local completions 52 | completions=$(tmuxinator completions "${words[@]}") 53 | COMPREPLY=( $(compgen -W "$completions" -- "$word") ) 54 | fi 55 | } 56 | complete -F _tmuxinator tmuxinator mux m 57 | 58 | # Git 59 | . ~/.git-completion 60 | command -v gh > /dev/null && . <(gh completion) 61 | 62 | __git_complete g __git_main 63 | function g() { 64 | local cmd=${1-status} 65 | shift 66 | git ${cmd} "$@" 67 | } 68 | 69 | __git_complete gf _git_fetch 70 | function gf() { 71 | git fetch --all --prune "$@" 72 | } 73 | 74 | __git_complete gc _git_commit 75 | function gc() { 76 | git commit "$@" 77 | } 78 | function gg() { 79 | git commit -m "$*" 80 | } 81 | function gam() { 82 | git commit --amend -m "$*" 83 | } 84 | 85 | __git_complete gd _git_diff 86 | function gd() { 87 | git diff "$@" 88 | } 89 | 90 | __git_complete ga _git_add 91 | function ga() { 92 | git add "${@:---all}" 93 | } 94 | 95 | __git_complete gp _git_push 96 | function gp() { 97 | git push "$@" 98 | } 99 | 100 | __git_complete gpl _git_pull 101 | function gpl() { 102 | git pull "$@" 103 | } 104 | 105 | __git_complete gr _git_rebase 106 | function gr() { 107 | git rebase "$@" 108 | } 109 | 110 | __git_complete rv _git_rebase 111 | function rv() { 112 | git revise "$@" 113 | } 114 | 115 | __git_complete gl _git_log 116 | function gl() { 117 | git log "$@" 118 | } 119 | 120 | # Docker 121 | function d() { 122 | local cmd=${1-ps} 123 | shift 124 | docker ${cmd} "$@" 125 | } 126 | 127 | # kubernetes / kubectl 128 | function k() { 129 | local cmd=${1-get services,pods -A} 130 | shift 131 | kubectl ${cmd} "$@" 132 | } 133 | 134 | # npm completion, takes about 1/4 second extra when starting every shell 135 | # which npm >/dev/null 2>&1 && . <(npm completion) 136 | # . ~/bin/npm-completion 137 | 138 | # pbcopy / pbpaste 139 | alias pbcopy='xsel --clipboard --input' 140 | alias pbpaste='xsel --clipboard --output' 141 | #alias pbcopy='wl-copy' 142 | #alias pbpaste='wl-paste' 143 | 144 | # directory 145 | function mkcd () { 146 | mkdir -p -- "$1" && cd -- "$1" 147 | } 148 | 149 | # temp directory 150 | alias t='cd $(mktemp -d)' 151 | 152 | # pfFocus 153 | alias pffocus="docker run --rm -i hugojosefson/pffocus" 154 | -------------------------------------------------------------------------------- /src/deps.ts: -------------------------------------------------------------------------------- 1 | // Compare old dependency with latest: 2 | // url=https://deno.land/std@0.95.0/path/posix.ts; new_url="$(echo "${url}"| sed -E "s/@[^/?]*//")"; meld <(echo "${url}"; curl -sfL "${url}") <(echo "${new_url}"; curl -sfL "${new_url}") 3 | 4 | export { 5 | dirname, 6 | normalize as normalizePath, 7 | } from "https://deno.land/std@0.150.0/path/mod.ts"; 8 | export { equals as equalsBytes } from "https://deno.land/std@0.150.0/bytes/equals.ts"; 9 | export { readAll } from "https://deno.land/std@0.150.0/streams/conversion.ts"; 10 | 11 | import { 12 | error, 13 | success, 14 | warning, 15 | } from "https://deno.land/x/colorlog@v1.0/mod.ts"; 16 | 17 | // colorlog uses any 18 | // deno-lint-ignore no-explicit-any 19 | type LogColorer = (val: any) => string; 20 | export const colorlog: { 21 | error: LogColorer; 22 | success: LogColorer; 23 | warning: LogColorer; 24 | } = { error, success, warning }; 25 | 26 | export interface PasswdEntry { 27 | username: string; 28 | uid: number; 29 | gid: number; 30 | homedir: string; 31 | } 32 | interface PasswdEntry_ { 33 | // parse-passwd has no typings 34 | // deno-lint-ignore no-explicit-any 35 | username: any; 36 | // parse-passwd has no typings 37 | // deno-lint-ignore no-explicit-any 38 | homedir: any; 39 | // parse-passwd has no typings 40 | // deno-lint-ignore no-explicit-any 41 | uid: any; 42 | // parse-passwd has no typings 43 | // deno-lint-ignore no-explicit-any 44 | gid: any; 45 | } 46 | 47 | import parsePasswd_ from "https://cdn.skypack.dev/pin/parse-passwd@v1.0.0-c1feX7BOq3S3SMESPpB1/mode=imports/optimized/parse-passwd.js?dts"; 48 | export const parsePasswd = (content: string): Array => { 49 | const parsed = parsePasswd_(content) as Array; 50 | return parsed 51 | .map( 52 | ( 53 | { username, uid, gid, homedir }: PasswdEntry_, 54 | ) => ({ 55 | username: username as string, 56 | homedir: homedir as string, 57 | uid: parseInt(uid as string, 10), 58 | gid: parseInt(gid as string, 10), 59 | }), 60 | ); 61 | }; 62 | 63 | import { stringify as stringifyYaml_ } from "https://cdn.skypack.dev/pin/yaml@v2.1.1-940wF4nVcO1JvartcxSp/mode=imports/optimized/yaml.js?dts"; 64 | // yaml can stringify any-thing ;) 65 | // deno-lint-ignore no-explicit-any 66 | export const stringifyYaml = (value: any): string => 67 | stringifyYaml_(value, undefined, undefined) || ""; 68 | 69 | import { fetch as fetchFile } from "https://deno.land/x/file_fetch@0.2.0/mod.ts"; 70 | export { fetchFile }; 71 | 72 | import memoize from "https://deno.land/x/memoizy@1.0.0/mod.ts"; 73 | export { memoize }; 74 | 75 | export { isDocker } from "https://deno.land/x/is_docker@v2.0.0/mod.ts"; 76 | 77 | import toposort_ from "https://cdn.skypack.dev/toposort@v2.0.2?dts"; 78 | export const toposort = (things: Array<[T, T]>): Array => 79 | toposort_(things); 80 | 81 | import { 82 | compose, 83 | composeUnary, 84 | pipe, 85 | pipeline, 86 | pipelineUnary, 87 | } from "https://deno.land/x/compose@1.3.2/index.js"; 88 | export { compose, composeUnary, pipe, pipeline, pipelineUnary }; 89 | 90 | import { paramCase } from "https://deno.land/x/case@2.1.1/mod.ts"; 91 | export function kebabCase(s: string): string { 92 | const kebab: string = paramCase(s); 93 | 94 | return kebab 95 | .replace(/([a-z])([0-9])/, "$1-$2") // Insert '-' between 'all' and the number. 96 | .replace(/\bv-4l/, "v4l"); // fix v4l (video for linux) 97 | } 98 | 99 | export { 100 | decode as decodeToml, 101 | encode as encodeToml, 102 | } from "https://deno.land/x/ini@v2.1.0/ini.ts"; 103 | -------------------------------------------------------------------------------- /src/run.ts: -------------------------------------------------------------------------------- 1 | import { NOOP } from "./commands/common/noop.ts"; 2 | import { config } from "./config.ts"; 3 | import { compose, toposort } from "./deps.ts"; 4 | import { Command, CommandResult } from "./model/command.ts"; 5 | import { Lock } from "./model/dependency.ts"; 6 | 7 | function getDependencyPairs(command: Command): Array<[Command, Command]> { 8 | if (command.dependencies.length === 0) { 9 | return [[NOOP(), command]]; 10 | } 11 | const thisCommandDependsOnItsDependencies: Array<[Command, Command]> = command 12 | .dependencies 13 | .map(( 14 | dep, 15 | ) => [dep, command]); 16 | 17 | const dependenciesDependOnTheirDependencies: Array<[Command, Command]> = 18 | command.dependencies.flatMap((dep) => getDependencyPairs(dep)); 19 | 20 | return [ 21 | ...thisCommandDependsOnItsDependencies, 22 | ...dependenciesDependOnTheirDependencies, 23 | ]; 24 | } 25 | 26 | type CommandForLog = { 27 | dependencies?: CommandForLog[]; 28 | locks?: Lock[]; 29 | }; 30 | 31 | const forLog = (depth: number) => (command: Command): CommandForLog => { 32 | const { dependencies, locks } = command; 33 | return (depth > 0) 34 | ? { 35 | dependencies: dependencies.map(forLog(depth - 1)), 36 | locks, 37 | } 38 | : {}; 39 | }; 40 | 41 | // deno-lint-ignore no-explicit-any 42 | const stringify = (o?: any): string => JSON.stringify(o, null, 2); 43 | // deno-lint-ignore no-explicit-any 44 | const stringifyLine = (o: any): string => JSON.stringify(o); 45 | 46 | export const sortCommands = (commands: Command[]): Command[] => { 47 | const dependencyPairs: [Command, Command][] = commands.flatMap( 48 | getDependencyPairs, 49 | ); 50 | 51 | const commandsInOrder: Command[] = toposort(dependencyPairs); 52 | 53 | config.VERBOSE && console.error( 54 | "=====================================================================================\n\ncommands:\n" + 55 | commands.map(compose(stringify, forLog(1))).join("\n") + 56 | "\n\n", 57 | ); 58 | 59 | config.VERBOSE && console.error( 60 | "=====================================================================================\n\dependencyPairs:\n" + 61 | dependencyPairs.map((pair) => pair.map(compose(stringifyLine, forLog(0)))) 62 | .join("\n") + 63 | "\n\n", 64 | ); 65 | 66 | config.VERBOSE && console.error( 67 | "=====================================================================================\n\commandsInOrder:\n" + 68 | commandsInOrder.map( 69 | compose( 70 | stringifyLine, 71 | (c: CommandForLog) => { 72 | delete c.dependencies; 73 | return c; 74 | }, 75 | forLog(1), 76 | ), 77 | ).join("\n") + 78 | "\n\n", 79 | ); 80 | return commandsInOrder; 81 | }; 82 | 83 | export async function run(commands: Command[]): Promise { 84 | const sortedCommands: Command[] = sortCommands(commands); 85 | 86 | const commandResults: CommandResult[] = []; 87 | for (const command of sortedCommands) { 88 | if (command.doneDeferred.isDone) { 89 | console.log(`Already done, so not enqueueing: ${command.toString()}\n`); 90 | } else if (await command.shouldSkip()) { 91 | console.log(`Should skip, so not enqueueing: ${command.toString()}\n`); 92 | } else { 93 | console.log(`\nWill enqueue: ${command.toString()}\n`); 94 | if (!config.NON_INTERACTIVE && prompt("OK to continue?", "y") === "n") { 95 | console.log("You did not answer 'y'. Quitting."); 96 | Deno.exit(0); 97 | } 98 | commandResults.push(await command.runWhenDependenciesAreDone()); 99 | } 100 | } 101 | 102 | return commandResults; 103 | } 104 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | // 2>/dev/null;DENO_VERSION_RANGE="^1.24";DENO_RUN_ARGS="--unstable --allow-all";: "Via https://github.com/hugojosefson/deno-shebang CC BY 4.0";set -e;(command -v sudo>/dev/null||(apt-get update -qq && apt-get install -yq sudo));(command -v unzip>/dev/null&&command -v curl>/dev/null||(sudo apt-get update -qq && sudo apt-get install -yqq curl unzip));V="$DENO_VERSION_RANGE";A="$DENO_RUN_ARGS";E="$(expr "$(echo "$V"|curl -Gso/dev/null -w%{url_effective} --data-urlencode @- "")" : '..\(.*\)...')";D="$(command -v deno||true)";t(){ d="$(mktemp)";rm "${d}";dirname "${d}";};f(){ m="$(command -v "$0"||true)";l="/* 2>/dev/null";! [ -z $m ]&&[ -r $m ]&&[ "$(head -c3 "$m")" = '#!/' ]&&(read x && read y &&[ "$x" = "#!/bin/sh" ]&&[ "$l" != "${y%"$l"*}" ])<"$m";};a(){ [ -n $D ];};s(){ a&&[ -x "$R/deno" ]&&[ "$R/deno" = "$D" ]&&return;deno eval "import{satisfies as e}from'https://deno.land/x/semver@v1.3.0/mod.ts';Deno.exit(e(Deno.version.deno,'$V')?0:1);">/dev/null 2>&1;};g(){ curl -sSfL "https://semver-version.deno.dev/api/github/denoland/deno/$U";};e(){ R="$(t)/deno-range-$V/bin";mkdir -p "$R";export PATH="$R:$PATH";[ -x "$R/deno" ]&&return;a&&s&&([ -L "$R/deno" ]||ln -s "$D" "$R/deno")&&return;v="$(g)";i="$(t)/deno-$v";[ -L "$R/deno" ]||ln -s "$i/bin/deno" "$R/deno";s && return;echo -n "Downloading temporary deno...">&2;curl -fsSL https://deno.land/x/install/install.sh|DENO_INSTALL="$i" sh -s "$v" 2>/dev/null >&2;};e;f&&exec deno run $A "$0" "$@";r="$(t)/cli.ts";cat > "$r" <<'//🔚' 3 | 4 | import { getCommand } from "./commands/index.ts"; 5 | import { config } from "./config.ts"; 6 | import { colorlog } from "./deps.ts"; 7 | 8 | import { Command, CommandResult } from "./model/command.ts"; 9 | import { RejectFn } from "./os/defer.ts"; 10 | import { isRunningAsRoot } from "./os/user/is-running-as-root.ts"; 11 | import { run } from "./run.ts"; 12 | import { errorAndExit, usageAndExit } from "./usage.ts"; 13 | 14 | export const cli = async () => { 15 | if (!await isRunningAsRoot()) { 16 | await errorAndExit( 17 | 3, 18 | "You must run this program as root. Try again with sudo :)", 19 | ); 20 | } 21 | 22 | const args: string[] = Deno.args; 23 | if (!args.length) { 24 | await usageAndExit(); 25 | } 26 | 27 | const commands: Command[] = await Promise.all(args.map(getCommand)); 28 | const runCommandsPromise = run(commands); 29 | try { 30 | await runCommandsPromise.then( 31 | (results: Array) => { 32 | results.forEach((result) => { 33 | if (result.stdout) console.error(colorlog.success(result.stdout)); 34 | if (result.stderr) console.error(colorlog.error(result.stderr)); 35 | if (!(result?.status?.success)) { 36 | console.error(JSON.stringify(result.status)); 37 | } 38 | }); 39 | const anyError: CommandResult | undefined = results.find((result) => 40 | (!result.status.success) || 41 | (result.status.code > 0) 42 | ); 43 | if (anyError) { 44 | const err: CommandResult = anyError; 45 | Deno.exit(err.status.code); 46 | } 47 | }, 48 | // Because Promise defines it as ?any: 49 | // deno-lint-ignore no-explicit-any 50 | (err?: any): RejectFn => { 51 | if (config.VERBOSE) { 52 | if (err?.message) { 53 | console.error("err.message: " + colorlog.error(err.message)); 54 | } 55 | if (err?.stack) { 56 | console.error("err.stack: " + colorlog.warning(err.stack)); 57 | } 58 | if (err?.stdout) { 59 | console.error("err.stdout: " + colorlog.success(err.stdout)); 60 | } 61 | if (err?.stderr) { 62 | console.error("err.stderr: " + colorlog.error(err.stderr)); 63 | } 64 | 65 | console.error("err: " + colorlog.error(JSON.stringify(err, null, 2))); 66 | } 67 | const code: number = err?.status?.code || err?.code || 1; 68 | Deno.exit(code); 69 | }, 70 | ); 71 | } finally { 72 | await runCommandsPromise.finally(() => { 73 | if (config.VERBOSE) { 74 | console.error("finally"); 75 | } 76 | Deno.exit(0); 77 | }); 78 | } 79 | }; 80 | 81 | if (import.meta.main) { 82 | await cli(); 83 | } 84 | 85 | //🔚 86 | // 2>/dev/null || :; sed -E 's#from "\.#from "https://raw.githubusercontent.com/hugojosefson/ubuntu-install-scripts/ubuntu-22.04/src#g' -i "$r";exec deno run $A "$r" "$@" 87 | -------------------------------------------------------------------------------- /src/os/exec.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../config.ts"; 2 | import { colorlog, PasswdEntry } from "../deps.ts"; 3 | import { CommandResult } from "../model/command.ts"; 4 | import { asStringPath, FileSystemPath } from "../model/dependency.ts"; 5 | import { DBUS_SESSION_BUS_ADDRESS, ROOT } from "./user/target-user.ts"; 6 | import { Ish, resolveValue, SimpleValue } from "../fn.ts"; 7 | 8 | export type ExecOptions = Pick & { 9 | verbose?: boolean; 10 | cwd?: string | FileSystemPath; 11 | stdin?: string | Uint8Array; 12 | }; 13 | 14 | export const pipeAndCollect = async ( 15 | from: (Deno.Reader & Deno.Closer) | null | undefined, 16 | to?: (Deno.Writer & Deno.Closer) | null | false, 17 | verbose?: boolean, 18 | ): Promise => { 19 | if (!from) throw new Error("Nothing to pipe from!"); 20 | 21 | const isVerbose: boolean = typeof verbose === "boolean" 22 | ? verbose 23 | : config.VERBOSE; 24 | 25 | const buf: Uint8Array = new Uint8Array(1024); 26 | let all: Uint8Array = Uint8Array.from([]); 27 | for ( 28 | let n: number | null = 0; 29 | typeof n === "number"; 30 | n = await from.read(buf) 31 | ) { 32 | if (n > 0) { 33 | const bytes: Uint8Array = buf.subarray(0, n); 34 | all = Uint8Array.from([...all, ...bytes]); 35 | if (isVerbose && to) { 36 | await to.write(bytes); 37 | } 38 | } 39 | } 40 | from?.close(); 41 | return new TextDecoder().decode(all); 42 | }; 43 | 44 | function runOptions( 45 | asUser: PasswdEntry, 46 | opts: ExecOptions, 47 | ): Pick { 48 | return { 49 | ...(opts.cwd ? { cwd: asStringPath(opts.cwd) } : {}), 50 | ...(asUser === ROOT 51 | ? { 52 | env: { 53 | DEBIAN_FRONTEND: "noninteractive", 54 | ...opts.env, 55 | }, 56 | } 57 | : { 58 | env: { 59 | DEBIAN_FRONTEND: "noninteractive", 60 | DBUS_SESSION_BUS_ADDRESS, 61 | ...opts.env, 62 | }, 63 | }), 64 | }; 65 | } 66 | 67 | export const ensureSuccessful = async ( 68 | asUser: PasswdEntry, 69 | cmd: Ish, 70 | options: ExecOptions = {}, 71 | ): Promise => { 72 | const effectiveCmd = [ 73 | ...(asUser === ROOT ? [] : [ 74 | "sudo", 75 | `--preserve-env=DBUS_SESSION_BUS_ADDRESS,DEBIAN_FRONTEND,XAUTHORITY,DISPLAY`, 76 | `--user=${asUser.username}`, 77 | "--non-interactive", 78 | "--", 79 | ]), 80 | ...(await resolveValue(cmd)), 81 | ]; 82 | config.VERBOSE && console.error( 83 | colorlog.warning( 84 | JSON.stringify({ options, user: asUser.username, cmd, effectiveCmd }), 85 | ), 86 | ); 87 | const stdinString = typeof options.stdin === "string" ? options.stdin : ""; 88 | const shouldPipeStdin: boolean = stdinString.length > 0 || 89 | options.stdin instanceof Uint8Array; 90 | 91 | const process: Deno.Process = Deno.run({ 92 | stdin: shouldPipeStdin ? "piped" : "null", 93 | stdout: "piped", 94 | stderr: "piped", 95 | cmd: effectiveCmd.map((x) => `${x}`), 96 | ...runOptions(asUser, options), 97 | }); 98 | 99 | if (shouldPipeStdin) { 100 | const stdinBytes = options.stdin instanceof Uint8Array 101 | ? options.stdin 102 | : new TextEncoder().encode(stdinString); 103 | try { 104 | await process.stdin?.write(stdinBytes); 105 | } finally { 106 | await process.stdin?.close(); 107 | } 108 | } 109 | 110 | const stdoutPromise = pipeAndCollect( 111 | process.stdout, 112 | Deno.stdout, 113 | options.verbose, 114 | ); 115 | const stderrPromise = pipeAndCollect( 116 | process.stderr, 117 | Deno.stderr, 118 | options.verbose, 119 | ); 120 | try { 121 | const status: Deno.ProcessStatus = await process.status(); 122 | if (status.success) { 123 | return { 124 | status, 125 | stdout: await stdoutPromise, 126 | stderr: await stderrPromise, 127 | }; 128 | } 129 | } catch (_e) { 130 | // ignore 131 | } 132 | 133 | return Promise.reject({ 134 | status: await process.status(), 135 | stdout: await stdoutPromise, 136 | stderr: await stderrPromise, 137 | }); 138 | }; 139 | 140 | export const symlink = ( 141 | owner: PasswdEntry, 142 | from: string, 143 | to: FileSystemPath, 144 | ): Promise => 145 | ensureSuccessful(owner, [ 146 | "ln", 147 | "-sf", 148 | from, 149 | to.path, 150 | ]); 151 | 152 | export const ensureSuccessfulStdOut = async ( 153 | asUser: PasswdEntry, 154 | cmd: Ish, 155 | options: ExecOptions = {}, 156 | ): Promise => 157 | (await ensureSuccessful(asUser, cmd, options)).stdout.trim(); 158 | 159 | export const isSuccessful = ( 160 | asUser: PasswdEntry, 161 | cmd: Ish, 162 | options: ExecOptions = {}, 163 | ): Promise => 164 | ensureSuccessful(asUser, cmd, options).then( 165 | () => Promise.resolve(true), 166 | () => Promise.resolve(false), 167 | ); 168 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import { isDocker, kebabCase } from "../deps.ts"; 2 | import { toObject } from "../fn.ts"; 3 | import { Command } from "../model/command.ts"; 4 | import { addHomeBinToPath } from "./add-home-bin-to-path.ts"; 5 | import { addNodeModulesBinToPath } from "./add-node_modules-bin-to-path.ts"; 6 | import { all1MinimalSanity } from "./all-1-minimal-sanity.ts"; 7 | import { all2DeveloperBase } from "./all-2-developer-base.ts"; 8 | import { all3DeveloperWeb } from "./all-3-developer-web.ts"; 9 | import { all4DeveloperJava } from "./all-4-developer-java.ts"; 10 | import { all5Personal } from "./all-5-personal.ts"; 11 | import { all6Gaming } from "./all-6-gaming.ts"; 12 | import { android } from "./android.ts"; 13 | import { bashAliases } from "./bash-aliases.ts"; 14 | import { bashGitPrompt } from "./bash-git-prompt.ts"; 15 | import { bash } from "./bash.ts"; 16 | import { brew, flatpak, InstallOsPackage, snap } from "./common/os-package.ts"; 17 | import { deno } from "./deno.ts"; 18 | import { desktopIsHome } from "./desktop-is-home.ts"; 19 | import { eog } from "./eog.ts"; 20 | import { git } from "./git.ts"; 21 | import { gnomeCustomKeybindingsBackup } from "./gnome-custom-keybindings-backup.ts"; 22 | import { gnomeShellExtensionInstaller } from "./gnome-shell-extension-installer.ts"; 23 | import { 24 | gsettingsAll, 25 | gsettingsDisableSomeKeyboardShortcuts, 26 | gsettingsEnableSomeKeyboardShortcuts, 27 | gsettingsLocalisation, 28 | gsettingsLookAndFeel, 29 | gsettingsPrivacy, 30 | gsettingsUsefulDefaults, 31 | gsettingsWindows, 32 | } from "./gsettings.ts"; 33 | import { docker } from "./docker.ts"; 34 | import { downloadsIsTmp } from "./downloads-is-tmp.ts"; 35 | import { fzf } from "./fzf.ts"; 36 | import { gedit } from "./gedit.ts"; 37 | import { gitk } from "./gitk.ts"; 38 | import { gnomeShellExtensions } from "./gnome-shell-extensions.ts"; 39 | import { idea } from "./idea.ts"; 40 | import { insync } from "./insync.ts"; 41 | import { isolateInDocker, isolateInDockerAll } from "./isolate-in-docker.ts"; 42 | import { java, sdkmanJava } from "./java.ts"; 43 | import { keybase } from "./keybase.ts"; 44 | import { mTemp } from "./m-temp.ts"; 45 | import { meld } from "./meld.ts"; 46 | import { minecraft } from "./minecraft.ts"; 47 | import { networkUtils } from "./network-utils.ts"; 48 | import { rust } from "./rust.ts"; 49 | import { saveBashHistory } from "./save-bash-history.ts"; 50 | import { signalDesktopViaDocker } from "./signal-desktop-via-docker.ts"; 51 | import { starship } from "./starship.ts"; 52 | import { toggleTerminal } from "./toggle-terminal.ts"; 53 | import { 54 | createTmuxinatorFiles, 55 | tmuxinatorByobuBashAliases, 56 | } from "./tmuxinator_byobu_bash_aliases.ts"; 57 | import { vim } from "./vim.ts"; 58 | import { virtualbox } from "./virtualbox.ts"; 59 | import { yubikey } from "./yubikey.ts"; 60 | import { UPGRADE_OS_PACKAGES } from "./refresh-os-packages.ts"; 61 | import { alacritty } from "./alacritty.ts"; 62 | import { node, nvm } from "./node.ts"; 63 | import { openMux } from "./open-mux.ts"; 64 | import { gnomeDisableWayland } from "./gnome-disable-wayland.ts"; 65 | import { downloadsIsCleanedOnBoot } from "./downloads-is-cleaned-on-boot.ts"; 66 | import { sudoNoPassword } from "./sudo-no-password.ts"; 67 | import { chrome } from "./chrome.ts"; 68 | import { brave } from "./brave.ts"; 69 | import { webstorm } from "./webstorm.ts"; 70 | import { signalDesktop } from "./signal-desktop.ts"; 71 | import { pass } from "./pass.ts"; 72 | import { mdr } from "./mdr.ts"; 73 | import { dmenu } from "./dmenu.ts"; 74 | import { firefoxLocal } from "./firefox-local.ts"; 75 | 76 | const commands: Record = { 77 | alacritty, 78 | all: Command.custom().withDependencies([ 79 | all3DeveloperWeb, 80 | all4DeveloperJava, 81 | all5Personal, 82 | all6Gaming, 83 | ]), 84 | all1MinimalSanity, 85 | all2DeveloperBase, 86 | all3DeveloperWeb, 87 | all4DeveloperJava, 88 | all5Personal, 89 | all6Gaming, 90 | addHomeBinToPath, 91 | addNodeModulesBinToPath, 92 | android, 93 | bash, 94 | bashAliases, 95 | bashGitPrompt, 96 | brave, 97 | brew, 98 | chrome, 99 | deno, 100 | desktopIsHome, 101 | dmenu, 102 | docker, 103 | downloadsIsCleanedOnBoot, 104 | downloadsIsTmp, 105 | eog, 106 | firefoxLocal, 107 | flatpak, 108 | fzf: await fzf(), 109 | gedit, 110 | git, 111 | gitk, 112 | gnomeCustomKeybindingsBackup, 113 | gnomeDisableWayland, 114 | gnomeShellExtensions, 115 | gnomeShellExtensionInstaller, 116 | gsettingsAll, 117 | gsettingsDisableSomeKeyboardShortcuts, 118 | gsettingsEnableSomeKeyboardShortcuts, 119 | gsettingsLocalisation, 120 | gsettingsLocalization: gsettingsLocalisation, 121 | gsettingsLookAndFeel, 122 | gsettingsPrivacy, 123 | gsettingsUsefulDefaults, 124 | gsettingsWindows, 125 | idea, 126 | insync, 127 | isInsideDocker: Command.custom().withRun(async () => 128 | await isDocker() 129 | ? "✓ You are in a docker container." 130 | : "✗ You are NOT in a docker container." 131 | ), 132 | isolateInDocker, 133 | isolateInDockerAll, 134 | java, 135 | keybase, 136 | libreoffice: Command.custom().withDependencies( 137 | [ 138 | "hunspell-en-gb", 139 | "hunspell-sv", 140 | "hyphen-en-gb", 141 | "hyphen-sv", 142 | "libreoffice", 143 | "libreoffice-help-en-gb", 144 | "libreoffice-l10n-en-gb", 145 | "libreoffice-l10n-sv", 146 | "libreoffice-lightproof-en", 147 | "mythes-en-us", 148 | "mythes-sv", 149 | ].map(InstallOsPackage.of), 150 | ), 151 | mTemp, 152 | mdr, 153 | meld, 154 | minecraft, 155 | networkUtils, 156 | nullCommand: Command.custom(), 157 | nvm, 158 | node, 159 | openMux, 160 | pass, 161 | rust, 162 | saveBashHistory, 163 | sdkmanJava, 164 | signalDesktop, 165 | signalDesktopViaDocker, 166 | snap, 167 | starship, 168 | sudoNoPassword, 169 | toggleTerminal, 170 | tmuxinatorByobuBashAliases, 171 | tmuxinatorFiles: Command.custom().withDependencies(createTmuxinatorFiles), 172 | upgradeOsPackages: UPGRADE_OS_PACKAGES, 173 | vim, 174 | virtualbox, 175 | webstorm, 176 | yubikey, 177 | }; 178 | 179 | const kebabCommands: Record = Object.entries(commands) 180 | .map(([key, value]) => [kebabCase(key), value] as [string, Command]) 181 | .reduce( 182 | toObject(), 183 | {}, 184 | ); 185 | 186 | export const getCommand = (name: string): Command => 187 | kebabCommands[name] || InstallOsPackage.of(name); 188 | 189 | export const availableCommands: Array = Object.keys(kebabCommands) 190 | .sort(); 191 | -------------------------------------------------------------------------------- /src/model/command.ts: -------------------------------------------------------------------------------- 1 | import { NOOP } from "../commands/common/noop.ts"; 2 | import { config } from "../config.ts"; 3 | import { Ish, resolveValue } from "../fn.ts"; 4 | import { defer, Deferred } from "../os/defer.ts"; 5 | import { run } from "../run.ts"; 6 | import { Lock } from "./dependency.ts"; 7 | 8 | export interface CommandResult { 9 | status: Deno.ProcessStatus; 10 | stdout: string; 11 | stderr: string; 12 | } 13 | 14 | export class Command { 15 | readonly dependencies: Array = new Array(0); 16 | readonly locks: Array = new Array(0); 17 | readonly skipIfAll: Array = new Array(0); 18 | readonly skipIfAny: Array = new Array(0); 19 | readonly doneDeferred: Deferred = defer(); 20 | readonly done: Promise = this.doneDeferred.promise; 21 | 22 | toString(): string { 23 | return this.constructor.name; 24 | } 25 | 26 | async runWhenDependenciesAreDone(): Promise { 27 | config.VERBOSE && console.error(`Running command `, this); 28 | if (this.doneDeferred.isDone) { 29 | return this.done; 30 | } 31 | 32 | const dependenciesDone = this.dependencies.map(({ done }) => done); 33 | const lockReleaserPromises = this.locks.map((lock) => lock.take()); 34 | await Promise.all(dependenciesDone); 35 | 36 | const lockReleasers = await Promise.all(lockReleaserPromises); 37 | try { 38 | const innerResult: RunResult = await (this.run().catch( 39 | this.doneDeferred.reject, 40 | )); 41 | config.VERBOSE && console.error(`Running command `, this, "DONE."); 42 | return this.resolve(innerResult); 43 | } finally { 44 | lockReleasers.forEach((releaseLock) => releaseLock()); 45 | } 46 | } 47 | 48 | static custom(): Command { 49 | return (new Command()); 50 | } 51 | 52 | async run(): Promise { 53 | } 54 | 55 | resolve( 56 | commandResult: RunResult, 57 | ): Promise { 58 | if (!commandResult) { 59 | this.doneDeferred.resolve({ 60 | status: { success: true, code: 0 }, 61 | stdout: `Success: ${this}`, 62 | stderr: "", 63 | }); 64 | return this.done; 65 | } 66 | if (typeof commandResult === "string") { 67 | this.doneDeferred.resolve({ 68 | status: { success: true, code: 0 }, 69 | stdout: commandResult, 70 | stderr: "", 71 | }); 72 | return this.done; 73 | } 74 | if (Array.isArray(commandResult)) { 75 | const postCommands: Command[] = commandResult; 76 | return run(postCommands).then((postCommandResults: CommandResult[]) => 77 | this.resolve(postCommandResults[postCommandResults.length]) 78 | ); 79 | } 80 | 81 | this.doneDeferred.resolve(commandResult); 82 | return this.done; 83 | } 84 | 85 | /** 86 | * Chains commands to make sure they are run one at a time, in the order given. 87 | * @param commands The commands to run in order. 88 | */ 89 | static sequential(commands: Command[]): Command { 90 | if (commands.length === 0) { 91 | return NOOP(); 92 | } 93 | if (commands.length === 1) { 94 | return commands[0]; 95 | } 96 | return commands.reduce( 97 | (acc, curr) => { 98 | curr.dependencies.push(acc); 99 | return curr; 100 | }, 101 | Command.custom(), 102 | ); 103 | } 104 | 105 | withDependencies(dependencies: Array): Command { 106 | if (this.dependencies.length === 0) { 107 | this.dependencies.push(...dependencies); 108 | return this; 109 | } 110 | return Command.sequential([ 111 | Command.custom().withDependencies(dependencies), 112 | this, 113 | ]); 114 | } 115 | 116 | withLocks(locks: Array): Command { 117 | this.locks.push(...locks); 118 | return this; 119 | } 120 | 121 | withRun(run: RunFunction): Command { 122 | this.run = run; 123 | return this; 124 | } 125 | 126 | withSkipIfAll(predicates: Array): Command { 127 | this.skipIfAll.push(...predicates); 128 | return this; 129 | } 130 | 131 | withSkipIfAny(predicates: Array): Command { 132 | this.skipIfAny.push(...predicates); 133 | return this; 134 | } 135 | 136 | private _shouldSkip: boolean | undefined = undefined; 137 | 138 | async shouldSkip(): Promise { 139 | if (this._shouldSkip === true) { 140 | if (!this.doneDeferred.isDone) { 141 | this.doneDeferred.resolve(this.alreadyDoneResult()); 142 | } 143 | return true; 144 | } 145 | 146 | if (this._shouldSkip === false) { 147 | return false; 148 | } 149 | 150 | try { 151 | if (this.skipIfAny.length > 0) { 152 | const skipIfAnyValuePromises: Promise[] = this.skipIfAny.map(( 153 | predicate, 154 | ) => resolveValue(predicate)); 155 | const skipIfAnyValues: boolean[] = await Promise.all( 156 | skipIfAnyValuePromises, 157 | ); 158 | const skipIfAnyResult: boolean = skipIfAnyValues.some(Boolean); 159 | if (skipIfAnyResult) { 160 | console.log("Skipping because of skipIfAny", skipIfAnyValues); 161 | this._shouldSkip = true; 162 | if (!this.doneDeferred.isDone) { 163 | this.doneDeferred.resolve(this.alreadyDoneResult()); 164 | } 165 | return true; 166 | } 167 | } 168 | if (this.skipIfAll.length > 0) { 169 | const skipIfAllValuePromises: Promise[] = this.skipIfAll.map( 170 | (predicate) => resolveValue(predicate), 171 | ); 172 | const skipIfAllValues: boolean[] = await Promise.all( 173 | skipIfAllValuePromises, 174 | ); 175 | const skipIfAllResult: boolean = skipIfAllValues.every(Boolean); 176 | if (skipIfAllResult) { 177 | console.log("Skipping because of skipIfAll", skipIfAllValues); 178 | this._shouldSkip = true; 179 | if (!this.doneDeferred.isDone) { 180 | this.doneDeferred.resolve(this.alreadyDoneResult()); 181 | } 182 | return true; 183 | } 184 | } 185 | } catch (e) { 186 | console.warn(`Some predicate failed, so we should run the command.`, e); 187 | } 188 | this._shouldSkip = false; 189 | return false; 190 | } 191 | 192 | private alreadyDoneResult(): CommandResult { 193 | return { 194 | status: { success: true, code: 0 }, 195 | stdout: `Already done: ${this.toString()}`, 196 | stderr: "", 197 | }; 198 | } 199 | } 200 | 201 | export type Predicate = Ish; 202 | export type RunResult = CommandResult | void | string | Command[]; 203 | export type RunFunction = () => Promise; 204 | -------------------------------------------------------------------------------- /src/commands/common/file-commands.ts: -------------------------------------------------------------------------------- 1 | import { dirname, equalsBytes, PasswdEntry } from "../../deps.ts"; 2 | import { Command, CommandResult, RunResult } from "../../model/command.ts"; 3 | import { asStringPath, FileSystemPath } from "../../model/dependency.ts"; 4 | import { ensureSuccessful, isSuccessful, symlink } from "../../os/exec.ts"; 5 | import { ROOT } from "../../os/user/target-user.ts"; 6 | import { fileEndsWithNewline } from "../../os/file-ends-with-newline.ts"; 7 | 8 | export abstract class AbstractFileCommand extends Command { 9 | readonly owner: PasswdEntry; 10 | readonly path: FileSystemPath; 11 | readonly mode?: number; 12 | 13 | protected constructor( 14 | owner: PasswdEntry, 15 | path: FileSystemPath, 16 | mode?: number, 17 | ) { 18 | super(); 19 | this.owner = owner; 20 | this.path = path; 21 | this.mode = mode; 22 | this.locks.push(this.path); 23 | } 24 | 25 | toString(): string { 26 | return `${this.constructor.name}(${this.owner.username}, ${this.path}, ${this.mode})`; 27 | } 28 | 29 | abstract run(): Promise; 30 | } 31 | 32 | const existsDir = async (dirSegments: Array): Promise => { 33 | try { 34 | const dirInfo: Deno.FileInfo = await Deno.stat(asPath(dirSegments)); 35 | return dirInfo.isDirectory; 36 | } catch { 37 | return false; 38 | } 39 | }; 40 | 41 | const existsPath = async (dirSegments: Array): Promise => { 42 | try { 43 | await Deno.stat(asPath(dirSegments)); 44 | return true; 45 | } catch { 46 | return false; 47 | } 48 | }; 49 | 50 | const asPath = (pathSegments: Array): string => 51 | "/" + pathSegments.join("/"); 52 | 53 | const getParentDirSegments = (dirSegments: Array) => 54 | dirSegments.slice(0, dirSegments.length - 1); 55 | 56 | const asPathSegments = (path: FileSystemPath): Array => 57 | path.path.split("/"); 58 | 59 | const mkdirp = async ( 60 | owner: PasswdEntry, 61 | dirSegments: Array, 62 | ): Promise => { 63 | if (await existsDir(dirSegments)) { 64 | await Deno.chown(asPath(dirSegments), owner.uid, owner.gid); 65 | return; 66 | } 67 | if (dirSegments.length === 0) { 68 | return; 69 | } 70 | await mkdirp(owner, getParentDirSegments(dirSegments)); 71 | await Deno.mkdir(asPath(dirSegments)).then( 72 | () => { 73 | }, 74 | (reason) => 75 | reason?.name === "AlreadyExists" 76 | ? Promise.resolve() 77 | : Promise.reject(reason), 78 | ); 79 | await Deno.chown(asPath(dirSegments), owner.uid, owner.gid); 80 | }; 81 | 82 | const backupFileUnlessContentAlready = async ( 83 | filePath: FileSystemPath, 84 | contents: string | Uint8Array, 85 | ): Promise => { 86 | if (!await existsPath(asPathSegments(filePath))) { 87 | return undefined; 88 | } 89 | if (typeof contents === "string") { 90 | if ((await Deno.readTextFile(filePath.path)) == contents) { 91 | return undefined; 92 | } 93 | } else { 94 | if (equalsBytes(contents, await Deno.readFile(filePath.path))) { 95 | return undefined; 96 | } 97 | } 98 | 99 | const backupFilePath: FileSystemPath = FileSystemPath.of( 100 | ROOT, 101 | `${filePath.path}.${Date.now()}.backup`, 102 | ); 103 | await Deno.rename(filePath.path, backupFilePath.path); 104 | return backupFilePath; 105 | }; 106 | 107 | /** 108 | * Creates a file. If it creates a backup of any existing file, its Promise resolves to that path. Otherwise to undefined. 109 | */ 110 | const createFile = async ( 111 | owner: PasswdEntry, 112 | path: FileSystemPath, 113 | contents: string | Uint8Array, 114 | shouldBackupAnyExistingFile = false, 115 | mode?: number, 116 | ): Promise => { 117 | await mkdirp(owner, dirname(path.path).split("/")); 118 | 119 | const data: Uint8Array = typeof contents === "string" 120 | ? new TextEncoder().encode(contents) 121 | : contents; 122 | const options: Deno.WriteFileOptions = mode ? { mode } : {}; 123 | 124 | const backupFilePath: FileSystemPath | undefined = shouldBackupAnyExistingFile 125 | ? await backupFileUnlessContentAlready(path, contents) 126 | : undefined; 127 | 128 | await Deno.writeFile(path.path, data, options); 129 | await Deno.chown(path.path, owner.uid, owner.gid); 130 | return backupFilePath; 131 | }; 132 | 133 | export class CreateFile extends AbstractFileCommand { 134 | readonly contents: string | Uint8Array; 135 | readonly shouldBackupAnyExistingFile: boolean; 136 | 137 | constructor( 138 | owner: PasswdEntry, 139 | path: FileSystemPath, 140 | contents: string | Uint8Array, 141 | shouldBackupAnyExistingFile: boolean = false, 142 | mode?: number, 143 | ) { 144 | super( 145 | owner, 146 | path, 147 | mode, 148 | ); 149 | this.contents = contents; 150 | this.shouldBackupAnyExistingFile = shouldBackupAnyExistingFile; 151 | } 152 | 153 | async run(): Promise { 154 | const backupFilePath: FileSystemPath | undefined = await createFile( 155 | this.owner, 156 | this.path, 157 | this.contents, 158 | this.shouldBackupAnyExistingFile, 159 | this.mode, 160 | ); 161 | return `Created file ${this.toString()}.` + 162 | (backupFilePath ? `\nBacked up previous file to ${backupFilePath}` : ""); 163 | } 164 | } 165 | 166 | const createDir = async ( 167 | owner: PasswdEntry, 168 | path: FileSystemPath, 169 | ) => { 170 | await mkdirp(owner, path.path.split("/")); 171 | }; 172 | 173 | export class CreateDir extends Command { 174 | readonly owner: PasswdEntry; 175 | readonly path: FileSystemPath; 176 | 177 | constructor(owner: PasswdEntry, path: FileSystemPath) { 178 | super(); 179 | this.locks.push(path); 180 | this.owner = owner; 181 | this.path = path; 182 | } 183 | 184 | toString(): string { 185 | return `${this.constructor.name}(${this.owner.username}, ${this.path})`; 186 | } 187 | 188 | async run(): Promise { 189 | await createDir(this.owner, this.path); 190 | return `Created dir ${this.toString()}.`; 191 | } 192 | } 193 | 194 | export const MODE_EXECUTABLE_775 = 0o755; 195 | 196 | const ensureLineInFile = 197 | (line: string) => 198 | async (owner: PasswdEntry, file: FileSystemPath): Promise => { 199 | if ( 200 | await isSuccessful(ROOT, [ 201 | "grep", 202 | "--fixed-strings", 203 | "--line-regexp", 204 | line, 205 | file.path, 206 | ]) 207 | ) { 208 | return; 209 | } 210 | const prefix = await fileEndsWithNewline(file) ? "" : "\n"; 211 | const suffix = "\n"; 212 | const data = new TextEncoder().encode(prefix + line + suffix); 213 | await Deno.writeFile(file.path, data, { 214 | append: true, 215 | create: true, 216 | }); 217 | await Deno.chown(file.path, owner.uid, owner.gid); 218 | }; 219 | 220 | function ensureUserInGroup( 221 | user: PasswdEntry, 222 | group: string, 223 | ): Promise { 224 | return ensureSuccessful( 225 | ROOT, 226 | ["usermod", "-aG", group, user.username], 227 | {}, 228 | ); 229 | } 230 | 231 | export class LineInFile extends AbstractFileCommand { 232 | readonly line: string; 233 | 234 | constructor(owner: PasswdEntry, path: FileSystemPath, line: string) { 235 | super( 236 | owner, 237 | path, 238 | ); 239 | this.line = line; 240 | } 241 | 242 | toString(): string { 243 | return `${this.constructor.name}(${this.owner.username}, ${this.path}, ${ 244 | JSON.stringify(this.line) 245 | })`; 246 | } 247 | 248 | async run(): Promise { 249 | await ensureLineInFile(this.line)(this.owner, this.path); 250 | return `Line ensured in file ${this.toString()}.`; 251 | } 252 | } 253 | 254 | export class UserInGroup extends Command { 255 | readonly user: PasswdEntry; 256 | readonly group: string; 257 | 258 | constructor(user: PasswdEntry, group: string) { 259 | super(); 260 | this.user = user; 261 | this.group = group; 262 | } 263 | 264 | async run(): Promise { 265 | return await ensureUserInGroup(this.user, this.group); 266 | } 267 | } 268 | 269 | async function isDirectoryEmpty(directory: FileSystemPath) { 270 | return (await (Deno.readDirSync(directory.path))[Symbol.iterator]().next()) 271 | .done; 272 | } 273 | 274 | export class Symlink extends AbstractFileCommand { 275 | readonly target: string; 276 | 277 | constructor( 278 | owner: PasswdEntry, 279 | from: string | FileSystemPath, 280 | to: FileSystemPath, 281 | ) { 282 | super(owner, to); 283 | this.target = asStringPath(from); 284 | } 285 | 286 | async run(): Promise { 287 | const ifExists = async ( 288 | pathStat: Deno.FileInfo, 289 | ) => { 290 | if ( 291 | pathStat.isSymlink && 292 | await Deno.readLink(this.path.path) === this.target 293 | ) { 294 | if ( 295 | pathStat.uid === this.owner.uid && pathStat.gid === this.owner.gid 296 | ) { 297 | return `"${this.path}" is already a symlink to "${this.target}".`; 298 | } 299 | await Deno.remove(this.path.path); 300 | await symlink(this.owner, this.target, this.path); 301 | 302 | return `Replaced correct (but incorrectly owned) symlink "${this.path}" to "${this.target}", with correctly owned symlink.`; 303 | } 304 | 305 | if (pathStat.isDirectory && await isDirectoryEmpty(this.path)) { 306 | await Deno.remove(this.path.path); 307 | await symlink(this.owner, this.target, this.path); 308 | 309 | return `Replaced empty directory "${this.path}" with a symlink to "${this.target}".`; 310 | } 311 | 312 | const newpath = `${this.path.path}-${Math.ceil(Math.random() * 10e5)}`; 313 | await Deno.rename(this.path.path, newpath); 314 | 315 | await symlink(this.owner, this.target, this.path); 316 | return `Renamed existing "${this.path}" to "${newpath}", then replaced it with a symlink to "${this.target}".`; 317 | }; 318 | 319 | const ifNotExists = async () => { 320 | await mkdirp(this.owner, getParentDirSegments(this.path.path.split("/"))); 321 | await symlink(this.owner, this.target, this.path); 322 | return `Created "${this.path}" as a symlink to "${this.target}".`; 323 | }; 324 | 325 | return await Deno.lstat(this.path.path) 326 | .then( 327 | ifExists, 328 | ifNotExists, 329 | ); 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/commands/gnome-shell-extensions.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { ensureSuccessful, ensureSuccessfulStdOut } from "../os/exec.ts"; 3 | import { targetUser } from "../os/user/target-user.ts"; 4 | import { InstallOsPackage } from "./common/os-package.ts"; 5 | import { 6 | gnomeShellExtensionInstaller, 7 | gnomeShellExtensionInstallerFile, 8 | } from "./gnome-shell-extension-installer.ts"; 9 | import { createTempDir } from "../os/create-temp-dir.ts"; 10 | import { dconfLoadRoot } from "./common/dconf-load.ts"; 11 | import { gsettingsToCmds } from "./common/gsettings-to-cmds.ts"; 12 | import { isDocker } from "../deps.ts"; 13 | import { SimpleValue } from "../fn.ts"; 14 | 15 | function findGnomeExtensionId(url: string): number { 16 | const id: number | undefined = url.split("/") 17 | .map((s) => parseInt(s, 10)) 18 | .find((n) => n > 0); 19 | 20 | if (id) { 21 | return id; 22 | } 23 | 24 | throw new Error( 25 | `Gnome extension id not found in url: ${JSON.stringify(url)}`, 26 | ); 27 | } 28 | 29 | function downloadGnomeExtensions(ids: number[]): string[][] { 30 | if (ids.length === 0) { 31 | return []; 32 | } 33 | return [[ 34 | gnomeShellExtensionInstallerFile.path, 35 | "--no-install", 36 | ...ids.map((id) => `${id}`), 37 | ]]; 38 | } 39 | 40 | function uninstallOrDisableGnomeExtensionCmd(uuid: string): string[] { 41 | return [ 42 | "sh", 43 | "-c", 44 | `gnome-extensions uninstall "${uuid}" 2>/dev/null || gnome-extensions disable "${uuid}" || true`, 45 | ]; 46 | } 47 | 48 | function disableGnomeExtensionCmd(uuid: string): string[] { 49 | return [ 50 | "sh", 51 | "-c", 52 | `gnome-extensions disable "${uuid}" || true`, 53 | ]; 54 | } 55 | 56 | function installGnomeExtensionCmd(uuid: string): string[] { 57 | return [ 58 | "gnome-extensions", 59 | "install", 60 | "--force", 61 | `${uuid}.shell-extension.zip`, 62 | ]; 63 | } 64 | 65 | function enableGnomeExtensionCmd(uuid: string): string[] { 66 | return [ 67 | "gnome-extensions", 68 | "enable", 69 | uuid, 70 | ]; 71 | } 72 | 73 | async function idToUuid(id: number): Promise { 74 | const response: Response = await fetch( 75 | `https://extensions.gnome.org/extension-info/?pk=${encodeURIComponent(id)}`, 76 | ); 77 | const { uuid }: { uuid: string } = await response.json(); 78 | if (!uuid) { 79 | throw new Error(`Gnome extension id ${id} not found`); 80 | } 81 | return uuid; 82 | } 83 | 84 | async function uuidToId(uuid: string): Promise { 85 | const response: Response = await fetch( 86 | `https://extensions.gnome.org/extension-info/?uuid=${ 87 | encodeURIComponent(uuid) 88 | }`, 89 | ); 90 | const { pk }: { pk: number } = await response.json(); 91 | if (!pk) { 92 | throw new Error(`Gnome extension uuid ${uuid} not found`); 93 | } 94 | return pk; 95 | } 96 | 97 | interface ExtensionConfig { 98 | gsettings?: string; 99 | dconf?: string; 100 | } 101 | 102 | export const gnomeShellExtensions = Command.custom() 103 | .withDependencies([ 104 | gnomeShellExtensionInstaller, 105 | ...[ 106 | "gnome-shell-extensions", 107 | "gnome-shell-extension-manager", 108 | "gnome-shell-extension-appindicator", 109 | ].map(InstallOsPackage.of), 110 | ]) 111 | .withRun(async () => { 112 | /** 113 | * Use /settings_diff to get the config for each extension. 114 | */ 115 | const extensionConfigs: Record = { 116 | // user installed 117 | "https://extensions.gnome.org/extension/7/removable-drive-menu/": {}, 118 | "https://extensions.gnome.org/extension/36/lock-keys/": { 119 | dconf: ` 120 | [org/gnome/shell/extensions/lockkeys] 121 | notification-preferences='osd' 122 | style='both' 123 | `, 124 | }, 125 | "https://extensions.gnome.org/extension/352/middle-click-to-close-in-overview/": 126 | { 127 | dconf: ` 128 | [org/gnome/shell/extensions/middleclickclose] 129 | close-button='middle' 130 | rearrange-delay=750 131 | `, 132 | }, 133 | "https://extensions.gnome.org/extension/1160/dash-to-panel/": { 134 | dconf: ` 135 | [org/gnome/shell/extensions/dash-to-panel] 136 | animate-app-switch=false 137 | animate-window-launch=false 138 | appicon-margin=4 139 | appicon-padding=4 140 | dot-style-focused='DOTS' 141 | dot-style-unfocused='DOTS' 142 | group-apps=false 143 | group-apps-label-font-size=14 144 | group-apps-label-max-width=500 145 | group-apps-use-fixed-width=false 146 | group-apps-use-launchers=false 147 | hide-overview-on-startup=true 148 | hot-keys=true 149 | hotkeys-overlay-combo='ALWAYS' 150 | leftbox-padding=-1 151 | panel-anchors='{"0":"MIDDLE"}' 152 | panel-element-positions='{"0":[{"element":"showAppsButton","visible":true,"position":"stackedTL"},{"element":"activitiesButton","visible":false,"position":"stackedTL"},{"element":"leftBox","visible":true,"position":"stackedTL"},{"element":"taskbar","visible":true,"position":"stackedTL"},{"element":"centerBox","visible":true,"position":"stackedBR"},{"element":"rightBox","visible":true,"position":"stackedBR"},{"element":"dateMenu","visible":true,"position":"stackedBR"},{"element":"systemMenu","visible":true,"position":"stackedBR"},{"element":"desktopButton","visible":true,"position":"stackedBR"}]}' 153 | panel-lengths='{"0":100}' 154 | panel-positions='{"0":"TOP"}' 155 | panel-sizes='{"0":32}' 156 | primary-monitor=0 157 | progress-show-count=true 158 | scroll-icon-action='PASS_THROUGH' 159 | scroll-panel-action='NOTHING' 160 | secondarymenu-contains-showdetails=true 161 | shortcut=@as [] 162 | shortcut-num-keys='BOTH' 163 | shortcut-previews=false 164 | shortcut-text='' 165 | show-appmenu=false 166 | show-tooltip=false 167 | show-window-previews=false 168 | status-icon-padding=-1 169 | tray-padding=-1 170 | window-preview-title-position='TOP' 171 | `, 172 | }, 173 | "https://extensions.gnome.org/extension/1689/always-show-titles-in-overview/": 174 | { 175 | dconf: ` 176 | [org/gnome/shell/extensions/always-show-titles-in-overview] 177 | app-icon-position='Bottom' 178 | do-not-show-app-icon-when-fullscreen=false 179 | hide-background=false 180 | hide-icon-for-video-player=false 181 | show-app-icon=true 182 | window-active-size-inc=15 183 | `, 184 | }, 185 | "https://extensions.gnome.org/extension/1720/weeks-start-on-monday-again/": 186 | { 187 | dconf: ` 188 | [org/gnome/shell/extensions/weeks-start-on-monday] 189 | start-day=1 190 | `, 191 | }, 192 | "https://extensions.gnome.org/extension/4655/date-menu-formatter/": { 193 | dconf: ` 194 | [org/gnome/shell/extensions/date-menu-formatter] 195 | pattern='y-MM-dd\\\\nkk:mm EEE' 196 | remove-messages-indicator=false 197 | `, 198 | }, 199 | 200 | // default ubuntu installed, make sure to enable them 201 | "https://extensions.gnome.org/extension/1301/ubuntu-appindicators/": { 202 | dconf: ` 203 | [org/gnome/shell/extensions/appindicator] 204 | icon-brightness=0.0 205 | icon-contrast=0.0 206 | icon-opacity=240 207 | icon-saturation=0.0 208 | icon-size=0 209 | tray-pos='right' 210 | `, 211 | }, 212 | "https://extensions.gnome.org/extension/2087/desktop-icons-ng-ding/": { 213 | dconf: ` 214 | [org/gnome/nautilus/preferences] 215 | click-policy='double' 216 | default-folder-viewer='list-view' 217 | default-sort-in-reverse-order=false 218 | default-sort-order='name' 219 | recursive-search='always' 220 | show-delete-permanently=false 221 | show-directory-item-counts='always' 222 | show-image-thumbnails='always' 223 | thumbnail-limit=uint64 1000 224 | 225 | [org/gnome/shell/extensions/ding] 226 | add-volumes-opposite=false 227 | dark-text-in-labels=false 228 | icon-size='standard' 229 | show-drop-place=true 230 | show-home=false 231 | show-link-emblem=true 232 | show-network-volumes=true 233 | show-trash=true 234 | show-volumes=true 235 | start-corner='top-right' 236 | use-nemo=false 237 | 238 | [org/gtk/settings/file-chooser] 239 | clock-format='24h' 240 | show-hidden=false 241 | `, 242 | }, 243 | }; 244 | const extensionConfigsByUuid: Record = Object 245 | .fromEntries( 246 | await Promise.all( 247 | Object.entries(extensionConfigs) 248 | .map(([url, config]) => [findGnomeExtensionId(url), config]) 249 | .map(async ([id, config]) => [await idToUuid(id), config]), 250 | ), 251 | ); 252 | const urls: string[] = Object.keys(extensionConfigs); 253 | const ids: number[] = urls.map(findGnomeExtensionId); 254 | 255 | const uuids: string[] = await Promise.all( 256 | ids.map(idToUuid), 257 | ); 258 | 259 | const uninstallOrDisableExtensionsCmds: string[][] = [ 260 | "dash-to-dock@micxgx.gmail.com", 261 | ] 262 | .map(uninstallOrDisableGnomeExtensionCmd); 263 | 264 | const installedExtensionUuids: string[] = (await ensureSuccessfulStdOut( 265 | targetUser, 266 | ["gnome-extensions", "list"], 267 | )).split("\n"); 268 | 269 | const uuidsToDownload: number[] = uuids.filter( 270 | (uuid) => !installedExtensionUuids.includes(uuid), 271 | ); 272 | 273 | const idsToDownload: number[] = await Promise.all( 274 | uuidsToDownload.map(uuidToId), 275 | ); 276 | 277 | const uuidsToInstall: string[] = await Promise.all( 278 | idsToDownload.map(idToUuid), 279 | ); 280 | 281 | const downloadExtensionsCmds: string[][] = downloadGnomeExtensions( 282 | idsToDownload, 283 | ); 284 | 285 | const enabledExtensionUuids: string[] = (await ensureSuccessfulStdOut( 286 | targetUser, 287 | ["gnome-extensions", "list", "--enabled"], 288 | )).split("\n"); 289 | 290 | const uuidsToDisable: string[] = enabledExtensionUuids.filter((uuid) => 291 | !uuids.includes(uuid) 292 | ); 293 | const uuidsToEnable: string[] = uuids.filter((uuid) => 294 | !enabledExtensionUuids.includes(uuid) 295 | ); 296 | 297 | const disableExtensionsCmds: string[][] = uuidsToDisable.map((uuid) => 298 | disableGnomeExtensionCmd(uuid) 299 | ); 300 | 301 | const dconfConfigs: string[] = uuids 302 | .map((uuid) => extensionConfigsByUuid[uuid]?.dconf) 303 | .filter(Boolean) 304 | .map((config) => config.trim()) 305 | .filter(Boolean); 306 | 307 | const gsettingsConfigs: string[] = uuids 308 | .map((uuid) => extensionConfigsByUuid[uuid]?.gsettings) 309 | .filter(Boolean) 310 | .map((config) => config.trim()) 311 | .filter(Boolean); 312 | 313 | const cwd = (await createTempDir(targetUser)).path; 314 | for ( 315 | const cmd: SimpleValue[] of [ 316 | ...disableExtensionsCmds, 317 | ...uninstallOrDisableExtensionsCmds, 318 | ].filter(checkPotentialCmd) 319 | ) { 320 | await ensureSuccessful(targetUser, cmd); 321 | } 322 | 323 | if (await isDocker()) { 324 | return; 325 | } 326 | 327 | for ( 328 | const cmd: SimpleValue[] of [ 329 | ...downloadExtensionsCmds, 330 | ...uuidsToInstall.map(installGnomeExtensionCmd), 331 | ...uuidsToEnable.map(enableGnomeExtensionCmd), 332 | ...gsettingsConfigs.map(gsettingsToCmds), 333 | ].filter(checkPotentialCmd) 334 | ) { 335 | try { 336 | await ensureSuccessful(targetUser, cmd, { cwd }); 337 | } catch (e) { 338 | throw new Error( 339 | `Failed to run ${ 340 | cmd.join(" ") 341 | } in ${cwd}. Please try again after logging in to gnome the next time.`, 342 | e, 343 | ); 344 | } 345 | } 346 | 347 | return dconfConfigs.map(dconfLoadRoot); 348 | }); 349 | 350 | function checkPotentialCmd(potentialCmd) { 351 | if (typeof potentialCmd === "string") { 352 | throw new Error( 353 | `Unexpected string: potentialCmd=${JSON.stringify(potentialCmd)}`, 354 | ); 355 | } 356 | if (!Array.isArray(potentialCmd)) { 357 | throw new Error( 358 | `Unexpected non-array: potentialCmd=${JSON.stringify(potentialCmd)}`, 359 | ); 360 | } 361 | if (potentialCmd.length === 0) { 362 | throw new Error( 363 | `Unexpected empty array: potentialCmd=${JSON.stringify(potentialCmd)}`, 364 | ); 365 | } 366 | return true; 367 | } 368 | -------------------------------------------------------------------------------- /src/commands/common/os-package.ts: -------------------------------------------------------------------------------- 1 | import { isDocker, memoize } from "../../deps.ts"; 2 | import { Command, RunResult } from "../../model/command.ts"; 3 | import { 4 | FileSystemPath, 5 | FLATPAK, 6 | OS_PACKAGE_SYSTEM, 7 | SNAP, 8 | } from "../../model/dependency.ts"; 9 | import { ensureSuccessful, isSuccessful } from "../../os/exec.ts"; 10 | import { ROOT, targetUser } from "../../os/user/target-user.ts"; 11 | import { LineInFile } from "./file-commands.ts"; 12 | import { Exec } from "../exec.ts"; 13 | import { REFRESH_OS_PACKAGES } from "../refresh-os-packages.ts"; 14 | import { Ish, resolveValue } from "../../fn.ts"; 15 | 16 | export type OsPackageName = string; 17 | export type BrewPackageName = string; 18 | export type FlatpakPackageName = string; 19 | export type SnapPackageName = string; 20 | export type RustPackageName = string; 21 | export type PackageName = 22 | | OsPackageName 23 | | BrewPackageName 24 | | FlatpakPackageName 25 | | SnapPackageName 26 | | RustPackageName; 27 | 28 | export abstract class AbstractPackageCommand 29 | extends Command { 30 | readonly packageName: T; 31 | 32 | protected constructor(packageName: T) { 33 | super(); 34 | this.packageName = packageName; 35 | } 36 | 37 | toString() { 38 | return JSON.stringify({ packageName: this.packageName }); 39 | } 40 | } 41 | 42 | export class InstallOsPackage extends AbstractPackageCommand { 43 | private constructor(packageName: OsPackageName) { 44 | super(packageName); 45 | this.locks.push(OS_PACKAGE_SYSTEM); 46 | this.dependencies.push(REFRESH_OS_PACKAGES); 47 | this.skipIfAll.push(() => isInstalledOsPackage(packageName)); 48 | } 49 | 50 | async run(): Promise { 51 | await ensureSuccessful(ROOT, [ 52 | "apt", 53 | "install", 54 | "-y", 55 | this.packageName, 56 | ], {}); 57 | 58 | return `Installed OS package ${this.packageName}.`; 59 | } 60 | 61 | static of: (packageName: OsPackageName) => InstallOsPackage = memoize( 62 | (packageName: OsPackageName): InstallOsPackage => 63 | new InstallOsPackage(packageName), 64 | ); 65 | } 66 | 67 | export class RemoveOsPackage extends AbstractPackageCommand { 68 | private constructor(packageName: OsPackageName) { 69 | super(packageName); 70 | this.locks.push(OS_PACKAGE_SYSTEM); 71 | this.dependencies.push(REFRESH_OS_PACKAGES); 72 | this.skipIfAll.push(async () => !(await isInstalledOsPackage(packageName))); 73 | } 74 | 75 | async run(): Promise { 76 | await ensureSuccessful(ROOT, [ 77 | "apt", 78 | "purge", 79 | "-y", 80 | "--auto-remove", 81 | this.packageName, 82 | ]); 83 | 84 | return `Removed package ${this.packageName}.`; 85 | } 86 | 87 | static of: (packageName: OsPackageName) => RemoveOsPackage = ( 88 | packageName: OsPackageName, 89 | ) => new RemoveOsPackage(packageName); 90 | } 91 | 92 | export class ReplaceOsPackage extends Command { 93 | readonly removePackageName: OsPackageName; 94 | readonly installPackageName: OsPackageName; 95 | 96 | private constructor( 97 | removePackageName: OsPackageName, 98 | installPackageName: OsPackageName, 99 | ) { 100 | super(); 101 | this.locks.push(OS_PACKAGE_SYSTEM); 102 | this.dependencies.push(REFRESH_OS_PACKAGES); 103 | 104 | this.removePackageName = removePackageName; 105 | this.installPackageName = installPackageName; 106 | 107 | this.skipIfAll.push(() => isInstalledOsPackage(installPackageName)); 108 | this.skipIfAll.push(async () => 109 | !(await isInstalledOsPackage(removePackageName)) 110 | ); 111 | } 112 | 113 | async run(): Promise { 114 | await ensureSuccessful(ROOT, [ 115 | "apt", 116 | "purge", 117 | "-y", 118 | this.removePackageName, 119 | this.installPackageName + "+", 120 | ]).catch(this.doneDeferred.reject); 121 | 122 | return `Replaced package ${this.removePackageName} with ${this.installPackageName}.`; 123 | } 124 | /** 125 | * @deprecated Use .of2() instead. 126 | */ 127 | static of(): Command { 128 | throw new Error("Use .of2() instead."); 129 | } 130 | static of2: ( 131 | removePackageName: OsPackageName, 132 | installPackageName: OsPackageName, 133 | ) => ReplaceOsPackage = ( 134 | removePackageName: OsPackageName, 135 | installPackageName: OsPackageName, 136 | ) => new ReplaceOsPackage(removePackageName, installPackageName); 137 | } 138 | 139 | const bashRc = FileSystemPath.of(targetUser, "~/.bashrc"); 140 | 141 | export const brewBashRcLine = 142 | `eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"`; 143 | const brewDeps = [ 144 | ...["build-essential", "procps", "curl", "file", "git"] 145 | .map( 146 | InstallOsPackage.of, 147 | ), 148 | new LineInFile( 149 | targetUser, 150 | bashRc, 151 | brewBashRcLine, 152 | ), 153 | ]; 154 | 155 | export const HOME_LINUXBREW = FileSystemPath.of(ROOT, "/home/linuxbrew"); 156 | const brewInstall = new Exec( 157 | brewDeps, 158 | [HOME_LINUXBREW], 159 | targetUser, 160 | { env: { NONINTERACTIVE: `1` } }, 161 | [ 162 | "bash", 163 | "-c", 164 | "curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | bash", 165 | ], 166 | ); 167 | 168 | export const brew = brewInstall.withSkipIfAll([ 169 | () => 170 | isSuccessful(targetUser, [ 171 | "bash", 172 | "-c", 173 | `${brewBashRcLine} && command -v brew`, 174 | ], {}), 175 | ]); 176 | 177 | export class InstallBrewPackage 178 | extends AbstractPackageCommand { 179 | private constructor(packageName: BrewPackageName) { 180 | super(packageName); 181 | this.locks.push(HOME_LINUXBREW); 182 | this.dependencies.push(brew); 183 | this.skipIfAll.push(() => isInstalledBrewPackage(packageName)); 184 | } 185 | 186 | async run(): Promise { 187 | await ensureSuccessful(targetUser, [ 188 | "bash", 189 | "-c", 190 | `${brewBashRcLine} && brew install --quiet ${this.packageName}`, 191 | ]); 192 | 193 | return `Installed Brew package ${this.packageName}.`; 194 | } 195 | 196 | static of: (packageName: BrewPackageName) => InstallBrewPackage = memoize( 197 | (packageName: BrewPackageName): InstallBrewPackage => 198 | new InstallBrewPackage(packageName), 199 | ); 200 | } 201 | 202 | export class RemoveBrewPackage extends AbstractPackageCommand { 203 | private constructor(packageName: BrewPackageName) { 204 | super(packageName); 205 | this.locks.push(HOME_LINUXBREW); 206 | this.dependencies.push(brew); 207 | this.skipIfAll.push(async () => 208 | !(await isInstalledBrewPackage(packageName)) 209 | ); 210 | } 211 | 212 | async run(): Promise { 213 | await ensureSuccessful(targetUser, [ 214 | "bash", 215 | "-c", 216 | `${brewBashRcLine} && brew remove --quiet ${this.packageName}`, 217 | ]); 218 | 219 | return `Removed Brew package ${this.packageName}.`; 220 | } 221 | 222 | static of: (packageName: BrewPackageName) => RemoveBrewPackage = ( 223 | packageName: BrewPackageName, 224 | ) => new RemoveBrewPackage(packageName); 225 | } 226 | 227 | export const flatpakOsPackages = ["xdg-desktop-portal-gtk", "flatpak"]; 228 | export const flatpak: Command = new Exec( 229 | flatpakOsPackages.map(InstallOsPackage.of), 230 | [FLATPAK], 231 | ROOT, 232 | {}, 233 | [ 234 | "flatpak", 235 | "remote-add", 236 | "--if-not-exists", 237 | "flathub", 238 | "https://flathub.org/repo/flathub.flatpakrepo", 239 | ], 240 | ) 241 | .withSkipIfAll([ 242 | () => 243 | isSuccessful(ROOT, [ 244 | "sh", 245 | "-c", 246 | "flatpak remotes --system --columns=name | grep flathub", 247 | ], { verbose: false }), 248 | ]); 249 | 250 | export const snapOsPackages = ["snapd", "snapd-xdg-open"]; 251 | export const snap: Command = new Exec( 252 | snapOsPackages.map(InstallOsPackage.of), 253 | [SNAP], 254 | ROOT, 255 | {}, 256 | ["snap", "refresh"], 257 | ); 258 | 259 | export class InstallFlatpakPackage 260 | extends AbstractPackageCommand { 261 | private constructor(packageName: FlatpakPackageName) { 262 | super(packageName); 263 | this.locks.push(FLATPAK); 264 | this.dependencies.push(flatpak); 265 | this.skipIfAll.push(() => isInstalledFlatpakPackage(packageName)); 266 | } 267 | 268 | async run(): Promise { 269 | await ensureSuccessful(ROOT, [ 270 | "flatpak", 271 | "install", 272 | "--or-update", 273 | "--noninteractive", 274 | ...(await isDocker() ? ["--no-deploy"] : []), 275 | "flathub", 276 | this.packageName, 277 | ]); 278 | 279 | return `Installed Flatpak package ${this.packageName}.`; 280 | } 281 | 282 | static of: ( 283 | packageName: FlatpakPackageName, 284 | ) => InstallFlatpakPackage = memoize( 285 | (packageName: FlatpakPackageName): InstallFlatpakPackage => 286 | new InstallFlatpakPackage(packageName), 287 | ); 288 | } 289 | 290 | export class InstallSnapPackage 291 | extends AbstractPackageCommand { 292 | private readonly classic: boolean; 293 | 294 | private constructor(packageName: SnapPackageName, classic: boolean) { 295 | super(packageName); 296 | this.classic = classic; 297 | this.locks.push(SNAP); 298 | this.dependencies.push(snap); 299 | this.skipIfAll.push(() => isInstalledSnapPackage(packageName)); 300 | } 301 | 302 | async run(): Promise { 303 | await ensureSuccessful(ROOT, [ 304 | "snap", 305 | "install", 306 | ...(this.classic ? ["--classic"] : []), 307 | this.packageName, 308 | ]); 309 | 310 | return `Installed Snap package ${this.packageName}${ 311 | this.classic ? " in classic mode" : "" 312 | }.`; 313 | } 314 | 315 | static of: ( 316 | packageName: SnapPackageName, 317 | ) => InstallSnapPackage = memoize( 318 | (packageName: SnapPackageName): InstallSnapPackage => 319 | new InstallSnapPackage(packageName, false), 320 | ); 321 | 322 | static ofClassic: ( 323 | packageName: SnapPackageName, 324 | ) => InstallSnapPackage = memoize( 325 | (packageName: SnapPackageName): InstallSnapPackage => 326 | new InstallSnapPackage(packageName, true), 327 | ); 328 | } 329 | 330 | export class RemoveFlatpakPackage 331 | extends AbstractPackageCommand { 332 | private constructor(packageName: FlatpakPackageName) { 333 | super(packageName); 334 | this.locks.push(FLATPAK); 335 | this.dependencies.push(flatpak); 336 | this.skipIfAll.push(async () => 337 | !(await isInstalledFlatpakPackage(packageName)) 338 | ); 339 | } 340 | 341 | async run(): Promise { 342 | await ensureSuccessful(ROOT, [ 343 | "flatpak", 344 | "uninstall", 345 | "--noninteractive", 346 | this.packageName, 347 | ]); 348 | 349 | return `Removed Flatpak package ${this.packageName}.`; 350 | } 351 | 352 | static of: (packageName: FlatpakPackageName) => RemoveFlatpakPackage = ( 353 | packageName: FlatpakPackageName, 354 | ) => new RemoveFlatpakPackage(packageName); 355 | } 356 | 357 | export class RemoveSnapPackage extends AbstractPackageCommand { 358 | private constructor(packageName: SnapPackageName) { 359 | super(packageName); 360 | this.locks.push(SNAP); 361 | this.dependencies.push(snap); 362 | this.skipIfAll.push(async () => 363 | !(await isInstalledSnapPackage(packageName)) 364 | ); 365 | } 366 | 367 | async run(): Promise { 368 | await ensureSuccessful(ROOT, [ 369 | "snap", 370 | "remove", 371 | this.packageName, 372 | ]); 373 | 374 | return `Removed Snap package ${this.packageName}.`; 375 | } 376 | 377 | static of: (packageName: SnapPackageName) => RemoveSnapPackage = ( 378 | packageName: SnapPackageName, 379 | ) => new RemoveSnapPackage(packageName); 380 | } 381 | 382 | function isInstalledOsPackage( 383 | packageName: OsPackageName, 384 | ): Promise { 385 | return isSuccessful(ROOT, [ 386 | "bash", 387 | "-c", 388 | `[[ "$(dpkg-query --show -f '\${status}' "${packageName}" 2>/dev/null)" == "install ok installed" ]]`, 389 | ], { verbose: false }); 390 | } 391 | 392 | export function isInstallableOsPackage( 393 | packageName: OsPackageName, 394 | ): Promise { 395 | return isSuccessful(ROOT, [ 396 | `dpkg`, 397 | `-l`, 398 | packageName, 399 | ], { verbose: false }); 400 | } 401 | 402 | function isInstalledBrewPackage( 403 | packageName: BrewPackageName, 404 | ): Promise { 405 | return isSuccessful(targetUser, [ 406 | "brew", 407 | "search", 408 | "--formula", 409 | `/^${packageName}$/`, 410 | ], { verbose: false }); 411 | } 412 | 413 | function isInstalledFlatpakPackage( 414 | packageName: FlatpakPackageName, 415 | ): Promise { 416 | return isSuccessful(ROOT, [ 417 | "bash", 418 | "-c", 419 | `flatpak list --columns application | grep --line-regexp '${packageName}'`, 420 | ], { verbose: false }); 421 | } 422 | 423 | function isInstalledSnapPackage( 424 | packageName: SnapPackageName, 425 | ): Promise { 426 | return isSuccessful(ROOT, ["snap", "list", "--", packageName], { 427 | verbose: false, 428 | }); 429 | } 430 | 431 | export function installOsPackageFromUrl( 432 | expectedPackageName: string, 433 | debUrl: Ish, 434 | ): Command { 435 | return new Exec( 436 | [ 437 | "curl", 438 | "gdebi-core", 439 | ].map(InstallOsPackage.of), 440 | [OS_PACKAGE_SYSTEM], 441 | ROOT, 442 | {}, 443 | async () => [ 444 | "bash", 445 | "-c", 446 | ` 447 | set -euo pipefail 448 | IFS=$'\n\t' 449 | 450 | tmp_file="$(mktemp --suffix=.deb)" 451 | trap 'rm -f -- "$tmp_file"' EXIT 452 | curl -sLf "${await resolveValue(debUrl)}" -o "$tmp_file" 453 | gdebi --non-interactive "$tmp_file" 454 | `, 455 | ], 456 | ) 457 | .withSkipIfAny([ 458 | () => isInstalledOsPackage(expectedPackageName), 459 | ]); 460 | } 461 | -------------------------------------------------------------------------------- /src/commands/files/starship.toml: -------------------------------------------------------------------------------- 1 | # Warning: This config does not include keys that have an unset value 2 | format = '$all' 3 | scan_timeout = 30 4 | command_timeout = 500 5 | add_newline = true 6 | 7 | [aws] 8 | format = 'on [$symbol($profile )(\($region\) )]($style)' 9 | symbol = '☁️ ' 10 | style = 'bold yellow' 11 | disabled = true 12 | 13 | [aws.region_aliases] 14 | 15 | [battery] 16 | threshold = 10 17 | style = 'red bold' 18 | 19 | [character] 20 | format = '$symbol ' 21 | success_symbol = '[❯](bold green)' 22 | error_symbol = '[❯](bold red)' 23 | vicmd_symbol = '[❮](bold green)' 24 | disabled = false 25 | 26 | [cmake] 27 | format = 'via [$symbol($version )]($style)' 28 | version_format = 'v${raw}' 29 | symbol = '△ ' 30 | style = 'bold blue' 31 | disabled = false 32 | detect_extensions = [] 33 | detect_files = [ 34 | 'CMakeLists.txt', 35 | 'CMakeCache.txt', 36 | ] 37 | detect_folders = [] 38 | 39 | [cmd_duration] 40 | min_time = 2000 41 | format = 'took [$duration]($style) ' 42 | style = 'yellow bold' 43 | show_milliseconds = false 44 | disabled = false 45 | show_notifications = false 46 | min_time_to_notify = 45000 47 | 48 | [conda] 49 | truncation_length = 1 50 | format = 'via [$symbol$environment]($style) ' 51 | symbol = '🅒 ' 52 | style = 'green bold' 53 | ignore_base = true 54 | disabled = false 55 | 56 | [crystal] 57 | format = 'via [$symbol($version )]($style)' 58 | version_format = 'v${raw}' 59 | symbol = '🔮 ' 60 | style = 'bold red' 61 | disabled = false 62 | detect_extensions = ['cr'] 63 | detect_files = ['shard.yml'] 64 | detect_folders = [] 65 | 66 | [dart] 67 | format = 'via [$symbol($version )]($style)' 68 | version_format = 'v${raw}' 69 | symbol = '🎯 ' 70 | style = 'bold blue' 71 | disabled = false 72 | detect_extensions = ['dart'] 73 | detect_files = [ 74 | 'pubspec.yaml', 75 | 'pubspec.yml', 76 | 'pubspec.lock', 77 | ] 78 | detect_folders = ['.dart_tool'] 79 | 80 | [deno] 81 | format = 'via [$symbol($version )]($style)' 82 | version_format = 'v${raw}' 83 | symbol = '🦕 ' 84 | style = 'green bold' 85 | disabled = false 86 | detect_extensions = [] 87 | detect_files = [ 88 | '.starship.deno', 89 | 'mod.ts', 90 | 'deps.ts', 91 | 'mod.js', 92 | 'deps.js', 93 | ] 94 | detect_folders = [] 95 | 96 | [directory] 97 | truncation_length = 3 98 | truncate_to_repo = true 99 | fish_style_pwd_dir_length = 0 100 | use_logical_path = true 101 | format = '[$path]($style)[$read_only]($read_only_style) ' 102 | style = 'cyan bold' 103 | disabled = false 104 | read_only = '🔒' 105 | read_only_style = 'red' 106 | truncation_symbol = '' 107 | home_symbol = '~' 108 | 109 | [directory.substitutions] 110 | 111 | [docker_context] 112 | symbol = '🐳 ' 113 | style = 'blue bold' 114 | format = 'via [$symbol$context]($style) ' 115 | only_with_files = true 116 | disabled = false 117 | detect_extensions = [] 118 | detect_files = [ 119 | 'docker-compose.yml', 120 | 'docker-compose.yaml', 121 | 'Dockerfile', 122 | ] 123 | detect_folders = [] 124 | 125 | [dotnet] 126 | format = '[$symbol($version )(🎯 $tfm )]($style)' 127 | version_format = 'v${raw}' 128 | symbol = '.NET ' 129 | style = 'blue bold' 130 | heuristic = true 131 | disabled = false 132 | detect_extensions = [ 133 | 'sln', 134 | 'csproj', 135 | 'fsproj', 136 | 'xproj', 137 | ] 138 | detect_files = [ 139 | 'global.json', 140 | 'project.json', 141 | 'Directory.Build.props', 142 | 'Directory.Build.targets', 143 | 'Packages.props', 144 | ] 145 | detect_folders = [] 146 | 147 | [elixir] 148 | format = 'via [$symbol($version \(OTP $otp_version\) )]($style)' 149 | version_format = 'v${raw}' 150 | symbol = '💧 ' 151 | style = 'bold purple' 152 | disabled = false 153 | detect_extensions = [] 154 | detect_files = ['mix.exs'] 155 | detect_folders = [] 156 | 157 | [elm] 158 | format = 'via [$symbol($version )]($style)' 159 | version_format = 'v${raw}' 160 | symbol = '🌳 ' 161 | style = 'cyan bold' 162 | disabled = false 163 | detect_extensions = ['elm'] 164 | detect_files = [ 165 | 'elm.json', 166 | 'elm-package.json', 167 | '.elm-version', 168 | ] 169 | detect_folders = ['elm-stuff'] 170 | 171 | [env_var] 172 | symbol = '' 173 | style = 'black bold dimmed' 174 | format = 'with [$env_value]($style) ' 175 | disabled = false 176 | 177 | [erlang] 178 | format = 'via [$symbol($version )]($style)' 179 | version_format = 'v${raw}' 180 | symbol = ' ' 181 | style = 'bold red' 182 | disabled = false 183 | detect_extensions = [] 184 | detect_files = [ 185 | 'rebar.config', 186 | 'erlang.mk', 187 | ] 188 | detect_folders = [] 189 | 190 | [gcloud] 191 | format = 'on [$symbol$account(@$domain)(\($region\))]($style) ' 192 | symbol = '☁️ ' 193 | style = 'bold blue' 194 | disabled = false 195 | 196 | [gcloud.region_aliases] 197 | 198 | [git_branch] 199 | format = 'on [$symbol$branch]($style)(:[$remote]($style)) ' 200 | symbol = ' ' 201 | style = 'bold purple' 202 | truncation_length = 9223372036854775807 203 | truncation_symbol = '…' 204 | only_attached = false 205 | always_show_remote = false 206 | disabled = false 207 | 208 | [git_commit] 209 | commit_hash_length = 7 210 | format = '[\($hash$tag\)]($style) ' 211 | style = 'green bold' 212 | only_detached = true 213 | disabled = false 214 | tag_symbol = '🏷 ' 215 | tag_disabled = false 216 | 217 | [git_state] 218 | rebase = 'REBASING' 219 | merge = 'MERGING' 220 | revert = 'REVERTING' 221 | cherry_pick = 'CHERRY-PICKING' 222 | bisect = 'BISECTING' 223 | am = 'AM' 224 | am_or_rebase = 'AM/REBASE' 225 | style = 'bold yellow' 226 | format = '\([$state( $progress_current/$progress_total)]($style)\) ' 227 | disabled = false 228 | 229 | [git_status] 230 | format = '([\[$all_status$ahead_behind\]]($style) )' 231 | style = 'red bold' 232 | stashed = '\$' 233 | ahead = '⇡' 234 | behind = '⇣' 235 | diverged = '⇕' 236 | conflicted = '=' 237 | deleted = '✘' 238 | renamed = '»' 239 | modified = '!' 240 | staged = '+' 241 | untracked = '?' 242 | disabled = false 243 | 244 | [golang] 245 | format = 'via [$symbol($version )]($style)' 246 | version_format = 'v${raw}' 247 | symbol = '🐹 ' 248 | style = 'bold cyan' 249 | disabled = false 250 | detect_extensions = ['go'] 251 | detect_files = [ 252 | 'go.mod', 253 | 'go.sum', 254 | 'glide.yaml', 255 | 'Gopkg.yml', 256 | 'Gopkg.lock', 257 | '.go-version', 258 | ] 259 | detect_folders = ['Godeps'] 260 | 261 | [helm] 262 | format = 'via [$symbol($version )]($style)' 263 | version_format = 'v${raw}' 264 | symbol = '⎈ ' 265 | style = 'bold white' 266 | disabled = false 267 | detect_extensions = [] 268 | detect_files = [ 269 | 'helmfile.yaml', 270 | 'Chart.yaml', 271 | ] 272 | detect_folders = [] 273 | 274 | [hg_branch] 275 | symbol = ' ' 276 | style = 'bold purple' 277 | format = 'on [$symbol$branch]($style) ' 278 | truncation_length = 9223372036854775807 279 | truncation_symbol = '…' 280 | disabled = true 281 | 282 | [hostname] 283 | ssh_only = true 284 | trim_at = '.' 285 | format = '[$hostname]($style) in ' 286 | style = 'green dimmed bold' 287 | disabled = false 288 | 289 | [java] 290 | disabled = false 291 | format = 'via [$symbol($version )]($style)' 292 | version_format = 'v${raw}' 293 | style = 'red dimmed' 294 | symbol = '☕ ' 295 | detect_extensions = [ 296 | 'java', 297 | 'class', 298 | 'jar', 299 | 'gradle', 300 | 'clj', 301 | 'cljc', 302 | ] 303 | detect_files = [ 304 | 'pom.xml', 305 | 'build.gradle.kts', 306 | 'build.sbt', 307 | '.java-version', 308 | 'deps.edn', 309 | 'project.clj', 310 | 'build.boot', 311 | ] 312 | detect_folders = [] 313 | 314 | [jobs] 315 | threshold = 1 316 | format = '[$symbol$number]($style) ' 317 | symbol = '✦' 318 | style = 'bold blue' 319 | disabled = false 320 | 321 | [julia] 322 | format = 'via [$symbol($version )]($style)' 323 | version_format = 'v${raw}' 324 | symbol = 'ஃ ' 325 | style = 'bold purple' 326 | disabled = false 327 | detect_extensions = ['jl'] 328 | detect_files = [ 329 | 'Project.toml', 330 | 'Manifest.toml', 331 | ] 332 | detect_folders = [] 333 | 334 | [kotlin] 335 | format = 'via [$symbol($version )]($style)' 336 | version_format = 'v${raw}' 337 | symbol = '🅺 ' 338 | style = 'bold blue' 339 | kotlin_binary = 'kotlin' 340 | disabled = false 341 | detect_extensions = [ 342 | 'kt', 343 | 'kts', 344 | ] 345 | detect_files = [] 346 | detect_folders = [] 347 | 348 | [kubernetes] 349 | symbol = '☸ ' 350 | format = '[$symbol$context( \($namespace\))]($style) in ' 351 | style = 'cyan bold' 352 | disabled = true 353 | 354 | [kubernetes.context_aliases] 355 | 356 | [lua] 357 | format = 'via [$symbol($version )]($style)' 358 | version_format = 'v${raw}' 359 | symbol = '🌙 ' 360 | style = 'bold blue' 361 | lua_binary = 'lua' 362 | disabled = false 363 | detect_extensions = ['lua'] 364 | detect_files = ['.lua-version'] 365 | detect_folders = ['lua'] 366 | 367 | [memory_usage] 368 | threshold = 75 369 | format = 'via $symbol[$ram( | $swap)]($style) ' 370 | style = 'white bold dimmed' 371 | symbol = '🐏 ' 372 | disabled = false 373 | 374 | [nim] 375 | format = 'via [$symbol($version )]($style)' 376 | version_format = 'v${raw}' 377 | symbol = '👑 ' 378 | style = 'yellow bold' 379 | disabled = false 380 | detect_extensions = [ 381 | 'nim', 382 | 'nims', 383 | 'nimble', 384 | ] 385 | detect_files = ['nim.cfg'] 386 | detect_folders = [] 387 | 388 | [nix_shell] 389 | format = 'via [$symbol$state( \($name\))]($style) ' 390 | symbol = '❄️ ' 391 | style = 'bold blue' 392 | impure_msg = 'impure' 393 | pure_msg = 'pure' 394 | disabled = false 395 | 396 | [nodejs] 397 | format = 'via [$symbol($version )]($style)' 398 | version_format = 'v${raw}' 399 | symbol = ' ' 400 | style = 'bold green' 401 | disabled = false 402 | not_capable_style = 'bold red' 403 | detect_extensions = [] 404 | detect_files = [ 405 | 'package.json', 406 | '.node-version', 407 | '.nvmrc', 408 | ] 409 | detect_folders = ['node_modules'] 410 | 411 | [ocaml] 412 | format = 'via [$symbol($version )(\($switch_indicator$switch_name\) )]($style)' 413 | version_format = 'v${raw}' 414 | global_switch_indicator = '' 415 | local_switch_indicator = '*' 416 | symbol = '🐫 ' 417 | style = 'bold yellow' 418 | disabled = false 419 | detect_extensions = [ 420 | 'opam', 421 | 'ml', 422 | 'mli', 423 | 're', 424 | 'rei', 425 | ] 426 | detect_files = [ 427 | 'dune', 428 | 'dune-project', 429 | 'jbuild', 430 | 'jbuild-ignore', 431 | '.merlin', 432 | ] 433 | detect_folders = [ 434 | '_opam', 435 | 'esy.lock', 436 | ] 437 | 438 | [openstack] 439 | format = 'on [$symbol$cloud(\($project\))]($style) ' 440 | symbol = '☁️ ' 441 | style = 'bold yellow' 442 | disabled = false 443 | 444 | [package] 445 | format = 'is [$symbol$version]($style) ' 446 | symbol = '📦 ' 447 | style = '208 bold' 448 | display_private = false 449 | disabled = false 450 | 451 | [perl] 452 | format = 'via [$symbol($version )]($style)' 453 | version_format = 'v${raw}' 454 | symbol = '🐪 ' 455 | style = '149 bold' 456 | disabled = false 457 | detect_extensions = [ 458 | 'pl', 459 | 'pm', 460 | 'pod', 461 | ] 462 | detect_files = [ 463 | 'Makefile.PL', 464 | 'Build.PL', 465 | 'cpanfile', 466 | 'cpanfile.snapshot', 467 | 'META.json', 468 | 'META.yml', 469 | '.perl-version', 470 | ] 471 | detect_folders = [] 472 | 473 | [php] 474 | format = 'via [$symbol($version )]($style)' 475 | version_format = 'v${raw}' 476 | symbol = '🐘 ' 477 | style = '147 bold' 478 | disabled = false 479 | detect_extensions = ['php'] 480 | detect_files = [ 481 | 'composer.json', 482 | '.php-version', 483 | ] 484 | detect_folders = [] 485 | 486 | [purescript] 487 | format = 'via [$symbol($version )]($style)' 488 | version_format = 'v${raw}' 489 | symbol = '<=> ' 490 | style = 'bold white' 491 | disabled = false 492 | detect_extensions = ['purs'] 493 | detect_files = ['spago.dhall'] 494 | detect_folders = [] 495 | 496 | [python] 497 | pyenv_version_name = false 498 | pyenv_prefix = 'pyenv ' 499 | python_binary = [ 500 | 'python', 501 | 'python3', 502 | 'python2', 503 | ] 504 | format = 'via [${symbol}${pyenv_prefix}(${version} )(\($virtualenv\) )]($style)' 505 | version_format = 'v${raw}' 506 | style = 'yellow bold' 507 | symbol = '🐍 ' 508 | disabled = false 509 | detect_extensions = ['py'] 510 | detect_files = [ 511 | 'requirements.txt', 512 | '.python-version', 513 | 'pyproject.toml', 514 | 'Pipfile', 515 | 'tox.ini', 516 | 'setup.py', 517 | '__init__.py', 518 | ] 519 | detect_folders = [] 520 | 521 | [red] 522 | format = 'via [$symbol($version )]($style)' 523 | symbol = '🔺 ' 524 | style = 'red bold' 525 | disabled = false 526 | detect_extensions = [ 527 | 'red', 528 | 'reds', 529 | ] 530 | detect_files = [] 531 | detect_folders = [] 532 | 533 | [ruby] 534 | format = 'via [$symbol($version )]($style)' 535 | version_format = 'v${raw}' 536 | symbol = '💎 ' 537 | style = 'bold red' 538 | disabled = false 539 | detect_extensions = ['rb'] 540 | detect_files = [ 541 | 'Gemfile', 542 | '.ruby-version', 543 | ] 544 | detect_folders = [] 545 | 546 | [rust] 547 | format = 'via [$symbol($version )]($style)' 548 | version_format = 'v${raw}' 549 | symbol = '🦀 ' 550 | style = 'bold red' 551 | disabled = false 552 | detect_extensions = ['rs'] 553 | detect_files = ['Cargo.toml'] 554 | detect_folders = [] 555 | 556 | [scala] 557 | format = 'via [$symbol($version )]($style)' 558 | version_format = 'v${raw}' 559 | disabled = false 560 | style = 'red bold' 561 | symbol = '🆂 ' 562 | detect_extensions = [ 563 | 'sbt', 564 | 'scala', 565 | ] 566 | detect_files = [ 567 | '.scalaenv', 568 | '.sbtenv', 569 | 'build.sbt', 570 | ] 571 | detect_folders = ['.metals'] 572 | 573 | [shell] 574 | format = '$indicator ' 575 | bash_indicator = 'bsh' 576 | fish_indicator = 'fsh' 577 | zsh_indicator = 'zsh' 578 | powershell_indicator = 'psh' 579 | ion_indicator = 'ion' 580 | elvish_indicator = 'esh' 581 | tcsh_indicator = 'tsh' 582 | unknown_indicator = '' 583 | disabled = true 584 | 585 | [shlvl] 586 | threshold = 1 587 | format = '[$symbol$shlvl]($style) ' 588 | symbol = '↕️ ' 589 | repeat = true 590 | style = 'bold yellow' 591 | disabled = false 592 | 593 | [singularity] 594 | symbol = '' 595 | format = '[$symbol\[$env\]]($style) ' 596 | style = 'blue bold dimmed' 597 | disabled = false 598 | 599 | [status] 600 | format = '[$symbol$status]($style) ' 601 | symbol = '✖' 602 | not_executable_symbol = '🚫' 603 | not_found_symbol = '🔍' 604 | sigint_symbol = '🧱' 605 | signal_symbol = '⚡' 606 | style = 'bold red' 607 | map_symbol = false 608 | recognize_signal_code = true 609 | disabled = false 610 | 611 | [swift] 612 | format = 'via [$symbol($version )]($style)' 613 | version_format = 'v${raw}' 614 | symbol = '🐦 ' 615 | style = 'bold 202' 616 | disabled = false 617 | detect_extensions = ['swift'] 618 | detect_files = ['Package.swift'] 619 | detect_folders = [] 620 | 621 | [terraform] 622 | format = 'via [$symbol$workspace]($style) ' 623 | version_format = 'v${raw}' 624 | symbol = '💠 ' 625 | style = 'bold 105' 626 | disabled = false 627 | detect_extensions = [ 628 | 'tf', 629 | 'hcl', 630 | ] 631 | detect_files = [] 632 | detect_folders = ['.terraform'] 633 | 634 | [time] 635 | format = 'at [$time]($style) ' 636 | style = 'bold yellow' 637 | use_12hr = false 638 | disabled = false 639 | utc_time_offset = 'local' 640 | time_range = '-' 641 | 642 | [username] 643 | format = '[$user]($style) in ' 644 | style_root = 'red bold' 645 | style_user = 'yellow bold' 646 | show_always = false 647 | disabled = false 648 | 649 | [vagrant] 650 | format = 'via [$symbol($version )]($style)' 651 | version_format = 'v${raw}' 652 | symbol = '⍱ ' 653 | style = 'cyan bold' 654 | disabled = false 655 | detect_extensions = [] 656 | detect_files = ['Vagrantfile'] 657 | detect_folders = [] 658 | 659 | [zig] 660 | format = 'via [$symbol($version )]($style)' 661 | version_format = 'v${raw}' 662 | symbol = '↯ ' 663 | style = 'bold yellow' 664 | disabled = false 665 | detect_extensions = ['zig'] 666 | detect_files = [] 667 | detect_folders = [] 668 | 669 | [custom] 670 | 671 | -------------------------------------------------------------------------------- /src/commands/gsettings.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../model/command.ts"; 2 | import { targetUser } from "../os/user/target-user.ts"; 3 | import { gsettingsToCmds } from "./common/gsettings-to-cmds.ts"; 4 | import { InstallOsPackage } from "./common/os-package.ts"; 5 | import { Exec } from "./exec.ts"; 6 | import { isDocker } from "../deps.ts"; 7 | import { SimpleValue } from "../fn.ts"; 8 | const docker = await isDocker(); 9 | 10 | const gsettingsExecCommand = (deps: Command[] = []) => { 11 | return (cmd: SimpleValue[]) => 12 | new Exec( 13 | [InstallOsPackage.of("libglib2.0-bin"), ...deps], 14 | [], 15 | targetUser, 16 | {}, 17 | cmd, 18 | ) 19 | .withSkipIfAny([docker]); 20 | }; 21 | 22 | export const gsettingsDisableSomeKeyboardShortcuts = Command.custom() 23 | .withDependencies( 24 | gsettingsToCmds(` 25 | org.freedesktop.ibus.general.hotkey trigger [] 26 | org.freedesktop.ibus.general.hotkey triggers [] 27 | org.gnome.desktop.interface menubar-accel '' 28 | org.gnome.desktop.wm.keybindings activate-window-menu @as [] 29 | org.gnome.desktop.wm.keybindings always-on-top @as [] 30 | org.gnome.desktop.wm.keybindings begin-move @as [] 31 | org.gnome.desktop.wm.keybindings begin-resize @as [] 32 | org.gnome.desktop.wm.keybindings cycle-group @as [] 33 | org.gnome.desktop.wm.keybindings cycle-group-backward @as [] 34 | org.gnome.desktop.wm.keybindings cycle-panels @as [] 35 | org.gnome.desktop.wm.keybindings cycle-panels-backward @as [] 36 | org.gnome.desktop.wm.keybindings cycle-windows @as [] 37 | org.gnome.desktop.wm.keybindings cycle-windows-backward @as [] 38 | org.gnome.desktop.wm.keybindings lower @as [] 39 | org.gnome.desktop.wm.keybindings maximize @as [] 40 | org.gnome.desktop.wm.keybindings maximize-horizontally @as [] 41 | org.gnome.desktop.wm.keybindings maximize-vertically @as [] 42 | org.gnome.desktop.wm.keybindings minimize @as [] 43 | org.gnome.desktop.wm.keybindings move-to-center @as [] 44 | org.gnome.desktop.wm.keybindings move-to-corner-ne @as [] 45 | org.gnome.desktop.wm.keybindings move-to-corner-nw @as [] 46 | org.gnome.desktop.wm.keybindings move-to-corner-se @as [] 47 | org.gnome.desktop.wm.keybindings move-to-corner-sw @as [] 48 | org.gnome.desktop.wm.keybindings move-to-monitor-down @as [] 49 | org.gnome.desktop.wm.keybindings move-to-monitor-left @as [] 50 | org.gnome.desktop.wm.keybindings move-to-monitor-right @as [] 51 | org.gnome.desktop.wm.keybindings move-to-monitor-up @as [] 52 | org.gnome.desktop.wm.keybindings move-to-side-e @as [] 53 | org.gnome.desktop.wm.keybindings move-to-side-n @as [] 54 | org.gnome.desktop.wm.keybindings move-to-side-s @as [] 55 | org.gnome.desktop.wm.keybindings move-to-side-w @as [] 56 | org.gnome.desktop.wm.keybindings move-to-workspace-1 @as [] 57 | org.gnome.desktop.wm.keybindings move-to-workspace-2 @as [] 58 | org.gnome.desktop.wm.keybindings move-to-workspace-3 @as [] 59 | org.gnome.desktop.wm.keybindings move-to-workspace-4 @as [] 60 | org.gnome.desktop.wm.keybindings move-to-workspace-5 @as [] 61 | org.gnome.desktop.wm.keybindings move-to-workspace-6 @as [] 62 | org.gnome.desktop.wm.keybindings move-to-workspace-7 @as [] 63 | org.gnome.desktop.wm.keybindings move-to-workspace-8 @as [] 64 | org.gnome.desktop.wm.keybindings move-to-workspace-9 @as [] 65 | org.gnome.desktop.wm.keybindings move-to-workspace-10 @as [] 66 | org.gnome.desktop.wm.keybindings move-to-workspace-11 @as [] 67 | org.gnome.desktop.wm.keybindings move-to-workspace-12 @as [] 68 | org.gnome.desktop.wm.keybindings move-to-workspace-down @as [] 69 | org.gnome.desktop.wm.keybindings move-to-workspace-last @as [] 70 | org.gnome.desktop.wm.keybindings move-to-workspace-left @as [] 71 | org.gnome.desktop.wm.keybindings move-to-workspace-right @as [] 72 | org.gnome.desktop.wm.keybindings move-to-workspace-up @as [] 73 | org.gnome.desktop.wm.keybindings panel-main-menu @as [] 74 | org.gnome.desktop.wm.keybindings raise @as [] 75 | org.gnome.desktop.wm.keybindings raise-or-lower @as [] 76 | org.gnome.desktop.wm.keybindings set-spew-mark @as [] 77 | org.gnome.desktop.wm.keybindings show-desktop @as [] 78 | org.gnome.desktop.wm.keybindings switch-applications @as [] 79 | org.gnome.desktop.wm.keybindings switch-applications-backward @as [] 80 | org.gnome.desktop.wm.keybindings switch-group @as [] 81 | org.gnome.desktop.wm.keybindings switch-group-backward @as [] 82 | org.gnome.desktop.wm.keybindings switch-input-source @as [] 83 | org.gnome.desktop.wm.keybindings switch-input-source-backward @as [] 84 | org.gnome.desktop.wm.keybindings switch-panels @as [] 85 | org.gnome.desktop.wm.keybindings switch-panels-backward @as [] 86 | org.gnome.desktop.wm.keybindings switch-to-workspace-1 @as [] 87 | org.gnome.desktop.wm.keybindings switch-to-workspace-2 @as [] 88 | org.gnome.desktop.wm.keybindings switch-to-workspace-3 @as [] 89 | org.gnome.desktop.wm.keybindings switch-to-workspace-4 @as [] 90 | org.gnome.desktop.wm.keybindings switch-to-workspace-5 @as [] 91 | org.gnome.desktop.wm.keybindings switch-to-workspace-6 @as [] 92 | org.gnome.desktop.wm.keybindings switch-to-workspace-7 @as [] 93 | org.gnome.desktop.wm.keybindings switch-to-workspace-8 @as [] 94 | org.gnome.desktop.wm.keybindings switch-to-workspace-9 @as [] 95 | org.gnome.desktop.wm.keybindings switch-to-workspace-10 @as [] 96 | org.gnome.desktop.wm.keybindings switch-to-workspace-11 @as [] 97 | org.gnome.desktop.wm.keybindings switch-to-workspace-12 @as [] 98 | org.gnome.desktop.wm.keybindings switch-to-workspace-down @as [] 99 | org.gnome.desktop.wm.keybindings switch-to-workspace-last @as [] 100 | org.gnome.desktop.wm.keybindings switch-to-workspace-left @as [] 101 | org.gnome.desktop.wm.keybindings switch-to-workspace-right @as [] 102 | org.gnome.desktop.wm.keybindings switch-to-workspace-up @as [] 103 | org.gnome.desktop.wm.keybindings toggle-above @as [] 104 | org.gnome.desktop.wm.keybindings toggle-fullscreen @as [] 105 | org.gnome.desktop.wm.keybindings toggle-on-all-workspaces @as [] 106 | org.gnome.desktop.wm.keybindings toggle-shaded [''] 107 | org.gnome.mutter.wayland.keybindings restore-shortcuts @as [] 108 | org.gnome.settings-daemon.plugins.media-keys calculator [''] 109 | org.gnome.settings-daemon.plugins.media-keys control-center [''] 110 | org.gnome.settings-daemon.plugins.media-keys decrease-text-size [''] 111 | org.gnome.settings-daemon.plugins.media-keys eject [''] 112 | org.gnome.settings-daemon.plugins.media-keys email [''] 113 | org.gnome.settings-daemon.plugins.media-keys help @as [] 114 | org.gnome.settings-daemon.plugins.media-keys home [''] 115 | org.gnome.settings-daemon.plugins.media-keys increase-text-size [''] 116 | org.gnome.settings-daemon.plugins.media-keys logout @as [] 117 | org.gnome.settings-daemon.plugins.media-keys magnifier @as [] 118 | org.gnome.settings-daemon.plugins.media-keys magnifier-zoom-in @as [] 119 | org.gnome.settings-daemon.plugins.media-keys magnifier-zoom-out @as [] 120 | org.gnome.settings-daemon.plugins.media-keys media [''] 121 | org.gnome.settings-daemon.plugins.media-keys mic-mute [''] 122 | org.gnome.settings-daemon.plugins.media-keys next [''] 123 | org.gnome.settings-daemon.plugins.media-keys on-screen-keyboard [''] 124 | org.gnome.settings-daemon.plugins.media-keys pause [''] 125 | org.gnome.settings-daemon.plugins.media-keys play [''] 126 | org.gnome.settings-daemon.plugins.media-keys previous [''] 127 | org.gnome.settings-daemon.plugins.media-keys screenreader @as [] 128 | org.gnome.settings-daemon.plugins.media-keys screensaver @as [] 129 | org.gnome.settings-daemon.plugins.media-keys search [''] 130 | org.gnome.settings-daemon.plugins.media-keys stop [''] 131 | org.gnome.settings-daemon.plugins.media-keys terminal @as [] 132 | org.gnome.settings-daemon.plugins.media-keys toggle-contrast [''] 133 | org.gnome.settings-daemon.plugins.media-keys volume-down [''] 134 | org.gnome.settings-daemon.plugins.media-keys volume-mute [''] 135 | org.gnome.settings-daemon.plugins.media-keys volume-up [''] 136 | org.gnome.settings-daemon.plugins.media-keys www [''] 137 | org.gnome.shell.keybindings focus-active-notification @as [] 138 | org.gnome.shell.keybindings open-application-menu @as [] 139 | org.gnome.shell.keybindings show-screen-recording-ui @as [] 140 | org.gnome.shell.keybindings switch-to-application-1 @as [] 141 | org.gnome.shell.keybindings switch-to-application-2 @as [] 142 | org.gnome.shell.keybindings switch-to-application-3 @as [] 143 | org.gnome.shell.keybindings switch-to-application-4 @as [] 144 | org.gnome.shell.keybindings switch-to-application-5 @as [] 145 | org.gnome.shell.keybindings switch-to-application-6 @as [] 146 | org.gnome.shell.keybindings switch-to-application-7 @as [] 147 | org.gnome.shell.keybindings switch-to-application-8 @as [] 148 | org.gnome.shell.keybindings switch-to-application-9 @as [] 149 | org.gnome.shell.keybindings toggle-application-view @as [] 150 | org.gnome.shell.keybindings toggle-message-tray @as [] 151 | org.gnome.shell.keybindings toggle-overview @as [] 152 | `).map(gsettingsExecCommand([ 153 | "ibus", 154 | "mutter", 155 | ].map(InstallOsPackage.of))), 156 | ); 157 | 158 | export const gsettingsEnableSomeKeyboardShortcuts = Command.custom() 159 | .withDependencies( 160 | gsettingsToCmds(` 161 | org.gnome.desktop.input-sources xkb-options ['compose:ralt'] 162 | org.gnome.desktop.peripherals.keyboard remember-numlock-state true 163 | org.gnome.desktop.wm.keybindings close ['q'] 164 | org.gnome.desktop.wm.keybindings panel-run-dialog ['F2'] 165 | org.gnome.desktop.wm.keybindings switch-windows ['Tab'] 166 | org.gnome.desktop.wm.keybindings switch-windows-backward ['Tab'] 167 | org.gnome.desktop.wm.keybindings toggle-maximized ['Up'] 168 | org.gnome.desktop.wm.keybindings unmaximize ['Down'] 169 | org.gnome.desktop.wm.preferences mouse-button-modifier '' 170 | org.gnome.desktop.wm.preferences resize-with-right-button true 171 | org.gnome.mutter.keybindings toggle-tiled-left ['Left'] 172 | org.gnome.mutter.keybindings toggle-tiled-right ['Right'] 173 | org.gnome.shell.keybindings screenshot ['Print'] 174 | org.gnome.shell.keybindings screenshot-window ['Print'] 175 | org.gnome.shell.keybindings show-screenshot-ui ['Print'] 176 | 177 | `).map(gsettingsExecCommand([ 178 | "mutter", 179 | ].map(InstallOsPackage.of))), 180 | ); 181 | 182 | export const gsettingsWindows = Command.custom() 183 | .withDependencies( 184 | gsettingsToCmds(` 185 | org.gnome.desktop.wm.preferences action-double-click-titlebar 'toggle-maximize' 186 | org.gnome.desktop.wm.preferences action-middle-click-titlebar 'toggle-maximize' 187 | org.gnome.desktop.wm.preferences action-right-click-titlebar 'menu' 188 | org.gnome.desktop.wm.preferences audible-bell true 189 | org.gnome.desktop.wm.preferences auto-raise-delay 500 190 | org.gnome.desktop.wm.preferences auto-raise false 191 | org.gnome.desktop.wm.preferences button-layout 'appmenu:minimize,maximize,close' 192 | org.gnome.desktop.wm.preferences disable-workarounds false 193 | org.gnome.desktop.wm.preferences focus-mode 'click' 194 | org.gnome.desktop.wm.preferences focus-new-windows 'smart' 195 | org.gnome.desktop.wm.preferences num-workspaces 1 196 | org.gnome.desktop.wm.preferences raise-on-click true 197 | org.gnome.desktop.wm.preferences resize-with-right-button true 198 | org.gnome.mutter attach-modal-dialogs false 199 | org.gnome.mutter dynamic-workspaces false 200 | org.gnome.mutter workspaces-only-on-primary false 201 | org.gnome.nautilus.window-state start-with-location-bar true 202 | org.gnome.nautilus.window-state start-with-sidebar true 203 | org.gnome.shell.overrides attach-modal-dialogs false 204 | org.gnome.shell.overrides workspaces-only-on-primary false 205 | `).map(gsettingsExecCommand([ 206 | "gnome-shell", 207 | "mutter", 208 | "nautilus", 209 | ].map(InstallOsPackage.of))), 210 | ); 211 | 212 | export const gsettingsPrivacy = Command.custom() 213 | .withDependencies( 214 | gsettingsToCmds(` 215 | org.gnome.desktop.notifications show-in-lock-screen false 216 | org.gnome.desktop.privacy old-files-age uint32 30 217 | org.gnome.desktop.privacy recent-files-max-age 30 218 | org.gnome.desktop.privacy remember-app-usage true 219 | org.gnome.desktop.privacy remember-recent-files true 220 | org.gnome.desktop.privacy remove-old-temp-files true 221 | org.gnome.desktop.privacy remove-old-trash-files true 222 | org.gnome.desktop.privacy report-technical-problems true 223 | org.gnome.desktop.remote-desktop.rdp enable false 224 | org.gnome.desktop.screensaver lock-enabled false 225 | org.gnome.desktop.screensaver ubuntu-lock-on-suspend false 226 | org.gnome.desktop.search-providers disable-external true 227 | org.gnome.desktop.session idle-delay uint32 0 228 | org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing' 229 | org.gnome.shell.weather automatic-location true 230 | org.gnome.system.location enabled true 231 | `).map(gsettingsExecCommand()), 232 | ); 233 | 234 | export const gsettingsLookAndFeel = Command.custom() 235 | .withDependencies( 236 | gsettingsToCmds(` 237 | org.gnome.desktop.interface color-scheme 'prefer-dark' 238 | org.gnome.desktop.interface enable-animations false 239 | org.gnome.desktop.interface gtk-theme 'Yaru-dark' 240 | org.gnome.gedit.preferences.editor scheme 'Yaru-dark' 241 | 242 | org.gnome.gnome-system-monitor cpu-smooth-graph true 243 | org.gnome.gnome-system-monitor cpu-stacked-area-chart true 244 | org.gnome.gnome-system-monitor current-tab 'resources' 245 | org.gnome.gnome-system-monitor disks-interval 5000 246 | org.gnome.gnome-system-monitor graph-update-interval 5000 247 | org.gnome.gnome-system-monitor kill-dialog true 248 | org.gnome.gnome-system-monitor maximized true 249 | 250 | org.gnome.nautilus.icon-view default-zoom-level 'larger' 251 | org.gnome.nautilus.list-view default-zoom-level 'small' 252 | org.gnome.nautilus.list-view use-tree-view false 253 | org.gnome.nautilus.preferences click-policy 'double' 254 | org.gnome.nautilus.preferences default-folder-viewer 'list-view' 255 | org.gnome.nautilus.preferences default-sort-in-reverse-order false 256 | org.gnome.nautilus.preferences default-sort-order 'name' 257 | org.gnome.nautilus.preferences recursive-search 'always' 258 | org.gnome.nautilus.preferences show-directory-item-counts 'always' 259 | org.gnome.nautilus.preferences show-image-thumbnails 'always' 260 | org.gnome.nautilus.preferences thumbnail-limit uint64 1000 261 | 262 | org.gnome.settings-daemon.plugins.color night-light-enabled false 263 | org.gnome.settings-daemon.plugins.color night-light-schedule-automatic false 264 | org.gnome.settings-daemon.plugins.color night-light-schedule-from 20.0 265 | org.gnome.settings-daemon.plugins.color night-light-schedule-to 5.0 266 | org.gnome.settings-daemon.plugins.color night-light-temperature uint32 2330 267 | `).map(gsettingsExecCommand([ 268 | "gnome-settings-daemon", 269 | "gnome-system-monitor", 270 | "nautilus", 271 | ].map(InstallOsPackage.of))), 272 | ); 273 | 274 | export const gsettingsUsefulDefaults = Command.custom() 275 | .withDependencies( 276 | gsettingsToCmds(` 277 | org.gnome.FileRoller.Dialogs.Extract recreate-folders true 278 | org.gnome.SessionManager auto-save-session false 279 | org.gnome.SessionManager auto-save-session-one-shot false 280 | org.gnome.desktop.notifications show-banners true 281 | org.gnome.desktop.peripherals.mouse natural-scroll false 282 | org.gnome.nautilus.compression default-compression-format 'tar.xz' 283 | `).map(gsettingsExecCommand([ 284 | "file-roller", 285 | "gnome-session", 286 | "nautilus", 287 | ].map(InstallOsPackage.of))), 288 | ); 289 | 290 | export const gsettingsLocalisation = Command.custom() 291 | .withDependencies( 292 | gsettingsToCmds(` 293 | org.gnome.desktop.calendar show-weekdate true 294 | org.gnome.desktop.datetime automatic-timezone true 295 | org.gnome.desktop.input-sources per-window false 296 | org.gnome.desktop.interface clock-format '24h' 297 | org.gnome.desktop.interface clock-show-date true 298 | org.gnome.desktop.interface clock-show-seconds false 299 | org.gnome.desktop.interface clock-show-weekday true 300 | org.gnome.gedit.plugins.time custom-format '%Y-%m-%d %H:%M:%S' 301 | org.gnome.gedit.plugins.time selected-format '%c' 302 | org.gtk.Settings.FileChooser clock-format '24h' 303 | `).map(gsettingsExecCommand([ 304 | "gedit", 305 | ].map(InstallOsPackage.of))), 306 | ); 307 | 308 | export const gsettingsGedit = Command.custom() 309 | .withDependencies( 310 | gsettingsToCmds(` 311 | org.gnome.gedit.preferences.editor auto-indent true 312 | org.gnome.gedit.preferences.editor auto-save-interval uint32 10 313 | org.gnome.gedit.preferences.editor auto-save true 314 | org.gnome.gedit.preferences.editor background-pattern 'grid' 315 | org.gnome.gedit.preferences.editor bracket-matching true 316 | org.gnome.gedit.preferences.editor create-backup-copy false 317 | org.gnome.gedit.preferences.editor display-line-numbers true 318 | org.gnome.gedit.preferences.editor display-right-margin true 319 | org.gnome.gedit.preferences.editor ensure-trailing-newline true 320 | org.gnome.gedit.preferences.editor highlight-current-line true 321 | org.gnome.gedit.preferences.editor insert-spaces true 322 | org.gnome.gedit.preferences.editor restore-cursor-position true 323 | org.gnome.gedit.preferences.editor right-margin-position uint32 100 324 | org.gnome.gedit.preferences.editor scheme 'oblivion' 325 | org.gnome.gedit.preferences.editor search-highlighting true 326 | org.gnome.gedit.preferences.editor smart-home-end 'after' 327 | org.gnome.gedit.preferences.editor syntax-highlighting true 328 | org.gnome.gedit.preferences.editor tabs-size uint32 4 329 | `).map(gsettingsExecCommand([ 330 | "gedit", 331 | ].map(InstallOsPackage.of))), 332 | ); 333 | 334 | export const gsettingsMeld = Command.custom() 335 | .withDependencies( 336 | gsettingsToCmds(` 337 | org.gnome.meld draw-spaces ['space', 'tab', 'newline', 'nbsp', 'leading', 'text', 'trailing'] 338 | org.gnome.meld filename-filters [('Backups', true, '#*# .#* ~* *~ *.{orig,bak,swp}'), ('OS-specific metadata', true, '.DS_Store ._* .Spotlight-V100 .Trashes Thumbs.db Desktop.ini'), ('Version Control', true, '_MTN .bzr .svn .svn .hg .fslckout _FOSSIL_ .fos CVS _darcs .git .svn .osc'), ('Binaries', true, '*.{pyc,a,obj,o,so,la,lib,dll,exe}'), ('Media', false, '*.{jpg,gif,png,bmp,wav,mp3,ogg,flac,avi,mpg,xcf,xpm}'), ('node_modules', true, 'node_modules'), ('.idea', true, '.idea'), ('.isolate-in-docker', true, '.isolate-in-docker'), ('.nyc_output', true, '.nyc_output')] 339 | org.gnome.meld highlight-current-line true 340 | org.gnome.meld highlight-syntax true 341 | org.gnome.meld insert-spaces-instead-of-tabs true 342 | org.gnome.meld show-line-numbers true 343 | `).map(gsettingsExecCommand([ 344 | "meld", 345 | ].map(InstallOsPackage.of))), 346 | ); 347 | 348 | export const gsettingsAll: Command = Command.custom().withDependencies([ 349 | gsettingsDisableSomeKeyboardShortcuts, 350 | gsettingsEnableSomeKeyboardShortcuts, 351 | gsettingsGedit, 352 | gsettingsLocalisation, 353 | gsettingsLookAndFeel, 354 | gsettingsMeld, 355 | gsettingsPrivacy, 356 | gsettingsUsefulDefaults, 357 | gsettingsWindows, 358 | ]); 359 | --------------------------------------------------------------------------------