diff --git a/Cargo.lock b/Cargo.lock index add417f..2a906cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -614,6 +614,7 @@ dependencies = [ "toml 1.1.4+spec-1.1.0", "winit", "winreg", + "winresource", ] [[package]] @@ -5784,6 +5785,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winresource" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +dependencies = [ + "toml 1.1.4+spec-1.1.0", + "version_check", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index fba75d1..3d1c6ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ winreg = "0.56.0" [build-dependencies] slint-build = "1.16.1" +winresource = "0.1.31" [profile.release] lto = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8861728 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ku6epXBOCTuK + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index a035382..c72ff02 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,84 @@ -## 🚀 Rust Browser Picker (Working Title) +
-Легкий и мгновенный переключатель браузеров на Rust + Slint. Замена тяжеловесным аналогам, которые долго загружаются. +bropicker -## 🎯 Цель +# bropicker -Создать утилиту, которая перехватывает открытие ссылок и предлагает выбрать браузер в красивом UI. Главный приоритет — скорость запуска (Cold Start < 100ms). +Лёгкий переключатель браузеров: кликаешь ссылку — bropicker спрашивает, каким браузером её открыть. -## 🛠 Технологический стек +**Rust + Slint, нативный рендер без WebView, cold start ~110 ms.** -- Язык: [Rust](https://www.rust-lang.org/) -- GUI: [Slint](https://slint.dev/) (Native-speed, Declarative UI) -- Запуск процессов: std::process::Command +Made with Slint -## 📊 Текущий статус +Тёмная тема Светлая тема -- Frameless-окно с прозрачным фоном: карточка парит над рабочим столом со скруглёнными углами -- Тёмная и светлая темы (переключение кнопкой, env `BP_THEME=light`) -- Перетаскивание окна за хедер, Esc — закрыть, центрирование при старте -- Дизайн-макеты: [refs/dark.html](refs/dark.html) / [refs/light.html](refs/light.html) +
-## 🔧 Разработка +## Возможности -План работ и цикл визуальной разработки с ИИ-агентом — [docs/dev-workflow.md](docs/dev-workflow.md). +- Frameless-окно с прозрачностью, перетаскивание за хедер, Esc — закрыть +- Тёмная и светлая темы +- Автодетект установленных браузеров из реестра (Firefox, Chrome, Edge, Zen, Opera, Brave, Яндекс…) +- Запоминание выбора по домену: повторные ссылки открываются сразу, без вопроса +- Галочка «Всегда спрашивать» — пикер вызывается даже для запомненных доменов +- Несколько профилей одного браузера как отдельные записи (флаги + эмодзи-иконка) +- Конфиг в TOML, открывается кнопкой шестерёнки + +## Сборка ```powershell -cargo run # запуск -tools/render-refs.ps1 # PNG-референсы из макетов -tools/run-and-shot.ps1 # собрать, снять скриншот окна -tools/run-and-shot.ps1 -Light # то же в светлой теме +cargo build --release ``` -## 🗺 Дорожная карта +## Установка обработчиком ссылок -1. Список браузеров с логотипами, запуск выбранного браузера с URL (MVP) -2. Регистрация в системе как браузер по умолчанию -3. config.toml + страница настроек +```powershell +tools\register.ps1 # скопирует сборку в %LOCALAPPDATA%\bropicker и откроет настройки Windows +``` + +В открывшемся окне выбери **bropicker** для HTTP и HTTPS. Откат: `tools\register.ps1 -Uninstall`. + +## Конфигурация + +`%APPDATA%\bropicker\config.toml` (шестерёнка в пикере открывает его в редакторе). +Пример с профилями и эмодзи: [docs/config.example.toml](docs/config.example.toml). + +```toml +[[browsers]] +name = "Chrome — Work" +path = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' +flags = '--profile-directory="Default"' +emoji = "💼" + +[remembered] +"github.com" = "Chrome — Work" + +[settings] +remember_choice = true +always_ask = true +``` + +## Разработка + +```powershell +just check # fmt + clippy -D warnings + тесты +just screenshots # перегенерация скриншотов README (тёмная + светлая) +just icons # перегенерация иконок из assets/icon.svg +just release # release-сборка +just register # установка/обновление обработчика ссылок +``` + +То же напрямую: `tools\check.ps1`, `tools\screenshots.ps1`, `tools\register.ps1`. +Планы и процесс: [docs/dev-workflow.md](docs/dev-workflow.md). + +## Лицензия + +Код bropicker — [MIT](LICENSE). + +Интерфейс построен на [Slint](https://slint.dev), который используется по +[Slint Royalty-free License 2.0](licenses/Slint-Royalty-free-2.0.md) +Текст лицензии также входит +в состав установки (`licenses/`). + +Логотипы браузеров — [alrrr/browser-logos](https://github.com/alrrr/browser-logos), MIT +([LICENSE](logos/LICENSE-browser-logos.txt)). diff --git a/assets/app.ico b/assets/app.ico new file mode 100644 index 0000000..3913bc5 Binary files /dev/null and b/assets/app.ico differ diff --git a/assets/icon-128.png b/assets/icon-128.png new file mode 100644 index 0000000..90a59ae Binary files /dev/null and b/assets/icon-128.png differ diff --git a/assets/icon-150.png b/assets/icon-150.png new file mode 100644 index 0000000..f327d50 Binary files /dev/null and b/assets/icon-150.png differ diff --git a/assets/icon-16.png b/assets/icon-16.png new file mode 100644 index 0000000..14b4cb9 Binary files /dev/null and b/assets/icon-16.png differ diff --git a/assets/icon-24.png b/assets/icon-24.png new file mode 100644 index 0000000..cf17218 Binary files /dev/null and b/assets/icon-24.png differ diff --git a/assets/icon-256.png b/assets/icon-256.png new file mode 100644 index 0000000..3885b1e Binary files /dev/null and b/assets/icon-256.png differ diff --git a/assets/icon-32.png b/assets/icon-32.png new file mode 100644 index 0000000..6ef1a1d Binary files /dev/null and b/assets/icon-32.png differ diff --git a/assets/icon-44.png b/assets/icon-44.png new file mode 100644 index 0000000..762bee6 Binary files /dev/null and b/assets/icon-44.png differ diff --git a/assets/icon-48.png b/assets/icon-48.png new file mode 100644 index 0000000..ea03b51 Binary files /dev/null and b/assets/icon-48.png differ diff --git a/assets/icon-50.png b/assets/icon-50.png new file mode 100644 index 0000000..9e8432b Binary files /dev/null and b/assets/icon-50.png differ diff --git a/assets/icon-512.png b/assets/icon-512.png new file mode 100644 index 0000000..de5c092 Binary files /dev/null and b/assets/icon-512.png differ diff --git a/assets/icon-64.png b/assets/icon-64.png new file mode 100644 index 0000000..4b13f70 Binary files /dev/null and b/assets/icon-64.png differ diff --git a/assets/icon-88.png b/assets/icon-88.png new file mode 100644 index 0000000..23af17a Binary files /dev/null and b/assets/icon-88.png differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..048a0cd Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/icon.svg b/assets/icon.svg new file mode 100644 index 0000000..e40066d --- /dev/null +++ b/assets/icon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/build.rs b/build.rs index b56b953..4cc9a1d 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,9 @@ -// Copyright © SixtyFPS GmbH -// SPDX-License-Identifier: MIT - fn main() { slint_build::compile("ui/main.slint").unwrap(); + + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + let mut res = winresource::WindowsResource::new(); + res.set_icon("assets/app.ico"); + res.compile().unwrap(); + } } diff --git a/docs/dev-workflow.md b/docs/dev-workflow.md index b87b57e..e906d38 100644 --- a/docs/dev-workflow.md +++ b/docs/dev-workflow.md @@ -169,10 +169,18 @@ HTML-макеты → PNG: **headless Edge** (`--headless --screenshot`), он Цель <100 ms чуть не достигнута; варианты оптимизации, если захочется: renderer-femtovg вместо skia (быстрее init GPU), отложенная загрузка конфига. Пока считаем приемлемым -- [ ] Иконка exe (.ico через build.rs winres) -- [ ] Выбрать лицензию проекта: GPLv3 (опенсорс) или Royalty-free Slint (закрытый десктоп, - атрибуция AboutSlint) — зафиксировать в README и LICENSE -- [ ] Скриншоты в README из tools/out +- [x] Иконка exe: `assets/icon.svg` (градиент + глобус + точки «выбора») + → `magick` → `assets/icon.png` + `assets/app.ico` (16-256) → `winresource` + в build.rs; проверено ExtractAssociatedIcon +- [x] Лицензия MIT (`LICENSE`); Slint — по Royalty-free License 2.0 (маршрут (b): + бейдж Made-with-Slint в README, текст лицензии в `licenses/` и в + составе установки `%LOCALAPPDATA%\bropicker\licenses\`). + ВАЖНО: маршрут (b) требует публичной страницы с бейджем; если репо + станет приватным — добавить виджет AboutSlint в окно настроек +- [x] Скриншоты в README: `docs/img/screenshot-{dark,light}.png`; + перегенерация — `just screenshots` (tools/screenshots.ps1 → shot.ps1 -Card: + DPI-aware обрезка до карточки, клип на 2px внутрь срезает AA-бахрому + скруглений, углы прозрачные — чужих пикселей нет) - [ ] Ручной сценарий приёмки: register → клик ссылки в другом приложении → пикер → выбор → вкладка открылась; повторный клик того же домена при always_ask=off → сразу браузер diff --git a/docs/img/screenshot-dark.png b/docs/img/screenshot-dark.png new file mode 100644 index 0000000..b300a80 Binary files /dev/null and b/docs/img/screenshot-dark.png differ diff --git a/docs/img/screenshot-light.png b/docs/img/screenshot-light.png new file mode 100644 index 0000000..79ca0ba Binary files /dev/null and b/docs/img/screenshot-light.png differ diff --git a/justfile b/justfile new file mode 100644 index 0000000..6d895d2 --- /dev/null +++ b/justfile @@ -0,0 +1,26 @@ +default: + @just --list + +# fmt + clippy -D warnings + тесты +check: + powershell -ExecutionPolicy Bypass -File tools/check.ps1 + +# release-сборка +release: + cargo build --release + +# перегенерация скриншотов для README (тёмная + светлая, только карточка) +screenshots: + powershell -ExecutionPolicy Bypass -File tools/screenshots.ps1 + +# перегенерация иконок из assets/icon.svg (icon.png, app.ico, PNG-лестница) +icons: + powershell -ExecutionPolicy Bypass -File tools/icons.ps1 + +# установка/обновление обработчика ссылок (+ открытие настроек Windows) +register: + powershell -ExecutionPolicy Bypass -File tools/register.ps1 + +# снять регистрацию и удалить %LOCALAPPDATA%\bropicker +unregister: + powershell -ExecutionPolicy Bypass -File tools/register.ps1 -Uninstall diff --git a/licenses/Slint-Royalty-free-2.0.md b/licenses/Slint-Royalty-free-2.0.md new file mode 100644 index 0000000..f087009 --- /dev/null +++ b/licenses/Slint-Royalty-free-2.0.md @@ -0,0 +1,43 @@ +# Slint Royalty-free Desktop, Mobile, and Web Applications License + +Version 2.0 + +## Preamble + +Slint is a toolkit that can be used to build user interfaces for applications. Slint (hereafter referred to as **Software**) is made available under different licenses by SixtyFPS GmbH incorporated at Oranienburger Str. 44, 16540 Hohen Neuendorf, Germany (**SixtyFPS**). The **Slint Royalty-free Desktop, Mobile, and Web Applications License** is suitable for those who develop desktop, mobile, or web applications and do not want to use open source components under copyleft licenses. + +## 1. Grant of Rights + +SixtyFPS hereby grants You a world-wide, royalty-free, non-exclusive license to use, reproduce, make available, modify, display, perform, distribute the Software as part of a Desktop, Mobile, or Web Application. + +A **Desktop Application** is a computer program that is designed to run on a general-purpose computer (PC or notebook), typically installed and executed locally on the computer's operating system. + +A **Mobile Application** is a computer program that is designed to run on a general-purpose mobile computer (mobile phone or tablet), typically installed and executed locally on the computer's operating system. + +A **Web Application** is a computer program that is designed to run in the sandbox environment provided by a web browser. + +Desktop Application, Mobile Application, and Web Application are hereafter referred to as **Application**. + +## 2. License Conditions - Attribution + +You may distribute the Software as part of an Application, modified or unmodified, provided that You do either of the following: + +(a) Display the [`AboutSlint`](https://docs.slint.dev/latest/docs/slint/reference/std-widgets/misc/aboutslint/) widget in an "About" screen or dialog that is accessible from the top level menu of the Application. In the absence of such a screen or dialog, display the widget in the "Splash Screen" of the Application. + +(b) Display the [Slint attribution badge](https://github.com/slint-ui/slint/tree/master/logo/MadeWithSlint-logo-whitebg.png) on a public webpage, preferably where the binaries of your Application can be downloaded from, in such a way that it can be easily found by any visitor to that page. + +## 3. Limitations + +The License does not permit to distribute or make the Software publicly available alone and without integration into an Application. For this purpose you may use the Software under the GNU General Public License, version 3. + +The License does not permit the use of the Software within Embedded Systems. An **Embedded System** is a computer system designed to perform a specific task within a larger mechanical or electrical system. + +The License does not permit the distribution of Application that exposes the APIs, in part or in total, of the Software. + +You may not remove or alter any license notices (including copyright notices, disclaimers of warranty, or limitations of liability) contained within the source code form of the Software. + +## 4. Warranty and Liability + +SixtyFPS is only liable for conflicting rights of third parties if SixtyFPS was aware of these rights without informing you. Unless required by applicable law or agreed to in writing, SixtyFPS provides the Software on an "as is" basis, without warranties or conditions of any kind, either express or implied, including, without limitation, any warranties or conditions of merchantability, or fitness for a particular purpose. + +Unless required by law, SixtyFPS won't be liable for any direct, indirect, incidental, or consequential damages arising in any way out of the use of the Software. diff --git a/tools/icons.ps1 b/tools/icons.ps1 new file mode 100644 index 0000000..c2ec5ee --- /dev/null +++ b/tools/icons.ps1 @@ -0,0 +1,12 @@ +$ErrorActionPreference = "Stop" + +$svg = "assets/icon.svg" + +magick -background none $svg -resize 256x256 assets/icon.png +magick -background none $svg -define icon:auto-resize=256,128,64,48,32,24,16 assets/app.ico + +foreach ($size in @(16, 24, 32, 44, 48, 50, 64, 88, 128, 150, 256, 512)) { + magick -background none $svg -resize "${size}x${size}" ("assets/icon-{0}.png" -f $size) +} + +Write-Host "icons regenerated: icon.png, app.ico, icon-{16..512}.png" diff --git a/tools/register.ps1 b/tools/register.ps1 index d3ad142..cdef45d 100644 --- a/tools/register.ps1 +++ b/tools/register.ps1 @@ -29,6 +29,11 @@ Copy-Item $src (Join-Path $installDir "bropicker.exe") -Force Copy-Item (Join-Path $PSScriptRoot "..\logos") (Join-Path $installDir "logos") -Recurse -Force Copy-Item (Join-Path $PSScriptRoot "..\icons") (Join-Path $installDir "icons") -Recurse -Force +$licensesDir = Join-Path $installDir "licenses" +New-Item -ItemType Directory -Force -Path $licensesDir | Out-Null +Copy-Item (Join-Path $PSScriptRoot "..\licenses\Slint-Royalty-free-2.0.md") $licensesDir -Force +Copy-Item (Join-Path $PSScriptRoot "..\logos\LICENSE-browser-logos.txt") $licensesDir -Force + $exe = Join-Path $installDir "bropicker.exe" Write-Host "Installed to: $exe" diff --git a/tools/screenshots.ps1 b/tools/screenshots.ps1 new file mode 100644 index 0000000..8786778 --- /dev/null +++ b/tools/screenshots.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = "Stop" + +cargo build +if ($LASTEXITCODE -ne 0) { exit 1 } + +New-Item -ItemType Directory -Force -Path docs\img | Out-Null +$exe = Join-Path $PWD "target\debug\bropicker.exe" + +$proc = Start-Process -FilePath $exe -WorkingDirectory $PWD -PassThru +Start-Sleep -Milliseconds 1800 +powershell -ExecutionPolicy Bypass -File "$PSScriptRoot\shot.ps1" -ProcId $proc.Id -Out docs\img\screenshot-dark.png -Card +Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue +if ($LASTEXITCODE -ne 0) { exit 1 } + +$env:BP_THEME = "light" +$proc = Start-Process -FilePath $exe -WorkingDirectory $PWD -PassThru +Start-Sleep -Milliseconds 1800 +powershell -ExecutionPolicy Bypass -File "$PSScriptRoot\shot.ps1" -ProcId $proc.Id -Out docs\img\screenshot-light.png -Card +Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue +Remove-Item Env:\BP_THEME -ErrorAction SilentlyContinue +if ($LASTEXITCODE -ne 0) { exit 1 } + +Write-Host "screenshots updated: docs\img\screenshot-{dark,light}.png" diff --git a/tools/shot.ps1 b/tools/shot.ps1 index dd80878..a4e619f 100644 --- a/tools/shot.ps1 +++ b/tools/shot.ps1 @@ -1,7 +1,8 @@ param( [string]$ProcName = "bropicker", [int]$ProcId = 0, - [string]$Out = "tools/out/app.png" + [string]$Out = "tools/out/app.png", + [switch]$Card ) $ErrorActionPreference = "Stop" @@ -18,6 +19,7 @@ public class BpWin { [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int cx, int cy, uint flags); + [DllImport("user32.dll")] public static extern uint GetDpiForWindow(IntPtr hWnd); [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; } } @@ -64,11 +66,50 @@ if ($w -le 0 -or $h -le 0) { } Add-Type -AssemblyName System.Drawing -$bmp = New-Object System.Drawing.Bitmap($w, $h) -$g = [System.Drawing.Graphics]::FromImage($bmp) +$full = New-Object System.Drawing.Bitmap($w, $h) +$g = [System.Drawing.Graphics]::FromImage($full) $g.CopyFromScreen($rect.Left, $rect.Top, 0, 0, (New-Object System.Drawing.Size($w, $h))) $g.Dispose() +if ($Card) { + $dpi = [BpWin]::GetDpiForWindow($found) + $scale = if ($dpi -gt 0) { $dpi / 96.0 } else { 1.0 } + $inset = [int][Math]::Round(20 * $scale) + $radius = [int][Math]::Round(16 * $scale) + + $clipInset = 2 + $cw = $w - 2 * $inset - 2 * $clipInset + $ch = $h - 2 * $inset - 2 * $clipInset + $srcX = $inset + $clipInset + $srcY = $inset + $clipInset + $radius = [Math]::Max(1, $radius - $clipInset) + + $bmp = New-Object System.Drawing.Bitmap($cw, $ch) + $g2 = [System.Drawing.Graphics]::FromImage($bmp) + $g2.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias + $g2.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic + + $cardPath = New-Object System.Drawing.Drawing2D.GraphicsPath + $cardPath.AddArc(0, 0, $radius, $radius, 180, 90) + $cardPath.AddArc($cw - $radius, 0, $radius, $radius, 270, 90) + $cardPath.AddArc($cw - $radius, $ch - $radius, $radius, $radius, 0, 90) + $cardPath.AddArc(0, $ch - $radius, $radius, $radius, 90, 90) + $cardPath.CloseFigure() + + $g2.SetClip($cardPath) + $srcRect = New-Object System.Drawing.Rectangle($srcX, $srcY, $cw, $ch) + $dstRect = New-Object System.Drawing.Rectangle(0, 0, $cw, $ch) + $g2.DrawImage($full, $dstRect, $srcRect, [System.Drawing.GraphicsUnit]::Pixel) + $g2.Dispose() + $cardPath.Dispose() + $full.Dispose() + + $w = $cw + $h = $ch +} else { + $bmp = $full +} + $outDir = Split-Path -Parent (Join-Path $PWD $Out) New-Item -ItemType Directory -Force -Path $outDir | Out-Null $outAbs = [System.IO.Path]::GetFullPath((Join-Path $PWD $Out))