small string to setup an ubuntu host with some personal settings
0
ubuntu-setup.sh
1#!/usr/bin/env bash
2set -euo pipefail
3
4# Install nushell + shell integrations (carapace, atuin, starship, zoxide, direnv)
5# Designed for Ubuntu hosts
6
7echo "=> Updating apt and installing prerequisites..."
8sudo apt update && sudo apt install -y curl git unzip
9
10get_latest_version() {
11 curl -sL "https://api.github.com/repos/$1/releases/latest" \
12 | grep '"tag_name"' \
13 | head -1 \
14 | sed -E 's/.*"([^"]+)".*/\1/'
15}
16
17ARCH=$(uname -m)
18
19# --- nushell ---
20if ! command -v nu &>/dev/null; then
21 echo "=> Installing nushell..."
22 case "$ARCH" in
23 x86_64) NU_ARCH="x86_64" ;;
24 aarch64) NU_ARCH="aarch64" ;;
25 *) echo "Unsupported arch: $ARCH"; exit 1 ;;
26 esac
27 NU_TAG=$(get_latest_version "nushell/nushell")
28 NU_VERSION=${NU_TAG#v}
29 NU_URL="https://github.com/nushell/nushell/releases/download/${NU_TAG}/nu-${NU_VERSION}-${NU_ARCH}-unknown-linux-gnu.tar.gz"
30 TMPDIR=$(mktemp -d)
31 echo " Downloading nushell ${NU_VERSION}..."
32 curl -sL "$NU_URL" | tar xz -C "$TMPDIR"
33 sudo cp "${TMPDIR}/nu-${NU_VERSION}-${NU_ARCH}-unknown-linux-gnu/nu" /usr/local/bin/
34 sudo cp "${TMPDIR}/nu-${NU_VERSION}-${NU_ARCH}-unknown-linux-gnu/plugin_"* /usr/local/bin/ 2>/dev/null || true
35 rm -rf "$TMPDIR"
36 echo " Installed nushell $(nu --version)"
37else
38 echo "=> nushell already installed ($(nu --version))"
39fi
40
41# --- carapace ---
42if ! command -v carapace &>/dev/null; then
43 echo "=> Installing carapace..."
44 case "$ARCH" in
45 x86_64) CAP_ARCH="amd64" ;;
46 aarch64) CAP_ARCH="arm64" ;;
47 esac
48 CAP_TAG=$(get_latest_version "carapace-sh/carapace-bin")
49 CAP_VERSION=${CAP_TAG#v}
50 CAP_URL="https://github.com/carapace-sh/carapace-bin/releases/download/${CAP_TAG}/carapace-bin_${CAP_VERSION}_linux_${CAP_ARCH}.tar.gz"
51 TMPDIR=$(mktemp -d)
52 echo " Downloading carapace ${CAP_VERSION}..."
53 curl -sL "$CAP_URL" | tar xz -C "$TMPDIR"
54 sudo cp "${TMPDIR}/carapace" /usr/local/bin/
55 rm -rf "$TMPDIR"
56 echo " Installed carapace ${CAP_VERSION}"
57else
58 echo "=> carapace already installed"
59fi
60
61# --- atuin ---
62if ! command -v atuin &>/dev/null; then
63 echo "=> Installing atuin..."
64 curl --proto '=https' --tlsv1.2 -sSf https://setup.atuin.sh | sh
65else
66 echo "=> atuin already installed ($(atuin --version))"
67fi
68
69# --- starship ---
70if ! command -v starship &>/dev/null; then
71 echo "=> Installing starship..."
72 curl -sS https://starship.rs/install.sh | sh -s -- -y
73else
74 echo "=> starship already installed ($(starship --version))"
75fi
76
77# --- zoxide ---
78if ! command -v zoxide &>/dev/null; then
79 echo "=> Installing zoxide..."
80 curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh
81else
82 echo "=> zoxide already installed"
83fi
84
85# --- direnv ---
86if ! command -v direnv &>/dev/null; then
87 echo "=> Installing direnv..."
88 sudo apt install -y direnv
89else
90 echo "=> direnv already installed"
91fi
92
93# --- fastfetch (optional, for pokefetch) ---
94if ! command -v fastfetch &>/dev/null; then
95 echo "=> Installing fastfetch..."
96 sudo apt install -y fastfetch 2>/dev/null || {
97 case "$ARCH" in
98 x86_64) FF_ARCH="amd64" ;;
99 aarch64) FF_ARCH="aarch64" ;;
100 esac
101 FF_TAG=$(get_latest_version "fastfetch-cli/fastfetch")
102 FF_VERSION=${FF_TAG#v}
103 TMPDIR=$(mktemp -d)
104 echo " Downloading fastfetch ${FF_VERSION}..."
105 curl -sL "https://github.com/fastfetch-cli/fastfetch/releases/download/${FF_TAG}/fastfetch-linux-${FF_ARCH}.deb" -o "${TMPDIR}/fastfetch.deb"
106 sudo dpkg -i "${TMPDIR}/fastfetch.deb"
107 rm -rf "$TMPDIR"
108 echo " Installed fastfetch ${FF_VERSION}"
109 }
110else
111 echo "=> fastfetch already installed"
112fi
113
114# --- pokeget-rs (optional, for pokefetch) ---
115if ! command -v pokeget &>/dev/null; then
116 echo "=> Installing pokeget-rs..."
117 case "$ARCH" in
118 x86_64) PG_ARCH="x86_64" ;;
119 aarch64) PG_ARCH="aarch64" ;;
120 esac
121 PG_TAG=$(get_latest_version "talwat/pokeget-rs")
122 PG_VERSION=${PG_TAG#v}
123 TMPDIR=$(mktemp -d)
124 echo " Downloading pokeget ${PG_VERSION}..."
125 curl -sL "https://github.com/talwat/pokeget-rs/releases/download/${PG_TAG}/pokeget-Linux-${PG_ARCH}.tar.gz" | tar xz -C "$TMPDIR"
126 sudo cp "${TMPDIR}/pokeget" /usr/local/bin/ 2>/dev/null || { echo " Could not find pokeget binary in archive, skipping"; }
127 rm -rf "$TMPDIR"
128 echo " Installed pokeget ${PG_VERSION}"
129else
130 echo "=> pokeget already installed"
131fi
132
133# --- nushell config ---
134echo "=> Setting up nushell config..."
135
136read -r -p " Reset all config files to defaults? [y/N] " RESET </dev/tty
137RESET=${RESET:-N}
138
139CONFIG_FILES=(
140 ~/.config/nushell/env.nu
141 ~/.config/nushell/config.nu
142 ~/.config/nushell/direnv.nu
143 ~/.config/nushell/vendor/zoxide.nu
144 ~/.config/atuin/config.toml
145 ~/.config/fastfetch/config.jsonc
146 ~/.config/starship.toml
147 ~/.cache/carapace/init.nu
148 ~/.local/share/atuin/init.nu
149)
150
151if [[ "${RESET,,}" == "y" ]]; then
152 echo " Removing existing config files..."
153 for f in "${CONFIG_FILES[@]}"; do
154 rm -f "$f"
155 done
156fi
157
158mkdir -p ~/.config/nushell ~/.config/nushell/vendor ~/.config/atuin ~/.cache/carapace ~/.local/share/atuin
159
160tee ~/.config/nushell/direnv.nu > /dev/null << 'DIRENV_EOF'
161$env.config = {
162 hooks: {
163 pre_prompt: [{ ||
164 if (which direnv | is-empty) {
165 return
166 }
167
168 direnv export json | from json | default {} | load-env
169 if 'ENV_CONVERSIONS' in $env and 'PATH' in $env.ENV_CONVERSIONS {
170 $env.PATH = do $env.ENV_CONVERSIONS.PATH.from_string $env.PATH
171 }
172 }]
173 }
174}
175DIRENV_EOF
176
177zoxide init nushell > ~/.config/nushell/vendor/zoxide.nu
178
179tee ~/.config/nushell/config.nu > /dev/null << 'NU_CONFIG_EOF'
180use std "path add"
181
182$env.config.show_banner = false
183
184path add "~/bin"
185path add "~/.local/bin"
186path add "~/.atuin/bin"
187
188source ./direnv.nu
189
190# carapace completions
191source ~/.cache/carapace/init.nu
192
193# atuin
194source ~/.local/share/atuin/init.nu
195
196# starship prompt
197$env.STARSHIP_SHELL = "nu"
198$env.PROMPT_COMMAND = { || starship prompt --cmd-duration $env.CMD_DURATION_MS }
199$env.PROMPT_COMMAND_RIGHT = ""
200$env.PROMPT_INDICATOR = ""
201$env.PROMPT_INDICATOR_VI_INSERT = ""
202$env.PROMPT_INDICATOR_VI_NORMAL = ""
203
204# zoxide
205source ./vendor/zoxide.nu
206
207
208def cl [] {
209 clear
210 if $env.TMUX? == null {
211 pokefetch
212 }
213}
214
215def pokefetch [] {
216 let FETCHER_CMD = "fastfetch --logo none"
217 let EXTRA_PADDING_H = 2
218 let EXTRA_PADDING_W = 2
219 let WIDTH = 38
220
221 let pokemon_name = (hostname)
222
223 let sprite = (pokeget $pokemon_name --hide-name | complete | get stdout)
224 let fetcher_height = (bash -c $FETCHER_CMD | lines | length)
225 let sprite_height = ($sprite | lines | length)
226
227 let pad_top = (($fetcher_height - $sprite_height) / 2 + $EXTRA_PADDING_H)
228 let pad_top = (if $pad_top < 0 { 0 } else { $pad_top })
229
230 let sprite_width = ($sprite | lines | each { |line|
231 $line | ansi strip | split chars | length
232 } | math max)
233
234 let pad_horizontal = (($WIDTH - $sprite_width) / 2 + $EXTRA_PADDING_W)
235 let pad_horizontal = (if $pad_horizontal < 0 { 0 } else { $pad_horizontal })
236 let pad_horizontal = ($pad_horizontal | math floor)
237 let pad_top = ($pad_top | math floor)
238
239 $sprite | fastfetch --file-raw - --logo-padding $pad_horizontal --logo-padding-top $pad_top
240}
241
242def ghosttify [user: string, host: string] {
243 let command = ["infocmp -x xterm-ghostty | ssh ", $user, "@", $host, " -- tic -x -"]
244 bash -c ($command | str join)
245}
246
247def --env --wrapped zc [...args: string] {
248 if $args == null or $args == [] {
249 cd ~
250 } else {
251 __zoxide_z ...$args
252 }
253}
254
255def --env --wrapped zic [...args: string] {
256 if $args == null or $args == [] {
257 __zoxide_zi
258 } else {
259 __zoxide_zi ...$args
260 }
261}
262
263if $env.TERM? == "xterm-ghostty" {
264 pokefetch
265}
266NU_CONFIG_EOF
267
268tee ~/.config/nushell/env.nu > /dev/null << 'NU_ENV_EOF'
269# env.nu - environment variables loaded before config.nu
270$env.ENV_CONVERSIONS = {
271 "PATH": {
272 from_string: { |s| $s | split row (char esep) | path expand --no-symlink }
273 to_string: { |v| $v | path expand --no-symlink | str join (char esep) }
274 }
275}
276NU_ENV_EOF
277
278# --- fastfetch config ---
279mkdir -p ~/.config/fastfetch
280tee ~/.config/fastfetch/config.jsonc > /dev/null << 'FASTFETCH_EOF'
281{
282 "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
283 "display": {
284 "separator": " "
285 },
286 "modules": [
287 {
288 "key": "\n",
289 "type": "custom"
290 },
291 {
292 "key": "{#39}╭───────────╮",
293 "type": "custom"
294 },
295 {
296 "key": "{#39}│ {#31} user {#39}│",
297 "type": "title",
298 "format": "{1}"
299 },
300 {
301 "key": "{#39}│ {#32} host {#39}│",
302 "type": "title",
303 "format": "{2}"
304 },
305 {
306 "key": "{#39}│ {#34} distro {#39}│",
307 "type": "os",
308 "format": "{3}"
309 },
310 {
311 "key": "{#39}│ {#35} kernel {#39}│",
312 "type": "kernel",
313 "format": "{1} {2}"
314 },
315 {
316 "key": "{#39}│ {#33} uptime {#39}│",
317 "type": "uptime",
318 "format": "{2} Hrs {3} Mins"
319 },
320 {
321 "key": "{#39}│ {#36} desktop {#39}│",
322 "type": "de"
323 },
324 {
325 "key": "{#39}│ {#31} term {#39}│",
326 "type": "terminal",
327 "format": "{5}"
328 },
329 {
330 "key": "{#39}│ {#32} shell {#39}│",
331 "type": "shell",
332 "format": "{6}"
333 },
334 {
335 "key": "{#39}│ {#34} disk {#39}│",
336 "type": "disk",
337 "folders": "/",
338 "format": "{1} / {2} ({3})"
339 },
340 {
341 "key": "{#39}│ {#35} memory {#39}│",
342 "type": "memory",
343 "format": "{1} / {2} ({3})"
344 },
345 {
346 "key": "{#39}│ {#36} network {#39}│",
347 "type": "localip",
348 "format": "{1}"
349 },
350 {
351 "key": "{#39}├───────────┤",
352 "type": "custom"
353 },
354 {
355 "key": "{#39}│ {#39} colors {#39}│ ",
356 "type": "colors",
357 "symbol": "circle"
358 },
359 {
360 "key": "{#39}╰───────────╯",
361 "type": "custom"
362 }
363 ]
364}
365FASTFETCH_EOF
366
367# --- atuin config ---
368tee ~/.config/atuin/config.toml > /dev/null << 'ATUIN_EOF'
369search_mode = "fuzzy"
370filter_mode = "host"
371workspaces = true
372style = "compact"
373inline_height = 10
374invert = false
375show_preview = true
376show_help = false
377show_tabs = false
378enter_accept = true
379
380[stats]
381common_subcommands = [
382 "apt",
383 "cargo",
384 "composer",
385 "dnf",
386 "docker",
387 "git",
388 "go",
389 "ip",
390 "kubectl",
391 "nix",
392 "nmcli",
393 "npm",
394 "pecl",
395 "pnpm",
396 "podman",
397 "port",
398 "systemctl",
399 "tmux",
400 "yarn",
401 "bun",
402 "hx",
403]
404common_prefix = ["sudo"]
405ignored_commands = ["cd", "ls", "l", "z"]
406
407[sync]
408records = true
409
410[theme]
411name = "oscura-midnight"
412ATUIN_EOF
413
414# --- starship config ---
415tee ~/.config/starship.toml > /dev/null << 'STARSHIP_EOF'
416format = "($nix_shell$container$git_metrics)$cmd_duration$hostname$localip$shlvl$shell$env_var$sudo$character\n"
417palette = "poimandres"
418right_format = "$singularity$kubernetes$directory$vcsh$fossil_branch$git_branch$git_commit$git_state$git_status$hg_branch$pijul_channel$docker_context$package$c$cpp$cmake$cobol$daml$dart$deno$dotnet$elixir$elm$erlang$fennel$fortran$golang$guix_shell$haskell$haxe$helm$java$julia$kotlin$gradle$lua$nim$nodejs$ocaml$opa$perl$php$pulumi$purescript$python$raku$rlang$red$ruby$rust$scala$solidity$swift$terraform$vlang$vagrant$xmake$zig$buf$conda$pixi$meson$spack$memory_usage$aws$gcloud$openstack$azure$crystal$custom$status$os$battery$time\n"
419
420[c]
421disabled = false
422format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
423style = "bg:overlay fg:pine"
424symbol = " "
425
426[directory]
427format = "[](fg:overlayd)[ $path ]($style)[](fg:overlayd) "
428style = "bg:overlayd fg:pine"
429truncate_to_repo = false
430truncation_length = 5
431truncation_symbol = ""
432
433[directory.substitutions]
434Documents = ""
435Downloads = " "
436Music = " "
437Pictures = " "
438nixos = " "
439
440[elixir]
441disabled = false
442format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
443style = "bg:overlay fg:pine"
444symbol = " "
445
446[elm]
447disabled = false
448format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
449style = "bg:overlay fg:pine"
450symbol = " "
451
452[fill]
453style = "fg:overlay"
454symbol = " "
455
456[git_branch]
457format = "[](fg:overlaydd)[ $symbol $branch ]($style)[](fg:overlaydd) "
458style = "bg:overlaydd fg:foam"
459symbol = ""
460
461[git_status]
462ahead = "[⇡(${count})](bg:overlaydd fg:foam)"
463behind = "[⇣(${count})](bg:overlaydd fg:rose)"
464deleted = "[✘($count)](style)"
465disabled = false
466diverged = "⇕[[](bg:overlaydd fg:iris)[⇡(${ahead_count})](bg:overlaydd fg:foam)[⇣(${behind_count})](bg:overlaydd fg:rose)[]](bg:overlaydd fg:iris)"
467format = "[](fg:overlaydd)([$all_status$ahead_behind]($style))[](fg:overlaydd) "
468modified = "[!($count)](bg:overlaydd fg:gold)"
469renamed = "[»($count)](bg:overlaydd fg:iris)"
470staged = "[++($count)](bg:overlaydd fg:gold)"
471stashed = "[($count)](bg:overlaydd fg:gold)"
472style = "bg:overlaydd fg:love"
473untracked = "[?($count)](bg:overlaydd fg:gold)"
474up_to_date = "[ ✓ ](bg:overlaydd fg:iris)"
475
476[golang]
477disabled = false
478format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
479style = "bg:overlay fg:pine"
480symbol = " "
481
482[haskell]
483disabled = false
484format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
485style = "bg:overlay fg:pine"
486symbol = " "
487
488[hostname]
489disabled = false
490format = "[](fg:overlayd)[ $hostname ]($style)[](fg:overlayd) "
491ssh_only = false
492style = "bg:overlayd fg:iris"
493
494[java]
495disabled = false
496format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
497style = "bg:overlay fg:pine"
498symbol = " "
499
500[julia]
501disabled = false
502format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
503style = "bg:overlay fg:pine"
504symbol = " "
505
506[nim]
507disabled = false
508format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
509style = "bg:overlay fg:pine"
510symbol = " "
511
512[nodejs]
513disabled = false
514format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
515style = "bg:overlay fg:pine"
516symbol = " "
517
518[palettes.base16]
519base00 = "#191724"
520base01 = "#1f1d2e"
521base02 = "#26233a"
522base03 = "#6e6a86"
523base04 = "#908caa"
524base05 = "#e0def4"
525base06 = "#e0def4"
526base07 = "#524f67"
527base08 = "#eb6f92"
528base09 = "#f6c177"
529base0A = "#ebbcba"
530base0B = "#31748f"
531base0C = "#9ccfd8"
532base0D = "#c4a7e7"
533base0E = "#f6c177"
534base0F = "#524f67"
535base10 = "#191724"
536base11 = "#191724"
537base12 = "#eb6f92"
538base13 = "#ebbcba"
539base14 = "#31748f"
540base15 = "#9ccfd8"
541base16 = "#c4a7e7"
542base17 = "#f6c177"
543black = "#191724"
544blue = "#c4a7e7"
545bright-black = "#6e6a86"
546bright-blue = "#c4a7e7"
547bright-cyan = "#9ccfd8"
548bright-green = "#31748f"
549bright-magenta = "#f6c177"
550bright-purple = "#f6c177"
551bright-red = "#eb6f92"
552bright-white = "#524f67"
553bright-yellow = "#ebbcba"
554brown = "#524f67"
555cyan = "#9ccfd8"
556green = "#31748f"
557magenta = "#f6c177"
558orange = "#f6c177"
559purple = "#f6c177"
560red = "#eb6f92"
561white = "#e0def4"
562yellow = "#ebbcba"
563
564[palettes.oscura-midnight]
565foam = "#54c0a3"
566gold = "#f9b98c"
567iris = "#e6e7a3"
568love = "#d84f68"
569overlay = "#232323"
570overlayd = "#161616"
571overlaydd = "#0b0b0f"
572pine = "#4ebe96"
573rose = "#e6e7a3"
574
575[palettes.poimandres]
576foam = "#54c0a3"
577gold = "#add7ff"
578iris = "#ffffff"
579love = "#d0679d"
580overlay = "#252b37"
581overlayd = "#171922"
582overlaydd = "#1b1e28"
583pine = "#4ebe96"
584rose = "#5de4c7"
585
586[python]
587disabled = false
588format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
589style = "bg:overlay fg:pine"
590symbol = " "
591
592[rust]
593disabled = false
594format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
595style = "bg:overlay fg:pine"
596symbol = " "
597
598[scala]
599disabled = false
600format = "[](fg:overlay)[$symbol$version]($style)[](fg:overlay) "
601style = "bg:overlay fg:pine"
602symbol = " "
603
604[time]
605disabled = false
606format = "[](fg:overlay)[ $time ]($style)[](fg:overlay)"
607style = "bg:overlay fg:rose"
608time_format = "%I:%M%P"
609use_12hr = true
610
611[username]
612disabled = false
613format = "[](fg:overlay)[ $user ]($style)[](fg:overlay) "
614show_always = true
615style_root = "bg:overlay fg:iris"
616style_user = "bg:overlay fg:iris"
617STARSHIP_EOF
618
619# --- carapace init ---
620echo "=> Initializing carapace..."
621carapace _carapace nushell | sed 's|"/homeless-shelter|$"($env.HOME)|g' > ~/.cache/carapace/init.nu
622
623# --- atuin init ---
624echo "=> Initializing atuin..."
625atuin init nu > ~/.local/share/atuin/init.nu
626
627# --- disable motd ---
628if [ -f ~/.hushlogin ]; then
629 echo "=> motd already disabled"
630else
631 echo "=> Disabling motd..."
632 touch ~/.hushlogin
633fi
634
635# --- set nushell as default shell ---
636CURRENT_SHELL=$(getent passwd "$USER" | cut -d: -f7)
637if [[ "$CURRENT_SHELL" != *"/nu"* ]]; then
638 echo "=> Setting nushell as default shell..."
639 NU_PATH=$(which nu)
640 if ! grep -q "$NU_PATH" /etc/shells 2>/dev/null; then
641 echo "$NU_PATH" | sudo tee -a /etc/shells
642 fi
643 sudo chsh -s "$NU_PATH" "$USER"
644 echo " Default shell changed to nushell (takes effect on next login)"
645else
646 echo "=> nushell is already the default shell"
647fi
648
649echo ""
650echo "=> Done! Log out and back in (or run 'nu') to start using nushell."