From 8fd5b9c630501f6e00cd1b519bcfba3d7e956c3b Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 22 Mar 2026 15:45:08 +0300 Subject: [PATCH] =?UTF-8?q?feat(windows):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20chezmoi-=D1=81=D0=BB=D0=BE=D0=B9=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20pwsh,=20terminal=20=D0=B8=20git?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - нужно перенести стабильную Windows-конфигурацию в dotfiles без поломки существующего stow/install.sh workflow для Linux и WSL. - нужно убрать типичные проблемы Git на Windows, включая line endings, из ручной настройки машины. - Что: - добавлен отдельный source dir `chezmoi/` с tiny stub для PowerShell 7 и managed profile в `~/.config/powershell`. - добавлены идемпотентная настройка Windows Terminal и managed Git baseline с миграцией локальных настроек в `~/.gitconfig.local`. - обновлён README: описан Windows workflow, helper-функции pwsh и состав Git baseline без персональных данных. - Проверка: - `chezmoi -S "$HOME\dotfiles\chezmoi" apply` - `pwsh -NoLogo -Command ''$PSVersionTable.PSVersion.ToString(); chcp; [Console]::InputEncoding.WebName; [Console]::OutputEncoding.WebName''` - `git config --global --includes --list --show-origin` --- README.md | 488 +++++++++--------- chezmoi/.chezmoiignore | 6 + .../run_20-configure-windows-terminal.ps1 | 267 ++++++++++ .../run_before_10-migrate-gitconfig.ps1 | 67 +++ .../Microsoft.PowerShell_profile.ps1.tmpl | 6 + chezmoi/dot_config/git/config | 14 + chezmoi/dot_config/powershell/profile.ps1 | 135 +++++ chezmoi/dot_gitconfig | 5 + 8 files changed, 750 insertions(+), 238 deletions(-) create mode 100644 chezmoi/.chezmoiignore create mode 100644 chezmoi/.chezmoiscripts/run_20-configure-windows-terminal.ps1 create mode 100644 chezmoi/.chezmoiscripts/run_before_10-migrate-gitconfig.ps1 create mode 100644 chezmoi/Documents/PowerShell/Microsoft.PowerShell_profile.ps1.tmpl create mode 100644 chezmoi/dot_config/git/config create mode 100644 chezmoi/dot_config/powershell/profile.ps1 create mode 100644 chezmoi/dot_gitconfig diff --git a/README.md b/README.md index 6200f93..50e22e2 100644 --- a/README.md +++ b/README.md @@ -1,260 +1,272 @@ -# Dotfiles - -Персональные конфигурационные файлы для настройки рабочего окружения. - -## 📦 Содержимое - -### Bash -- **Файлы:** `bash/.bash_custom`, `bash/.bash_functions`, `bash/.bash_prompt` -- Подключение кастомных bash-файлов (функции, prompt, секреты) поверх стандартного `~/.bashrc` -- Кастомный двухстрочный prompt (подключается из `~/.bash_prompt`) -- Поддержка цветного вывода и автодополнения - -### Tmux -- **Файл:** `tmux/.tmux.conf` -- Два префикса: `Ctrl+b` (стандартный) и `Ctrl+Space` (удобный) -- Визуальный стиль с синей цветовой схемой -- Vi-режим для копирования -- Горячие клавиши: - - `|` / `-` — разделение панелей - - `Alt+стрелки` — изменение размера панелей и переключение окон - - `r` — перезагрузка конфига - -### Vim -- **Файл:** `vim/.vimrc` -- Минималистичная конфигурация без плагинов -- 2 пробела вместо табов (оптимально для YAML/XML) -- Поддержка мыши и подсветка поиска -- Leader key: `Space` - - `w` — сохранить - - `h` — убрать подсветку поиска - - `p` — вставка без форматирования - -### Zed -- **Директория:** `zed/.config/zed/` -- Структура для конфигурации Zed редактора - -### VS Code (WSL) -- **Файл:** `vscode/.vscode-server/server-env-setup` -- Поддержка `~/.vscode-server/server-env-setup` для VS Code Server в WSL -- Подгрузка `~/.env.secrets` для расширений, задач/debug и терминалов VS Code -- Автоматическая настройка прокси: если `hproxy.local` резолвится, выставляются `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` (socks5), `NO_PROXY` и их lowercase-варианты -- Режим `fail-open`: если `~/.env.secrets` отсутствует или не загружается — warning в stderr, VS Code продолжает запуск - -### Opencode -- **Директория:** `opencode/.config/opencode/` -- Конфигурация AI-ассистента opencode с MCP-серверами: - - **context7** — документация библиотек - - **web-reader** — чтение веб-страниц - - **web-search-prime** — поиск в интернете - - **zread** — чтение GitHub репозиториев - - **playwright** — браузерная автоматизация -- Требует переменных окружения (см. `.env.example`) -- Скиллы берутся из общего пакета `agents/.agents/skills/` через симлинк `~/.config/opencode/skills` - -### Agents (shared skills) -- **Директория:** `agents/.agents/skills/` -- Каноничное место для общих скиллов в формате `~/.agents/skills//SKILL.md` - -### Codex -- **Директория:** `codex/.codex/skills/` -- Тонкие симлинки в `~/.codex/skills/` на общие skills из `~/.agents/skills/` - -### Claude -- **Директория:** `claude/.claude/` -- Тонкий симлинк `~/.claude/skills` на общие skills из `~/.agents/skills/` -- **Двухуровневые настройки:** - - `settings.json` (в dotfiles, read-only) — стабильная база: permissions, statusLine - - `settings.local.json` (локальный, не в git) — машинно-специфичное: model, effortLevel -- Claude Code мержит оба файла, `settings.local.json` имеет приоритет -- `settings.json` защищён `chmod 444`, чтобы Claude Code не перезаписывал tracked файл - -### Gemini -- **Директория:** `gemini/.gemini/` -- Политики для Gemini CLI: pre-approved readonly команды - -## 🚀 Установка - -### Автоматическая (рекомендуется) - -```bash -sudo apt install stow -git clone https://github.com/ddmitry/dotfiles ~/dotfiles -cd ~/dotfiles -./install.sh -``` - -Скрипт `install.sh`: -- Применяет все stow пакеты -- Создаёт `~/.claude/settings.local.json` с дефолтными model/effortLevel -- Защищает `settings.json` от перезаписи (`chmod 444`) -- Автоматически добавляет подключение bash_custom в `~/.bashrc` -- В WSL предлагает настроить автоматический резолв `hproxy.local` (см. ниже) - -### Stow вручную - -```bash -sudo apt install stow -cd /path/to/dotfiles -stow bash tmux vim zed vscode agents opencode codex claude gemini -``` - -После stow добавьте в `~/.bashrc` (если не используете `install.sh`): -```bash -[ -f ~/.bash_custom ] && source ~/.bash_custom -``` - -Создайте локальные настройки Claude: -```bash -cat > ~/.claude/settings.local.json <<'EOF' -{ - "model": "opus", - "effortLevel": "high" -} -EOF -``` - +# Dotfiles + +Персональные конфигурационные файлы для настройки рабочего окружения. + +## 📦 Содержимое + +### Bash +- **Файлы:** `bash/.bash_custom`, `bash/.bash_functions`, `bash/.bash_prompt` +- Подключение кастомных bash-файлов (функции, prompt, секреты) поверх стандартного `~/.bashrc` +- Кастомный двухстрочный prompt (подключается из `~/.bash_prompt`) +- Поддержка цветного вывода и автодополнения + +### Tmux +- **Файл:** `tmux/.tmux.conf` +- Два префикса: `Ctrl+b` (стандартный) и `Ctrl+Space` (удобный) +- Визуальный стиль с синей цветовой схемой +- Vi-режим для копирования +- Горячие клавиши: + - `|` / `-` — разделение панелей + - `Alt+стрелки` — изменение размера панелей и переключение окон + - `r` — перезагрузка конфига + +### Vim +- **Файл:** `vim/.vimrc` +- Минималистичная конфигурация без плагинов +- 2 пробела вместо табов (оптимально для YAML/XML) +- Поддержка мыши и подсветка поиска +- Leader key: `Space` + - `w` — сохранить + - `h` — убрать подсветку поиска + - `p` — вставка без форматирования + +### Zed +- **Директория:** `zed/.config/zed/` +- Структура для конфигурации Zed редактора + +### VS Code (WSL) +- **Файл:** `vscode/.vscode-server/server-env-setup` +- Поддержка `~/.vscode-server/server-env-setup` для VS Code Server в WSL +- Подгрузка `~/.env.secrets` для расширений, задач/debug и терминалов VS Code +- Автоматическая настройка прокси: если `hproxy.local` резолвится, выставляются `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` (socks5), `NO_PROXY` и их lowercase-варианты +- Режим `fail-open`: если `~/.env.secrets` отсутствует или не загружается — warning в stderr, VS Code продолжает запуск + +### Opencode +- **Директория:** `opencode/.config/opencode/` +- Конфигурация AI-ассистента opencode с MCP-серверами: + - **context7** — документация библиотек + - **web-reader** — чтение веб-страниц + - **web-search-prime** — поиск в интернете + - **zread** — чтение GitHub репозиториев + - **playwright** — браузерная автоматизация +- Требует переменных окружения (см. `.env.example`) +- Скиллы берутся из общего пакета `agents/.agents/skills/` через симлинк `~/.config/opencode/skills` + +### Agents (shared skills) +- **Директория:** `agents/.agents/skills/` +- Каноничное место для общих скиллов в формате `~/.agents/skills//SKILL.md` + +### Codex +- **Директория:** `codex/.codex/skills/` +- Тонкие симлинки в `~/.codex/skills/` на общие skills из `~/.agents/skills/` + +### Claude +- **Директория:** `claude/.claude/` +- Тонкий симлинк `~/.claude/skills` на общие skills из `~/.agents/skills/` +- **Двухуровневые настройки:** + - `settings.json` (в dotfiles, read-only) — стабильная база: permissions, statusLine + - `settings.local.json` (локальный, не в git) — машинно-специфичное: model, effortLevel +- Claude Code мержит оба файла, `settings.local.json` имеет приоритет +- `settings.json` защищён `chmod 444`, чтобы Claude Code не перезаписывал tracked файл + +### Gemini +- **Директория:** `gemini/.gemini/` +- Политики для Gemini CLI: pre-approved readonly команды + +## 🚀 Установка + +### Автоматическая (рекомендуется) + +```bash +sudo apt install stow +git clone https://github.com/ddmitry/dotfiles ~/dotfiles +cd ~/dotfiles +./install.sh +``` + +Скрипт `install.sh`: +- Применяет все stow пакеты +- Создаёт `~/.claude/settings.local.json` с дефолтными model/effortLevel +- Защищает `settings.json` от перезаписи (`chmod 444`) +- Автоматически добавляет подключение bash_custom в `~/.bashrc` +- В WSL предлагает настроить автоматический резолв `hproxy.local` (см. ниже) + +### Stow вручную + +```bash +sudo apt install stow +cd /path/to/dotfiles +stow bash tmux vim zed vscode agents opencode codex claude gemini +``` + +После stow добавьте в `~/.bashrc` (если не используете `install.sh`): +```bash +[ -f ~/.bash_custom ] && source ~/.bash_custom +``` + +Создайте локальные настройки Claude: +```bash +cat > ~/.claude/settings.local.json <<'EOF' +{ + "model": "opus", + "effortLevel": "high" +} +EOF +``` + ### Windows (chezmoi) -Для Windows используется [chezmoi](https://www.chezmoi.io/) — кросс-платформенный менеджер dotfiles. +Для Windows предпочтителен отдельный source dir `chezmoi/` внутри этого репозитория. Это позволяет держать Windows-слой рядом с остальными dotfiles, не ломая существующий `stow`/`install.sh` workflow для Linux/WSL. > 💡 **Примечание:** chezmoi также работает на Linux/WSL, но stow предпочтительнее для полной функциональности. **⚠️ Требования:** - Developer Mode должен быть включён (для создания симлинков) > Settings → System → For developers → Developer Mode → On -- PowerShell 5.0+ или PowerShell Core +- `pwsh` 7.x для повседневной работы +- `Windows PowerShell 5.1` остаётся legacy-режимом и не тюнится агрессивно -**Установка (два шага):** +**Быстрое применение из уже существующего `~/dotfiles`:** ```powershell -# 1. Установка chezmoi winget install twpayne.chezmoi -# 2. Инициализация (клонирует репо) -chezmoi init ddmitry/dotfiles - -# 3. Первое применение (подготавливает структуру) -chezmoi apply - -# 4. Второе применение (копирует файлы в правильные места) -chezmoi apply +# Применить только Windows-layer из репозитория +chezmoi -S "$HOME\dotfiles\chezmoi" apply ``` -**Примечания:** -- После шага 4 все файлы будут на месте: `~/.vimrc`, `~/.agents/skills/`, `~/.config/opencode/`, и т.д. -- Симлинки для skills создаются автоматически только если включен Developer Mode -- Без Developer Mode AI-ассистенты будут работать с прямыми копиями skills +Этот режим не требует менять глобальный source `chezmoi`: достаточно запускать `chezmoi -S "$HOME\dotfiles\chezmoi" apply` из уже существующего репозитория. -**Что доступно на Windows:** -- ✅ Vim -- ✅ Agents (shared skills) -- ✅ Codex (с симлинками на skills) -- ✅ Claude (с симлинками на skills) -- ✅ Opencode (с симлинками на skills) +**Что делает текущий Windows-layer:** +- ✅ ставит маленький live-stub в `~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1` +- ✅ хранит основную логику `pwsh` в `~/.config/powershell/profile.ps1` +- ✅ раскладывает Git на managed baseline в `~/.config/git/config` и локальные overrides в `~/.gitconfig.local` +- ✅ оставляет hook'и для локальных добавок: `~/.config/powershell/profile.extra.ps1` и `~/.config/powershell/profile.d/*.ps1` +- ✅ идемпотентно приводит `Windows Terminal` к базовому состоянию без полной перезаписи всего `settings.json` -**Что НЕ доступно на Windows:** -- ❌ Bash (Linux-only) -- ❌ Tmux (Linux-only) -- ❌ Zed (пути конфигурации отличаются) +**Какие helper-функции есть в `pwsh`:** +- `Reload-Profile` — перечитывает текущий профиль без перезапуска сессии +- `which` — показывает все найденные команды по имени через `Get-Command -All` +- `touch` — создаёт файл или обновляет `LastWriteTime` +- `mkcd` — создаёт директорию и сразу переходит в неё +- `open` — открывает путь через `Invoke-Item` +- `croot` — переходит в корень текущего Git-репозитория +- `wt-here` — открывает новое окно Windows Terminal в текущей директории +- `wt-tab-here` — открывает новую вкладку Windows Terminal в текущей директории +- `wt-split-here` — открывает split-pane Windows Terminal в текущей директории +- `codevpn` — запускает `code .` с временно выставленными `HTTP_PROXY` и `HTTPS_PROXY` + +**Что сознательно не делается:** +- ❌ `Windows PowerShell 5.1` не форсируется в UTF-8 и не меняется без явного запроса +- ❌ Linux/WSL-поток не переносится в chezmoi и продолжает жить через `stow`/`install.sh` + +**Что фиксирует Git baseline для Windows:** +- `core.autocrlf = false` +- `core.eol = lf` +- `core.quotepath = false` +- `core.longpaths = true` +- `fetch.prune = true` +- `pull.ff = only` +- `init.defaultBranch = main` +- `user.name`, `user.email`, `core.editor` и `core.sshCommand` в baseline не входят и должны жить в `~/.gitconfig.local` ## 🔧 Требования - -### Linux/WSL -- Linux (протестировано на Ubuntu) -- Bash 4.0+ -- Tmux 2.0+ -- Vim 8.0+ - + +### Linux/WSL +- Linux (протестировано на Ubuntu) +- Bash 4.0+ +- Tmux 2.0+ +- Vim 8.0+ + ### Windows - Windows 10/11 с Developer Mode -- PowerShell 5.0+ или PowerShell Core +- PowerShell 7.x - [chezmoi](https://www.chezmoi.io/) - -## 📝 Дополнительные файлы - -Для полной настройки создайте: -- `~/.bash_aliases` — персональные алиасы -- `~/.env.secrets` — приватные переменные окружения (API ключи) - -## 🔑 Переменные окружения - -### Кастомные функции bash - -Файл `bash/.bash_functions` содержит пользовательские функции (proxy_on/off, myip и др.). -При использовании `stow bash` он будет доступен как `~/.bash_functions`. - -### Приватные переменные (секреты) - -1. Скопируйте шаблон: -```bash -cp bash/.env.secrets.example ~/.env.secrets -``` - -2. Отредактируйте и добавьте API ключи: -```bash -# Context7 API (https://context7.com) -export CONTEXT7_API_KEY="your_context7_api_key_here" - -# Z.AI API для LLM, поиска и чтения (https://z.ai) -export ZAI_API_KEY="your_zai_api_key_here" -``` - -Файл `~/.env.secrets` автоматически подгружается при старте shell. - -### WSL: настройка hproxy.local - -Имя `hproxy.local` используется как алиас для хоста, на котором работает прокси-сервер. Это позволяет функциям `proxy_on`/`proxy_off` и VS Code Server не зависеть от конкретного IP-адреса. - -**NAT mode** (по умолчанию в WSL2) — IP шлюза меняется при каждом запуске, поэтому нужен автоматический boot-скрипт: - -```bash -# Вариант 1: через install.sh (интерактивно) -./install.sh # ответьте "y" на вопрос про wsl.conf - -# Вариант 2: вручную -sudo tee -a /etc/wsl.conf <<'EOF' - -[boot] -command = "/path/to/dotfiles/scripts/setup-hproxy.sh" -EOF -``` - -После изменения `wsl.conf` перезапустите WSL: `wsl --shutdown`. - -Обновить `hproxy.local` без перезапуска WSL: -```bash -sudo scripts/setup-hproxy.sh -# или -update_hproxy # интерактивная функция из .bash_functions -``` - -**Mirror mode** (`networkingMode=mirrored` в `.wslconfig`) — WSL разделяет сетевой стек с хостом, IP не меняется. Достаточно один раз прописать запись в Windows hosts: - -``` -# C:\Windows\System32\drivers\etc\hosts -192.168.1.100 hproxy.local -``` - -### VS Code Server в WSL - -Файл `vscode/.vscode-server/server-env-setup` выполняется при каждом старте VS Code Server в WSL и делает две вещи: - -1. **Загружает `~/.env.secrets`** — API ключи становятся доступны расширениям, задачам и терминалам VS Code. Если файл отсутствует или содержит ошибку, выводится warning в stderr, запуск не блокируется. - -2. **Настраивает прокси** — если `hproxy.local` резолвится (через `/etc/hosts` или DNS в режиме mirror), автоматически выставляются: - - `HTTP_PROXY` / `http_proxy` → `http://hproxy.local:12334` - - `HTTPS_PROXY` / `https_proxy` → `http://hproxy.local:12334` - - `ALL_PROXY` / `all_proxy` → `socks5://hproxy.local:12334` - - `NO_PROXY` / `no_proxy` → `hproxy.local,localhost,127.0.0.1,.concept-software.ru` - -После установки/обновления файла сделайте одно из действий: -- `Developer: Reload Window` в VS Code -- или `Remote-WSL: Reconnect to WSL` - -Если у вас уже есть локальный `~/.vscode-server/server-env-setup`, stow может сообщить конфликт. -Тогда сохраните старую версию и примените stow повторно: -```bash -mv ~/.vscode-server/server-env-setup ~/.vscode-server/server-env-setup.bak.$(date +%Y%m%d%H%M%S) -stow -vt ~ vscode -``` + +## 📝 Дополнительные файлы + +Для полной настройки создайте: +- `~/.bash_aliases` — персональные алиасы +- `~/.env.secrets` — приватные переменные окружения (API ключи) + +## 🔑 Переменные окружения + +### Кастомные функции bash + +Файл `bash/.bash_functions` содержит пользовательские функции (proxy_on/off, myip и др.). +При использовании `stow bash` он будет доступен как `~/.bash_functions`. + +### Приватные переменные (секреты) + +1. Скопируйте шаблон: +```bash +cp bash/.env.secrets.example ~/.env.secrets +``` + +2. Отредактируйте и добавьте API ключи: +```bash +# Context7 API (https://context7.com) +export CONTEXT7_API_KEY="your_context7_api_key_here" + +# Z.AI API для LLM, поиска и чтения (https://z.ai) +export ZAI_API_KEY="your_zai_api_key_here" +``` + +Файл `~/.env.secrets` автоматически подгружается при старте shell. + +### WSL: настройка hproxy.local + +Имя `hproxy.local` используется как алиас для хоста, на котором работает прокси-сервер. Это позволяет функциям `proxy_on`/`proxy_off` и VS Code Server не зависеть от конкретного IP-адреса. + +**NAT mode** (по умолчанию в WSL2) — IP шлюза меняется при каждом запуске, поэтому нужен автоматический boot-скрипт: + +```bash +# Вариант 1: через install.sh (интерактивно) +./install.sh # ответьте "y" на вопрос про wsl.conf + +# Вариант 2: вручную +sudo tee -a /etc/wsl.conf <<'EOF' + +[boot] +command = "/path/to/dotfiles/scripts/setup-hproxy.sh" +EOF +``` + +После изменения `wsl.conf` перезапустите WSL: `wsl --shutdown`. + +Обновить `hproxy.local` без перезапуска WSL: +```bash +sudo scripts/setup-hproxy.sh +# или +update_hproxy # интерактивная функция из .bash_functions +``` + +**Mirror mode** (`networkingMode=mirrored` в `.wslconfig`) — WSL разделяет сетевой стек с хостом, IP не меняется. Достаточно один раз прописать запись в Windows hosts: + +``` +# C:\Windows\System32\drivers\etc\hosts +192.168.1.100 hproxy.local +``` + +### VS Code Server в WSL + +Файл `vscode/.vscode-server/server-env-setup` выполняется при каждом старте VS Code Server в WSL и делает две вещи: + +1. **Загружает `~/.env.secrets`** — API ключи становятся доступны расширениям, задачам и терминалам VS Code. Если файл отсутствует или содержит ошибку, выводится warning в stderr, запуск не блокируется. + +2. **Настраивает прокси** — если `hproxy.local` резолвится (через `/etc/hosts` или DNS в режиме mirror), автоматически выставляются: + - `HTTP_PROXY` / `http_proxy` → `http://hproxy.local:12334` + - `HTTPS_PROXY` / `https_proxy` → `http://hproxy.local:12334` + - `ALL_PROXY` / `all_proxy` → `socks5://hproxy.local:12334` + - `NO_PROXY` / `no_proxy` → `hproxy.local,localhost,127.0.0.1,.concept-software.ru` + +После установки/обновления файла сделайте одно из действий: +- `Developer: Reload Window` в VS Code +- или `Remote-WSL: Reconnect to WSL` + +Если у вас уже есть локальный `~/.vscode-server/server-env-setup`, stow может сообщить конфликт. +Тогда сохраните старую версию и примените stow повторно: +```bash +mv ~/.vscode-server/server-env-setup ~/.vscode-server/server-env-setup.bak.$(date +%Y%m%d%H%M%S) +stow -vt ~ vscode +``` diff --git a/chezmoi/.chezmoiignore b/chezmoi/.chezmoiignore new file mode 100644 index 0000000..99f28bf --- /dev/null +++ b/chezmoi/.chezmoiignore @@ -0,0 +1,6 @@ +{{ if ne .chezmoi.os "windows" }} +AppData/** +Documents/** +dot_config/** +dot_gitconfig +{{ end }} diff --git a/chezmoi/.chezmoiscripts/run_20-configure-windows-terminal.ps1 b/chezmoi/.chezmoiscripts/run_20-configure-windows-terminal.ps1 new file mode 100644 index 0000000..46a8c46 --- /dev/null +++ b/chezmoi/.chezmoiscripts/run_20-configure-windows-terminal.ps1 @@ -0,0 +1,267 @@ +if (-not $IsWindows) { + return +} + +$settingsPath = Join-Path $env:LOCALAPPDATA 'Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json' +if (-not (Test-Path -LiteralPath $settingsPath)) { + Write-Warning "Windows Terminal settings not found: $settingsPath" + return +} + +$PowerShellGuid = '{574e775e-4f2a-5b96-ac1e-a2962a402336}' +$LegacyPowerShellGuid = '{61c54bbd-c2c6-5271-96e7-009a87ff44bf}' +$changed = $false + +function Get-JsonPropertyValue { + param( + [Parameter(Mandatory = $true)] + $Object, + [Parameter(Mandatory = $true)] + [string]$Name + ) + + $Property = $Object.PSObject.Properties[$Name] + if ($null -eq $Property) { + return $null + } + + return $Property.Value +} + +function Test-JsonValueEqual { + param( + $Left, + $Right + ) + + if ($null -eq $Left -and $null -eq $Right) { + return $true + } + + if ($null -eq $Left -or $null -eq $Right) { + return $false + } + + if ( + $Left -is [System.Collections.IEnumerable] -and + -not ($Left -is [string]) + ) { + return (ConvertTo-Json $Left -Depth 100 -Compress) -eq (ConvertTo-Json $Right -Depth 100 -Compress) + } + + return $Left -eq $Right +} + +function Set-JsonPropertyValue { + param( + [Parameter(Mandatory = $true)] + $Object, + [Parameter(Mandatory = $true)] + [string]$Name, + $Value + ) + + $Property = $Object.PSObject.Properties[$Name] + if ($null -eq $Property) { + $Object | Add-Member -NotePropertyName $Name -NotePropertyValue $Value + $script:changed = $true + return + } + + if (-not (Test-JsonValueEqual -Left $Property.Value -Right $Value)) { + $Property.Value = $Value + $script:changed = $true + } +} + +function Test-IsPowerShell7Profile { + param( + [Parameter(Mandatory = $true)] + $Profile + ) + + $Source = [string](Get-JsonPropertyValue -Object $Profile -Name source) + $Name = [string](Get-JsonPropertyValue -Object $Profile -Name name) + $CommandLine = [string](Get-JsonPropertyValue -Object $Profile -Name commandline) + + if ($Source -eq 'Windows.Terminal.PowershellCore') { + return $true + } + + if ($CommandLine -match '(?i)(^|[\\/ ])pwsh(?:\.exe)?(?:\s|$)') { + return $true + } + + return $Name -match '^(PowerShell|PowerShell 7)$' +} + +function Test-IsDisposablePowerShell7Profile { + param( + [Parameter(Mandatory = $true)] + $Profile + ) + + $Source = [string](Get-JsonPropertyValue -Object $Profile -Name source) + $Name = [string](Get-JsonPropertyValue -Object $Profile -Name name) + $CommandLine = [string](Get-JsonPropertyValue -Object $Profile -Name commandline) + + return ( + $Name -match '^(PowerShell|PowerShell 7)$' -and + ( + $Source -eq 'Windows.Terminal.PowershellCore' -or + $CommandLine -match '(?i)(^|[\\/ ])pwsh(?:\.exe)?(?:\s|$)' + ) + ) +} + +try { + $originalSettings = Get-Content -LiteralPath $settingsPath -Raw + $settings = $originalSettings | ConvertFrom-Json +} +catch { + Write-Warning "Failed to read Windows Terminal settings: $settingsPath" + Write-Warning $_ + return +} + +if ($null -eq (Get-JsonPropertyValue -Object $settings -Name profiles)) { + $settings | Add-Member -NotePropertyName profiles -NotePropertyValue ([pscustomobject]@{ + defaults = [pscustomobject]@{} + list = @() + }) + $changed = $true +} + +if ($null -eq (Get-JsonPropertyValue -Object $settings.profiles -Name list)) { + $settings.profiles | Add-Member -NotePropertyName list -NotePropertyValue @() + $changed = $true +} + +$profiles = @($settings.profiles.list) +$powerShell7CandidateIndices = New-Object System.Collections.Generic.List[int] +$legacyPowerShellIndex = $null + +for ($i = 0; $i -lt $profiles.Count; $i++) { + $profile = $profiles[$i] + $guid = [string](Get-JsonPropertyValue -Object $profile -Name guid) + $source = [string](Get-JsonPropertyValue -Object $profile -Name source) + $commandLine = [string](Get-JsonPropertyValue -Object $profile -Name commandline) + + if (Test-IsPowerShell7Profile -Profile $profile) { + [void]$powerShell7CandidateIndices.Add($i) + } + + if ( + $guid -eq $LegacyPowerShellGuid -or + $commandLine -match '(?i)WindowsPowerShell\\v1\.0\\powershell\.exe' + ) { + $legacyPowerShellIndex = $i + } + + if ($source -eq 'Windows.Terminal.Azure') { + Set-JsonPropertyValue -Object $profile -Name hidden -Value $true + } +} + +$powerShell7Index = $null +foreach ($candidateIndex in $powerShell7CandidateIndices) { + $candidate = $profiles[$candidateIndex] + if ([string](Get-JsonPropertyValue -Object $candidate -Name source) -eq 'Windows.Terminal.PowershellCore') { + $powerShell7Index = $candidateIndex + break + } +} + +if ($null -eq $powerShell7Index -and $powerShell7CandidateIndices.Count -gt 0) { + $powerShell7Index = $powerShell7CandidateIndices[0] +} + +if ($null -eq $powerShell7Index) { + $profiles += [pscustomobject]@{ + commandline = 'pwsh.exe' + guid = $PowerShellGuid + hidden = $false + name = 'PowerShell 7' + startingDirectory = '%USERPROFILE%' + } + $powerShell7Index = $profiles.Count - 1 + $changed = $true +} + +$powerShell7Profile = $profiles[$powerShell7Index] +if ([string]::IsNullOrWhiteSpace([string](Get-JsonPropertyValue -Object $powerShell7Profile -Name guid))) { + Set-JsonPropertyValue -Object $powerShell7Profile -Name guid -Value $PowerShellGuid +} +if ($null -eq (Get-JsonPropertyValue -Object $powerShell7Profile -Name commandline) -and [string](Get-JsonPropertyValue -Object $powerShell7Profile -Name source) -ne 'Windows.Terminal.PowershellCore') { + Set-JsonPropertyValue -Object $powerShell7Profile -Name commandline -Value 'pwsh.exe' +} +Set-JsonPropertyValue -Object $powerShell7Profile -Name name -Value 'PowerShell 7' +Set-JsonPropertyValue -Object $powerShell7Profile -Name hidden -Value $false +Set-JsonPropertyValue -Object $powerShell7Profile -Name startingDirectory -Value '%USERPROFILE%' +Set-JsonPropertyValue -Object $settings -Name defaultProfile -Value ([string](Get-JsonPropertyValue -Object $powerShell7Profile -Name guid)) + +if ($null -ne $legacyPowerShellIndex) { + $legacyPowerShellProfile = $profiles[$legacyPowerShellIndex] + Set-JsonPropertyValue -Object $legacyPowerShellProfile -Name name -Value 'Windows PowerShell 5.1 (Legacy)' + Set-JsonPropertyValue -Object $legacyPowerShellProfile -Name hidden -Value $false +} + +$removedIndices = New-Object System.Collections.Generic.HashSet[int] +foreach ($candidateIndex in $powerShell7CandidateIndices) { + if ($candidateIndex -eq $powerShell7Index) { + continue + } + + if (Test-IsDisposablePowerShell7Profile -Profile $profiles[$candidateIndex]) { + if ($removedIndices.Add($candidateIndex)) { + $changed = $true + } + } +} + +$orderedProfiles = New-Object System.Collections.Generic.List[object] +$addedIndices = New-Object System.Collections.Generic.HashSet[int] + +function Add-ProfileByIndex { + param( + [int]$Index + ) + + if ($Index -lt 0) { + return + } + + if ($removedIndices.Contains($Index)) { + return + } + + if ($addedIndices.Add($Index)) { + $orderedProfiles.Add($profiles[$Index]) + } +} + +Add-ProfileByIndex -Index $powerShell7Index +if ($null -ne $legacyPowerShellIndex) { + Add-ProfileByIndex -Index $legacyPowerShellIndex +} + +for ($i = 0; $i -lt $profiles.Count; $i++) { + Add-ProfileByIndex -Index $i +} + +$orderedProfilesArray = @($orderedProfiles.ToArray()) +if ((ConvertTo-Json $profiles -Depth 100 -Compress) -ne (ConvertTo-Json $orderedProfilesArray -Depth 100 -Compress)) { + $changed = $true +} +$settings.profiles.list = $orderedProfilesArray + +if (-not $changed) { + Write-Host 'Windows Terminal settings already match the managed baseline.' + return +} + +$backupPath = '{0}.bak-{1}' -f $settingsPath, (Get-Date -Format 'yyyyMMdd-HHmmss') +Copy-Item -LiteralPath $settingsPath -Destination $backupPath -Force +$settings | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $settingsPath -Encoding utf8 +Write-Host "Updated Windows Terminal settings: $settingsPath" +Write-Host "Backup saved to: $backupPath" diff --git a/chezmoi/.chezmoiscripts/run_before_10-migrate-gitconfig.ps1 b/chezmoi/.chezmoiscripts/run_before_10-migrate-gitconfig.ps1 new file mode 100644 index 0000000..5093a5b --- /dev/null +++ b/chezmoi/.chezmoiscripts/run_before_10-migrate-gitconfig.ps1 @@ -0,0 +1,67 @@ +if (-not $IsWindows) { + return +} + +$GlobalGitConfig = Join-Path $HOME '.gitconfig' +$LocalGitConfig = Join-Path $HOME '.gitconfig.local' +$BackupPath = '{0}.pre-chezmoi.bak-{1}' -f $GlobalGitConfig, (Get-Date -Format 'yyyyMMdd-HHmmss') +$ManagedStub = @' +[include] + path = ~/.config/git/config + +[include] + path = ~/.gitconfig.local +'@ + +$ManagedKeys = @( + 'core.autocrlf', + 'core.eol', + 'core.quotepath', + 'core.longpaths', + 'fetch.prune', + 'pull.ff', + 'init.defaultBranch' +) + +function Test-ManagedGitStub { + param( + [Parameter(Mandatory = $true)] + [string]$Content + ) + + $NormalizedContent = ($Content -replace "`r", '').Trim() + $NormalizedStub = ($ManagedStub -replace "`r", '').Trim() + return $NormalizedContent -eq $NormalizedStub +} + +if (-not (Test-Path -LiteralPath $GlobalGitConfig)) { + if (-not (Test-Path -LiteralPath $LocalGitConfig)) { + New-Item -ItemType File -Path $LocalGitConfig -Force | Out-Null + } + return +} + +$CurrentContent = Get-Content -LiteralPath $GlobalGitConfig -Raw +if (Test-ManagedGitStub -Content $CurrentContent) { + if (-not (Test-Path -LiteralPath $LocalGitConfig)) { + New-Item -ItemType File -Path $LocalGitConfig -Force | Out-Null + } + return +} + +if (-not (Test-Path -LiteralPath $LocalGitConfig)) { + Copy-Item -LiteralPath $GlobalGitConfig -Destination $BackupPath -Force + Copy-Item -LiteralPath $GlobalGitConfig -Destination $LocalGitConfig -Force + + foreach ($Key in $ManagedKeys) { + git config --file $LocalGitConfig --unset-all $Key 2>$null + } + + Write-Host "Migrated existing ~/.gitconfig to ~/.gitconfig.local" + Write-Host "Backup saved to: $BackupPath" + return +} + +Copy-Item -LiteralPath $GlobalGitConfig -Destination $BackupPath -Force +Write-Warning "~/.gitconfig.local already exists; current ~/.gitconfig was backed up before managed stub overwrite." +Write-Host "Backup saved to: $BackupPath" diff --git a/chezmoi/Documents/PowerShell/Microsoft.PowerShell_profile.ps1.tmpl b/chezmoi/Documents/PowerShell/Microsoft.PowerShell_profile.ps1.tmpl new file mode 100644 index 0000000..bc66d21 --- /dev/null +++ b/chezmoi/Documents/PowerShell/Microsoft.PowerShell_profile.ps1.tmpl @@ -0,0 +1,6 @@ +# Keep the live PowerShell 7 profile tiny and stable. +$ManagedProfile = Join-Path $HOME '.config\powershell\profile.ps1' + +if (Test-Path -LiteralPath $ManagedProfile) { + . $ManagedProfile +} diff --git a/chezmoi/dot_config/git/config b/chezmoi/dot_config/git/config new file mode 100644 index 0000000..84b4a2e --- /dev/null +++ b/chezmoi/dot_config/git/config @@ -0,0 +1,14 @@ +[core] + autocrlf = false + eol = lf + quotepath = false + longpaths = true + +[fetch] + prune = true + +[pull] + ff = only + +[init] + defaultBranch = main diff --git a/chezmoi/dot_config/powershell/profile.ps1 b/chezmoi/dot_config/powershell/profile.ps1 new file mode 100644 index 0000000..cceccc1 --- /dev/null +++ b/chezmoi/dot_config/powershell/profile.ps1 @@ -0,0 +1,135 @@ +# Keep PowerShell 7 console text I/O on UTF-8 for predictable native tool behavior. +if ($IsWindows -and $PSVersionTable.PSEdition -eq 'Core' -and $Host.Name -eq 'ConsoleHost') { + chcp 65001 > $null + $Utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [Console]::InputEncoding = $Utf8NoBom + [Console]::OutputEncoding = $Utf8NoBom + $OutputEncoding = $Utf8NoBom +} + +# Keep interactive editing comfortable without depending on extra prompt frameworks. +if ( + $Host.Name -eq 'ConsoleHost' -and + -not [Console]::IsOutputRedirected -and + $Host.UI.SupportsVirtualTerminal -and + (Get-Module -ListAvailable -Name PSReadLine) +) { + Import-Module PSReadLine -ErrorAction SilentlyContinue + Set-PSReadLineOption -PredictionSource History -ErrorAction SilentlyContinue + Set-PSReadLineOption -PredictionViewStyle ListView -ErrorAction SilentlyContinue + Set-PSReadLineOption -HistoryNoDuplicates:$true -ErrorAction SilentlyContinue +} + +function Reload-Profile { + . $PROFILE +} + +function which { + param( + [Parameter(Mandatory = $true)] + [string]$Name + ) + + Get-Command -Name $Name -All | Select-Object CommandType, Name, Source +} + +function touch { + param( + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] + [string[]]$Path + ) + + foreach ($Item in $Path) { + if (Test-Path -LiteralPath $Item) { + (Get-Item -LiteralPath $Item).LastWriteTime = Get-Date + } + else { + New-Item -ItemType File -Path $Item -Force | Out-Null + } + } +} + +function mkcd { + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + New-Item -ItemType Directory -Path $Path -Force | Out-Null + Set-Location -Path $Path +} + +function open { + param( + [string]$Path = '.' + ) + + Invoke-Item -Path $Path +} + +function croot { + if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-Error 'git is not available in PATH.' + return + } + + $Root = git rev-parse --show-toplevel 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($Root)) { + Write-Error 'Not inside a Git repository.' + return + } + + Set-Location -Path $Root.Trim() +} + +if (Get-Command wt -ErrorAction SilentlyContinue) { + function wt-here { + wt -d (Get-Location).ProviderPath + } + + function wt-tab-here { + wt -w 0 new-tab -d (Get-Location).ProviderPath + } + + function wt-split-here { + wt -w 0 split-pane -d (Get-Location).ProviderPath + } +} + +function codevpn { + $PreviousHttpProxy = $env:HTTP_PROXY + $PreviousHttpsProxy = $env:HTTPS_PROXY + + try { + $env:HTTP_PROXY = 'http://127.0.0.1:12334' + $env:HTTPS_PROXY = 'http://127.0.0.1:12334' + code . + } + finally { + $env:HTTP_PROXY = $PreviousHttpProxy + $env:HTTPS_PROXY = $PreviousHttpsProxy + } +} + +# Keep stable hooks for machine-local overrides and future managed fragments. +$ExtraProfile = Join-Path $HOME '.config\powershell\profile.extra.ps1' +if (Test-Path -LiteralPath $ExtraProfile) { + . $ExtraProfile +} + +$ExtraProfileDir = Join-Path $HOME '.config\powershell\profile.d' +if (Test-Path -LiteralPath $ExtraProfileDir) { + foreach ($ProfileFragment in (Get-ChildItem -Path $ExtraProfileDir -Filter '*.ps1' -File | Sort-Object Name)) { + . $ProfileFragment.FullName + } +} + +# Import the Chocolatey Profile that contains the necessary code to enable +# tab-completions to function for `choco`. +# Be aware that if you are missing these lines from your profile, tab completion +# for `choco` will not function. +# See https://ch0.co/tab-completion for details. +$ChocolateyProfile = "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" +if (Test-Path -LiteralPath $ChocolateyProfile) { + Import-Module $ChocolateyProfile +} diff --git a/chezmoi/dot_gitconfig b/chezmoi/dot_gitconfig new file mode 100644 index 0000000..a850061 --- /dev/null +++ b/chezmoi/dot_gitconfig @@ -0,0 +1,5 @@ +[include] + path = ~/.config/git/config + +[include] + path = ~/.gitconfig.local