Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Hyprland desktop setup on Fedora Linux, styled with Catppuccin Mocha. Built for
│ │ ├── reload.sh # Reload compositor, bar, and notifications
│ │ └── wallpaper.sh # Per-workspace wallpaper listener
│ ├── kitty/
│ │ └── kitty.conf # GPU terminal with 144Hz input tuning
│ │ └── kitty.conf # GPU terminal with configured redraw delays
│ ├── rofi/
│ │ ├── config.rasi # Application launcher
│ │ ├── scripts/
Expand Down Expand Up @@ -109,11 +109,11 @@ https://github.com/user-attachments/assets/e41b7de6-2717-4371-a81f-c4a124757f12
|---|---|
| [Hyprland](https://github.com/hyprwm/Hyprland) | Wayland tiling compositor |
| [Waybar](https://github.com/Alexays/Waybar) | Floating status bar with workspace indicators |
| [awww](https://codeberg.org/LGFae/awww) | Wallpaper daemon running 144 FPS bezier transitions |
| [awww](https://codeberg.org/LGFae/awww) | Wallpaper daemon with transitions configured at 144 FPS |
| [SwayNC](https://github.com/ErikReider/SwayNotificationCenter) | Notification center and quick settings |
| [hyprlock](https://github.com/hyprwm/hyprlock) | Screen locker with screenshot blur |
| [hypridle](https://github.com/hyprwm/hypridle) | Idle listener and sleep controller |
| [Kitty](https://github.com/kovidgoyal/kitty) | Terminal emulator running 6ms refresh and 1ms input latency |
| [Kitty](https://github.com/kovidgoyal/kitty) | Terminal emulator with 6ms repaint and 1ms input delay settings |
| [Rofi](https://github.com/davatorium/rofi) | App launcher, power menu, and picker scripts |
| [Fastfetch](https://github.com/fastfetch-cli/fastfetch) | System information summary |

Expand Down Expand Up @@ -154,13 +154,19 @@ The main modifier key is `SUPER`.

## Installation & setup

The installer requires `python3` and **Git 2.30 or later** (`git config --fixed-value`).
The option is documented in [Git 2.30's configuration reference](https://github.com/git/git/blob/v2.30.0/Documentation/git-config.txt).
`delta` is needed only for the installed Git presentation settings, not to run
the installer. Symlinked `.bashrc` and `.gitconfig` files require manual integration;
do not replace those links to run this installer.

To link all configuration directories (`hypr`, `waybar`, `kitty`, `rofi`, `swaync`, `fastfetch`, `btop`, `starship.toml`) to `~/.config/`:

```bash
./scripts/install-symlinks.sh
```

Existing conflicting configuration files or directories will be safely backed up automatically with a timestamp before creating the symlinks.
Existing conflicting files, directories, and symlinks are preserved in unique backup directories before replacement.
Comment thread
SamuelIVX marked this conversation as resolved.

## System optimizations

Expand Down Expand Up @@ -191,3 +197,21 @@ Clipboard history supports Enter to copy, Delete to delete the selected entry,
and Ctrl+Delete to clear history after confirmation. Escape leaves the clipboard
and history alone. Wallpaper selection displays relative paths and handles
punctuation and duplicate filenames without regex matching.


### Portable Bash and Git settings

The installer links `shell/dotfiles.sh` into `~/.bashrc.d/` and includes
`git/developer.gitconfig` from local Git configuration. Shell tools are initialized
only in interactive shells, and missing optional tools are skipped. Local Git
identity, credentials, signing, and differing presentation overrides are preserved.

Migration removes only the exact recognized legacy Bash block and matching Git
presentation settings. It backs up changed startup files and replaces `.bashrc`
atomically, preserving its mode. Symlink conflicts are saved as `BACKUP/original`.
Restore the printed backups to undo installation.

The Bash loader check matches literal text without executing `.bashrc`. If that
text exists only in an unreachable block, manually source the fragment from an
active part of `.bashrc`. Locally edited legacy blocks may also need manual
integration to avoid duplicate initialization.
87 changes: 87 additions & 0 deletions docs/performance-baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Workstation performance baseline

## TL;DR

Preserve the current visuals and target balanced responsiveness. Configuration is
not a benchmark. The measurements below were captured in a restricted agent
sandbox, not a complete Hyprland session; no desktop performance gain is claimed.

## Recorded evidence

| Check | Observation | Limit |
|---|---|---|
| OS | Fedora 43 KDE edition | Read from host os-release |
| Memory/storage | About 30 GiB RAM, 8 GiB zram, Btrfs on NVMe | Snapshot, not a pressure test |
| Shell with current startup | 20 samples; median 39.32 ms; range 38.53–41.25 ms | Non-login interactive Bash, no PTY, output discarded |
| Shell without Fastfetch | 20 samples; median 33.95 ms; range 32.84–35.54 ms | Temporary rc file; original startup untouched |
| Fastfetch difference | 5.37 ms between medians | Sandbox observation only; retain current behavior |
| Codec package | mesa-va-drivers-freeworld installed; mesa-va-drivers absent | Package inventory does not prove application decoding |
| Power configuration | `/etc/tuned/active_profile`: balanced; EPP: balance_performance | Live TuneD activation could not be queried |
| Monitor/compositor | Unverified | Hyprland socket access denied |
| Boot/failed services | Unverified | System and user bus access denied |
| Desktop idle CPU/power | Unverified | Process visibility excludes the live desktop |

The historical samples above collected each variant as a separate batch. Cache
warm-up or changing host load can bias that difference. The repeat procedure
below interleaves variants and alternates first position within each pair; the
historical numbers have not been remeasured with this procedure.

## Repeat shell startup measurement

Run from your normal host terminal. This measures startup without terminal
rendering, so also compare perceived opening time in Kitty. The temporary variant
intercepts the direct Fastfetch startup invocation; it never edits dotfiles.

```python
import pathlib, statistics, subprocess, tempfile, time

original = pathlib.Path.home().joinpath('.bashrc').read_text()
with tempfile.TemporaryDirectory() as directory:
variants = ('current', 'without_fastfetch')
samples = {variant: [] for variant in variants}
configs = {}
for variant in variants:
rc = pathlib.Path(directory) / variant
# Intercept only direct fastfetch calls in this disposable child shell.
# The tracked fragment and existing rc both use direct calls.
rc.write_text(('fastfetch() { :; }\n' if variant == 'without_fastfetch' else '') + original)
configs[variant] = rc
for round_number in range(20):
order = variants if round_number % 2 == 0 else variants[::-1]
for variant in order:
rc = configs[variant]
start = time.perf_counter()
subprocess.run(['bash', '--noprofile', '--rcfile', str(rc), '-ic', 'exit'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
samples[variant].append((time.perf_counter() - start) * 1000)
for variant, values in samples.items():
assert len(values) == 20
print(variant, 'median/range ms:', statistics.median(values), min(values), max(values))
```

This temporary function does not intercept an explicit `command fastfetch` or
absolute executable path; inspect customized startup files before using it.
Retain Fastfetch unless repeated host measurements or your preference justify
moving it to an on-demand command.

## Complete the host baseline

- Capture `systemd-analyze`, `systemd-analyze critical-chain`, and failed system/user
units across three comparable cold boots. Report medians and ranges, not a single
longest service: services can start in parallel.
- Run `hyprctl monitors`, `hyprctl configerrors`, `tuned-adm active`, and
`tuned-adm verify`. Confirm actual resolution/refresh and power-profile activation.
- Count `wallpaper.sh` listener processes after login and after ten `SUPER+A`
refreshes. There should be one listener.
- With the same apps open and no builds/downloads, capture three sixty-second
idle samples using `pidstat -u -r 1 60` if installed. Record compositor, bar,
wallpaper, CPU activity, and available hardware power readings separately.
- Inspect `vainfo` and the browser's media diagnostics during the same video test.
Confirm the codec, rendering/decoding device, CPU use, and dropped frames.

## Change policy

Keep blur, transparency, animations, VRR, Kitty delays, and wallpaper transition
rate unchanged in this series. Test one proposed tuning change at a time with the
same workload. Adopt it only if improvement is repeatable beyond baseline variation,
and record any visual, latency, or idle-power tradeoff plus rollback.
11 changes: 11 additions & 0 deletions git/developer.gitconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Portable diff presentation; identity, credentials, and signing remain local.
[core]
pager = delta
[interactive]
diffFilter = delta --color-only
[delta]
navigate = true
light = false
side-by-side = false
line-numbers = true
syntax-theme = "Catppuccin Mocha"
49 changes: 26 additions & 23 deletions scripts/install-symlinks.sh
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
#!/usr/bin/env bash
# Link tracked configs and portable fragments, preserving local settings in backups.
set -euo pipefail
for tool in python3 git; do
command -v "$tool" >/dev/null || { echo "Missing required tool: $tool" >&2; exit 1; }
done
DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"

DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONFIG_SOURCE="$DOTFILES_DIR/.config"
TARGET_DIR="$HOME/.config"

echo "[+] Installing dotfiles symlinks into $TARGET_DIR..."
mkdir -p "$TARGET_DIR"

for item in "$CONFIG_SOURCE"/*; do
base="$(basename "$item")"
target="$TARGET_DIR/$base"

if [ -L "$target" ] && [ "$(readlink -f "$target")" = "$item" ]; then
echo " [=] $base is already linked."
continue
# Preserve conflicts, including dangling links, before installing one source.
link_item() {
local source=$1 target=$2 backup
mkdir -p "$(dirname "$target")"
if [[ -L $target && $(readlink -f "$target") == "$source" ]]; then
Comment thread
SamuelIVX marked this conversation as resolved.
echo "Already linked: $target"
return
fi

if [ -e "$target" ]; then
backup="${target}.backup-$(date +%s)"
echo " [!] Backing up existing $base to $backup"
mv "$target" "$backup"
if [[ -e $target || -L $target ]]; then
backup=$(mktemp -d "${target}.backup-XXXXXX")
mv -- "$target" "$backup/original"
echo "Backup: $backup/original"
fi
ln -s -- "$source" "$target"
echo "Linked: $target"
}

ln -s "$item" "$target"
echo " [+] Linked $base -> $item"
# Migrate before linking so unsupported shell ownership fails without changing configs.
python3 "$DOTFILES_DIR/scripts/migrate-shell.py"
for item in "$DOTFILES_DIR/.config/"*; do
link_item "$item" "$HOME/.config/$(basename "$item")"
done

echo "[✓] All dotfiles successfully linked!"
link_item "$DOTFILES_DIR/shell/dotfiles.sh" "$HOME/.bashrc.d/dotfiles.sh"
link_item "$DOTFILES_DIR/git/developer.gitconfig" "$HOME/.config/git/developer.gitconfig"
echo 'Configuration installed. Open a new interactive shell to use the fragment.'
135 changes: 135 additions & 0 deletions scripts/migrate-shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Migrate only the recognized inline Bash block, preserving other local content."""
from pathlib import Path
import shutil
import subprocess
import os
import tempfile

LEGACY = '''alias syu="sudo dnf upgrade"

# Modern developer aliases
if command -v eza >/dev/null 2>&1; then
alias ls="eza --icons --group-directories-first"
alias ll="eza -la --icons --group-directories-first"
alias tree="eza --tree --icons"
fi

if command -v bat >/dev/null 2>&1; then
alias cat="bat --paging=never"
fi

if command -v duf >/dev/null 2>&1; then
alias df="duf"
fi

# Zoxide smart jumping (replaces cd)
command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init bash)"

# FZF keybindings & fuzzy completion
command -v fzf >/dev/null 2>&1 && eval "$(fzf --bash)"

eval "$(starship init bash)"
fastfetch
'''
DIRECTORY_LOADER = '''if [ -d ~/.bashrc.d ]; then
for rc in ~/.bashrc.d/*; do
if [ -f "$rc" ]; then
. "$rc"
fi
done
fi'''
HOOK = '''# Dotfiles portable developer configuration
if [ -r "$HOME/.bashrc.d/dotfiles.sh" ]; then
. "$HOME/.bashrc.d/dotfiles.sh"
fi
'''


def migrate(path: Path) -> None:
"""Back up and migrate an exact known block; never remove edited local code.

Args:
path: Local Bash startup file.
Raises:
ValueError: The startup file is a symlink requiring manual integration.
OSError: Backup or update fails.
"""
if path.is_symlink():
raise ValueError('Symlinked .bashrc: integrate shell/dotfiles.sh manually')
original = path.read_text() if path.exists() else ''
updated = original.replace(LEGACY, '')
# shortcut: loader detection matches raw text; manually source the fragment when a loader is unreachable.
if DIRECTORY_LOADER not in updated and HOOK not in updated:
updated = updated.rstrip('\n') + '\n\n' + HOOK
Comment thread
SamuelIVX marked this conversation as resolved.
if updated == original:
return
if path.exists():
with tempfile.NamedTemporaryFile(prefix=path.name + '.backup-', dir=path.parent, delete=False) as backup:
shutil.copy2(path, backup.name)
print(f'Bash backup: {backup.name}')
mode = path.stat().st_mode & 0o7777 if path.exists() else 0o600
temporary_name = None
try:
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', prefix=path.name + '.tmp-',
dir=path.parent, delete=False) as temporary:
temporary_name = temporary.name
temporary.write(updated)
temporary.flush()
os.fchmod(temporary.fileno(), mode)
os.fsync(temporary.fileno())
os.replace(temporary_name, path)
finally:
if temporary_name is not None:
Path(temporary_name).unlink(missing_ok=True)


def migrate_git(path: Path, include: Path) -> None:
"""Prepend a portable include and remove only matching legacy Git values.

Local identity and differing overrides stay after the include and take priority.
Raises ValueError for symlinked files and OSError or CalledProcessError on failure.
"""
if path.is_symlink():
raise ValueError('Symlinked .gitconfig: integrate git/developer.gitconfig manually')
original = path.read_text() if path.exists() else ''
settings = {
'core.pager': 'delta', 'interactive.diffFilter': 'delta --color-only',
'delta.navigate': 'true', 'delta.light': 'false', 'delta.side-by-side': 'false',
'delta.line-numbers': 'true', 'delta.syntax-theme': 'Catppuccin Mocha',
}
with tempfile.TemporaryDirectory(dir=path.parent) as directory:
draft = Path(directory) / 'gitconfig'
draft.write_text(original)
for key, value in settings.items():
result = subprocess.run(['git', 'config', '--file', str(draft), '--fixed-value',
'--unset-all', key, value], capture_output=True, text=True, check=False)
if result.returncode not in (0, 5):
raise RuntimeError(f'Cannot migrate Git setting {key}: {result.stderr}')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Let Git quote the path instead of implementing its configuration grammar.
prefix = Path(directory) / 'include'
subprocess.run(['git', 'config', '--file', str(prefix), '--add', 'include.path', str(include)], check=True)
existing = subprocess.run(['git', 'config', '--file', str(draft), '--get-all', 'include.path'],
capture_output=True, text=True, check=False)
if existing.returncode not in (0, 1):
raise RuntimeError('Cannot read Git include settings')
updated = draft.read_text()
if str(include) not in existing.stdout.splitlines():
updated = prefix.read_text() + updated
if updated == original:
return
if path.exists():
with tempfile.NamedTemporaryFile(prefix=path.name + '.backup-', dir=path.parent, delete=False) as backup:
shutil.copy2(path, backup.name)
print(f'Git backup: {backup.name}')
draft.chmod(path.stat().st_mode & 0o777)
else:
draft.chmod(0o600)
draft.write_text(updated)
os.replace(draft, path)


if __name__ == '__main__':
home = Path.home()
migrate(home / '.bashrc')
migrate_git(home / '.gitconfig', home / '.config/git/developer.gitconfig')
15 changes: 15 additions & 0 deletions shell/dotfiles.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Portable interactive developer aliases, navigation, completion, and prompt.
[[ $- == *i* ]] || return 0
alias syu='sudo dnf upgrade'
if command -v eza >/dev/null 2>&1; then
alias ls='eza --icons --group-directories-first'
alias ll='eza -la --icons --group-directories-first'
alias tree='eza --tree --icons'
fi
command -v bat >/dev/null 2>&1 && alias cat='bat --paging=never'
command -v duf >/dev/null 2>&1 && alias df=duf
if command -v zoxide >/dev/null 2>&1; then eval "$(zoxide init bash)"; fi
if command -v fzf >/dev/null 2>&1; then eval "$(fzf --bash)"; fi
if command -v starship >/dev/null 2>&1; then eval "$(starship init bash)"; fi
if command -v fastfetch >/dev/null 2>&1; then fastfetch; fi
Loading
Loading