diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a36189..4effc95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ## [Unreleased] +## [1.1.0] - 2026-05-18 +### Added +- Russian UI translation. On first launch the application asks which language to use (`1) English`, `2) Русский`) and writes the answer to a fresh `config.ini` next to `netswitch.exe`. To change the language later, edit `language = en` / `language = ru` in that file — the comment at the top of the file explains how, in both languages. +- Bilingual `README.ru.md` linked from the main `README.md`. + ## [1.0.3] - 2026-05-17 ### Changed - Update check no longer interrupts startup with an interactive prompt. If a newer release is available, a quiet right-aligned `update available (vX.Y.Z)` hint is printed in dim grey directly under the banner — no key press required. @@ -31,7 +36,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and - Auto-update check on startup: polls GitHub `/releases/latest` with a 3-second timeout and offers to open the download page if a newer version exists. Silent on offline / API errors. - MIT licensed. -[Unreleased]: https://github.com/Engelgardt23/netswitch/compare/v1.0.3...HEAD +[Unreleased]: https://github.com/Engelgardt23/netswitch/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/Engelgardt23/netswitch/compare/v1.0.3...v1.1.0 [1.0.3]: https://github.com/Engelgardt23/netswitch/compare/v1.0.2...v1.0.3 [1.0.2]: https://github.com/Engelgardt23/netswitch/compare/v1.0.1...v1.0.2 [1.0.1]: https://github.com/Engelgardt23/netswitch/compare/v1.0.0...v1.0.1 diff --git a/README.md b/README.md index aee3f82..484f5f0 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![Latest release](https://img.shields.io/github/v/release/Engelgardt23/netswitch)](https://github.com/Engelgardt23/netswitch/releases/latest) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +🇬🇧 English | [🇷🇺 На русском](README.ru.md) + A tiny portable tool to flip a Windows network adapter between a **static IP** and **DHCP** with a few keystrokes. Built for the recurring engineer chore of "give my laptop NIC 10.10.10.1 so I can talk to a server's BMC" and "now put it back on DHCP so I can have internet again." diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..03767e1 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,62 @@ +# netswitch + +[![Последний релиз](https://img.shields.io/github/v/release/Engelgardt23/netswitch)](https://github.com/Engelgardt23/netswitch/releases/latest) +[![Лицензия: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + +[🇬🇧 English](README.md) | 🇷🇺 На русском + +Маленький портативный инструмент, который за пару нажатий переключает сетевой адаптер Windows между **статическим IP** и **DHCP**. + +Решает регулярную задачу инженера: «дай моему ноуту 10.10.10.1, чтобы я мог достучаться до BMC сервера», а потом «верни обратно на DHCP, чтобы был интернет». + +> **Автор: engelgardt.** + +--- + +## Скачать + +Последний релиз: [**страница релизов**](https://github.com/Engelgardt23/netswitch/releases/latest). +Архив `netswitch-portable-vX.Y.Z.zip` (~30 КБ). + +## Запуск + +1. Распакуй куда угодно. +2. Двойной клик по `netswitch.exe`. +3. **При первом запуске** программа спросит язык интерфейса (1 — English, 2 — Русский). Ответ запишется в `config.ini` рядом с exe — потом можно поменять руками. +4. Подтверди UAC (admin нужен для `netsh interface ipv4 set address`). +5. Выбери сетевой адаптер из списка. +6. Выбери режим: + - **Статический**: введи IP (по умолчанию `10.10.10.1`), маску (по умолчанию `255.255.255.0`), шлюз (опционально). + - **DHCP**: подтверди — адаптер вернётся в DHCP для IP и DNS. + +## Что фильтруется + +В выбор попадают только настоящие проводные физические адаптеры. Wi-Fi, VPN, виртуалки, Hyper-V, VMware, VirtualBox, TAP/TUN, WireGuard, OpenVPN, Tailscale, ZeroTier, Bluetooth, Loopback, WAN Miniport — всё пропускается. + +## Проверка обновлений + +При каждом запуске тулза стучится в GitHub `/releases/latest` (таймаут 3 секунды). Если есть свежая версия — справа в шапке появится тусклая надпись `доступно обновление (vX.Y.Z)`. Если интернета нет — молчит. + +## Конфиг + +При первом запуске рядом с `netswitch.exe` появится `config.ini`: + +```ini +# Чтобы сменить язык интерфейса, измените 'language' ниже. +# Допустимые значения: en, ru +[General] +language = ru +``` + +## Сборка из исходников + +Скрипт один — `netswitch.ps1`. Для пересборки `.exe`: + +``` +Install-Module ps2exe -Scope CurrentUser +Invoke-ps2exe -inputFile netswitch.ps1 -outputFile netswitch.exe -requireAdmin -title "netswitch" -version 1.1.0.0 +``` + +## Лицензия + +MIT — см. [LICENSE](LICENSE). diff --git a/src/netswitch.ps1 b/src/netswitch.ps1 index e595a25..062e4b8 100644 --- a/src/netswitch.ps1 +++ b/src/netswitch.ps1 @@ -1,7 +1,7 @@ -# netswitch v1.0.0 - quick NIC IP / DHCP toggle +# netswitch - quick NIC IP / DHCP toggle # made by engelgardt -$NetswitchVersion = '1.0.3' +$NetswitchVersion = '1.1.0' $GithubRepo = 'Engelgardt23/netswitch' $ErrorActionPreference = 'Stop' @@ -19,6 +19,125 @@ if (-not $me.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit } +# --- Locate config.ini next to the exe / script --- +function Get-AppDir { + if ($PSCommandPath) { return (Split-Path $PSCommandPath -Parent) } + try { + return Split-Path ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName) -Parent + } catch { + return (Get-Location).Path + } +} +$AppDir = Get-AppDir +$ConfigPath = Join-Path $AppDir 'config.ini' + +# --- Bilingual UI strings --- +$STR = @{ + en = @{ + no_adapters = 'No physical wired adapters found.' + press_enter = 'Press Enter to exit' + available_adapters = 'Available adapters:' + select_adapter = 'Select adapter number' + invalid_selection = 'Invalid selection.' + selected = 'Selected: {0}' + mode_header = 'Mode:' + mode_static = ' 1) Static IP' + mode_dhcp = ' 2) DHCP' + mode_choice = 'Choice [1]' + setting_dhcp = 'Setting {0} to DHCP...' + done = 'Done.' + ip_prompt = 'IP address [10.10.10.1]' + mask_prompt = 'Subnet mask [255.255.255.0]' + gw_prompt = 'Gateway (Enter to skip)' + setting_static = 'Setting {0} -> {1} / {2}{3}' + via_gw = ' via {0}' + current_config = 'Current IPv4 config:' + update_available = 'update available ({0})' + lang_select = 'Select language / Выберите язык:' + lang_en = ' 1) English' + lang_ru = ' 2) Русский' + lang_invalid = 'Please enter 1 or 2 / Введите 1 или 2' + banner_subtitle = 'NIC IP/DHCP toggle' + } + ru = @{ + no_adapters = 'Подходящие проводные адаптеры не найдены.' + press_enter = 'Нажмите Enter для выхода' + available_adapters = 'Доступные адаптеры:' + select_adapter = 'Введите номер адаптера' + invalid_selection = 'Неверный выбор.' + selected = 'Выбрано: {0}' + mode_header = 'Режим:' + mode_static = ' 1) Статический IP' + mode_dhcp = ' 2) DHCP' + mode_choice = 'Выбор [1]' + setting_dhcp = 'Перевожу {0} в режим DHCP...' + done = 'Готово.' + ip_prompt = 'IP-адрес [10.10.10.1]' + mask_prompt = 'Маска подсети [255.255.255.0]' + gw_prompt = 'Шлюз (Enter — пропустить)' + setting_static = 'Назначаю {0} -> {1} / {2}{3}' + via_gw = ' через {0}' + current_config = 'Текущая конфигурация IPv4:' + update_available = 'доступно обновление ({0})' + lang_select = 'Select language / Выберите язык:' + lang_en = ' 1) English' + lang_ru = ' 2) Русский' + lang_invalid = 'Please enter 1 or 2 / Введите 1 или 2' + banner_subtitle = 'переключатель NIC IP/DHCP' + } +} + +# --- First-run language prompt + config write --- +function Read-Language { + Write-Host '' + Write-Host $STR.en.lang_select + Write-Host $STR.en.lang_en + Write-Host $STR.en.lang_ru + while ($true) { + $c = (Read-Host '>').Trim() + if ($c -eq '1') { return 'en' } + if ($c -eq '2') { return 'ru' } + Write-Host $STR.en.lang_invalid -ForegroundColor Yellow + } +} + +function Write-DefaultConfig([string]$lang) { + $header = @" +# --------------------------------------------------------------------------- +# netswitch configuration +# +# To change the interface language, edit the 'language' value below. +# Valid values: en, ru +# +# Чтобы сменить язык интерфейса, измените значение 'language' ниже. +# Допустимые значения: en, ru +# --------------------------------------------------------------------------- + +[General] +language = $lang +"@ + try { Set-Content -Path $ConfigPath -Value $header -Encoding UTF8 } catch { } +} + +function Read-Config { + if (-not (Test-Path $ConfigPath)) { + $l = Read-Language + Write-DefaultConfig $l + return @{ language = $l } + } + $lang = 'en' + foreach ($line in (Get-Content $ConfigPath -ErrorAction SilentlyContinue)) { + if ($line -match '^\s*language\s*=\s*([a-zA-Z]+)\s*$') { + $v = $matches[1].ToLower() + if ($v -eq 'ru' -or $v -eq 'en') { $lang = $v } + } + } + return @{ language = $lang } +} + +$config = Read-Config +$L = $STR[$config.language] + # --- Update check (silent: returns latest tag if newer, else empty) --- function Get-NetswitchUpdate { try { @@ -33,11 +152,11 @@ function Get-NetswitchUpdate { while ($parts.Count -lt 3) { $parts += 0 } ,$parts[0..2] } - $L = & $toTuple $latest - $C = & $toTuple $NetswitchVersion + $LV = & $toTuple $latest + $CV = & $toTuple $NetswitchVersion for ($i = 0; $i -lt 3; $i++) { - if ($L[$i] -gt $C[$i]) { return $r.tag_name } - if ($L[$i] -lt $C[$i]) { return '' } + if ($LV[$i] -gt $CV[$i]) { return $r.tag_name } + if ($LV[$i] -lt $CV[$i]) { return '' } } return '' } catch { @@ -47,19 +166,19 @@ function Get-NetswitchUpdate { $latestTag = Get-NetswitchUpdate # --- Banner --- -Write-Host "" -Write-Host "==============================================" -ForegroundColor Cyan -Write-Host " netswitch v$NetswitchVersion - NIC IP/DHCP toggle" -ForegroundColor Cyan -Write-Host "==============================================" -ForegroundColor Cyan +Write-Host '' +Write-Host '==============================================' -ForegroundColor Cyan +Write-Host (" netswitch v$NetswitchVersion - " + $L.banner_subtitle) -ForegroundColor Cyan +Write-Host '==============================================' -ForegroundColor Cyan if ($latestTag) { - $msg = "update available ($latestTag)" + $msg = ($L.update_available -f $latestTag) $w = 0 try { $w = $Host.UI.RawUI.WindowSize.Width } catch { $w = 0 } if ($w -lt ($msg.Length + 2)) { $w = $msg.Length + 2 } $pad = $w - $msg.Length - 1 Write-Host ((' ' * [Math]::Max(0, $pad)) + $msg) -ForegroundColor DarkGray } -Write-Host "" +Write-Host '' # --- Pick adapter (physical wired only) --- $skipDescriptionPattern = 'VPN|Virtual|AnyConnect|TAP-|TUN-|Bluetooth|Loopback|WAN Miniport|Hyper-V|VMware|VirtualBox|WireGuard|OpenVPN|Tailscale|ZeroTier' @@ -74,11 +193,11 @@ $adapters = @(Get-NetAdapter | Where-Object { } | Sort-Object ifIndex) if ($adapters.Count -eq 0) { - Write-Host "No physical wired adapters found." -ForegroundColor Red - Read-Host "Press Enter to exit"; exit 1 + Write-Host $L.no_adapters -ForegroundColor Red + Read-Host $L.press_enter; exit 1 } -Write-Host "Available adapters:" +Write-Host $L.available_adapters for ($i = 0; $i -lt $adapters.Count; $i++) { $a = $adapters[$i] $ips = (Get-NetIPAddress -InterfaceIndex $a.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | @@ -87,56 +206,58 @@ for ($i = 0; $i -lt $adapters.Count; $i++) { } do { - $sel = (Read-Host "Select adapter number").Trim() + $sel = (Read-Host $L.select_adapter).Trim() $valid = ($sel -match '^\d+$') -and ([int]$sel -ge 1) -and ([int]$sel -le $adapters.Count) - if (-not $valid) { Write-Host "Invalid selection." -ForegroundColor Red } + if (-not $valid) { Write-Host $L.invalid_selection -ForegroundColor Red } } while (-not $valid) $nic = $adapters[[int]$sel - 1] -Write-Host "" -Write-Host "Selected: $($nic.Name)" -ForegroundColor Green +Write-Host '' +Write-Host ($L.selected -f $nic.Name) -ForegroundColor Green # --- Mode --- -Write-Host "" -Write-Host "Mode:" -Write-Host " 1) Static IP" -Write-Host " 2) DHCP" -$modeChoice = Read-Host "Choice [1]" +Write-Host '' +Write-Host $L.mode_header +Write-Host $L.mode_static +Write-Host $L.mode_dhcp +$modeChoice = Read-Host $L.mode_choice if ([string]::IsNullOrWhiteSpace($modeChoice)) { $modeChoice = '1' } if ($modeChoice.Trim() -eq '2') { # --- DHCP --- - Write-Host "" - Write-Host "Setting $($nic.Name) to DHCP..." -ForegroundColor Yellow + Write-Host '' + Write-Host ($L.setting_dhcp -f $nic.Name) -ForegroundColor Yellow $null = & netsh interface ipv4 set address name="$($nic.Name)" source=dhcp 2>&1 $null = & netsh interface ipv4 set dnsservers name="$($nic.Name)" source=dhcp 2>&1 - Write-Host "Done." -ForegroundColor Green + Write-Host $L.done -ForegroundColor Green } else { # --- Static --- - $ip = Read-Host "IP address [10.10.10.1]" + $ip = Read-Host $L.ip_prompt if ([string]::IsNullOrWhiteSpace($ip)) { $ip = '10.10.10.1' } - $mask = Read-Host "Subnet mask [255.255.255.0]" + $mask = Read-Host $L.mask_prompt if ([string]::IsNullOrWhiteSpace($mask)) { $mask = '255.255.255.0' } - $gw = Read-Host "Gateway (Enter to skip)" + $gw = Read-Host $L.gw_prompt - Write-Host "" - Write-Host "Setting $($nic.Name) -> $ip / $mask$( if ($gw) { " via $gw" })" -ForegroundColor Yellow + $gwTail = if ([string]::IsNullOrWhiteSpace($gw)) { '' } else { ($L.via_gw -f $gw) } + + Write-Host '' + Write-Host ($L.setting_static -f $nic.Name, $ip, $mask, $gwTail) -ForegroundColor Yellow if ([string]::IsNullOrWhiteSpace($gw)) { $null = & netsh interface ipv4 set address name="$($nic.Name)" static $ip $mask 2>&1 } else { $null = & netsh interface ipv4 set address name="$($nic.Name)" static $ip $mask $gw 2>&1 } - Write-Host "Done." -ForegroundColor Green + Write-Host $L.done -ForegroundColor Green } # --- Show current state --- -Write-Host "" -Write-Host "Current IPv4 config:" -ForegroundColor Cyan +Write-Host '' +Write-Host $L.current_config -ForegroundColor Cyan Get-NetIPAddress -InterfaceIndex $nic.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.PrefixOrigin -ne 'WellKnown' } | Format-Table IPAddress, PrefixLength, PrefixOrigin -AutoSize -Read-Host "Press Enter to exit" +Read-Host $L.press_enter