chore: first commit

This commit is contained in:
Clément Désiles 2025-07-25 20:23:54 +02:00
parent 5c4016357f
commit c612cc7839
88 changed files with 3255 additions and 0 deletions

29
.editorconfig Normal file
View File

@ -0,0 +1,29 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8
# 2 space indentation
[*.{yml,sh},Vagrantfile,Dockerfile*]
indent_size = 2
indent_style = space
# 4 space indentation
[*.{py,md}]
indent_size = 4
indent_style = space
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
tab_width = 8

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
inventory/*
!inventory/hosts.example
!inventory/host_vars/
inventory/host_vars/*
!inventory/host_vars/example.yml
inventory_data/
playbooks/*
!playbooks/example.yml
TODO.md

22
GIST.md Normal file
View File

@ -0,0 +1,22 @@
# Ansible Gists
Scripts that took time to write and that I'm not sure I will use again.
## Convert netmask to CIDR notation
```yml
- name: Convert netmask to CIDR notation
set_fact:
cidr_notation: >-
{{
(interface.netmask.split('.') |
map('int') |
map('string') |
map('regex_replace', '^(.*)$', '\\1|int|format("08b")') |
map('regex_replace', '^(.*)$', '{{\\1}}') |
join('') |
regex_replace('0+$', '') |
length)
}}
when: interface.netmask is defined and network_file.stat.exists != true
```

40
README.md Normal file
View File

@ -0,0 +1,40 @@
# Homelab Ansible Playbooks
This repository contains Ansible playbooks and roles I use to manage my NAS and several VMs 👨‍💻.
This project is designed for personal/familial scale maintenance, if you find this useful for your use, want to share advises or security concerns, feel free to drop me a line.
This is a good playground to learn and I encourage you to adapt these roles to your needs. While they might not be production-ready for all environments, I'm open to adapting them for [Ansible Galaxy]((https://galaxy.ansible.com)) if there's community interest!
## Requirements
```sh
ansible-galaxy collection install -r requirements.yml
```
## Usage
```sh
ansible-playbook -i inventory.yml playbook.yml
```
## Target devices configuration
Requirements:
- sshd up and running
- public key copied:
```sh
ssh-copy-id -i ~/.ssh/id_rsa.pub username@remote_host
```
- python3 installed (`pacman -Syu python3`)
## Developping
Linting:
```sh
npx prettier --write .
```

5
ansible.cfg Normal file
View File

@ -0,0 +1,5 @@
[defaults]
interpreter_python=/usr/bin/python3
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ControlPath=/tmp/ansible-ssh-%h-%p-%r

View File

@ -0,0 +1,113 @@
# Network configuration
# ---------------------
network_interfaces:
- name: lan0
type: ethernet
mac_address: 02:a0:c9:8d:7e:b6
ipv4:
address: 192.168.1.2/24
gateway: 192.168.1.254
nameservers:
- 1.1.1.1
- 8.8.8.8
- name: lan1
type: ethernet
mac_address: 0a:3f:5b:1c:d2:e4
# NTP servers configuration
# -------------------------
ntp_pools:
- "0.uk.pool.ntp.org"
- "1.uk.pool.ntp.org"
- "2.uk.pool.ntp.org"
- "3.uk.pool.ntp.org"
ntp_timezone: "Europe/London"
ntp_allowed_networks:
- "127.0.0.1"
- "::1"
- "192.168.1.0 mask 255.255.255.0"
- "192.168.20.0 mask 255.255.255.224"
ntp_firewall_allowed_sources:
- 192.168.1.0/24 # lan0
- 192.168.20.0/27 # wg0
disk_partitioning:
- device: /dev/nvme0n1
layout_file: inventory_data/partition_layouts/omer.nvme0n1.sfdisk
partitions:
- name: EFI
device: /dev/nvme0n1p1
size: 512M
type: EFI
- name: SWAP
device: /dev/nvme0n1p2
size: 1G
type: swap
- name: ROOT
device: /dev/nvme0n1p3
size: 500G
type: ext4
- name: SLOG
device: /dev/nvme0n1p4
size: 400G
type: zfs
- name: CLUB
device: /dev/nvme0n1p5
size: 2.7TiB
type: zfs
# ZFS pool configuration
# ----------------------
zfs_pools:
- name: omer
type: raidz1
devices:
- ata-SAMSUNG_MZ7LN512HMJP-00000_S1G2NSAF934567
- ata-SAMSUNG_MZ7LN512HMJP-00000_S1G3NSAF934568
options:
ashift: 12
root: /mnt/omer
state: present
zfs_datasets:
- name: omer/photos
extra_zfs_properties:
mountpoint: /mnt/omer/photos
state: present
- name: omer/movies
extra_zfs_properties:
mountpoint: /mnt/omer/movies
state: present
# Wireguard "client" VPN configuration
# ------------------------------------
wireguard_address: 192.168.20.4/27
wireguard_peers:
- name: "Marge server"
public_key: fB6zC8oWpQxN4yR2sT1uA7vJ9kH3mG5eD0cLlI8bV6aF2dP3eXwZ1qY4rU7tO9
allowed_ips:
- 192.168.20.1/32
endpoint: 192.168.1.56:51820
wireguard_dns: 192.168.20.1
wireguard_server_mode: false
# NFS server configuration
# ------------------------
nfs_clients:
- name: all_wg0_rw_clients
host: "192.168.20.0/255.255.255.224"
options: "rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000,insecure"
- name: laptop_lan0_rw_clients
host: "192.168.1.167"
options: "rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000,insecure"
nfs_shares:
- dir: /mnt/omer/movies
clients: "{{ nfs_clients }}"
- dir: /mnt/omer/photos
clients: "{{ nfs_clients }}"
nfs_server_firewall_allowed_sources:
- 192.168.1.0/24 # lan0
- 192.168.20.0/27 # wg0
nfs_bind_addresses:
- 192.168.20.4
- 192.168.1.2

10
inventory/hosts.example Normal file
View File

@ -0,0 +1,10 @@
# hosts.yml
all:
children:
servers:
hosts:
lisa:
bart:
marge:
homer:
maggie:

10
playbook.yml Normal file
View File

@ -0,0 +1,10 @@
- hosts: all
become: true
roles:
- role: networking
- role: sshd
- role: disks
- role: wireguard
- role: zsh
- role: archlinux
- role: podman

6
playbooks/example.yml Normal file
View File

@ -0,0 +1,6 @@
- hosts: marge
become: true
roles:
- role: ntpd
- role: fail2ban
- role: unbound

3
requirements.yml Normal file
View File

@ -0,0 +1,3 @@
collections:
- name: ansible.netcommon
- name: community.general

View File

@ -0,0 +1,5 @@
arch_locale: en_US.UTF-8
yay_src_path: /opt/yay
yay_git_repo: https://aur.archlinux.org/yay.git
paru_src_path: /opt/paru
paru_git_repo: https://aur.archlinux.org/paru.git

View File

@ -0,0 +1,15 @@
---
- name: Configure locales
block:
- name: activate locale
command:
cmd: localectl set-locale LANG={{ arch_locale }}
- name: edit /etc/locale.gen
lineinfile:
dest: /etc/locale.gen
state: present
regexp: "{{ arch_locale }}"
line: "{{ arch_locale }} UTF-8"
- name: regenerate locales
command:
cmd: locale-gen

View File

@ -0,0 +1,12 @@
---
- name: Skip Archlinux installation
meta: end_play
when: ansible_facts['os_family'] != 'Archlinux'
- name: Archlinux base setup
include_tasks: "{{ item }}"
loop:
- pacman.yml
- locales.yml
- yay.yml
- paru.yml

View File

@ -0,0 +1,45 @@
---
- name: Check if pacman is not locked
stat:
path: /var/lib/pacman/db.lck
register: pacman_lock
failed_when: pacman_lock.stat.exists
changed_when: false
## Only if your are sure of what you are doing
# - name: Unlock pacman lock
# file:
# path: /var/lib/pacman/db.lck
# state: absent
- name: Install reflector (looking for fastest mirror)
pacman:
name: reflector
state: present
- name: Stat pacman mirrorlist
stat:
path: /etc/pacman.d/mirrorlist
register: mirrorlist
# Probably not here if it's a fresh install
- name: Stat pacman mirrorlist.bak
stat:
path: /etc/pacman.d/mirrorlist.bak
register: mirrorlist_bak
- name: Backup and update pacman mirrorlist if older than 7 days
shell: >
cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak &&
reflector --latest 20 --protocol https --sort rate
--save /etc/pacman.d/mirrorlist
when: mirrorlist_bak.stat.exists is false or
(mirrorlist.stat.exists and
(ansible_date_time.epoch | int - mirrorlist.stat.mtime) > 604800)
- name: Configure pacman to output colors
lineinfile:
dest: /etc/pacman.conf
state: present
regexp: "^(.*)Color"
line: "Color"

View File

@ -0,0 +1,60 @@
---
- name: Check if paru is already installed
stat:
path: /usr/bin/paru
register: paru
- name: Install paru
block:
- name: Install build dependencies
package:
name:
- base-devel
- git
state: present
- name: Disable sudo password prompt (makepkg sudoers hack)
lineinfile:
dest: /etc/sudoers
state: present
regexp: "^#?%wheel"
line: "%wheel ALL=(ALL) NOPASSWD: ALL"
validate: /usr/sbin/visudo -cf %s
- command:
cmd: whoami
no_log: true
become: false
register: main_user
- set_fact:
main_user: "{{ main_user.stdout }}"
no_log: true
- name: Create paru sources dir
file:
path: "{{ paru_src_path }}"
state: directory
owner: "{{ main_user }}"
- name: Clone git sources
become: false
git:
repo: "{{ paru_git_repo }}"
dest: "{{ paru_src_path }}"
# note: this only works because SUDOERS password prompt is disabled
- name: Build and install
become: false
command:
chdir: "{{ paru_src_path }}"
cmd: "makepkg -si -f --noconfirm"
- name: Restore sudo with password prompt
lineinfile:
dest: /etc/sudoers
state: present
regexp: "^#?%wheel"
line: "%wheel ALL=(ALL:ALL) ALL"
validate: /usr/sbin/visudo -cf %s
when: not paru.stat.exists

View File

@ -0,0 +1,60 @@
---
- name: Check if yay is already installed
stat:
path: /usr/bin/yay
register: yay
- name: Install yay
block:
- name: Install build dependencies
package:
name:
- base-devel
- git
state: present
- name: Disable sudo password prompt (makepkg sudoers hack)
lineinfile:
dest: /etc/sudoers
state: present
regexp: "^#?%wheel"
line: "%wheel ALL=(ALL) NOPASSWD: ALL"
validate: /usr/sbin/visudo -cf %s
- command:
cmd: whoami
no_log: true
become: false
register: main_user
- set_fact:
main_user: "{{ main_user.stdout }}"
no_log: true
- name: Create yay sources dir
file:
path: "{{ yay_src_path }}"
state: directory
owner: "{{ main_user }}"
- name: Clone git sources
become: false
git:
repo: "{{ yay_git_repo }}"
dest: "{{ yay_src_path }}"
# note: this only works because SUDOERS password prompt is disabled
- name: Build and install
become: false
command:
chdir: "{{ yay_src_path }}"
cmd: "makepkg -si -f --noconfirm"
- name: Restore sudo with password prompt
lineinfile:
dest: /etc/sudoers
state: present
regexp: "^#?%wheel"
line: "%wheel ALL=(ALL:ALL) ALL"
validate: /usr/sbin/visudo -cf %s
when: not yay.stat.exists

View File

@ -0,0 +1 @@
ssd_trim_periodicity: monthly

View File

@ -0,0 +1,7 @@
---
- name: Ensure disks are formatted correctly
include_tasks: partitioning.yml
loop: "{{ disk_partitioning | default([]) }}"
- name: Enable trim SSD if there is at least one
include_tasks: trim-ssd.yml

View File

@ -0,0 +1,42 @@
---
- name: Install sfdisk
package:
name: util-linux
state: present
changed_when: false
- name: Install hdparm
package:
name: hdparm
state: present
changed_when: false
- name: Load expected layout from file (controller side)
set_fact:
expected_layout: "{{ lookup('file', item.layout_file) }}"
changed_when: false
- name: Get current layout from remote
command: "sfdisk --dump {{ item.device }}"
register: current_layout
changed_when: false
- name: Compare expected vs current layout
vars:
current_clean: "{{ current_layout.stdout | trim | regex_replace('\\s+', ' ') }}"
expected_clean: "{{ expected_layout | trim | regex_replace('\\s+', ' ') }}"
set_fact:
layout_differs: "{{ current_clean != expected_clean }}"
changed_when: false
- name: Copy layout file to remote (only if different)
copy:
content: "{{ expected_layout }}"
dest: "/tmp/expected-{{ item.device | basename }}.sfdisk"
mode: "0644"
when: layout_differs
- name: Apply partition table using sfdisk
command: >
sfdisk {{ item.device }} < {{ item.layout_file }}
when: layout_differs

View File

@ -0,0 +1,43 @@
---
# see: https://wiki.archlinux.org/title/Solid_state_drive#Periodic_TRIM
- name: Check if there is at least one SSD
set_fact:
has_at_least_one_ssd: "{{ ansible_facts.devices | dict2items | selectattr('value.rotational', 'equalto', '0') | list | length > 0}}"
changed_when: false
- name: Skip trim role
meta: end_play
when: not has_at_least_one_ssd
- name: install trim tools
package:
name: util-linux
state: present
changed_when: false
- name: edit trim periodicity if needed
template:
src: templates/fstrim.timer.j2
dest: "/etc/systemd/system/fstrim.timer.d/override.conf"
owner: root
group: root
mode: "0644"
register: timer_config
- name: systemd daemon reload
systemd:
daemon_reload: yes
when: timer_config.changed
- name: enable periodic trim
systemd:
name: fstrim.timer
enabled: yes
state: started
changed_when: false
- name: install nvme-cli
package:
name: nvme-cli
state: present
changed_when: false

View File

@ -0,0 +1,2 @@
[Timer]
OnCalendar={{ ssd_trim_periodicity }}

View File

@ -0,0 +1 @@
docker_projects_dir: /opt/docker

View File

@ -0,0 +1,53 @@
---
# see: https://docs.docker.com/engine/storage/drivers/zfs-driver/
# see: https://github.com/portainer/portainer
#
# Archlinux: only if your target is meant to frequently build docker images
# see: https://stackoverflow.com/a/78352698
- name: uninstall docker
block:
- name: Include uninstall tasks
include_tasks: uninstall.yml
- name: Skip docker installation
meta: end_play
when: uninstall_docker | lower in ['yes', 'y']
- name: install docker
package:
name: docker
- name: enable the service
service:
name: "docker"
enabled: true
state: started
- command:
cmd: whoami
no_log: true
become: false
register: main_user
- set_fact:
main_user: "{{ main_user.stdout }}"
no_log: true
- name: create projects directory
file:
path: "{{ docker_projects_dir }}"
state: directory
owner: "{{ main_user }}"
group: "{{ main_user }}"
- name: allow user to use docker
user:
name: "{{ main_user }}"
groups: docker
append: yes
register: docker_group
- name: inform the user that user needs to logout and login again
debug:
msg: "Please logout and login again to make sure the user is added to the docker group"
when: docker_group.changed

View File

@ -0,0 +1,19 @@
---
- name: uninstall docker
package:
name: docker
state: absent
- name: prompt the user for confirmation
ansible.builtin.pause:
prompt: "[IRREVERSIBLE] Are you sure you want to delete {{ docker_projects_dir }}?"
echo: yes
register: confirmation
- name: remote projects directory
file:
path: "{{ docker_projects_dir }}"
state: absent
owner: "{{ main_user }}"
group: "{{ main_user }}"
when: confirmation.user_input | lower in ['yes', 'y']

View File

@ -0,0 +1,2 @@
fail2ban_firewall: ufw
fail2ban_backend: systemd

View File

@ -0,0 +1,59 @@
---
# see: https://wiki.archlinux.org/title/Fail2ban
- name: Install fail2ban
package:
name: fail2ban
state: present
- name: Ensure fail2ban configuration is only owned by root
file:
path: /etc/fail2ban
owner: root
group: root
mode: 0700
recurse: yes
- name: Install Fail2ban Config
block:
- name: General configuration
template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
mode: "0600"
- name: Service custom jail
template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "0600"
loop:
- { src: sshd-jail.local.j2, dest: /etc/fail2ban/jail.d/sshd.local }
- { src: nginx-jail.local.j2, dest: /etc/fail2ban/jail.d/nginx.local }
- name: Service hardening (read-only root rights)
block:
- name: Check if hardening configuration is already applied
stat:
path: /etc/systemd/system/fail2ban.service.d/override.conf
register: override_conf
- name: Create configuration directory
file:
path: /etc/systemd/system/fail2ban.service.d
state: directory
owner: root
group: root
mode: 0700
- name: Apply hardening configuration
template:
src: hardened.fail2ban.conf.j2
dest: /etc/systemd/system/fail2ban.service.d/override.conf
when: not override_conf.stat.exists
- name: Reload systemd
systemd:
daemon_reload: yes
when: not override_conf.stat.exists
- name: Start and enable fail2ban
service:
name: fail2ban
state: started
enabled: yes

View File

@ -0,0 +1,11 @@
[Service]
PrivateDevices=yes
PrivateTmp=yes
ProtectHome=read-only
ProtectSystem=strict
ReadWritePaths=-/var/run/fail2ban
ReadWritePaths=-/var/lib/fail2ban
ReadWritePaths=-/var/log/fail2ban.log
ReadWritePaths=-/var/spool/postfix/maildrop
ReadWritePaths=-/run/xtables.lock
CapabilityBoundingSet=CAP_AUDIT_READ CAP_DAC_READ_SEARCH CAP_NET_ADMIN CAP_NET_RAW

View File

@ -0,0 +1,7 @@
[DEFAULT]
bantime = 1d
banaction = {{fail2ban_firewall}}
allowipv6 = true
ignoreip = 127.0.0.1/8
backend = {{fail2ban_backend}}
ignoreself = true

View File

@ -0,0 +1,6 @@
[nginx-http-auth]
enabled = true
port = http, https
maxretry = 2
findtime = 1d
bantime = 2w

View File

@ -0,0 +1,6 @@
[sshd]
enabled = true
filter = sshd
maxretry = 5
findtime = 1d
bantime = 2w

View File

@ -0,0 +1,3 @@
- name: install oryx
cmd: paru -S oryx
when: ansible_facts['os_family'] == 'Archlinux'

View File

@ -0,0 +1,31 @@
# net-config
This role configures a network interface.
## Requirements
None
## Example Playbook
```yaml
- hosts: servers
roles:
- role: net-config
interface:
name: lan0
mac_address: 02:a0:c9:8d:7e:b6
address: 192.168.1.2/24
gateway: 192.168.1.254
nameservers:
- 1.1.1.1
- 8.8.8.8
```
## License
MIT
## Author Information
Jokester <main@jokester.fr>

View File

@ -0,0 +1,19 @@
---
galaxy_info:
author: Jokester <main@jokester.fr>
description: Configure the network as set in the inventory
license: MIT
min_ansible_version: "2.10"
galaxy_tags:
- network
- configuration
- linux
- debian
- archlinux
platforms:
- name: Debian
versions:
- all
- name: ArchLinux
versions:
- all

View File

@ -0,0 +1,36 @@
---
- name: Check if the interface ipv4 address is defined
block:
- debug:
msg: "Warning: iface {{ interface.name }} has no defined ipv4 address, skipping configuration"
- name: Skip net-config role for {{ interface.name }}
meta: end_play
when: interface.ipv4.address is not defined
- name: Check if the interface is already configured
stat:
path: /etc/systemd/network/20-{{ interface.name }}.network
register: network_file
- name: What patch is needed
debug:
msg: >-
{%- if network_file.stat.exists == true -%}
iface {{ interface.name }} is already configured, no action needed.
{%- else -%}
iface {{ interface.name }} will be configured.
{%- endif -%}
- name: Create systemd-network link file
when: network_file.stat.exists != true
template:
src: systemd.network.j2
dest: /etc/systemd/network/20-{{ interface.name }}.network
owner: root
group: root
mode: "0644"
- name: Notify a reload is required
set_fact:
network_reload_required: true
when: network_file.stat.exists != true

View File

@ -0,0 +1,9 @@
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).
# The primary network interface
allow-hotplug {{ interface.name }}
iface {{ interface.name }} inet dhcp
# This is an autoconfigured IPv6 interface
iface {{ interface.name }} inet6 auto

View File

@ -0,0 +1,9 @@
[Match]
Name={{ interface.name }}
[Network]
Address={{ interface.ipv4.address }}
Gateway={{ interface.ipv4.gateway }}
{% for dns in interface.ipv4.nameservers %}
DNS={{ dns }}
{% endfor %}

View File

@ -0,0 +1,32 @@
# net-persist
This role prevent the machine interface to change its name, thus to make unexpected changes to the network configuration. This rely on the mac address of the interface to map it to a static interface name.
If for some reason you might change your mac address (on a virtual machine for example), please update your inventory accordingly.
## Requirements
None
## Input variables
- `interface`:
```python
{
'mac_address': '02:a0:c9:8d:7e:b6',
'ip': '192.168.1.2',
'netmask': '255.255.255.0',
'gateway': '192.168.1.254',
'dns': ['1.1.1.1', '8.8.8.8'],
'name': 'lan0'
}
```
## License
MIT
## Author Information
Jokester <main@jokester.fr>

View File

@ -0,0 +1,19 @@
---
galaxy_info:
author: Jokester <main@jokester.fr>
description: Force the primary network interface to be 'lan0'
license: MIT
min_ansible_version: "2.10"
galaxy_tags:
- network
- persistent
- debian
- archlinux
- lan0
platforms:
- name: Debian
versions:
- all
- name: ArchLinux
versions:
- all

View File

@ -0,0 +1,34 @@
---
- name: Check if the interface is already named as expected
set_fact:
interface_original_name: "{{ ansible_facts.interfaces
| select('in', ansible_facts)
| map('extract', ansible_facts)
| selectattr('pciid', 'defined')
| selectattr('macaddress', 'equalto', interface.mac_address)
| map(attribute='device')
| first
}}"
- name: What patch is needed
debug:
msg: >-
{%- if interface_original_name != interface.name -%}
iface {{ interface_original_name }} ({{ interface.mac_address }}) will be patched to {{ interface.name }}.
{%- else -%}
iface {{ interface.name }} is already set, no action needed.
{%- endif -%}
- name: Create persistent-net link file
when: interface_original_name != interface.name
template:
src: persistent-net.link.j2
dest: /etc/systemd/network/10-persistent-net-{{ interface.name }}.link
owner: root
group: root
mode: "0644"
- name: Notify a reboot is required
set_fact:
reboot_required: true
when: interface_original_name != interface.name

View File

@ -0,0 +1,10 @@
# -----------------------------------------------------------------------------------
# Forcing {{ interface.name }} to be predictable
# see https://wiki.debian.org/NetworkInterfaceNames#CUSTOM_SCHEMES_USING_.LINK_FILES
# see https://wiki.archlinux.org/title/Network_configuration#Change_interface_name
# -----------------------------------------------------------------------------------
[Match]
MACAddress={{ interface.mac_address }}
[Link]
Name={{ interface.name }}

View File

@ -0,0 +1,41 @@
# Networking
This role configures the networking on the target machine.
## Requirements
Roles:
- net-persist
- net-config
## Inventory Variables
| Name | Description | Required |
| ------------------ | ----------------------------------------------- | -------- |
| network.interfaces | A dictionary of network interfaces to configure | yes |
Example:
```yaml
network:
interfaces:
lan0:
mac_address: 02:a0:c9:8d:7e:b6
ip: 192.168.1.2
netmask: 255.255.255.0
gateway: 192.168.1.254
dns:
- 1.1.1.1
- 8.8.8.8
lan1:
mac_address: 0a:3f:5b:1c:d2:e4
```
## License
MIT
## Author Information
Jokester <main@jokester.fr>

View File

@ -0,0 +1,18 @@
---
galaxy_info:
author: Jokester <main@jokester.fr>
description: Meta role to configure the network
license: MIT
min_ansible_version: "2.10"
galaxy_tags:
- network
- linux
- debian
- archlinux
platforms:
- name: Debian
versions:
- all
- name: ArchLinux
versions:
- all

View File

@ -0,0 +1,31 @@
---
- name: "Setup persistent network interface(s)"
include_role:
name: net-persist
public: yes
vars:
interface: "{{ item }}"
loop: "{{ hostvars[inventory_hostname].network_interfaces | default([]) }}"
- name: "Configure network interface(s)"
include_role:
name: net-config
public: yes
vars:
interface: "{{ item }}"
loop: "{{ hostvars[inventory_hostname].network_interfaces | default([]) }}"
- name: Reload networkd and resolved
systemd:
name: "{{ item }}"
state: reloaded
daemon_reload: yes
loop:
- systemd-networkd
- systemd-resolved
when: reboot_required is false and network_reload_required is true
- name: Reboot the machine
when: reboot_required is true
ansible.builtin.reboot:
reboot_timeout: 60

View File

@ -0,0 +1,38 @@
# NFS Server
This configuration is meant to be simple. We do not use a keberos server, nor fine-grained user ACLs here. I try not to mess up with ZFS options either.
Security is only guaranteed by the network (and firewal). Security is based on the IP address of the client, so I suggest to use a VPN if you want to avoid ARP poisoning on your LAN.
## In a nutshell
**Supports:**
- NFSv4 (TCP/UDP)
- UFW firewal configuration
- Reload service and exportfs on configuration change
**Limitations:**
- Access control limited to the IP address of the client (unsecure)
## Inventory
Example of `nfs_shares` you can declare:
```yaml
nfs_shares:
- dir: "/srv/nfs/photos"
clients:
- host: "192.168.1.100" # privileged user with write a access
options: "rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000,insecure"
- host: "192.168.1.0/24" # readonly access for other lan clients
options: "ro,sync,no_subtree_check"
```
> Note: to make the share accessible from MacOS, you might use the `insecure` option (allowing to bind port numbers > 1024).
## Ressources
- https://wiki.archlinux.org/title/NFS
- https://www.fkylewright.com/wordpress/2023/06/functional-automount-of-network-shares-in-macos/

View File

@ -0,0 +1,19 @@
---
# Example:
# nfs_shares:
# - dir: "/srv/nfs/photos"
# clients:
# - host: "192.168.1.100" # privileged user with write a access
# options: "rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000,insecure"
# - host: "192.168.1.0/24" # readonly access for other lan clients
# options: "ro,sync,no_subtree_check"
nfs_shares: []
nfs_configuration_file: "/etc/nfs.conf"
nfs_exports_file: "/etc/exports"
nfs_port: 2049
nfs_server_firewall_allowed_sources:
- 127.0.0.0/8

View File

@ -0,0 +1,9 @@
---
- name: "Reload systemd and restart nfs-server"
ansible.builtin.systemd:
name: "nfsv4-server"
state: restarted
daemon_reload: yes
- name: "Update exportfs"
ansible.builtin.command: exportfs -ra

View File

@ -0,0 +1,38 @@
---
- name: install nfs-server
package:
name: "{{ (ansible_facts['os_family'] == 'Archlinux') | ternary('nfs-utils', 'nfs-kernel-server') }}"
state: present
- name: configure nfs configuration
ansible.builtin.template:
src: templates/nfs.conf.j2
dest: "{{ nfs_configuration_file }}"
owner: root
group: root
mode: "0644"
notify: Reload systemd and restart nfs-server
- name: configure nfs-server exports
ansible.builtin.template:
src: templates/exports.j2
dest: "{{ nfs_exports_file }}"
owner: root
group: root
mode: "0644"
notify: Update exportfs
- name: systemd service for nfs-server is started and enabled
ansible.builtin.systemd:
name: nfsv4-server
state: started
enabled: true
- name: setup firewall rules for nfs on port
community.general.ufw:
rule: allow
src: "{{ item }}"
port: "{{ nfs_port }}"
proto: any
direction: in
with_items: "{{ nfs_server_firewall_allowed_sources | default([]) }}"

View File

@ -0,0 +1,8 @@
# {{ ansible_managed }}
#
{% for share in nfs_shares %}
{% for client in share.clients %}
{{ share.dir }} {{ client.host }}({{ client.options }})
{% endfor %}
{% endfor %}

View File

@ -0,0 +1,4 @@
[nfsd]
{% for ip in nfs_bind_addresses %}
host={{ ip }}
{% endfor %}

View File

@ -0,0 +1,26 @@
# NTP configuration file
ntp_config_file: "/etc/ntp.conf"
# NTP servers to use.
ntp_pools: -" 0.uk.pool.ntp.org"
-" 1.uk.pool.ntp.org"
-" 2.uk.pool.ntp.org"
-" 3.uk.pool.ntp.org"
# System timezone
ntp_timezone: "Europe/London"
# NTP drift file location
# (keeps track of your clock's time deviation)
ntp_drift_file: "/var/lib/ntp/ntp.drift"
# NTP security restrictions
ntp_restrict: "kod nomodify notrap nopeer noquery limited"
# Networks allowed to query this ntpd server
ntp_allowed_networks:
- "127.0.0.1"
- "::1"
# - "192.168.1.0 mask 255.255.255.0"
ntp_port: 123

View File

@ -0,0 +1,6 @@
---
- name: "Restart ntpd service"
ansible.builtin.systemd:
name: "ntpd"
state: restarted
reload: yes

48
roles/ntpd/tasks/main.yml Normal file
View File

@ -0,0 +1,48 @@
---
- name: install NTP package
package:
name: "ntp"
state: present
update_cache: yes
- name: set system timezone to {{ ntp_timezone }}"
community.general.timezone:
name: "{{ ntp_timezone }}"
notify: "Restart ntpd service"
- name: ensure NTP drift file directory exists
ansible.builtin.file:
path: "{{ ntp_drift_file | dirname }}"
state: directory
owner: "ntp"
group: "ntp"
mode: "0750"
- name: setup systems timezone
community.general.timezone:
name: "{{ ntp_timezone }}"
notify: Restart chronyd # Redémarrer chrony peut être utile après un changement de TZ pour qu'il la prenne bien en compte dans ses logs/opérations
- name: "configure {{ ntp_config_file }}"
ansible.builtin.template:
src: "ntp.conf.j2"
dest: "{{ ntp_config_file }}"
owner: root
group: root
mode: "0644"
notify: "Restart ntpd service"
- name: "ensure ntpd service is started and enabled"
ansible.builtin.systemd:
name: "ntpd"
state: started
enabled: true
- name: "configure ufw firewall"
community.general.ufw:
rule: allow
port: "{{ ntp_port }}"
proto: udp
src: "{{ item }}"
direction: in
loop: "{{ ntp_firewall_allowed_sources | default([]) }}"

View File

@ -0,0 +1,21 @@
# {{ ansible_managed }}
#
# NTP configuration file for ntpd
restrict default {{ ntp_restrict }}
{% for network in ntp_allowed_networks %}
restrict {{ network }}
{% endfor %}
# Use servers from the NTP Pool Project. 'iburst' speeds up initial synchronization.
{% for pool_host in ntp_pools %}
pool {{ pool_host }} iburst
{% endfor %}
# Frequency drift file
driftfile {{ ntp_drift_file }}
# Disable the monitoring facility (monlist) to prevent ntpq -c monlist DDOS attacks.
# @see CVE-2013-5211
disable monitor

View File

@ -0,0 +1 @@
podman_projects_dir: /opt/podman

View File

@ -0,0 +1,20 @@
- name: install podman
package:
name: podman
- command:
cmd: whoami
no_log: true
become: false
register: main_user
- set_fact:
main_user: "{{ main_user.stdout }}"
no_log: true
- name: create projects directory
file:
path: "{{ podman_projects_dir }}"
state: directory
owner: "{{ main_user }}"
group: "{{ main_user }}"

View File

@ -0,0 +1,8 @@
ssh_port: 22
ssh_allowed_network: "192.168.1.0/24"
ssh_allowed_vpn_network: "192.168.27.0/27"
ssh_users: "jokester" # space separated if many
ssh_config_dir: "/etc/ssh"
sshd_config: "{{ ssh_config_dir}}/sshd_config"
sshd_banner: "{{ ssh_config_dir}}/banner"
sshd_binary: "/usr/sbin/sshd"

78
roles/sshd/tasks/main.yml Normal file
View File

@ -0,0 +1,78 @@
---
- include_vars: "{{ item }}"
with_first_found:
- "vars/{{ ansible_facts['os_family'] }}.yml"
- "vars/debian.yml"
- name: Install OpenSSH
package:
name: "{{ ssh_package_name }}"
state: present
- name: Install UFW
package:
name: ufw
state: present
- name: Enable SSH
service:
name: "{{ ssh_service_name }}"
enabled: yes
- name: Allow SSH incoming connection on local network
ufw:
rule: allow
port: "{{ ssh_port }}"
proto: tcp
from: "{{ ssh_allowed_network }}"
direction: in
- name: Allow SSH incoming connection on vpn network
ufw:
rule: allow
port: "{{ ssh_port }}"
proto: tcp
from: "{{ ssh_allowed_vpn_network }}"
direction: in
- name: Add SSH public key to authorized_keys
authorized_key:
user: "{{ item }}"
state: present
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
comment: "{{ lookup('env', 'USER') | default('ansible') }}@{{ lookup('pipe', 'hostname -s') }}"
loop: "{{ ssh_users.split() }}"
- name: Create an SSH banner
template:
src: templates/sshd_banner.j2
dest: "{{ sshd_banner }}"
owner: root
group: root
mode: "0644"
- name: Remove motd on Debian
file:
path: /etc/motd
state: absent
when: ansible_facts['os_family'] == 'Debian'
- name: Hardening sshd_config
template:
src: templates/sshd_config.j2
dest: "{{ sshd_config }}"
owner: root
group: root
mode: "0600"
validate: "{{ sshd_binary }} -t -f %s"
register: ssh_hardening_task
- name: Restart SSH service
service:
name: "{{ ssh_service_name }}"
state: restarted
when: ssh_hardening_task.changed
- name: Enable UFW
community.general.ufw:
state: enabled

View File

@ -0,0 +1,47 @@
*******************************************
GALACTIC EMPIRE SECURE TERMINAL
*******************************************
{% if ansible_host == 'andromeda' %}
⣠⣴⣾⣿⣿⣿⣿⣷⣦⣄
⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄
⢀⣿⣿⣿⣿⡿⠛⢿⡿⠛⢻⣿⣿⣿⣿⡀ <IMPERIAL SECURITY
⢸⣿⣿⣿⣿⡇ ⢸⣷⣶⣾⣿⣿⣿⣿⡇ IDENTIFICATION DROID
⠈⠉⠉⠉⠉⠁ ⠈⠉⠉⠉⠉⠉⠉⠉⠁
⢀⣤⣀⣾⣿⣿⣿⠟⠛⠛⠛⠛⠻⣿⣿⣿⣷⣀⣤⡀
⢸⣿⣿⣿⣿⣿⣿⣤⣤⣤⣤⣤⣤⣿⣿⣿⣿⣿⣿⡇
⢸⣿⣿⣿⣿⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⣿⣿⣿⣿⡇
⢸⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⡇
⢸⣿⡟⢿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⡿⢻⣿⡇
⢸⣿⡇⠈⠙⠛⢛⣿⣿⣤⣤⣿⣿⡛⠛⠋⠁⢸⣿⡇
⣤⣼⣿⣧⣤⡀ ⠙⠛⠛⠛⠛⠛⠛⠋ ⢀⣤⣼⣿⣧⣤
⠛⠛⠛⠛⠛⠁ ⠈⠛⠛⠛⠛⠛
{% elif ansible_host == 'omega' %}
⣀⣤⣴⣶⣾⣿⣿⣿⣿⣷⡶⠦
⢀⣴⣾⣿⣿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣤⡄
⣰⣿⣿⣿⠋ ⠈⢻⣿⣿⣿⣿⣿⣿⡟⠛⠛⠃
⣼⣿⣿⣿⡇ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧
⢰⣿⣿⣿⣿⣧⡀ ⣠⣿⣿⣿⣿⣿⣿⠿⠟⠛⠁
⣾⣿⣿⣿⣿⣿⣿⣶⣤⣤⣴⣾⣿⣿⣿⣿⣿⣿⣷⣶⣶⣶⣶⣶⣶⣶
⣉⠉⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠉⣉
⢿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣶⣶⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⠿⠿
⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠛⠛⠋⠉
⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣤⣤⣤⣤⡄
⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏
⠈⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣤⡄
⠉⠛⠻⠿⢿⣿⣿⣿⣿⠟⠉⠉⠉⠉
{% else %}
ACCESS DENIED - UNKNOWN STAR SYSTEM
{% endif %}
You have reached a terminal of the Galactic
Empire's secure network. Unauthorized access
will result in tracking and possible Force
action.
{% if ansible_hostname is defined %}
Server: {{ ansible_hostname }}
{% endif %}
*******************************************
Beep beep-wooOOoo! Brrrp! Zzt zzt-whirl!
*******************************************

View File

@ -0,0 +1,64 @@
# Hardened SSH Configuration
# Protocol version
Protocol 2
# Address family
AddressFamily inet
# Supported authentication methods
AuthenticationMethods publickey
# Authentication
PermitRootLogin no
MaxAuthTries 3
MaxSessions 2
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
KerberosAuthentication no
GSSAPIAuthentication no
UsePAM yes
# Login timeout and grace period
LoginGraceTime 30s
ClientAliveInterval 300
ClientAliveCountMax 2
MaxStartups 10:30:60
# Forwarding
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PermitTTY yes
# User environment
PermitUserEnvironment no
# Logging and auditing
SyslogFacility AUTH
LogLevel VERBOSE
# Banner
Banner /etc/ssh/banner
# SFTP
Subsystem sftp internal-sftp
# Idle timeout (1 hour)
ClientAliveInterval 300
ClientAliveCountMax 12
# Restrict access to specific users/groups (customize as needed)
AllowUsers {{ ssh_users }}
# AllowGroups sshusers wheel
# Other security settings
HostbasedAuthentication no
IgnoreRhosts yes
PermitUserRC no
StrictModes yes
Compression no
{% if ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian' %}
UsePrivilegeSeparation sandbox
{% endif %}

View File

@ -0,0 +1,2 @@
ssh_package_name: "openssh"
ssh_service_name: "sshd"

View File

@ -0,0 +1,2 @@
ssh_package_name: "openssh-server"
ssh_service_name: "ssh"

70
roles/unbound/README.md Normal file
View File

@ -0,0 +1,70 @@
# Testing
## DNS leaks
```
browse https://www.dnsleaktest.com/
```
## DNSSEC
Testing DNSSEC validation
At this point we have a working server with supposedly working DNSSEC validation. Obviously we work on trust, but verify. To check that we have indeed a working validating server, we can run the following command:
```sh
dig www.nic.cz. +dnssec
```
The header section of the result should look like this:
```
; <<>> DiG 9.4.2-P2 <<>> www.nic.cz. +dnssec
;; global options: printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18417
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
```
See the bolded ad in the flags line? Now compare this to the output of the same command, but run on my MacBook using the ISPs resolver:
```
; <<>> DiG 9.10.6 <<>> www.nic.cz. +dnssec
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12527
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
```
The ISPs resolver doesnt support DNSSEC in this case, so you can see the ad flag missing. That flag indicates that the result from the upstream server validated.
# Race condition with wireguard
On unbound side:
```
systemd[1]: Starting unbound.service - Unbound DNS server...
unbound[74430]: [1747167722] unbound[74430:0] error: can't bind socket: Cannot assign requested address for 192.168.27.1>
unbound[74430]: [1747167722] unbound[74430:0] fatal error: could not open ports
systemd[1]: unbound.service: Main process exited, code=exited, status=1/FAILURE
systemd[1]: unbound.service: Failed with result 'exit-code'.
systemd[1]: Failed to start unbound.service - Unbound DNS server.
```
On wireguard side:
```
systemd[1]: Starting wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0...
wg-quick[72187]: [#] ip link add wg0 type wireguard
wg-quick[72187]: [#] wg setconf wg0 /dev/fd/63
wg-quick[72187]: [#] ip -4 address add 192.168.27.1/27 dev wg0
wg-quick[72187]: [#] ip link set mtu 1420 up dev wg0
wg-quick[72215]: [#] resolvconf -a tun.wg0 -m 0 -x
wg-quick[72261]: [1747167556] unbound-control[72261:0] warning: control-enable is 'no' in the config file.
wg-quick[72261]: [1747167556] unbound-control[72261:0] error: connect: Connection refused for 127.0.0.1 port 8953
wg-quick[72217]: run-parts: /etc/resolvconf/update.d/unbound exited with return code 1
wg-quick[72187]: [#] ip link delete dev wg0
systemd[1]: wg-quick@wg0.service: Main process exited, code=exited, status=1/FAILURE
systemd[1]: wg-quick@wg0.service: Failed with result 'exit-code'.
systemd[1]: Failed to start wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0.
```

View File

@ -0,0 +1,16 @@
unbound_config_base_path: /etc/unbound
unbound_config_path: "{{ unbound_config_base_path }}/unbound.conf"
unbound_root_hints_path: "{{ unbound_config_base_path }}/root.hints"
unbound_anchor_root_key: "{{ unbound_config_base_path }}/root.key"
unbound_ad_servers_config_path: "{{ unbound_config_base_path }}/ad_servers.conf"
unbound_custom_lan_config_path: "{{ unbound_config_base_path }}/lan.conf"
unbound_custom_vpn_config_path: "{{ unbound_config_base_path }}/vpn.conf"
unbound_custom_lan_domain: "example.lan"
unbound_port: 53
unbound_apparmor_profile_path: /etc/apparmor.d/usr.sbin.unbound
unbound_firewall_allowed_sources:
- 192.168.1.0/24 # lan0
- 192.168.27.0/27 # wg0
unbound_custom_lan_records:
"example.lan":
v4: 192.168.1.2

View File

@ -0,0 +1,16 @@
---
- name: Check Unbound config syntax
ansible.builtin.command: unbound-checkconf "{{ unbound_config_path }}"
register: unbound_validation
changed_when: false
failed_when: unbound_validation.rc != 0
- name: Reload systemd and restart unbound
ansible.builtin.systemd:
name: unbound
state: restarted
daemon_reload: yes
- name: Reload AppArmor profile
ansible.builtin.command: apparmor_parser -r {{ unbound_apparmor_profile_path }}
when: ansible_facts.apparmor.status == "enabled"

View File

@ -0,0 +1,131 @@
---
# see: https://calomel.org/unbound_dns.html
# see: https://wiki.archlinux.org/title/Unbound
- name: install unbound
package:
name: unbound
state: present
# Note: on archlinux this is already shipped within unbound
- name: install unbound-anchor on debian/ubuntu
package:
name: unbound-anchor
state: present
when: ansible_facts['os_family'] == 'Debian'
- name: ensure unbound configuration is owned by unbound
ansible.builtin.shell: |
find "{{ unbound_config_base_path }}" -type d -exec chmod 755 {} \;
find "{{ unbound_config_base_path }}" -type f -exec chmod 644 {} \;
chown -R unbound:unbound "{{ unbound_config_base_path }}"
args:
executable: /bin/bash
- name: ensure apparmor profile for unbound exists
ansible.builtin.copy:
dest: /etc/apparmor.d/usr.sbin.unbound
content: |
/etc/unbound/** r,
/var/lib/unbound/** rwk,
owner: root
group: root
mode: "0644"
when: ansible_facts.apparmor.status == "enabled"
notify:
- Reload AppArmor profile
- name: check if root.hints exists
stat:
path: "{{ unbound_root_hints_path }}"
register: root_hints
- name: update root.hints (if older than 6 months or missing)
block:
- name: download latest root hints from internic
ansible.builtin.get_url:
url: https://www.internic.net/domain/named.root
dest: "{{ unbound_root_hints_path }}"
owner: unbound
group: unbound
mode: "0644"
when: >
(not root_hints.stat.exists) or
(ansible_date_time.epoch | int - root_hints.stat.mtime > 15552000)
- name: check if unbound ad_servers configuration exists
stat:
path: "{{ unbound_ad_servers_config_path }}"
register: ad_servers
- name: update the ad_servers list if older than 2 weeks or missing
block:
- name: download stevenblack's hosts file
ansible.builtin.get_url:
url: https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
dest: /tmp/hosts.txt
mode: "0644"
- name: convert hosts file to unbound format
ansible.builtin.shell: |
grep '^0\.0\.0\.0' /tmp/hosts.txt | awk '{print "local-zone: \""$2"\" always_nxdomain"}' > "{{ unbound_ad_servers_config_path }}" &&
chown unbound:unbound "{{ unbound_ad_servers_config_path }}"
args:
executable: /bin/bash
- name: clean up temporary file
ansible.builtin.file:
path: /tmp/hosts.txt
state: absent
when: >
(not ad_servers.stat.exists) or
(ansible_date_time.epoch | int - ad_servers.stat.mtime > 1209600)
- name: initialize dnssec trust anchor if missing
ansible.builtin.command: unbound-anchor -a {{ unbound_anchor_root_key }}
args:
creates: "{{ unbound_anchor_root_key }}"
- name: install unbound config
template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: unbound
group: unbound
loop:
- { src: unbound.conf.j2, dest: "{{ unbound_config_path }}" }
- { src: custom-lan.conf.j2, dest: "{{ unbound_custom_lan_config_path }}" }
- { src: custom-vpn.conf.j2, dest: "{{ unbound_custom_vpn_config_path }}" }
notify:
- Check Unbound config syntax
- Reload systemd and restart unbound
- name: make sure unbound starts after wg-quick@wg0
block:
- name: ensure unbound.service.d directory exists
ansible.builtin.file:
path: /etc/systemd/system/unbound.service.d
state: directory
mode: "0755"
- name: configure unbound systemd service
ansible.builtin.copy:
dest: /etc/systemd/system/unbound.service.d/override.conf
content: |
[Unit]
After=wg-quick@wg0.service
Requires=wg-quick@wg0.service
notify: Reload systemd and restart unbound
- name: enables unbound service
ansible.builtin.service:
name: unbound
enabled: yes
state: started
- name: firewall ufw rules for unbound
community.general.ufw:
rule: allow
port: "{{ unbound_port }}"
proto: any
src: "{{ item }}"
direction: in
loop: "{{ unbound_firewall_allowed_sources | default([]) }}"

View File

@ -0,0 +1,9 @@
# {{ ansible_managed }}
view:
name: "lan"
{% for host, ips in unbound_custom_lan_records.items() %}
local-data: "{{ host }}. IN A {{ ips.v4 }}"
{% if ips.v6 is defined %}
local-data: "{{ host }}. IN AAAA {{ ips.v6 }}"
{% endif %}
{% endfor %}

View File

@ -0,0 +1,9 @@
# {{ ansible_managed }}
view:
name: "vpn"
{% for host, ips in unbound_custom_vpn_records.items() %}
local-data: "{{ host }}. IN A {{ ips.v4 }}"
{% if ips.v6 is defined %}
local-data: "{{ host }}. IN AAAA {{ ips.v6 }}"
{% endif %}
{% endfor %}

View File

@ -0,0 +1,110 @@
# {{ ansible_managed }}
# see: https://unix.stackexchange.com/a/617194
# see: https://unix.stackexchange.com/questions/617091/can-you-specify-a-different-configuration-for-different-interfaces-in-unbound
# see: https://www.dnsleaktest.com
server:
verbosity: 0
# listening port
port: {{ unbound_port }}
# Define interfaces binds
interface: lo
interface: lan0
interface: wg0
# Define access controls (note that ufw might be also configured)
access-control: 0.0.0.0/0 refuse
access-control: 192.168.1.0/24 allow # lan0 interface
access-control: 192.168.27.0/27 allow # wg0 interface
access-control: ::0/0 refuse
access-control: ::1 allow
# Specify custom local answers for each interface by using views:
access-control-view: 192.168.1.56/24 lan
access-control-view: 192.168.27.1/27 vpn
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: yes
# You want to leave this to no unless you have *native* IPv6. With 6to4 and
# Terredo tunnels your web browser should favor IPv4 for the same reasons
prefer-ip6: no
# Use this only when you downloaded the list of primary root servers!
# If you use the default dns-root-data package, unbound will find it automatically
root-hints: "{{ unbound_root_hints_path }}"
# enable to not answer id.server and hostname.bind queries.
hide-identity: yes
# enable to not answer version.server and version.bind queries.
hide-version: yes
# Trust glue only if it is within the server's authority
harden-glue: yes
# Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS
harden-dnssec-stripped: yes
# Don't use Capitalization randomization as it known to cause DNSSEC issues sometimes
# see https://discourse.pi-hole.net/t/unbound-stubby-or-dnscrypt-proxy/9378 for further details
use-caps-for-id: no
# the time to live (TTL) value lower bound, in seconds. Default 0.
# If more than an hour could easily give trouble due to stale data.
cache-min-ttl: 3600
# the time to live (TTL) value cap for RRsets and messages in the
# cache. Items are not cached for longer. In seconds.
cache-max-ttl: 86400
# Reduce EDNS reassembly buffer size.
# IP fragmentation is unreliable on the Internet today, and can cause
# transmission failures when large DNS messages are sent via UDP. Even
# when fragmentation does work, it may not be secure; it is theoretically
# possible to spoof parts of a fragmented DNS message, without easy
# detection at the receiving end. Recently, there was an excellent study
# >>> Defragmenting DNS - Determining the optimal maximum UDP response size for DNS <<<
# by Axel Koolhaas, and Tjeerd Slokker (https://indico.dns-oarc.net/event/36/contributions/776/)
# in collaboration with NLnet Labs explored DNS using real world data from the
# the RIPE Atlas probes and the researchers suggested different values for
# IPv4 and IPv6 and in different scenarios. They advise that servers should
# be configured to limit DNS messages sent over UDP to a size that will not
# trigger fragmentation on typical network links. DNS servers can switch
# from UDP to TCP when a DNS response is too big to fit in this limited
# buffer size. This value has also been suggested in DNS Flag Day 2020.
edns-buffer-size: 1232
# Perform prefetching of close to expired message cache entries
# This only applies to domains that have been frequently queried
prefetch: yes
# One thread should be sufficient, can be increased on beefy machines.
# In reality for most users running on small networks or on a single machine,
# it should be unnecessary to seek performance enhancement by increasing num-threads above 1.
num-threads: 1
# Ensure kernel buffer is large enough to not lose messages in traffic spikes
so-rcvbuf: 1m
# Ensure privacy of local IP ranges
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: 172.16.0.0/12
private-address: 10.0.0.0/8
private-address: fd00::/8
private-address: fe80::/10
# Allow the domain (and its subdomains) to contain private addresses.
# local-data statements are allowed to contain private addresses too.
private-domain: "{{ unbound_custom_lan_domain }}"
# Enable DNSSEC
auto-trust-anchor-file: "{{ unbound_anchor_root_key }}"
include: "{{ unbound_ad_servers_config_path }}"
include: "{{ unbound_custom_lan_config_path }}"
include: "{{ unbound_custom_vpn_config_path }}"

View File

@ -0,0 +1,8 @@
wireguard_primary_interface: "{{ network_interfaces.0.name }}"
wireguard_port: 51820 # static port to receive input connections
wireguard_server_mode: true # enables NAT and open port
wireguard_interface: wg0
wireguard_config_base_path: /etc/wireguard
wireguard_address: 192.168.27.1/27
wireguard_dns: 192.168.27.1
wireguard_peers: []

View File

@ -0,0 +1,60 @@
- name: install wireguard
package:
name: "{{ (ansible_facts['os_family'] == 'Archlinux') | ternary('wireguard-tools', 'wireguard') }}"
state: present
# to support "DNS=" if used in a "client way"
- name: install openresolv/resolveconf
package:
name: "{{ (ansible_facts['os_family'] == 'Archlinux') | ternary('openresolv', 'resolvconf') }}"
state: present
- name: ensure wireguard configuration is only owned by root
file:
path: "{{ wireguard_config_base_path }}"
owner: root
group: root
mode: 0700
recurse: yes
- name: check if private key exists
stat:
path: "{{ wireguard_config_base_path }}/privatekey"
register: pkey_file
- name: generate wireguard keys if not present
shell: wg genkey | tee {{ wireguard_config_base_path }}/privatekey | wg pubkey > {{ wireguard_config_base_path }}/publickey
when: not pkey_file.stat.exists
- name: retrieve wireguard private key from file
slurp:
src: "{{ wireguard_config_base_path }}/privatekey"
register: private_key
- name: set wireguard private key
set_fact:
wireguard_private_key: "{{ private_key['content'] | b64decode }}"
- name: disable "dns=" instruction if unbound is used to avoid race conditions at startup
set_fact:
wireguard_dns:
when: unbound_custom_lan_records is defined
- name: install wireguard config
template:
src: wireguard.conf.j2
dest: /etc/wireguard/{{ wireguard_interface }}.conf
- name: start and enable service
service:
name: wg-quick@{{ wireguard_interface }}
state: started
enabled: yes
daemon_reload: yes
- name: configure the firewall for wireguard
community.general.ufw:
rule: allow
port: "{{ wireguard_port }}"
proto: udp
direction: in

View File

@ -0,0 +1,16 @@
[Interface]
Address = {{ wireguard_address }}
{% if wireguard_dns %}DNS = {{ wireguard_dns }}
{% endif %}
PrivateKey = {{ wireguard_private_key }}
{% if wireguard_server_mode %}PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o {{ wireguard_primary_interface }} -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o {{ wireguard_primary_interface }} -j MASQUERADE
ListenPort = {{ wireguard_port }}
{% endif %}
{% for peer in wireguard_peers %}# {{ peer.name }}
[Peer]
PublicKey = {{ peer.public_key }}
AllowedIPs = {{ peer.allowed_ips | join(',') }}
{% if peer.endpoint is defined %}Endpoint = {{ peer.endpoint }}{% endif %}
{% endfor %}

43
roles/zfs/README.md Normal file
View File

@ -0,0 +1,43 @@
# Disks
Ansible community support for ZFS is limited to create filesystems, volumes and snapshots. There is no support for managing zpools, so here it is.
## Inventory
Here is an example inventory file you can use with this role:
```yaml
zfs_pools:
- name: peace
type: raidz1
devices:
- ata-SOME-DISK-LABEL-1
- ata-SOME-DISK-LABEL-2
options:
ashift: 12
root: /mnt/peace
state: present
```
And you will get raid1 zpool peace with two disks, with 12 ashift.
You can use a variety of options, see the [zpoolprops(7)](https://openzfs.github.io/openzfs-docs/man/master/7/zpoolprops.7.html) man page.
And for your zfs filesystems:
```yaml
zfs_datasets:
- name: peace/pictures
state: present
- name: peace/movies
state: present
extra_zfs_properties:
mountpoint: /mnt/peace/movies
quota: 500G
```
## References
- https://docs.ansible.com/ansible/latest/collections/community/general/zfs_module.html
- https://github.com/mrlesmithjr/ansible-zfs/blob/master/tasks/manage_zfs.yml
- https://wiki.archlinux.org/title/ZFS

View File

View File

@ -0,0 +1,16 @@
---
# due to Ansible limitations, we cannot loop over a block, so we loop over a distinct tasks file...
# @see https://stackoverflow.com/a/58911694
- name: set ownership on dataset mountpoint
block:
- name: get the mountpoint
ansible.builtin.shell: "zfs get -H -o value mountpoint {{ dataset.name }}"
register: mountpoint
changed_when: false
- name: set owner of mountpoints
file:
path: "{{ mountpoint.stdout }}"
owner: "{{ dataset.user | default(main_user) }}"
group: "{{ dataset.group | default(main_user) }}"
state: directory
recurse: true

View File

@ -0,0 +1,25 @@
---
# see: https://docs.ansible.com/ansible/latest/collections/community/general/zfs_module.html
- name: managing filesystems, volumes, snapshots
zfs:
name: "{{ item.name }}"
state: "{{ item.state }}"
extra_zfs_properties: "{{ item.extra_zfs_properties|default(omit) }}"
origin: "{{ item.origin|default(omit) }}"
with_items: "{{ zfs_datasets }}"
- command:
cmd: whoami
no_log: true
become: false
register: main_user
- set_fact:
main_user: "{{ main_user.stdout }}"
no_log: true
- name: set dataset ownership
include_tasks: "./dataset-ownership.yml"
loop: "{{ zfs_datasets }}"
loop_control:
loop_var: dataset

View File

@ -0,0 +1,12 @@
---
# due to Ansible limitations, we cannot loop over a block, so we loop over a distinct tasks file...
# @see https://stackoverflow.com/a/58911694
- name: prompt the user for confirmation
ansible.builtin.pause:
prompt: "[IRREVERSIBLE] Are you sure you want to delete zpool {{ zpool.name }}?"
echo: yes
register: confirmation
- name: deleting zpool
ansible.builtin.command: "zpool destroy {{ zpool.name }}"
when: confirmation.user_input | lower in ['yes', 'y']

View File

@ -0,0 +1,91 @@
---
- name: Check if zfs-linux-lts is installed
command: pacman -Qi zfs-dkms
register: zfs_dkms_installed
changed_when: false
failed_when: false
- name: Install zfs
when: zfs_dkms_installed.stderr
block:
- name: disable SUDOERS password prompt for makepkg
no_log: true
lineinfile:
dest: /etc/sudoers
state: present
regexp: "^#?%wheel"
line: "%wheel ALL=(ALL) NOPASSWD: ALL"
validate: /usr/sbin/visudo -cf %s
# Note: I successfully tried zfs-linux-lts and zfs-linux-lts-headers, but
# the next kernel upgrade failed: cyclic dependency.
# Using dkms with the lts linux kernel is a better approach IMO.
- name: Install zfs
become: false
command:
cmd: "paru -S --noconfirm zfs-dkms zfs-utils"
- name: Restore SUDOERS password prompt after yay
no_log: true
lineinfile:
dest: /etc/sudoers
state: present
regexp: "^#?%wheel"
line: "%wheel ALL=(ALL:ALL) ALL"
validate: /usr/sbin/visudo -cf %s
- name: check if /etc/hostid is present
stat:
path: /etc/hostid
register: hostid
changed_when: false
- name: generate /etc/hostid if not present
when: not hostid.stat.exists
command: zgenhostid $(hostid)
- name: Check if zrepl is installed
command: pacman -Qi zrepl
register: zrepl_installed
changed_when: false
failed_when: false
- name: Install zrepl
when: zrepl_installed.stderr
block:
- name: disable SUDOERS password prompt for makepkg
no_log: true
lineinfile:
dest: /etc/sudoers
state: present
regexp: "^#?%wheel"
line: "%wheel ALL=(ALL) NOPASSWD: ALL"
validate: /usr/sbin/visudo -cf %s
- name: Install zrepl
become: false
command:
cmd: "paru -S --noconfirm zrepl"
- name: Restore SUDOERS password prompt after paru
no_log: true
lineinfile:
dest: /etc/sudoers
state: present
regexp: "^#?%wheel"
line: "%wheel ALL=(ALL:ALL) ALL"
validate: /usr/sbin/visudo -cf %s
- name: Enable zfs services
service:
name: "{{ item }}"
enabled: true
state: started
loop:
- zfs.target
- zfs-import.target
- zfs-volumes.target
- zfs-import-scan.service
- zfs-volume-wait.service
# Please note /etc/zfs/zpool.cache should not be present, as it is deprecated
# and we are using zfs-import-scan.service, which rely on blkid.

9
roles/zfs/tasks/main.yml Normal file
View File

@ -0,0 +1,9 @@
---
- name: Install ZFS
include_tasks: install.yml
- name: Configure Zpools
include_tasks: pools.yml
- name: "Setup ZFS datasets: filesystems, snapshots, volumes"
include_tasks: datasets.yml

47
roles/zfs/tasks/pools.yml Normal file
View File

@ -0,0 +1,47 @@
---
# tasks file for zfs management
# Based on: https://github.com/mrlesmithjr/ansible-zfs/blob/master/tasks/manage_zfs.yml
# Expected variables in your inventory: zfs_pools.
- name: checking existing zpool(s)
ansible.builtin.shell: "zpool list -H -o name"
changed_when: false
register: current_zp_state
check_mode: no
when: zfs_pools is defined
- name: gather zpool status
ansible.builtin.shell: zpool status
changed_when: false
register: zpool_devices
when: zfs_pools is defined
- name: creating basic zpool(s)
ansible.builtin.command: "zpool create {{ '-o '+ item.options.items() |map('join', '=') | join (' -o ') if item.options is defined else '' }} {{ item.name }} {{ item.devices|join (' ') }}"
with_items: "{{ zfs_pools }}"
when:
- zfs_pools is defined
- item.type == "basic"
- item.name not in current_zp_state.stdout
- item.state == "present"
- item.devices[0] not in zpool_devices.stdout
- name: creating mirror/zraid zpool(s)
ansible.builtin.command: "zpool create {{ '-o '+ item.options.items() |map('join', '=') | join (' -o ') if item.options is defined else '' }} {{ item.name }} {{ item.type }} {{ item.devices|join (' ') }}"
with_items: "{{ zfs_pools }}"
when:
- zfs_pools is defined
- item.type != "basic"
- item.name not in current_zp_state.stdout
- item.state == "present"
- item.devices[0] not in zpool_devices.stdout
- name: deleting zpool(s) with care
include_tasks: "./delete-pool.yml"
when:
- zfs_pools is defined
- zpool.name in current_zp_state.stdout_lines
- zpool.state == "absent"
loop: "{{ zfs_pools }}"
loop_control:
loop_var: zpool

View File

@ -0,0 +1,9 @@
zsh_home: "{{ '/root' if zsh_user == 'root' else '/home/' + zsh_user }}"
zsh_base_config: "{{ zsh_home }}/.zshrc"
zsh_config_path: "{{ zsh_home }}/.config/zsh"
zsh_config_file: "{{ zsh_config_path }}/.zshrc"
zsh_p10k_theme_config: "{{ zsh_config_path }}/p10k.zsh"
zsh_users:
- jokester
- root
zsh_plugins_path: "/opt/zsh/plugins"

14
roles/zsh/tasks/main.yml Normal file
View File

@ -0,0 +1,14 @@
---
- name: install zsh
package:
name: zsh
state: present
- name: install zsh plugins
include_tasks: plugins.yml
- name: setup zsh for the user(s)
include_tasks: user-setup.yml
vars:
zsh_user: "{{ item }}"
loop: "{{ zsh_users | default([]) }}"

View File

@ -0,0 +1,50 @@
---
- name: ensure plugins directory exists
ansible.builtin.file:
path: "{{ zsh_plugins_path }}"
state: directory
owner: root
group: users
mode: "0755"
- name: add a readme file to advice from where this comes
ansible.builtin.copy:
dest: "{{ zsh_plugins_path }}/README.md"
content: |
# zsh plugins
Managed by Ansible
mode: "0644"
group: users
owner: root
- name: "git clone plugins"
git:
repo: "{{ item.repo }}"
dest: "{{ item.dest }}"
update: yes
version: master
loop:
- {
repo: https://github.com/zsh-users/zsh-syntax-highlighting.git,
dest: "{{ zsh_plugins_path }}/zsh-syntax-highlighting",
}
- {
repo: https://github.com/zsh-users/zsh-autosuggestions.git,
dest: "{{ zsh_plugins_path }}/zsh-autosuggestions",
}
- {
repo: https://github.com/romkatv/powerlevel10k.git,
dest: "{{ zsh_plugins_path }}/powerlevel10k",
}
- name: assert plugins are available for any user
ansible.builtin.file:
path: "{{ item }}"
owner: root
group: users
mode: "0755"
state: directory
loop:
- "{{ zsh_plugins_path }}/zsh-syntax-highlighting"
- "{{ zsh_plugins_path }}/zsh-autosuggestions"
- "{{ zsh_plugins_path }}/powerlevel10k"

View File

@ -0,0 +1,44 @@
- name: setup zsh base config
ansible.builtin.template:
src: main.zshrc.j2
dest: "{{ zsh_base_config }}"
owner: "{{ zsh_user }}"
group: "{{ zsh_user }}"
mode: "0600"
- name: setup .config/zsh directory
ansible.builtin.file:
path: "{{ zsh_config_path }}"
state: directory
owner: "{{ zsh_user }}"
group: "{{ zsh_user }}"
mode: "0700"
- name: configure zsh config
ansible.builtin.template:
src: zshrc.j2
dest: "{{ zsh_config_file }}"
owner: "{{ zsh_user }}"
group: "{{ zsh_user }}"
mode: "0600"
- name: copy aliases
ansible.builtin.copy:
src: ./templates/aliases
dest: "{{ zsh_config_path }}/aliases"
owner: "{{ zsh_user }}"
group: "{{ zsh_user }}"
mode: "0600"
- name: change default shell to zsh
user:
name: "{{ zsh_user }}"
shell: /bin/zsh
- name: configure powerlevel10k theme
ansible.builtin.copy:
src: "./templates/{{ 'root.p10k.zsh' if zsh_user == 'root' else 'user.p10k.zsh' }}"
dest: "{{ zsh_p10k_theme_config }}"
owner: "{{ zsh_user }}"
group: "{{ zsh_user }}"
mode: "0600"

View File

@ -0,0 +1,3 @@
alias dc="docker compose"
alias pc="podman compose"
alias w="cd ~/workspace"

View File

@ -0,0 +1,2 @@
# {{ ansible_managed }}
source {{ zsh_config_file }}

View File

@ -0,0 +1,788 @@
'builtin' 'local' '-a' 'p10k_config_opts'
[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases')
[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob')
[[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand')
'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand'
() {
emulate -L zsh -o extended_glob
unset -m '(POWERLEVEL9K_*|DEFAULT_USER)~POWERLEVEL9K_GITSTATUS_DIR'
[[ $ZSH_VERSION == (5.<1->*|<6->.*) ]] || return
typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(
os_icon # os identifier
dir # current directory
vcs # git status
# prompt_char # prompt symbol
)
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(
status # exit code of the last command
command_execution_time # duration of the last command
background_jobs # presence of background jobs
direnv # direnv status (https://direnv.net/)
asdf # asdf version manager (https://github.com/asdf-vm/asdf)
virtualenv # python virtual environment (https://docs.python.org/3/library/venv.html)
anaconda # conda environment (https://conda.io/)
pyenv # python environment (https://github.com/pyenv/pyenv)
goenv # go environment (https://github.com/syndbg/goenv)
nodenv # node.js version from nodenv (https://github.com/nodenv/nodenv)
nvm # node.js version from nvm (https://github.com/nvm-sh/nvm)
nodeenv # node.js environment (https://github.com/ekalinin/nodeenv)
# node_version # node.js version
# go_version # go version (https://golang.org)
# rust_version # rustc version (https://www.rust-lang.org)
# dotnet_version # .NET version (https://dotnet.microsoft.com)
# php_version # php version (https://www.php.net/)
# laravel_version # laravel php framework version (https://laravel.com/)
# java_version # java version (https://www.java.com/)
# package # name@version from package.json (https://docs.npmjs.com/files/package.json)
rbenv # ruby version from rbenv (https://github.com/rbenv/rbenv)
rvm # ruby version from rvm (https://rvm.io)
fvm # flutter version management (https://github.com/leoafarias/fvm)
luaenv # lua version from luaenv (https://github.com/cehoffman/luaenv)
jenv # java version from jenv (https://github.com/jenv/jenv)
plenv # perl version from plenv (https://github.com/tokuhirom/plenv)
perlbrew # perl version from perlbrew (https://github.com/gugod/App-perlbrew)
phpenv # php version from phpenv (https://github.com/phpenv/phpenv)
scalaenv # scala version from scalaenv (https://github.com/scalaenv/scalaenv)
haskell_stack # haskell version from stack (https://haskellstack.org/)
kubecontext # current kubernetes context (https://kubernetes.io/)
terraform # terraform workspace (https://www.terraform.io)
# terraform_version # terraform version (https://www.terraform.io)
aws # aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html)
aws_eb_env # aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/)
azure # azure account name (https://docs.microsoft.com/en-us/cli/azure)
gcloud # google cloud cli account and project (https://cloud.google.com/)
google_app_cred # google application credentials (https://cloud.google.com/docs/authentication/production)
toolbox # toolbox name (https://github.com/containers/toolbox)
context # user@hostname
nordvpn # nordvpn connection status, linux only (https://nordvpn.com/)
ranger # ranger shell (https://github.com/ranger/ranger)
yazi # yazi shell (https://github.com/sxyazi/yazi)
nnn # nnn shell (https://github.com/jarun/nnn)
lf # lf shell (https://github.com/gokcehan/lf)
xplr # xplr shell (https://github.com/sayanarijit/xplr)
vim_shell # vim shell indicator (:sh)
midnight_commander # midnight commander shell (https://midnight-commander.org/)
nix_shell # nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html)
chezmoi_shell # chezmoi shell (https://www.chezmoi.io/)
vi_mode # vi mode (you don't need this if you've enabled prompt_char)
# vpn_ip # virtual private network indicator
# load # CPU load
# disk_usage # disk usage
# ram # free RAM
# swap # used swap
todo # todo items (https://github.com/todotxt/todo.txt-cli)
timewarrior # timewarrior tracking status (https://timewarrior.net/)
taskwarrior # taskwarrior task count (https://taskwarrior.org/)
per_directory_history # Oh My Zsh per-directory-history local/global indicator
# cpu_arch # CPU architecture
time # current time
# ip # ip address and bandwidth usage for a specified network interface
# public_ip # public IP address
# proxy # system-wide http/https/ftp proxy
# battery # internal battery
# wifi # wifi speed
# example # example user-defined segment (see prompt_example function below)
)
typeset -g POWERLEVEL9K_MODE=nerdfont-v3
typeset -g POWERLEVEL9K_ICON_PADDING=none
typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT=
typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=false
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_PREFIX='%242F╭─'
typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_PREFIX='%242F├─'
typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_PREFIX='%242F╰─'
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_SUFFIX='%242F─╮'
typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_SUFFIX='%242F─┤'
typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_SUFFIX='%242F─╯'
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR=' '
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_BACKGROUND=
typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_GAP_BACKGROUND=
if [[ $POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR != ' ' ]]; then
# The color of the filler. You'll probably want to match the color of POWERLEVEL9K_MULTILINE
# ornaments defined above.
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND=242
# Start filler from the edge of the screen if there are no left segments on the first line.
typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_FIRST_SEGMENT_END_SYMBOL='%{%}'
# End filler on the edge of the screen if there are no right segments on the first line.
typeset -g POWERLEVEL9K_EMPTY_LINE_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='%{%}'
fi
typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR='\uE0B1'
typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR='\uE0B3'
typeset -g POWERLEVEL9K_LEFT_SEGMENT_SEPARATOR='\uE0B0'
typeset -g POWERLEVEL9K_RIGHT_SEGMENT_SEPARATOR='\uE0B2'
typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0B0'
typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0B2'
typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL=''
typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL=''
typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL=
typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND=232
typeset -g POWERLEVEL9K_OS_ICON_BACKGROUND=7
typeset -g POWERLEVEL9K_PROMPT_CHAR_BACKGROUND=
typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=76
typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=196
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION=''
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION=''
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='V'
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION='▶'
typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true
typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL=
typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL=
typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_{LEFT,RIGHT}_WHITESPACE=
typeset -g POWERLEVEL9K_DIR_BACKGROUND=4
typeset -g POWERLEVEL9K_DIR_FOREGROUND=254
typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique
typeset -g POWERLEVEL9K_SHORTEN_DELIMITER=
typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=250
typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=255
typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=true
local anchor_files=(
.bzr
.citc
.git
.hg
.node-version
.python-version
.go-version
.ruby-version
.lua-version
.java-version
.perl-version
.php-version
.tool-versions
.mise.toml
.shorten_folder_marker
.svn
.terraform
CVS
Cargo.toml
composer.json
go.mod
package.json
stack.yaml
)
typeset -g POWERLEVEL9K_SHORTEN_FOLDER_MARKER="(${(j:|:)anchor_files})"
typeset -g POWERLEVEL9K_DIR_TRUNCATE_BEFORE_MARKER=false
typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=1
typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80
typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40
typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT=50
typeset -g POWERLEVEL9K_DIR_HYPERLINK=false
typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=v3
typeset -g POWERLEVEL9K_VCS_CLEAN_BACKGROUND=2
typeset -g POWERLEVEL9K_VCS_MODIFIED_BACKGROUND=3
typeset -g POWERLEVEL9K_VCS_UNTRACKED_BACKGROUND=2
typeset -g POWERLEVEL9K_VCS_CONFLICTED_BACKGROUND=3
typeset -g POWERLEVEL9K_VCS_LOADING_BACKGROUND=8
typeset -g POWERLEVEL9K_VCS_BRANCH_ICON='\uF126 '
typeset -g POWERLEVEL9K_VCS_UNTRACKED_ICON='?'
function my_git_formatter() {
emulate -L zsh
if [[ -n $P9K_CONTENT ]]; then
# If P9K_CONTENT is not empty, use it. It's either "loading" or from vcs_info (not from
# gitstatus plugin). VCS_STATUS_* parameters are not available in this case.
typeset -g my_git_format=$P9K_CONTENT
return
fi
# Styling for different parts of Git status.
local meta='%7F' # white foreground
local clean='%0F' # black foreground
local modified='%0F' # black foreground
local untracked='%0F' # black foreground
local conflicted='%1F' # red foreground
local res
if [[ -n $VCS_STATUS_LOCAL_BRANCH ]]; then
local branch=${(V)VCS_STATUS_LOCAL_BRANCH}
# If local branch name is at most 32 characters long, show it in full.
# Otherwise show the first 12 … the last 12.
# Tip: To always show local branch name in full without truncation, delete the next line.
(( $#branch > 32 )) && branch[13,-13]="…" # <-- this line
res+="${clean}${(g::)POWERLEVEL9K_VCS_BRANCH_ICON}${branch//\%/%%}"
fi
if [[ -n $VCS_STATUS_TAG
# Show tag only if not on a branch.
# Tip: To always show tag, delete the next line.
&& -z $VCS_STATUS_LOCAL_BRANCH # <-- this line
]]; then
local tag=${(V)VCS_STATUS_TAG}
# If tag name is at most 32 characters long, show it in full.
# Otherwise show the first 12 … the last 12.
# Tip: To always show tag name in full without truncation, delete the next line.
(( $#tag > 32 )) && tag[13,-13]="…" # <-- this line
res+="${meta}#${clean}${tag//\%/%%}"
fi
# Display the current Git commit if there is no branch and no tag.
# Tip: To always display the current Git commit, delete the next line.
[[ -z $VCS_STATUS_LOCAL_BRANCH && -z $VCS_STATUS_TAG ]] && # <-- this line
res+="${meta}@${clean}${VCS_STATUS_COMMIT[1,8]}"
# Show tracking branch name if it differs from local branch.
if [[ -n ${VCS_STATUS_REMOTE_BRANCH:#$VCS_STATUS_LOCAL_BRANCH} ]]; then
res+="${meta}:${clean}${(V)VCS_STATUS_REMOTE_BRANCH//\%/%%}"
fi
# Display "wip" if the latest commit's summary contains "wip" or "WIP".
if [[ $VCS_STATUS_COMMIT_SUMMARY == (|*[^[:alnum:]])(wip|WIP)(|[^[:alnum:]]*) ]]; then
res+=" ${modified}wip"
fi
if (( VCS_STATUS_COMMITS_AHEAD || VCS_STATUS_COMMITS_BEHIND )); then
# ⇣42 if behind the remote.
(( VCS_STATUS_COMMITS_BEHIND )) && res+=" ${clean}${VCS_STATUS_COMMITS_BEHIND}"
# ⇡42 if ahead of the remote; no leading space if also behind the remote: ⇣42⇡42.
(( VCS_STATUS_COMMITS_AHEAD && !VCS_STATUS_COMMITS_BEHIND )) && res+=" "
(( VCS_STATUS_COMMITS_AHEAD )) && res+="${clean}${VCS_STATUS_COMMITS_AHEAD}"
elif [[ -n $VCS_STATUS_REMOTE_BRANCH ]]; then
# Tip: Uncomment the next line to display '=' if up to date with the remote.
# res+=" ${clean}="
fi
# ⇠42 if behind the push remote.
(( VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" ${clean}${VCS_STATUS_PUSH_COMMITS_BEHIND}"
(( VCS_STATUS_PUSH_COMMITS_AHEAD && !VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" "
# ⇢42 if ahead of the push remote; no leading space if also behind: ⇠42⇢42.
(( VCS_STATUS_PUSH_COMMITS_AHEAD )) && res+="${clean}${VCS_STATUS_PUSH_COMMITS_AHEAD}"
# *42 if have stashes.
(( VCS_STATUS_STASHES )) && res+=" ${clean}*${VCS_STATUS_STASHES}"
# 'merge' if the repo is in an unusual state.
[[ -n $VCS_STATUS_ACTION ]] && res+=" ${conflicted}${VCS_STATUS_ACTION}"
# ~42 if have merge conflicts.
(( VCS_STATUS_NUM_CONFLICTED )) && res+=" ${conflicted}~${VCS_STATUS_NUM_CONFLICTED}"
# +42 if have staged changes.
(( VCS_STATUS_NUM_STAGED )) && res+=" ${modified}+${VCS_STATUS_NUM_STAGED}"
# !42 if have unstaged changes.
(( VCS_STATUS_NUM_UNSTAGED )) && res+=" ${modified}!${VCS_STATUS_NUM_UNSTAGED}"
# ?42 if have untracked files. It's really a question mark, your font isn't broken.
# See POWERLEVEL9K_VCS_UNTRACKED_ICON above if you want to use a different icon.
# Remove the next line if you don't want to see untracked files at all.
(( VCS_STATUS_NUM_UNTRACKED )) && res+=" ${untracked}${(g::)POWERLEVEL9K_VCS_UNTRACKED_ICON}${VCS_STATUS_NUM_UNTRACKED}"
# "─" if the number of unstaged files is unknown. This can happen due to
# POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY (see below) being set to a non-negative number lower
# than the number of files in the Git index, or due to bash.showDirtyState being set to false
# in the repository config. The number of staged and untracked files may also be unknown
# in this case.
(( VCS_STATUS_HAS_UNSTAGED == -1 )) && res+=" ${modified}"
typeset -g my_git_format=$res
}
functions -M my_git_formatter 2>/dev/null
typeset -g POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY=-1
typeset -g POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN='~'
typeset -g POWERLEVEL9K_VCS_DISABLE_GITSTATUS_FORMATTING=true
typeset -g POWERLEVEL9K_VCS_CONTENT_EXPANSION='${$((my_git_formatter()))+${my_git_format}}'
typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1
typeset -g POWERLEVEL9K_VCS_BACKENDS=(git)
typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true
typeset -g POWERLEVEL9K_STATUS_OK=true
typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔'
typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND=2
typeset -g POWERLEVEL9K_STATUS_OK_BACKGROUND=0
typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true
typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔'
typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND=2
typeset -g POWERLEVEL9K_STATUS_OK_PIPE_BACKGROUND=0
typeset -g POWERLEVEL9K_STATUS_ERROR=true
typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='✘'
typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND=3
typeset -g POWERLEVEL9K_STATUS_ERROR_BACKGROUND=1
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true
typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='✘'
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND=3
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_BACKGROUND=1
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='✘'
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND=3
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_BACKGROUND=1
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=0
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_BACKGROUND=3
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s'
typeset -g POWERLEVEL9K_BACKGROUND_JOBS_FOREGROUND=6
typeset -g POWERLEVEL9K_BACKGROUND_JOBS_BACKGROUND=0
typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false
typeset -g POWERLEVEL9K_DIRENV_FOREGROUND=3
typeset -g POWERLEVEL9K_DIRENV_BACKGROUND=0
typeset -g POWERLEVEL9K_ASDF_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_BACKGROUND=7
typeset -g POWERLEVEL9K_ASDF_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_ASDF_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_ASDF_SHOW_ON_UPGLOB=
typeset -g POWERLEVEL9K_ASDF_RUBY_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_RUBY_BACKGROUND=1
typeset -g POWERLEVEL9K_ASDF_PYTHON_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_PYTHON_BACKGROUND=4
typeset -g POWERLEVEL9K_ASDF_GOLANG_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_GOLANG_BACKGROUND=4
typeset -g POWERLEVEL9K_ASDF_NODEJS_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_NODEJS_BACKGROUND=2
typeset -g POWERLEVEL9K_ASDF_RUST_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_RUST_BACKGROUND=208
typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_BACKGROUND=5
typeset -g POWERLEVEL9K_ASDF_FLUTTER_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_FLUTTER_BACKGROUND=4
typeset -g POWERLEVEL9K_ASDF_LUA_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_LUA_BACKGROUND=4
typeset -g POWERLEVEL9K_ASDF_JAVA_FOREGROUND=1
typeset -g POWERLEVEL9K_ASDF_JAVA_BACKGROUND=7
typeset -g POWERLEVEL9K_ASDF_PERL_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_PERL_BACKGROUND=4
typeset -g POWERLEVEL9K_ASDF_ERLANG_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_ERLANG_BACKGROUND=1
typeset -g POWERLEVEL9K_ASDF_ELIXIR_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_ELIXIR_BACKGROUND=5
typeset -g POWERLEVEL9K_ASDF_POSTGRES_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_POSTGRES_BACKGROUND=6
typeset -g POWERLEVEL9K_ASDF_PHP_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_PHP_BACKGROUND=5
typeset -g POWERLEVEL9K_ASDF_HASKELL_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_HASKELL_BACKGROUND=3
typeset -g POWERLEVEL9K_ASDF_JULIA_FOREGROUND=0
typeset -g POWERLEVEL9K_ASDF_JULIA_BACKGROUND=2
typeset -g POWERLEVEL9K_NORDVPN_FOREGROUND=7
typeset -g POWERLEVEL9K_NORDVPN_BACKGROUND=4
typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_CONTENT_EXPANSION=
typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_VISUAL_IDENTIFIER_EXPANSION=
typeset -g POWERLEVEL9K_RANGER_FOREGROUND=3
typeset -g POWERLEVEL9K_RANGER_BACKGROUND=0
typeset -g POWERLEVEL9K_YAZI_FOREGROUND=3
typeset -g POWERLEVEL9K_YAZI_BACKGROUND=0
typeset -g POWERLEVEL9K_NNN_FOREGROUND=0
typeset -g POWERLEVEL9K_NNN_BACKGROUND=6
typeset -g POWERLEVEL9K_LF_FOREGROUND=0
typeset -g POWERLEVEL9K_LF_BACKGROUND=6
typeset -g POWERLEVEL9K_XPLR_FOREGROUND=0
typeset -g POWERLEVEL9K_XPLR_BACKGROUND=6
typeset -g POWERLEVEL9K_VIM_SHELL_FOREGROUND=0
typeset -g POWERLEVEL9K_VIM_SHELL_BACKGROUND=2
typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_FOREGROUND=3
typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_BACKGROUND=0
typeset -g POWERLEVEL9K_NIX_SHELL_FOREGROUND=0
typeset -g POWERLEVEL9K_NIX_SHELL_BACKGROUND=4
typeset -g POWERLEVEL9K_CHEZMOI_SHELL_FOREGROUND=0
typeset -g POWERLEVEL9K_CHEZMOI_SHELL_BACKGROUND=4
typeset -g POWERLEVEL9K_DISK_USAGE_NORMAL_FOREGROUND=3
typeset -g POWERLEVEL9K_DISK_USAGE_NORMAL_BACKGROUND=0
typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_FOREGROUND=0
typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_BACKGROUND=3
typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_FOREGROUND=7
typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_BACKGROUND=1
typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL=90
typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_LEVEL=95
typeset -g POWERLEVEL9K_DISK_USAGE_ONLY_WARNING=false
typeset -g POWERLEVEL9K_VI_MODE_FOREGROUND=0
typeset -g POWERLEVEL9K_VI_COMMAND_MODE_STRING=NORMAL
typeset -g POWERLEVEL9K_VI_MODE_NORMAL_BACKGROUND=2
typeset -g POWERLEVEL9K_VI_VISUAL_MODE_STRING=VISUAL
typeset -g POWERLEVEL9K_VI_MODE_VISUAL_BACKGROUND=4
typeset -g POWERLEVEL9K_VI_OVERWRITE_MODE_STRING=OVERTYPE
typeset -g POWERLEVEL9K_VI_MODE_OVERWRITE_BACKGROUND=3
typeset -g POWERLEVEL9K_VI_INSERT_MODE_STRING=
typeset -g POWERLEVEL9K_VI_MODE_INSERT_FOREGROUND=8
typeset -g POWERLEVEL9K_RAM_FOREGROUND=0
typeset -g POWERLEVEL9K_RAM_BACKGROUND=3
typeset -g POWERLEVEL9K_SWAP_FOREGROUND=0
typeset -g POWERLEVEL9K_SWAP_BACKGROUND=3
typeset -g POWERLEVEL9K_LOAD_WHICH=5
typeset -g POWERLEVEL9K_LOAD_NORMAL_FOREGROUND=0
typeset -g POWERLEVEL9K_LOAD_NORMAL_BACKGROUND=2
typeset -g POWERLEVEL9K_LOAD_WARNING_FOREGROUND=0
typeset -g POWERLEVEL9K_LOAD_WARNING_BACKGROUND=3
typeset -g POWERLEVEL9K_LOAD_CRITICAL_FOREGROUND=0
typeset -g POWERLEVEL9K_LOAD_CRITICAL_BACKGROUND=1
typeset -g POWERLEVEL9K_TODO_FOREGROUND=0
typeset -g POWERLEVEL9K_TODO_BACKGROUND=8
typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_TOTAL=true
typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_FILTERED=false
typeset -g POWERLEVEL9K_TIMEWARRIOR_FOREGROUND=255
typeset -g POWERLEVEL9K_TIMEWARRIOR_BACKGROUND=8
typeset -g POWERLEVEL9K_TIMEWARRIOR_CONTENT_EXPANSION='${P9K_CONTENT:0:24}${${P9K_CONTENT:24}:+…}'
typeset -g POWERLEVEL9K_TASKWARRIOR_FOREGROUND=0
typeset -g POWERLEVEL9K_TASKWARRIOR_BACKGROUND=6
typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_FOREGROUND=0
typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_BACKGROUND=5
typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_FOREGROUND=0
typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_BACKGROUND=3
typeset -g POWERLEVEL9K_CPU_ARCH_FOREGROUND=0
typeset -g POWERLEVEL9K_CPU_ARCH_BACKGROUND=3
typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=1
typeset -g POWERLEVEL9K_CONTEXT_ROOT_BACKGROUND=0
typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_FOREGROUND=3
typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_BACKGROUND=0
typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=3
typeset -g POWERLEVEL9K_CONTEXT_BACKGROUND=0
typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%n@%m'
typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_TEMPLATE='%n@%m'
typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE='%n@%m'
typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_{CONTENT,VISUAL_IDENTIFIER}_EXPANSION=
typeset -g POWERLEVEL9K_VIRTUALENV_FOREGROUND=0
typeset -g POWERLEVEL9K_VIRTUALENV_BACKGROUND=4
typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_PYTHON_VERSION=false
typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_WITH_PYENV=false
typeset -g POWERLEVEL9K_VIRTUALENV_{LEFT,RIGHT}_DELIMITER=
typeset -g POWERLEVEL9K_ANACONDA_FOREGROUND=0
typeset -g POWERLEVEL9K_ANACONDA_BACKGROUND=4
typeset -g POWERLEVEL9K_ANACONDA_CONTENT_EXPANSION='${${${${CONDA_PROMPT_MODIFIER#\(}% }%\)}:-${CONDA_PREFIX:t}}'
typeset -g POWERLEVEL9K_PYENV_FOREGROUND=0
typeset -g POWERLEVEL9K_PYENV_BACKGROUND=4
typeset -g POWERLEVEL9K_PYENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_PYENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_PYENV_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_PYENV_CONTENT_EXPANSION='${P9K_CONTENT}${${P9K_CONTENT:#$P9K_PYENV_PYTHON_VERSION(|/*)}:+ $P9K_PYENV_PYTHON_VERSION}'
typeset -g POWERLEVEL9K_GOENV_FOREGROUND=0
typeset -g POWERLEVEL9K_GOENV_BACKGROUND=4
typeset -g POWERLEVEL9K_GOENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_GOENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_GOENV_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_NODENV_FOREGROUND=2
typeset -g POWERLEVEL9K_NODENV_BACKGROUND=0
typeset -g POWERLEVEL9K_NODENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_NODENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_NODENV_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_NVM_FOREGROUND=0
typeset -g POWERLEVEL9K_NVM_BACKGROUND=5
typeset -g POWERLEVEL9K_NVM_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_NVM_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_NODEENV_FOREGROUND=2
typeset -g POWERLEVEL9K_NODEENV_BACKGROUND=0
typeset -g POWERLEVEL9K_NODEENV_SHOW_NODE_VERSION=false
typeset -g POWERLEVEL9K_NODEENV_{LEFT,RIGHT}_DELIMITER=
typeset -g POWERLEVEL9K_NODE_VERSION_FOREGROUND=7
typeset -g POWERLEVEL9K_NODE_VERSION_BACKGROUND=2
typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_GO_VERSION_FOREGROUND=255
typeset -g POWERLEVEL9K_GO_VERSION_BACKGROUND=2
typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_RUST_VERSION_FOREGROUND=0
typeset -g POWERLEVEL9K_RUST_VERSION_BACKGROUND=208
typeset -g POWERLEVEL9K_RUST_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_DOTNET_VERSION_FOREGROUND=7
typeset -g POWERLEVEL9K_DOTNET_VERSION_BACKGROUND=5
typeset -g POWERLEVEL9K_DOTNET_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_PHP_VERSION_FOREGROUND=0
typeset -g POWERLEVEL9K_PHP_VERSION_BACKGROUND=5
typeset -g POWERLEVEL9K_PHP_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_LARAVEL_VERSION_FOREGROUND=1
typeset -g POWERLEVEL9K_LARAVEL_VERSION_BACKGROUND=7
typeset -g POWERLEVEL9K_RBENV_FOREGROUND=0
typeset -g POWERLEVEL9K_RBENV_BACKGROUND=1
typeset -g POWERLEVEL9K_RBENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_RBENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_RBENV_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_JAVA_VERSION_FOREGROUND=1
typeset -g POWERLEVEL9K_JAVA_VERSION_BACKGROUND=7
typeset -g POWERLEVEL9K_JAVA_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_JAVA_VERSION_FULL=false
typeset -g POWERLEVEL9K_PACKAGE_FOREGROUND=0
typeset -g POWERLEVEL9K_PACKAGE_BACKGROUND=6
typeset -g POWERLEVEL9K_RVM_FOREGROUND=0
typeset -g POWERLEVEL9K_RVM_BACKGROUND=240
typeset -g POWERLEVEL9K_RVM_SHOW_GEMSET=false
typeset -g POWERLEVEL9K_RVM_SHOW_PREFIX=false
typeset -g POWERLEVEL9K_FVM_FOREGROUND=0
typeset -g POWERLEVEL9K_FVM_BACKGROUND=4
typeset -g POWERLEVEL9K_LUAENV_FOREGROUND=0
typeset -g POWERLEVEL9K_LUAENV_BACKGROUND=4
typeset -g POWERLEVEL9K_LUAENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_LUAENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_LUAENV_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_JENV_FOREGROUND=1
typeset -g POWERLEVEL9K_JENV_BACKGROUND=7
typeset -g POWERLEVEL9K_JENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_JENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_JENV_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_PLENV_FOREGROUND=0
typeset -g POWERLEVEL9K_PLENV_BACKGROUND=4
typeset -g POWERLEVEL9K_PLENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_PLENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_PLENV_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_PERLBREW_FOREGROUND=67
typeset -g POWERLEVEL9K_PERLBREW_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_PERLBREW_SHOW_PREFIX=false
typeset -g POWERLEVEL9K_PHPENV_FOREGROUND=0
typeset -g POWERLEVEL9K_PHPENV_BACKGROUND=5
typeset -g POWERLEVEL9K_PHPENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_PHPENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_PHPENV_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_SCALAENV_FOREGROUND=0
typeset -g POWERLEVEL9K_SCALAENV_BACKGROUND=1
typeset -g POWERLEVEL9K_SCALAENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_SCALAENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_SCALAENV_SHOW_SYSTEM=true
typeset -g POWERLEVEL9K_HASKELL_STACK_FOREGROUND=0
typeset -g POWERLEVEL9K_HASKELL_STACK_BACKGROUND=3
typeset -g POWERLEVEL9K_HASKELL_STACK_SOURCES=(shell local)
typeset -g POWERLEVEL9K_HASKELL_STACK_ALWAYS_SHOW=true
typeset -g POWERLEVEL9K_TERRAFORM_SHOW_DEFAULT=false
typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=(
# '*prod*' PROD # These values are examples that are unlikely
# '*test*' TEST # to match your needs. Customize them as needed.
'*' OTHER)
typeset -g POWERLEVEL9K_TERRAFORM_OTHER_FOREGROUND=4
typeset -g POWERLEVEL9K_TERRAFORM_OTHER_BACKGROUND=0
typeset -g POWERLEVEL9K_TERRAFORM_VERSION_FOREGROUND=4
typeset -g POWERLEVEL9K_TERRAFORM_VERSION_BACKGROUND=0
typeset -g POWERLEVEL9K_TERRAFORM_VERSION_SHOW_ON_COMMAND='terraform|tf'
typeset -g POWERLEVEL9K_KUBECONTEXT_SHOW_ON_COMMAND='kubectl|helm|kubens|kubectx|oc|istioctl|kogito|k9s|helmfile|flux|fluxctl|stern|kubeseal|skaffold|kubent|kubecolor|cmctl|sparkctl'
typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=(
# '*prod*' PROD # These values are examples that are unlikely
# '*test*' TEST # to match your needs. Customize them as needed.
'*' DEFAULT)
typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_FOREGROUND=7
typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_BACKGROUND=5
typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION=
POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${P9K_KUBECONTEXT_CLOUD_CLUSTER:-${P9K_KUBECONTEXT_NAME}}'
POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${${:-/$P9K_KUBECONTEXT_NAMESPACE}:#/default}'
typeset -g POWERLEVEL9K_AWS_SHOW_ON_COMMAND='aws|awless|cdk|terraform|pulumi|terragrunt'
typeset -g POWERLEVEL9K_AWS_CLASSES=(
# '*prod*' PROD # These values are examples that are unlikely
# '*test*' TEST # to match your needs. Customize them as needed.
'*' DEFAULT)
typeset -g POWERLEVEL9K_AWS_DEFAULT_FOREGROUND=7
typeset -g POWERLEVEL9K_AWS_DEFAULT_BACKGROUND=1
typeset -g POWERLEVEL9K_AWS_CONTENT_EXPANSION='${P9K_AWS_PROFILE//\%/%%}${P9K_AWS_REGION:+ ${P9K_AWS_REGION//\%/%%}}'
typeset -g POWERLEVEL9K_AWS_EB_ENV_FOREGROUND=2
typeset -g POWERLEVEL9K_AWS_EB_ENV_BACKGROUND=0
typeset -g POWERLEVEL9K_AZURE_SHOW_ON_COMMAND='az|terraform|pulumi|terragrunt'
typeset -g POWERLEVEL9K_AZURE_CLASSES=(
# '*prod*' PROD # These values are examples that are unlikely
# '*test*' TEST # to match your needs. Customize them as needed.
'*' OTHER)
typeset -g POWERLEVEL9K_AZURE_OTHER_FOREGROUND=7
typeset -g POWERLEVEL9K_AZURE_OTHER_BACKGROUND=4
typeset -g POWERLEVEL9K_GCLOUD_SHOW_ON_COMMAND='gcloud|gcs|gsutil'
typeset -g POWERLEVEL9K_GCLOUD_FOREGROUND=7
typeset -g POWERLEVEL9K_GCLOUD_BACKGROUND=4
typeset -g POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT_ID//\%/%%}'
typeset -g POWERLEVEL9K_GCLOUD_COMPLETE_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT_NAME//\%/%%}'
typeset -g POWERLEVEL9K_GCLOUD_REFRESH_PROJECT_NAME_SECONDS=60
typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_SHOW_ON_COMMAND='terraform|pulumi|terragrunt'
typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=(
# '*:*prod*:*' PROD # These values are examples that are unlikely
# '*:*test*:*' TEST # to match your needs. Customize them as needed.
'*' DEFAULT)
typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_FOREGROUND=7
typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_BACKGROUND=4
typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_CONTENT_EXPANSION='${P9K_GOOGLE_APP_CRED_PROJECT_ID//\%/%%}'
typeset -g POWERLEVEL9K_TOOLBOX_FOREGROUND=0
typeset -g POWERLEVEL9K_TOOLBOX_BACKGROUND=3
typeset -g POWERLEVEL9K_TOOLBOX_CONTENT_EXPANSION='${P9K_TOOLBOX_NAME:#fedora-toolbox-*}'
typeset -g POWERLEVEL9K_PUBLIC_IP_FOREGROUND=7
typeset -g POWERLEVEL9K_PUBLIC_IP_BACKGROUND=0
typeset -g POWERLEVEL9K_VPN_IP_FOREGROUND=0
typeset -g POWERLEVEL9K_VPN_IP_BACKGROUND=6
typeset -g POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION=
typeset -g POWERLEVEL9K_VPN_IP_INTERFACE='(gpd|wg|(.*tun)|tailscale)[0-9]*|(zt.*)'
typeset -g POWERLEVEL9K_VPN_IP_SHOW_ALL=false
typeset -g POWERLEVEL9K_IP_BACKGROUND=4
typeset -g POWERLEVEL9K_IP_FOREGROUND=0
typeset -g POWERLEVEL9K_IP_CONTENT_EXPANSION='${P9K_IP_RX_RATE:+⇣$P9K_IP_RX_RATE }${P9K_IP_TX_RATE:+⇡$P9K_IP_TX_RATE }$P9K_IP_IP'
typeset -g POWERLEVEL9K_IP_INTERFACE='[ew].*'
typeset -g POWERLEVEL9K_PROXY_FOREGROUND=4
typeset -g POWERLEVEL9K_PROXY_BACKGROUND=0
typeset -g POWERLEVEL9K_BATTERY_LOW_THRESHOLD=20
typeset -g POWERLEVEL9K_BATTERY_LOW_FOREGROUND=1
typeset -g POWERLEVEL9K_BATTERY_{CHARGING,CHARGED}_FOREGROUND=2
typeset -g POWERLEVEL9K_BATTERY_DISCONNECTED_FOREGROUND=3
typeset -g POWERLEVEL9K_BATTERY_STAGES='\UF008E\UF007A\UF007B\UF007C\UF007D\UF007E\UF007F\UF0080\UF0081\UF0082\UF0079'
typeset -g POWERLEVEL9K_BATTERY_VERBOSE=false
typeset -g POWERLEVEL9K_BATTERY_BACKGROUND=0
typeset -g POWERLEVEL9K_WIFI_FOREGROUND=0
typeset -g POWERLEVEL9K_WIFI_BACKGROUND=4
typeset -g POWERLEVEL9K_TIME_FOREGROUND=0
typeset -g POWERLEVEL9K_TIME_BACKGROUND=7
typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M:%S}'
typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false
function prompt_example() {
p10k segment -b 1 -f 3 -i '⭐' -t 'hello, %n'
}
function instant_prompt_example() {
# Since prompt_example always makes the same `p10k segment` calls, we can call it from
# instant_prompt_example. This will give us the same `example` prompt segment in the instant
# and regular prompts.
prompt_example
}
typeset -g POWERLEVEL9K_EXAMPLE_FOREGROUND=3
typeset -g POWERLEVEL9K_EXAMPLE_BACKGROUND=1
typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=off
typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose
typeset -g POWERLEVEL9K_DISABLE_HOT_RELOAD=true
(( ! $+functions[p10k] )) || p10k reload
}
typeset -g POWERLEVEL9K_CONFIG_FILE=${${(%):-%x}:a}
(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]}
'builtin' 'unset' 'p10k_config_opts'

View File

@ -0,0 +1,199 @@
# Generated by Powerlevel10k configuration wizard on 2025-05-12 at 22:01 CEST.
# Based on romkatv/powerlevel10k/config/p10k-pure.zsh, checksum 07533.
# Wizard options: nerdfont-v3 + powerline, small icons, pure, rprompt, 24h time,
# 2 lines, compact, transient_prompt, instant_prompt=verbose.
# Type `p10k configure` to generate another config.
#
# Config file for Powerlevel10k with the style of Pure (https://github.com/sindresorhus/pure).
#
# Differences from Pure:
#
# - Git:
# - `@c4d3ec2c` instead of something like `v1.4.0~11` when in detached HEAD state.
# - No automatic `git fetch` (the same as in Pure with `PURE_GIT_PULL=0`).
#
# Apart from the differences listed above, the replication of Pure prompt is exact. This includes
# even the questionable parts. For example, just like in Pure, there is no indication of Git status
# being stale; prompt symbol is the same in command, visual and overwrite vi modes; when prompt
# doesn't fit on one line, it wraps around with no attempt to shorten it.
#
# If you like the general style of Pure but not particularly attached to all its quirks, type
# `p10k configure` and pick "Lean" style. This will give you slick minimalist prompt while taking
# advantage of Powerlevel10k features that aren't present in Pure.
# Temporarily change options.
'builtin' 'local' '-a' 'p10k_config_opts'
[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases')
[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob')
[[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand')
'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand'
() {
emulate -L zsh -o extended_glob
# Unset all configuration options.
unset -m '(POWERLEVEL9K_*|DEFAULT_USER)~POWERLEVEL9K_GITSTATUS_DIR'
# Zsh >= 5.1 is required.
[[ $ZSH_VERSION == (5.<1->*|<6->.*) ]] || return
# Prompt colors.
local grey='242'
local red='1'
local yellow='3'
local blue='4'
local magenta='5'
local cyan='6'
local white='7'
# Left prompt segments.
typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(
# =========================[ Line #1 ]=========================
# context # user@host
dir # current directory
vcs # git status
# command_execution_time # previous command duration
# =========================[ Line #2 ]=========================
newline # \n
# virtualenv # python virtual environment
prompt_char # prompt symbol
)
# Right prompt segments.
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(
# =========================[ Line #1 ]=========================
command_execution_time # previous command duration
virtualenv # python virtual environment
context # user@host
time # current time
# =========================[ Line #2 ]=========================
newline # \n
)
# Basic style options that define the overall prompt look.
typeset -g POWERLEVEL9K_BACKGROUND= # transparent background
typeset -g POWERLEVEL9K_{LEFT,RIGHT}_{LEFT,RIGHT}_WHITESPACE= # no surrounding whitespace
typeset -g POWERLEVEL9K_{LEFT,RIGHT}_SUBSEGMENT_SEPARATOR=' ' # separate segments with a space
typeset -g POWERLEVEL9K_{LEFT,RIGHT}_SEGMENT_SEPARATOR= # no end-of-line symbol
typeset -g POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION= # no segment icons
# Add an empty line before each prompt except the first. This doesn't emulate the bug
# in Pure that makes prompt drift down whenever you use the Alt-C binding from fzf or similar.
typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=false
# Magenta prompt symbol if the last command succeeded.
typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS}_FOREGROUND=$magenta
# Red prompt symbol if the last command failed.
typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS}_FOREGROUND=$red
# Default prompt symbol.
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION=''
# Prompt symbol in command vi mode.
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION=''
# Prompt symbol in visual vi mode is the same as in command mode.
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION=''
# Prompt symbol in overwrite vi mode is the same as in command mode.
typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=false
# Grey Python Virtual Environment.
typeset -g POWERLEVEL9K_VIRTUALENV_FOREGROUND=$grey
# Don't show Python version.
typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_PYTHON_VERSION=false
typeset -g POWERLEVEL9K_VIRTUALENV_{LEFT,RIGHT}_DELIMITER=
# Blue current directory.
typeset -g POWERLEVEL9K_DIR_FOREGROUND=$blue
# Context format when root: user@host. The first part white, the rest grey.
typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE="%F{$white}%n%f%F{$grey}@%m%f"
# Context format when not root: user@host. The whole thing grey.
typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE="%F{$grey}%n@%m%f"
# Don't show context unless root or in SSH.
typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_CONTENT_EXPANSION=
# Show previous command duration only if it's >= 5s.
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=5
# Don't show fractional seconds. Thus, 7s rather than 7.3s.
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0
# Duration format: 1d 2h 3m 4s.
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s'
# Yellow previous command duration.
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=$yellow
# Grey Git prompt. This makes stale prompts indistinguishable from up-to-date ones.
typeset -g POWERLEVEL9K_VCS_FOREGROUND=$grey
# Disable async loading indicator to make directories that aren't Git repositories
# indistinguishable from large Git repositories without known state.
typeset -g POWERLEVEL9K_VCS_LOADING_TEXT=
# Don't wait for Git status even for a millisecond, so that prompt always updates
# asynchronously when Git state changes.
typeset -g POWERLEVEL9K_VCS_MAX_SYNC_LATENCY_SECONDS=0
# Cyan ahead/behind arrows.
typeset -g POWERLEVEL9K_VCS_{INCOMING,OUTGOING}_CHANGESFORMAT_FOREGROUND=$cyan
# Don't show remote branch, current tag or stashes.
typeset -g POWERLEVEL9K_VCS_GIT_HOOKS=(vcs-detect-changes git-untracked git-aheadbehind)
# Don't show the branch icon.
typeset -g POWERLEVEL9K_VCS_BRANCH_ICON=
# When in detached HEAD state, show @commit where branch normally goes.
typeset -g POWERLEVEL9K_VCS_COMMIT_ICON='@'
# Don't show staged, unstaged, untracked indicators.
typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED}_ICON=
# Show '*' when there are staged, unstaged or untracked files.
typeset -g POWERLEVEL9K_VCS_DIRTY_ICON='*'
# Show '⇣' if local branch is behind remote.
typeset -g POWERLEVEL9K_VCS_INCOMING_CHANGES_ICON=':⇣'
# Show '⇡' if local branch is ahead of remote.
typeset -g POWERLEVEL9K_VCS_OUTGOING_CHANGES_ICON=':⇡'
# Don't show the number of commits next to the ahead/behind arrows.
typeset -g POWERLEVEL9K_VCS_{COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=1
# Remove space between '⇣' and '⇡' and all trailing spaces.
typeset -g POWERLEVEL9K_VCS_CONTENT_EXPANSION='${${${P9K_CONTENT/⇣* :⇡/⇣⇡}// }//:/ }'
# Grey current time.
typeset -g POWERLEVEL9K_TIME_FOREGROUND=$grey
# Format for the current time: 09:51:02. See `man 3 strftime`.
typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M:%S}'
# If set to true, time will update when you hit enter. This way prompts for the past
# commands will contain the start times of their commands rather than the end times of
# their preceding commands.
typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false
# Transient prompt works similarly to the builtin transient_rprompt option. It trims down prompt
# when accepting a command line. Supported values:
#
# - off: Don't change prompt when accepting a command line.
# - always: Trim down prompt when accepting a command line.
# - same-dir: Trim down prompt when accepting a command line unless this is the first command
# typed after changing current working directory.
typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always
# Instant prompt mode.
#
# - off: Disable instant prompt. Choose this if you've tried instant prompt and found
# it incompatible with your zsh configuration files.
# - quiet: Enable instant prompt and don't print warnings when detecting console output
# during zsh initialization. Choose this if you've read and understood
# https://github.com/romkatv/powerlevel10k#instant-prompt.
# - verbose: Enable instant prompt and print a warning when detecting console output during
# zsh initialization. Choose this if you've never tried instant prompt, haven't
# seen the warning, or if you are unsure what this all means.
typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose
# Hot reload allows you to change POWERLEVEL9K options after Powerlevel10k has been initialized.
# For example, you can type POWERLEVEL9K_BACKGROUND=red and see your prompt turn red. Hot reload
# can slow down prompt by 1-2 milliseconds, so it's better to keep it turned off unless you
# really need it.
typeset -g POWERLEVEL9K_DISABLE_HOT_RELOAD=true
# If p10k is already loaded, reload configuration.
# This works even with POWERLEVEL9K_DISABLE_HOT_RELOAD=true.
(( ! $+functions[p10k] )) || p10k reload
}
# Tell `p10k configure` which file it should overwrite.
typeset -g POWERLEVEL9K_CONFIG_FILE=${${(%):-%x}:a}
(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]}
'builtin' 'unset' 'p10k_config_opts'

View File

@ -0,0 +1,24 @@
# {{ ansible_managed }}
export ZSH_PLUGINS={{ zsh_plugins_path }}
# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc.
# Initialization code that may require console input (password prompts, [y/n]
# confirmations, etc.) must go above this block; everything else may go below.
if [[ -r "$HOME/.cache/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "$HOME/.cache/p10k-instant-prompt-${(%):-%n}.zsh"
fi
# To customize prompt, run `p10k configure` or edit the following file.
source {{ zsh_p10k_theme_config }}
# Enable syntax highlighting for zsh
source $ZSH_PLUGINS/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
# Enable autosuggestions
source $ZSH_PLUGINS/zsh-autosuggestions/zsh-autosuggestions.zsh
# Enable powerlevel10k theme
source $ZSH_PLUGINS/powerlevel10k/powerlevel10k.zsh-theme
export ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8'
export PROMPT="%n@%m:%~$ "