Initial commit: Hyprland 0.57 rice for Fedora 44
Dark minimalist setup for a sysadmin workstation: rounded blur windows with the DMS pink accent, keyboard-driven SSH hub (Mod+S), run-or-raise app binds, Alt+Tab window switching, OCR of screen regions, hotkey cheatsheet, DMS network-editor plugin and the systemd session target for portals. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# generated by DMS / local state
|
||||
hypr/dms/
|
||||
*.bak*
|
||||
__pycache__/
|
||||
@@ -0,0 +1,94 @@
|
||||
# hyprland-rising
|
||||
|
||||
Hyprland 0.57 на Fedora 44: тёмный минималистичный райс для рабочей машины сисадмина.
|
||||
Скруглённые окна с блюром, розовый акцент из темы DMS, всё управляется с клавиатуры.
|
||||
|
||||
## Что внутри
|
||||
|
||||
| Путь | Что это |
|
||||
|---|---|
|
||||
| `hypr/hyprland.lua` | основной конфиг (Lua-формат Hyprland 0.57): оформление, анимации, бинды, правила окон |
|
||||
| `hypr/hypridle.conf` | затемнение (2,5 мин) → блокировка (5 мин) → погасание экрана (10 мин) |
|
||||
| `hypr/cheatsheet.txt` | справка по хоткеям, открывается на `Mod+F1` (поиск по тексту) |
|
||||
| `hypr/scripts/ssh-menu.sh`, `ssh_hosts.py` | SSH-меню на `Mod+S` (см. ниже) |
|
||||
| `hypr/scripts/raise.sh` | «показать, если запущено, иначе запустить» для `Mod+B/T/K` |
|
||||
| `hypr/scripts/prev-window.sh` | `Alt+Tab` — предыдущее окно |
|
||||
| `hypr/scripts/ocr.sh` | `Mod+Shift+O` — область экрана → текст в буфер (рус+англ) |
|
||||
| `hypr/scripts/tmux-broadcast.conf` | отдельный tmux для broadcast-окон SSH-меню |
|
||||
| `dms/plugins/networkEditor` | плагин DMS: кнопка в панели, открывает `nm-connection-editor` |
|
||||
| `dms/settings.json` | настройки DMS (панель, тема `pink`, лаунчер) |
|
||||
| `systemd/user/hyprland-session.target` | нужен порталам (`xdg-desktop-portal`) вне uwsm |
|
||||
| `kitty/kitty.conf` | терминал |
|
||||
| `sync.sh` | копирует живой конфиг из `~/.config` в репозиторий |
|
||||
|
||||
Панель, уведомления, лаунчер, блокировка, обои и буфер обмена — это
|
||||
DankMaterialShell (DMS), Hyprland отвечает только за окна.
|
||||
|
||||
## SSH-меню (`Mod+S`)
|
||||
|
||||
Плавающее окно (fzf в kitty) со всеми хостами из `~/.ssh/config`:
|
||||
статус доступности, группы цветом, плашка `PROD`, превью с пингом.
|
||||
`Enter` — подключиться (окно становится обычным), `Alt+T` tmux на сервере,
|
||||
`Alt+F` SFTP, `Alt+W` веб-интерфейс, `Alt+K` разослать ключ, `Alt+N` новый хост,
|
||||
`Alt+E` править конфиг, `Alt+G` группа, `Alt+R` PROD, `Tab` отметить,
|
||||
`Alt+B` broadcast в tmux на отмеченные хосты, `Alt+X` команда на отмеченных хостах.
|
||||
|
||||
Метаданные хоста хранятся комментариями прямо над `Host` в `~/.ssh/config`:
|
||||
|
||||
```
|
||||
# group: Proxmox
|
||||
# env: prod
|
||||
# url: https://10.0.0.10:8006
|
||||
Host proxmox1
|
||||
HostName 10.0.0.10
|
||||
User root
|
||||
```
|
||||
|
||||
Сам `~/.ssh/config` и ключи в репозиторий не входят. Перед любой записью меню делает
|
||||
бэкап в `~/.local/state/ssh-menu/backups`. Для хостов с `env: prod` перед broadcast
|
||||
и запуском команды спрашивается подтверждение.
|
||||
|
||||
## Установка
|
||||
|
||||
Hyprland нет в репозиториях Fedora 44, он ставится из COPR:
|
||||
|
||||
```bash
|
||||
sudo dnf copr enable sachesi/hyprland
|
||||
sudo dnf install hyprland hyprlock hypridle hyprpaper hyprpicker \
|
||||
xdg-desktop-portal-hyprland hyprland-guiutils hyprpolkitagent cliphist playerctl
|
||||
# инструменты, которые используют скрипты
|
||||
sudo dnf install fzf tmux kitty jq grim slurp wl-clipboard brightnessctl \
|
||||
nautilus keepassxc nm-connection-editor tesseract tesseract-langpack-eng \
|
||||
tesseract-langpack-rus ImageMagick jetbrains-mono-fonts
|
||||
```
|
||||
|
||||
DMS ставится из COPR `avengemedia/dms` (см. документацию DMS). В автозапуске и на биндах
|
||||
используются Flatpak-приложения: `com.google.Chrome`, `org.telegram.desktop`,
|
||||
`org.remmina.Remmina`, `com.nextcloud.desktopclient.nextcloud`.
|
||||
|
||||
```bash
|
||||
git clone git@git.ibrash.duckdns.org:aibrash/hyprland-rising.git
|
||||
cd hyprland-rising
|
||||
cp -r hypr ~/.config/
|
||||
mkdir -p ~/.config/dms/plugins ~/.config/systemd/user
|
||||
cp -r dms/plugins/networkEditor ~/.config/dms/plugins/
|
||||
cp systemd/user/hyprland-session.target ~/.config/systemd/user/
|
||||
systemctl --user daemon-reload
|
||||
```
|
||||
|
||||
Чтобы плагин `networkEditor` заработал, включите его в настройках DMS (Plugins) и добавьте виджет
|
||||
в панель. Либо скопируйте `dms/settings.json` в `~/.config/dms/` и создайте
|
||||
`~/.config/dms/plugin_settings.json` с содержимым `{"networkEditor": {"enabled": true}}`, затем `dms restart`.
|
||||
После этого выберите в GDM сессию **Hyprland** (не `hyprland-uwsm`).
|
||||
|
||||
## Под себя
|
||||
|
||||
* В `hypr/hyprland.lua` имена мониторов (`eDP-1`, `HDMI-A-1`) и масштаб 1.25 — под конкретный ноутбук.
|
||||
* Раскладка (`us,ru`, переключение Alt+Shift), чувствительность тачпада и мыши — там же, в блоке `input`.
|
||||
* Блокировку экрана делает DMS через `hypr/scripts/lock.sh` (с запасным вариантом swaylock).
|
||||
|
||||
## Обновление репозитория
|
||||
|
||||
```bash
|
||||
./sync.sh && git add -A && git commit -m "…" && git push
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.DCommon.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
// One icon in the bar: click opens the full NetworkManager editor.
|
||||
// Under Hyprland an already open editor is focused instead of starting a second one (raise.sh falls back to a plain launch).
|
||||
PluginComponent {
|
||||
id: root
|
||||
|
||||
pillClickAction: () => {
|
||||
Quickshell.execDetached(["sh", "-c", "~/.config/hypr/scripts/raise.sh '^nm-connection-editor$' nm-connection-editor || nm-connection-editor"]);
|
||||
}
|
||||
|
||||
horizontalBarPill: Component {
|
||||
DIcon {
|
||||
name: "settings_ethernet"
|
||||
color: Theme.surfaceText
|
||||
size: root.iconSize
|
||||
}
|
||||
}
|
||||
|
||||
verticalBarPill: Component {
|
||||
DIcon {
|
||||
name: "settings_ethernet"
|
||||
color: Theme.surfaceText
|
||||
size: root.iconSize
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "networkEditor",
|
||||
"name": "Network Editor",
|
||||
"description": "Bar button that opens nm-connection-editor (full NetworkManager editor: VPN, WireGuard, IP/DNS, routes)",
|
||||
"version": "1.0.0",
|
||||
"author": "aibrash",
|
||||
"type": "widget",
|
||||
"capabilities": ["dankbar-widget"],
|
||||
"component": "./NetworkEditorWidget.qml",
|
||||
"icon": "settings_ethernet",
|
||||
"requires": ["nm-connection-editor"],
|
||||
"permissions": []
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
{
|
||||
"dockConfigs": [
|
||||
{
|
||||
"id": "dock",
|
||||
"name": "Dock",
|
||||
"enabled": false,
|
||||
"screenPreferences": [
|
||||
"all"
|
||||
],
|
||||
"showOnLastDisplay": true,
|
||||
"position": 2,
|
||||
"mode": "compact",
|
||||
"taskbarAlign": "center",
|
||||
"widgetExpansion": "popout",
|
||||
"iconSize": 40,
|
||||
"spacing": 4,
|
||||
"itemSpacing": 4,
|
||||
"margin": 0,
|
||||
"bottomGap": 0,
|
||||
"transparency": 1,
|
||||
"followInterfaceStyle": true,
|
||||
"autoHide": false,
|
||||
"smartAutoHide": false,
|
||||
"useOverlayLayer": false,
|
||||
"editOnRightClick": false,
|
||||
"showOnFullscreen": false,
|
||||
"openOnOverview": false,
|
||||
"groupByApp": false,
|
||||
"separatePinnedAndRunningApps": false,
|
||||
"restoreSpecialWorkspaceOnClick": false,
|
||||
"isolateDisplays": false,
|
||||
"indicatorStyle": "circle",
|
||||
"borderEnabled": false,
|
||||
"borderColor": "surfaceText",
|
||||
"borderOpacity": 1,
|
||||
"borderThickness": 1,
|
||||
"launcherEnabled": false,
|
||||
"launcherLogoMode": "apps",
|
||||
"launcherLogoCustomPath": "",
|
||||
"launcherLogoColorOverride": "",
|
||||
"launcherLogoSizeOffset": 0,
|
||||
"launcherLogoBrightness": 0.5,
|
||||
"launcherLogoContrast": 1,
|
||||
"maxVisibleApps": 0,
|
||||
"maxVisibleRunningApps": 0,
|
||||
"showOverflowBadge": true,
|
||||
"showTrash": false,
|
||||
"trashFileManager": "default",
|
||||
"trashCustomCommand": "",
|
||||
"magnification": false,
|
||||
"magnificationScale": 130,
|
||||
"magnificationProfile": "parabolic",
|
||||
"magnificationExpand": false,
|
||||
"order": [],
|
||||
"widgets": [
|
||||
{
|
||||
"id": "dock_launcher",
|
||||
"widgetId": "dockLauncher",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "dock_apps",
|
||||
"widgetId": "appsDock",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "dock_trash",
|
||||
"widgetId": "dockTrash",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"barElevationEnabled": false,
|
||||
"barConfigs": [
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Main Bar",
|
||||
"enabled": true,
|
||||
"position": 0,
|
||||
"screenPreferences": [
|
||||
"all"
|
||||
],
|
||||
"showOnLastDisplay": true,
|
||||
"leftWidgets": [
|
||||
{
|
||||
"id": "launcherButton",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "workspaceSwitcher",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "focusedWindow",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"centerWidgets": [
|
||||
"music",
|
||||
"clock",
|
||||
"weather"
|
||||
],
|
||||
"rightWidgets": [
|
||||
{
|
||||
"id": "systemTray",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "vpn",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "networkEditor",
|
||||
"enabled": true
|
||||
},
|
||||
"keyboard_layout_name",
|
||||
"clipboard",
|
||||
"cpuUsage",
|
||||
"memUsage",
|
||||
"notificationButton",
|
||||
{
|
||||
"id": "battery",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "controlCenterButton",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"spacing": 4,
|
||||
"innerPadding": 4,
|
||||
"bottomGap": 0,
|
||||
"transparency": 1,
|
||||
"widgetTransparency": 1,
|
||||
"squareCorners": false,
|
||||
"noBackground": false,
|
||||
"gothCornersEnabled": false,
|
||||
"gothCornerRadiusOverride": false,
|
||||
"gothCornerRadiusValue": 12,
|
||||
"borderEnabled": false,
|
||||
"borderColor": "surfaceText",
|
||||
"borderOpacity": 1,
|
||||
"borderThickness": 1,
|
||||
"fontScale": 1,
|
||||
"autoHide": false,
|
||||
"autoHideDelay": 250,
|
||||
"openOnOverview": false,
|
||||
"visible": true,
|
||||
"popupGapsAuto": true,
|
||||
"popupGapsManual": 4,
|
||||
"followInterfaceStyle": true,
|
||||
"widgetFollowInterfaceStyle": true,
|
||||
"dot": false,
|
||||
"island": true,
|
||||
"islandFloating": false,
|
||||
"islandPlacement": "free",
|
||||
"islandOuterGap": 0,
|
||||
"islandAlongOffset": 0,
|
||||
"islandReserveThickness": 41,
|
||||
"islandRouteLauncher": "follow",
|
||||
"islandRouteControlCenter": "follow",
|
||||
"islandInteractionMode": "hybrid",
|
||||
"islandChargingPulse": true,
|
||||
"hoverPopouts": false,
|
||||
"hoverPopoutDelay": 200,
|
||||
"islandHomeLayout": [
|
||||
{
|
||||
"id": "workspaces",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "media",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "clock",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "weather",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "status",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "volume",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "brightness",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "notifications",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "privacy",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "tray",
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bar1793185825664",
|
||||
"name": "Bar 2",
|
||||
"enabled": false,
|
||||
"position": 1,
|
||||
"screenPreferences": [
|
||||
"all"
|
||||
],
|
||||
"showOnLastDisplay": true,
|
||||
"leftWidgets": [
|
||||
"launcherButton",
|
||||
"workspaceSwitcher",
|
||||
"focusedWindow"
|
||||
],
|
||||
"centerWidgets": [
|
||||
"music",
|
||||
"clock",
|
||||
"weather"
|
||||
],
|
||||
"rightWidgets": [
|
||||
"systemTray",
|
||||
"privacyIndicator",
|
||||
"vpn",
|
||||
"keyboard_layout_name",
|
||||
"clipboard",
|
||||
"cpuUsage",
|
||||
"cpuTemp",
|
||||
"memUsage",
|
||||
"notificationButton",
|
||||
"battery",
|
||||
"controlCenterButton"
|
||||
],
|
||||
"spacing": 4,
|
||||
"innerPadding": 4,
|
||||
"bottomGap": 0,
|
||||
"transparency": 1,
|
||||
"widgetTransparency": 1,
|
||||
"squareCorners": false,
|
||||
"noBackground": false,
|
||||
"gothCornersEnabled": false,
|
||||
"gothCornerRadiusOverride": false,
|
||||
"gothCornerRadiusValue": 12,
|
||||
"borderEnabled": false,
|
||||
"borderColor": "surfaceText",
|
||||
"borderOpacity": 1,
|
||||
"borderThickness": 1,
|
||||
"fontScale": 1,
|
||||
"autoHide": false,
|
||||
"autoHideDelay": 250,
|
||||
"openOnOverview": false,
|
||||
"visible": true,
|
||||
"popupGapsAuto": true,
|
||||
"popupGapsManual": 4,
|
||||
"followInterfaceStyle": true,
|
||||
"widgetFollowInterfaceStyle": true,
|
||||
"islandFloating": false,
|
||||
"islandPlacement": "free",
|
||||
"islandOuterGap": 0,
|
||||
"islandAlongOffset": 0,
|
||||
"islandReserveThickness": 41,
|
||||
"islandRouteLauncher": "follow",
|
||||
"islandRouteControlCenter": "follow",
|
||||
"islandInteractionMode": "hybrid",
|
||||
"islandChargingPulse": true,
|
||||
"hoverPopouts": false,
|
||||
"hoverPopoutDelay": 200,
|
||||
"hideOnLastDisplay": false
|
||||
},
|
||||
{
|
||||
"id": "dot1793185921302",
|
||||
"name": "Dot",
|
||||
"enabled": false,
|
||||
"position": 0,
|
||||
"screenPreferences": [
|
||||
"all"
|
||||
],
|
||||
"showOnLastDisplay": true,
|
||||
"leftWidgets": [
|
||||
"launcherButton",
|
||||
"workspaceSwitcher",
|
||||
"focusedWindow"
|
||||
],
|
||||
"centerWidgets": [
|
||||
"music",
|
||||
"clock",
|
||||
"weather"
|
||||
],
|
||||
"rightWidgets": [
|
||||
"systemTray",
|
||||
"privacyIndicator",
|
||||
"vpn",
|
||||
"keyboard_layout_name",
|
||||
"clipboard",
|
||||
"cpuUsage",
|
||||
"cpuTemp",
|
||||
"memUsage",
|
||||
"notificationButton",
|
||||
"battery",
|
||||
"controlCenterButton"
|
||||
],
|
||||
"spacing": 4,
|
||||
"innerPadding": 4,
|
||||
"bottomGap": 0,
|
||||
"transparency": 0.95,
|
||||
"widgetTransparency": 1,
|
||||
"squareCorners": false,
|
||||
"noBackground": false,
|
||||
"gothCornersEnabled": false,
|
||||
"gothCornerRadiusOverride": false,
|
||||
"gothCornerRadiusValue": 12,
|
||||
"borderEnabled": false,
|
||||
"borderColor": "surfaceText",
|
||||
"borderOpacity": 1,
|
||||
"borderThickness": 1,
|
||||
"fontScale": 1,
|
||||
"autoHide": false,
|
||||
"autoHideDelay": 250,
|
||||
"openOnOverview": false,
|
||||
"visible": true,
|
||||
"popupGapsAuto": true,
|
||||
"popupGapsManual": 4,
|
||||
"followInterfaceStyle": false,
|
||||
"widgetFollowInterfaceStyle": true,
|
||||
"dot": true,
|
||||
"island": false,
|
||||
"islandFloating": false,
|
||||
"islandPlacement": "free",
|
||||
"islandOuterGap": 0,
|
||||
"islandAlongOffset": 0,
|
||||
"islandReserveThickness": 41,
|
||||
"islandRouteLauncher": "follow",
|
||||
"islandRouteControlCenter": "follow",
|
||||
"islandInteractionMode": "hybrid",
|
||||
"islandChargingPulse": true,
|
||||
"hoverPopouts": false,
|
||||
"hoverPopoutDelay": 200
|
||||
}
|
||||
],
|
||||
"configVersion": 42,
|
||||
"popupTransparency": 0.95,
|
||||
"fontFamily": "JetBrains Mono Medium",
|
||||
"audioVisualizerEnabled": true,
|
||||
"scrollTitleEnabled": true,
|
||||
"dashOptions": {
|
||||
"wallpaper": {
|
||||
"panelColumns": 5
|
||||
}
|
||||
},
|
||||
"batteryChargeLimit": 80,
|
||||
"controlCenterColumns": 9,
|
||||
"controlCenterWidgets": [
|
||||
{
|
||||
"id": "user",
|
||||
"enabled": true,
|
||||
"w": 4,
|
||||
"h": 1,
|
||||
"col": 0,
|
||||
"row": 0
|
||||
},
|
||||
{
|
||||
"id": "settings",
|
||||
"enabled": true,
|
||||
"w": 1,
|
||||
"h": 1,
|
||||
"small": true,
|
||||
"col": 4,
|
||||
"row": 0
|
||||
},
|
||||
{
|
||||
"id": "lock",
|
||||
"enabled": true,
|
||||
"w": 1,
|
||||
"h": 1,
|
||||
"small": true,
|
||||
"col": 5,
|
||||
"row": 0
|
||||
},
|
||||
{
|
||||
"id": "power",
|
||||
"enabled": true,
|
||||
"w": 1,
|
||||
"h": 1,
|
||||
"small": true,
|
||||
"col": 6,
|
||||
"row": 0
|
||||
},
|
||||
{
|
||||
"id": "brightnessSlider",
|
||||
"enabled": true,
|
||||
"w": 4,
|
||||
"h": 1,
|
||||
"col": 0,
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"id": "volumeSlider",
|
||||
"enabled": true,
|
||||
"w": 4,
|
||||
"h": 1,
|
||||
"col": 4,
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"id": "wifi",
|
||||
"enabled": true,
|
||||
"w": 4,
|
||||
"h": 1,
|
||||
"col": 0,
|
||||
"row": 2
|
||||
},
|
||||
{
|
||||
"id": "bluetooth",
|
||||
"enabled": true,
|
||||
"w": 4,
|
||||
"h": 1,
|
||||
"col": 4,
|
||||
"row": 2
|
||||
},
|
||||
{
|
||||
"id": "audioOutput",
|
||||
"enabled": true,
|
||||
"w": 4,
|
||||
"h": 1,
|
||||
"col": 0,
|
||||
"row": 3
|
||||
},
|
||||
{
|
||||
"id": "audioInput",
|
||||
"enabled": true,
|
||||
"w": 4,
|
||||
"h": 1,
|
||||
"col": 4,
|
||||
"row": 3
|
||||
},
|
||||
{
|
||||
"id": "nightMode",
|
||||
"enabled": true,
|
||||
"w": 1,
|
||||
"h": 1,
|
||||
"col": 7,
|
||||
"row": 0,
|
||||
"small": true
|
||||
},
|
||||
{
|
||||
"id": "battery",
|
||||
"enabled": true,
|
||||
"w": 4,
|
||||
"h": 1,
|
||||
"col": 0,
|
||||
"row": 4
|
||||
},
|
||||
{
|
||||
"id": "runningApps",
|
||||
"enabled": true,
|
||||
"w": 8,
|
||||
"h": 1,
|
||||
"small": true,
|
||||
"col": 0,
|
||||
"row": 5
|
||||
},
|
||||
{
|
||||
"id": "edit",
|
||||
"enabled": true,
|
||||
"w": 1,
|
||||
"h": 1,
|
||||
"small": true,
|
||||
"col": 8,
|
||||
"row": 0
|
||||
},
|
||||
{
|
||||
"id": "builtin_vpn",
|
||||
"enabled": true,
|
||||
"w": 4,
|
||||
"h": 1,
|
||||
"col": 4,
|
||||
"row": 4
|
||||
}
|
||||
],
|
||||
"monoFontFamily": "JetBrains Mono",
|
||||
"displayFontFamily": "JetBrains Mono",
|
||||
"fontWeight": 300,
|
||||
"fontScale": 1,
|
||||
"currentThemeName": "pink",
|
||||
"wallpaperFillMode": "Fill",
|
||||
"connectedFrameBarStyleBackups": {},
|
||||
"frameEnabled": false,
|
||||
"dashCards": [
|
||||
{
|
||||
"id": "clock",
|
||||
"w": 2,
|
||||
"h": 1,
|
||||
"col": 0,
|
||||
"row": 0
|
||||
},
|
||||
{
|
||||
"id": "weather",
|
||||
"w": 1,
|
||||
"h": 1,
|
||||
"col": 2,
|
||||
"row": 0
|
||||
},
|
||||
{
|
||||
"id": "notifications",
|
||||
"w": 3,
|
||||
"h": 5,
|
||||
"col": 3,
|
||||
"row": 0
|
||||
},
|
||||
{
|
||||
"id": "calendar",
|
||||
"w": 3,
|
||||
"h": 3,
|
||||
"col": 0,
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"id": "media",
|
||||
"w": 3,
|
||||
"h": 1,
|
||||
"col": 0,
|
||||
"row": 4
|
||||
}
|
||||
],
|
||||
"builtInPluginSettings": {
|
||||
"dms_qr_generator": {
|
||||
"trigger": "qrg"
|
||||
},
|
||||
"dms_power": {
|
||||
"trigger": "pw"
|
||||
},
|
||||
"dms_clipboard_search": {
|
||||
"trigger": "cb"
|
||||
},
|
||||
"dms_settings_search": {
|
||||
"trigger": "?"
|
||||
}
|
||||
},
|
||||
"launcherStyle": "island",
|
||||
"dankLauncherV2Size": "compact",
|
||||
"appLauncherGridColumns": 4,
|
||||
"spotlightSectionViewModes": {
|
||||
"apps": "grid"
|
||||
},
|
||||
"dankLauncherV2BorderEnabled": false,
|
||||
"dankLauncherV2BorderThickness": 5,
|
||||
"dankLauncherV2BorderColor": "surfaceText",
|
||||
"rememberLastQuery": true,
|
||||
"bluetoothMprisEnabled": true,
|
||||
"dashTabs": [
|
||||
{
|
||||
"id": "wallpaper",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "overview",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "media",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "weather",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "wellbeing",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "notifications",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"desktopWidgetInstances": [],
|
||||
"notificationPopupPosition": -1,
|
||||
"notificationFocusedMonitor": false,
|
||||
"notificationOverlayEnabled": true,
|
||||
"notificationCompactMode": false,
|
||||
"osdPosition": 5,
|
||||
"networkPreference": "auto",
|
||||
"avatarRing": "none",
|
||||
"lockScreenWidgetInstances": [
|
||||
{
|
||||
"id": "lock_clock",
|
||||
"widgetType": "desktopClock",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"style": "expressive",
|
||||
"transparency": 0,
|
||||
"weight": 0,
|
||||
"twoTone": true,
|
||||
"showDate": false,
|
||||
"showDigitalSeconds": false,
|
||||
"showAnalogNumbers": false,
|
||||
"showAnalogSeconds": true,
|
||||
"autoPosition": true,
|
||||
"colorMode": "default",
|
||||
"customColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lock_date",
|
||||
"widgetType": "lockDate",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"format": "short",
|
||||
"colorMode": "default",
|
||||
"customColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lock_auth",
|
||||
"widgetType": "lockAuth",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"style": "expressive",
|
||||
"profileVisibility": "always",
|
||||
"passwordVisibility": "always"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lock_notifications",
|
||||
"widgetType": "lockNotifications",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"mode": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lock_status",
|
||||
"widgetType": "lockStatus",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"showMediaPlayer": true,
|
||||
"showWeather": true,
|
||||
"background": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lock_power",
|
||||
"widgetType": "lockPower",
|
||||
"enabled": false,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"shape": "round"
|
||||
}
|
||||
}
|
||||
],
|
||||
"greeterWidgetInstances": [
|
||||
{
|
||||
"id": "lock_clock",
|
||||
"widgetType": "desktopClock",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"style": "expressive",
|
||||
"transparency": 0,
|
||||
"weight": 0,
|
||||
"twoTone": true,
|
||||
"showDate": false,
|
||||
"showDigitalSeconds": false,
|
||||
"showAnalogNumbers": false,
|
||||
"showAnalogSeconds": true,
|
||||
"autoPosition": true,
|
||||
"colorMode": "default",
|
||||
"customColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lock_date",
|
||||
"widgetType": "lockDate",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"format": "short",
|
||||
"colorMode": "default",
|
||||
"customColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lock_auth",
|
||||
"widgetType": "lockAuth",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"style": "expressive",
|
||||
"profileVisibility": "always",
|
||||
"passwordVisibility": "always"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lock_status",
|
||||
"widgetType": "lockStatus",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"showMediaPlayer": true,
|
||||
"showWeather": true,
|
||||
"background": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lock_power",
|
||||
"widgetType": "lockPower",
|
||||
"enabled": false,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true,
|
||||
"shape": "round"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "greeter_session",
|
||||
"widgetType": "greeterSession",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"displayPreferences": [
|
||||
"all"
|
||||
],
|
||||
"syncPositionAcrossScreens": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"notepadEdgeGap": 3,
|
||||
"appDrawerSectionViewModes": {
|
||||
"apps": "list"
|
||||
},
|
||||
"dmsWindowsFloatingSeeded": [
|
||||
"hyprland"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
ПРОГРАММЫ
|
||||
Mod+Return Терминал (kitty)
|
||||
Mod+D Лаунчер приложений (DMS)
|
||||
Mod+S SSH-меню (см. раздел ниже)
|
||||
Mod+B Chrome: показать, если открыт (повтор — следующее окно Chrome), иначе запустить
|
||||
Mod+Shift+B Новое окно Chrome
|
||||
Mod+T Telegram: показать, если открыт, иначе запустить
|
||||
Mod+K KeePassXC: показать, если открыт, иначе запустить
|
||||
Mod+O Файлы (Nautilus)
|
||||
Mod+` Выдвижной терминал (scratchpad)
|
||||
Mod+Shift+V История буфера обмена
|
||||
|
||||
ОКНА
|
||||
Mod+H/J/K/L Фокус влево/вниз/вверх/вправо (также стрелки)
|
||||
Mod+Shift+H/J/K/L Переместить окно (также стрелки)
|
||||
Mod+Shift+Q Закрыть окно
|
||||
Mod+F На весь экран
|
||||
Mod+Shift+Space Плавающее / тайловое
|
||||
Mod+Shift+F Плавающее / тайловое
|
||||
Mod+C Центрировать плавающее окно
|
||||
Mod+Shift+T Закрепить плавающее окно поверх всех
|
||||
Mod+E Сменить направление разбиения
|
||||
Mod+P Pseudo-tiling
|
||||
Mod+W Собрать окна во вкладки (группа)
|
||||
Mod+[ / Mod+] Предыдущая / следующая вкладка группы
|
||||
Mod+R Режим ресайза (H/J/K/L или стрелки, Enter/Esc выход)
|
||||
Mod+ЛКМ / ПКМ Перетащить / изменить размер окна мышью
|
||||
|
||||
SSH-МЕНЮ (Mod+S) ● доступен ○ нет ◐ через jump
|
||||
Enter Подключиться (окно становится обычным)
|
||||
Alt+T SSH + tmux (attach/create "main" на сервере)
|
||||
Alt+F SFTP в Nautilus
|
||||
Alt+W Веб-интерфейс хоста в Chrome (первый раз спросит URL, сохранит как # url:)
|
||||
Alt+Y Скопировать user@host:port
|
||||
Alt+K Разослать SSH-ключ (ssh-copy-id) на отмеченные хосты / создать новый ключ
|
||||
Alt+N Новый хост (мастер) → в ~/.ssh/config, бэкап в ~/.local/state/ssh-menu
|
||||
Alt+E Править ~/.ssh/config в micro на этом хосте
|
||||
Alt+G Задать группу отмеченным хостам
|
||||
Alt+R Пометить/снять PROD (красный фон окна и заголовок)
|
||||
Tab / Alt+A / Alt+Z Отметить хост / все / снять
|
||||
Alt+B Broadcast: одна tmux-панель на отмеченный хост, ввод идёт во все (Alt+S в окне — вкл/выкл синхронизацию, Alt+Z — зум панели)
|
||||
Alt+X Выполнить команду на отмеченных хостах параллельно (одинаковые ответы склеиваются)
|
||||
Для PROD-хостов Alt+B / Alt+X сначала спрашивают подтверждение
|
||||
Ввод user@host Подключиться к хосту, которого нет в списке
|
||||
Esc Закрыть меню
|
||||
|
||||
РАБОЧИЕ СТОЛЫ И МОНИТОРЫ
|
||||
Mod+1..0 Перейти на стол 1..10 (повтор = назад)
|
||||
Mod+Shift+1..0 Перенести окно на стол 1..10
|
||||
Alt+Tab Предыдущее окно (ещё раз — обратно)
|
||||
Mod+Alt+Tab Следующее окно на этом столе
|
||||
Mod+Tab Следующий стол
|
||||
Mod+Shift+Tab Предыдущий стол
|
||||
Mod+колесо мыши Листать столы
|
||||
Жест 3 пальца Листать столы
|
||||
Mod+, / Mod+. Фокус на левый / правый монитор
|
||||
Mod+Shift+- Убрать окно в scratchpad
|
||||
Mod+- Показать scratchpad
|
||||
|
||||
СИСТЕМА
|
||||
Mod+N Центр управления (DMS)
|
||||
Mod+Shift+N Уведомления
|
||||
Mod+Shift+X Заблокировать экран
|
||||
Mod+Shift+E / P Меню питания
|
||||
Mod+Shift+C Перезагрузить конфиг Hyprland
|
||||
Mod+F1 Эта справка
|
||||
|
||||
СКРИНШОТЫ
|
||||
Print Область → файл + буфер
|
||||
Mod+Shift+S Область → файл + буфер
|
||||
Shift+Print Весь экран
|
||||
Ctrl+Print Активное окно
|
||||
Mod+Ctrl+C Пипетка цвета
|
||||
Mod+Shift+O OCR: область экрана → текст в буфер (рус+англ)
|
||||
|
||||
МУЛЬТИМЕДИА
|
||||
Громкость / яркость / плеер: функциональные клавиши (OSD от DMS)
|
||||
@@ -0,0 +1,22 @@
|
||||
general {
|
||||
lock_cmd = ~/.config/hypr/scripts/lock.sh
|
||||
before_sleep_cmd = ~/.config/hypr/scripts/lock.sh
|
||||
after_sleep_cmd = hyprctl dispatch dpms on
|
||||
}
|
||||
|
||||
listener {
|
||||
timeout = 150
|
||||
on-timeout = brightnessctl -s set 15%
|
||||
on-resume = brightnessctl -r
|
||||
}
|
||||
|
||||
listener {
|
||||
timeout = 300
|
||||
on-timeout = ~/.config/hypr/scripts/lock.sh
|
||||
}
|
||||
|
||||
listener {
|
||||
timeout = 600
|
||||
on-timeout = hyprctl dispatch dpms off
|
||||
on-resume = hyprctl dispatch dpms on
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
-- Hyprland 0.57 (Lua). Clean dark work setup; shell = DankMaterialShell.
|
||||
-- Sway config stays in ~/.config/sway as a fallback session.
|
||||
|
||||
local terminal = "kitty"
|
||||
local mod = "SUPER"
|
||||
|
||||
----------------
|
||||
---- OUTPUT ----
|
||||
----------------
|
||||
-- eDP-1 at 1.25 scale is 1536 logical px wide, so HDMI sits right of it.
|
||||
hl.monitor({ output = "eDP-1", mode = "preferred", position = "0x0", scale = 1.25 })
|
||||
hl.monitor({ output = "HDMI-A-1", mode = "preferred", position = "1536x0", scale = 1 })
|
||||
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto" })
|
||||
|
||||
-----------------
|
||||
---- STARTUP ----
|
||||
-----------------
|
||||
hl.env("XCURSOR_SIZE", "24")
|
||||
hl.env("HYPRCURSOR_SIZE", "24")
|
||||
hl.env("XDG_CURRENT_DESKTOP", "Hyprland")
|
||||
hl.env("XDG_SESSION_TYPE", "wayland")
|
||||
hl.env("QT_QPA_PLATFORM", "wayland;xcb")
|
||||
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto")
|
||||
|
||||
hl.on("hyprland.start", function()
|
||||
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE")
|
||||
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE")
|
||||
-- portals (file pickers, screen sharing, Nautilus) need graphical-session.target; sway had sway-session.target for this
|
||||
hl.exec_cmd("systemctl --user start hyprland-session.target")
|
||||
hl.exec_cmd("sh -c 'pgrep -x dms >/dev/null || exec dms run'")
|
||||
hl.exec_cmd("systemctl --user start hyprpolkitagent")
|
||||
hl.exec_cmd("hypridle")
|
||||
hl.exec_cmd("nm-applet --indicator")
|
||||
hl.exec_cmd("wl-paste --type text --watch cliphist store")
|
||||
hl.exec_cmd("wl-paste --type image --watch cliphist store")
|
||||
hl.exec_cmd(terminal .. " --class scratch")
|
||||
hl.exec_cmd("sh -c 'sleep 5; flatpak info com.nextcloud.desktopclient.nextcloud >/dev/null 2>&1 && ! pgrep -f \"[n]extcloud.*--background\" >/dev/null && flatpak run com.nextcloud.desktopclient.nextcloud --background'")
|
||||
hl.exec_cmd("sh -c 'sleep 5; flatpak info org.remmina.Remmina >/dev/null 2>&1 && ! pgrep -f \"[R]emmina.* -i\" >/dev/null && flatpak run org.remmina.Remmina -i'")
|
||||
hl.exec_cmd("sh -c 'sleep 8; command -v geary >/dev/null && ! pgrep -x geary >/dev/null && geary --gapplication-service'")
|
||||
end)
|
||||
|
||||
-----------------------
|
||||
---- LOOK AND FEEL ----
|
||||
-----------------------
|
||||
hl.config({
|
||||
general = {
|
||||
gaps_in = 6,
|
||||
gaps_out = 14,
|
||||
border_size = 2,
|
||||
col = {
|
||||
active_border = { colors = { "rgba(e91e63ff)", "rgba(ff6398ff)", "rgba(f8bbd9ff)", "rgba(e91e63ff)" }, angle = 45 },
|
||||
inactive_border = "rgba(3a2a2eaa)",
|
||||
},
|
||||
resize_on_border = true,
|
||||
allow_tearing = false,
|
||||
layout = "dwindle",
|
||||
},
|
||||
decoration = {
|
||||
rounding = 16,
|
||||
rounding_power = 4, -- squircle corners
|
||||
active_opacity = 1.0,
|
||||
inactive_opacity = 0.96,
|
||||
dim_inactive = true,
|
||||
dim_strength = 0.06,
|
||||
shadow = {
|
||||
enabled = true,
|
||||
range = 30,
|
||||
render_power = 3,
|
||||
color = 0x66e91e63, -- soft pink glow on the focused window
|
||||
color_inactive = 0x66000000,
|
||||
},
|
||||
blur = {
|
||||
enabled = true,
|
||||
size = 10,
|
||||
passes = 4,
|
||||
noise = 0.02,
|
||||
vibrancy = 0.3,
|
||||
brightness = 1.05,
|
||||
popups = true,
|
||||
},
|
||||
},
|
||||
animations = { enabled = true },
|
||||
binds = { workspace_back_and_forth = true },
|
||||
dwindle = { preserve_split = true, force_split = 2 }, -- new window always opens to the right/below
|
||||
xwayland = { force_zero_scaling = true }, -- crisp X11 apps on the 1.25x laptop panel
|
||||
cursor = { hide_on_key_press = true, inactive_timeout = 5 },
|
||||
misc = {
|
||||
force_default_wallpaper = 0,
|
||||
disable_hyprland_logo = true,
|
||||
focus_on_activate = true,
|
||||
key_press_enables_dpms = true, -- wake the screen on any key / mouse move (hypridle turns it off)
|
||||
mouse_move_enables_dpms = true,
|
||||
animate_manual_resizes = true,
|
||||
animate_mouse_windowdragging = true,
|
||||
},
|
||||
})
|
||||
|
||||
hl.curve("easeOutQuint", { type = "bezier", points = { {0.23, 1}, {0.32, 1} } })
|
||||
hl.curve("easeInOutCubic", { type = "bezier", points = { {0.65, 0.05}, {0.36, 1} } })
|
||||
hl.curve("quick", { type = "bezier", points = { {0.15, 0}, {0.1, 1} } })
|
||||
hl.curve("linear", { type = "bezier", points = { {0, 0}, {1, 1} } })
|
||||
hl.curve("snappy", { type = "spring", mass = 1, stiffness = 340, damping = 24 })
|
||||
hl.curve("bouncy", { type = "spring", mass = 1, stiffness = 280, damping = 17 })
|
||||
|
||||
hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" })
|
||||
hl.animation({ leaf = "border", enabled = true, speed = 4, bezier = "easeOutQuint" })
|
||||
hl.animation({ leaf = "borderangle", enabled = true, speed = 60, bezier = "linear", style = "loop" }) -- slowly rotating gradient border
|
||||
hl.animation({ leaf = "windows", enabled = true, speed = 3.2, spring = "snappy" })
|
||||
hl.animation({ leaf = "windowsIn", enabled = true, speed = 3.0, spring = "bouncy", style = "popin 88%" })
|
||||
hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.2, bezier = "quick", style = "popin 88%" })
|
||||
hl.animation({ leaf = "fade", enabled = true, speed = 2, bezier = "quick" })
|
||||
hl.animation({ leaf = "fadeDim", enabled = true, speed = 2.5, bezier = "quick" })
|
||||
hl.animation({ leaf = "layers", enabled = true, speed = 2.6, bezier = "easeOutQuint" })
|
||||
hl.animation({ leaf = "layersIn", enabled = true, speed = 2.6, bezier = "easeOutQuint", style = "slide" })
|
||||
hl.animation({ leaf = "layersOut", enabled = true, speed = 1.0, bezier = "quick", style = "fade" })
|
||||
hl.animation({ leaf = "workspaces", enabled = true, speed = 3.0, bezier = "easeInOutCubic", style = "slide" })
|
||||
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 2.8, bezier = "easeOutQuint", style = "slidevert" })
|
||||
|
||||
-- No gaps/border/rounding for a single tiled window (sway smart_gaps equivalent).
|
||||
hl.workspace_rule({ workspace = "w[tv1]", gaps_out = 0, gaps_in = 0 })
|
||||
hl.workspace_rule({ workspace = "f[1]", gaps_out = 0, gaps_in = 0 })
|
||||
hl.window_rule({ name = "smart-gaps-1", match = { float = false, workspace = "w[tv1]" }, border_size = 0, rounding = 0 })
|
||||
hl.window_rule({ name = "smart-gaps-2", match = { float = false, workspace = "f[1]" }, border_size = 0, rounding = 0 })
|
||||
|
||||
---------------
|
||||
---- INPUT ----
|
||||
---------------
|
||||
hl.config({
|
||||
input = {
|
||||
kb_layout = "us,ru",
|
||||
kb_options = "grp:alt_shift_toggle,grp_led:scroll",
|
||||
follow_mouse = 2, -- click to focus, like sway's focus_follows_mouse no
|
||||
sensitivity = -0.5,
|
||||
touchpad = {
|
||||
natural_scroll = true,
|
||||
tap_to_click = true,
|
||||
tap_button_map = "lrm",
|
||||
disable_while_typing = true,
|
||||
middle_button_emulation = true,
|
||||
},
|
||||
},
|
||||
})
|
||||
hl.device({ name = "type:touchpad", sensitivity = 0.3 })
|
||||
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
|
||||
|
||||
---------------------
|
||||
---- KEYBINDINGS ----
|
||||
---------------------
|
||||
local function b(keys, dsp, opts) hl.bind(keys, dsp, opts) end
|
||||
local function dms(args) return hl.dsp.exec_cmd("dms ipc call " .. args) end
|
||||
|
||||
b(mod .. " + Return", hl.dsp.exec_cmd(terminal))
|
||||
b(mod .. " + SHIFT + Q", hl.dsp.window.close())
|
||||
b(mod .. " + D", dms("spotlight toggle"))
|
||||
b(mod .. " + SHIFT + E", dms("powermenu toggle"))
|
||||
b(mod .. " + SHIFT + P", dms("powermenu toggle"))
|
||||
b(mod .. " + SHIFT + C", hl.dsp.reload_config())
|
||||
b(mod .. " + N", dms("control-center toggle"))
|
||||
b(mod .. " + SHIFT + N", dms("notifications toggle"))
|
||||
b(mod .. " + SHIFT + V", dms("clipboard toggle"))
|
||||
b(mod .. " + SHIFT + X", hl.dsp.exec_cmd("~/.config/hypr/scripts/lock.sh"))
|
||||
b(mod .. " + O", hl.dsp.exec_cmd("nautilus --new-window"))
|
||||
|
||||
-- focus / move (vim keys + arrows)
|
||||
local dirs = { h = "left", j = "down", k = "up", l = "right", left = "left", down = "down", up = "up", right = "right" }
|
||||
for key, d in pairs(dirs) do
|
||||
b(mod .. " + " .. key, hl.dsp.focus({ direction = d }))
|
||||
b(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ direction = d }))
|
||||
end
|
||||
|
||||
-- layout
|
||||
b(mod .. " + F", hl.dsp.window.fullscreen())
|
||||
b(mod .. " + SHIFT + SPACE", hl.dsp.window.float({ action = "toggle" }))
|
||||
b(mod .. " + SHIFT + F", hl.dsp.window.float({ action = "toggle" }))
|
||||
b(mod .. " + C", hl.dsp.window.center())
|
||||
b(mod .. " + E", hl.dsp.layout("togglesplit"))
|
||||
b(mod .. " + P", hl.dsp.window.pseudo())
|
||||
b(mod .. " + W", hl.dsp.group.toggle())
|
||||
b("ALT + TAB", hl.dsp.exec_cmd("~/.config/hypr/scripts/prev-window.sh")) -- previous window (press again to go back)
|
||||
b(mod .. " + ALT + TAB", hl.dsp.window.cycle_next()) -- next window on this workspace
|
||||
b(mod .. " + TAB", hl.dsp.focus({ workspace = "e+1" }))
|
||||
b(mod .. " + SHIFT + TAB", hl.dsp.focus({ workspace = "e-1" }))
|
||||
b(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
|
||||
b(mod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
|
||||
|
||||
-- workspaces 1..10
|
||||
for i = 1, 10 do
|
||||
local key = i % 10
|
||||
b(mod .. " + " .. key, hl.dsp.focus({ workspace = i }))
|
||||
b(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }))
|
||||
end
|
||||
|
||||
-- scratchpad
|
||||
b(mod .. " + grave", hl.dsp.workspace.toggle_special("scratch"))
|
||||
b(mod .. " + SHIFT + minus", hl.dsp.window.move({ workspace = "special:scratch" }))
|
||||
b(mod .. " + minus", hl.dsp.workspace.toggle_special("scratch"))
|
||||
|
||||
-- mouse
|
||||
b(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
|
||||
b(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
|
||||
|
||||
-- resize submap (mod+r, like sway's resize mode)
|
||||
b(mod .. " + R", hl.dsp.submap("resize"))
|
||||
hl.define_submap("resize", "reset", function()
|
||||
b("h", hl.dsp.window.resize({ x = -40, y = 0, relative = true }), { repeating = true })
|
||||
b("l", hl.dsp.window.resize({ x = 40, y = 0, relative = true }), { repeating = true })
|
||||
b("k", hl.dsp.window.resize({ x = 0, y = -40, relative = true }), { repeating = true })
|
||||
b("j", hl.dsp.window.resize({ x = 0, y = 40, relative = true }), { repeating = true })
|
||||
b("left", hl.dsp.window.resize({ x = -40, y = 0, relative = true }), { repeating = true })
|
||||
b("right", hl.dsp.window.resize({ x = 40, y = 0, relative = true }), { repeating = true })
|
||||
b("up", hl.dsp.window.resize({ x = 0, y = -40, relative = true }), { repeating = true })
|
||||
b("down", hl.dsp.window.resize({ x = 0, y = 40, relative = true }), { repeating = true })
|
||||
b("Return", hl.dsp.submap("reset"))
|
||||
b("Escape", hl.dsp.submap("reset"))
|
||||
end)
|
||||
|
||||
-- sysadmin toolbox
|
||||
b(mod .. " + S", hl.dsp.exec_cmd("kitty --class ssh-menu -o font_size=12 -o env=SSH_MENU=1 --title 'ssh menu' ~/.config/hypr/scripts/ssh-menu.sh")) -- SSH hub (fzf)
|
||||
local raise = "~/.config/hypr/scripts/raise.sh "
|
||||
b(mod .. " + T", hl.dsp.exec_cmd(raise .. "'^org.telegram.desktop$' flatpak run org.telegram.desktop")) -- focus if open, else launch
|
||||
b(mod .. " + B", hl.dsp.exec_cmd(raise .. "'^google-chrome$' flatpak run com.google.Chrome")) -- again = next Chrome window
|
||||
b(mod .. " + SHIFT + B", hl.dsp.exec_cmd("flatpak run com.google.Chrome --new-window")) -- always a new window
|
||||
b(mod .. " + F1", hl.dsp.exec_cmd("~/.config/hypr/scripts/cheatsheet.sh")) -- hotkey reference
|
||||
b(mod .. " + K", hl.dsp.exec_cmd(raise .. "'^org.keepassxc.KeePassXC$' keepassxc"))
|
||||
b(mod .. " + SHIFT + T", hl.dsp.window.pin()) -- keep a floating window on top
|
||||
b(mod .. " + bracketright", hl.dsp.group.next())
|
||||
b(mod .. " + bracketleft", hl.dsp.group.prev())
|
||||
|
||||
-- two monitors: laptop <-> external (focus only)
|
||||
b(mod .. " + comma", hl.dsp.focus({ monitor = "-1" }))
|
||||
b(mod .. " + period", hl.dsp.focus({ monitor = "+1" }))
|
||||
|
||||
-- screenshots (grim + slurp; saved to ~/Pictures/Screenshots and copied)
|
||||
local shot = "mkdir -p ~/Pictures/Screenshots; f=~/Pictures/Screenshots/$(date +%F_%H-%M-%S).png; "
|
||||
b("Print", hl.dsp.exec_cmd(shot .. "grim -g \"$(slurp)\" - | tee $f | wl-copy"))
|
||||
b(mod .. " + SHIFT + S", hl.dsp.exec_cmd(shot .. "grim -g \"$(slurp)\" - | tee $f | wl-copy"))
|
||||
b("SHIFT + Print", hl.dsp.exec_cmd(shot .. "grim - | tee $f | wl-copy"))
|
||||
b("CTRL + Print", hl.dsp.exec_cmd(shot .. "grim -g \"$(hyprctl activewindow -j | jq -r '\"\\(.at[0]),\\(.at[1]) \\(.size[0])x\\(.size[1])\"')\" - | tee $f | wl-copy"))
|
||||
b(mod .. " + CTRL + C", hl.dsp.exec_cmd("hyprpicker -a")) -- color picker
|
||||
b(mod .. " + SHIFT + O", hl.dsp.exec_cmd("~/.config/hypr/scripts/ocr.sh")) -- screen region -> text in clipboard
|
||||
|
||||
-- media / brightness / volume via DMS (shows OSD)
|
||||
b("XF86AudioRaiseVolume", dms("audio increment 5"), { locked = true, repeating = true })
|
||||
b("XF86AudioLowerVolume", dms("audio decrement 5"), { locked = true, repeating = true })
|
||||
b("XF86AudioMute", dms("audio mute"), { locked = true })
|
||||
b("XF86AudioMicMute", dms("audio micmute"), { locked = true })
|
||||
b("XF86MonBrightnessUp", dms("brightness increment 5 \"\""), { locked = true, repeating = true })
|
||||
b("XF86MonBrightnessDown", dms("brightness decrement 5 \"\""), { locked = true, repeating = true })
|
||||
b("XF86AudioPlay", dms("mpris playPause"), { locked = true })
|
||||
b("XF86AudioNext", dms("mpris next"), { locked = true })
|
||||
b("XF86AudioPrev", dms("mpris previous"), { locked = true })
|
||||
|
||||
-----------------
|
||||
---- RULES ------
|
||||
-----------------
|
||||
hl.window_rule({ name = "suppress-maximize", match = { class = ".*" }, suppress_event = "maximize" })
|
||||
hl.window_rule({
|
||||
name = "xwayland-drag-fix",
|
||||
match = { class = "^$", title = "^$", xwayland = true, float = true, fullscreen = false, pin = false },
|
||||
no_focus = true,
|
||||
})
|
||||
hl.window_rule({
|
||||
name = "float-utils",
|
||||
match = { class = "^(pavucontrol|nm-connection-editor|blueman-manager|org.pulseaudio.pavucontrol)$" },
|
||||
float = true, size = "700 500", center = true,
|
||||
})
|
||||
hl.window_rule({ name = "float-mpv", match = { class = "^mpv$" }, float = true })
|
||||
hl.window_rule({ name = "idle-inhibit-fs", match = { class = "^(firefox|mpv|google-chrome|org.telegram.desktop)$" }, idle_inhibit = "fullscreen" })
|
||||
-- no auto-lock in the middle of an RDP/VNC session
|
||||
hl.window_rule({ name = "idle-inhibit-rdp", match = { class = "^(org.remmina.Remmina|com.freerdp.FreeRDP|xfreerdp)$" }, idle_inhibit = "focus" })
|
||||
-- file dialogs from the portal and Chrome picture-in-picture
|
||||
hl.window_rule({ name = "float-portal-dialogs", match = { class = "^xdg-desktop-portal-gtk$" }, float = true, size = "900 600", center = true })
|
||||
hl.window_rule({ name = "pip", match = { title = "^Picture in picture$" }, float = true, pin = true, size = "480 270", move = "monitor_w-500 monitor_h-290" })
|
||||
hl.window_rule({
|
||||
name = "scratch-term",
|
||||
match = { class = "^scratch$" },
|
||||
workspace = "special:scratch silent", float = true, size = "1000 560", center = true,
|
||||
})
|
||||
|
||||
hl.window_rule({ name = "float-ssh-menu", match = { class = "^ssh-menu$" }, float = true, size = "1400 780", center = true })
|
||||
hl.window_rule({ name = "float-keepass", match = { class = "^org.keepassxc.KeePassXC$" }, float = true, size = "1300 860", center = true })
|
||||
|
||||
-- Blur the DMS surfaces (bar, launcher, popups, notifications).
|
||||
hl.layer_rule({ name = "dms-blur", match = { namespace = "^dms:.*" }, blur = true, blur_popups = true, ignore_alpha = 0.3 })
|
||||
hl.layer_rule({ name = "noanim-slurp", match = { namespace = "^selection$" }, no_anim = true })
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Searchable hotkey reference; type to filter, Esc to close.
|
||||
pkill -x fuzzel && exit 0
|
||||
fuzzel --dmenu --prompt 'hotkeys ❯ ' --width 70 --lines 24 \
|
||||
--font 'JetBrains Mono:size=13' \
|
||||
--background 191112f2 --text-color f0dee0ff --prompt-color e91e63ff \
|
||||
--selection-color 261d1eff --selection-text-color f0dee0ff \
|
||||
--border-color e91e63ff --border-width 2 --border-radius 16 \
|
||||
< "$HOME/.config/hypr/cheatsheet.txt" >/dev/null
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# Lock with DankMaterialShell (matches the shell theme). Fall back to swaylock-effects / swaylock
|
||||
# when DMS is not running or the IPC call fails.
|
||||
pgrep -x swaylock >/dev/null && exit 0
|
||||
if pgrep -x dms >/dev/null && timeout 3 dms ipc call lock lock >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
fx=$HOME/.local/bin/swaylock-effects
|
||||
if [ -x "$fx" ]; then
|
||||
exec "$fx" -f -C "$HOME/.config/swaylock/config-effects"
|
||||
fi
|
||||
exec swaylock -f
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# Select a screen region, OCR it (English + Russian) and put the text in the clipboard.
|
||||
if ! command -v tesseract >/dev/null; then
|
||||
notify-send -a ocr "OCR" "tesseract is not installed:\nsudo dnf install tesseract tesseract-langpack-eng tesseract-langpack-rus"
|
||||
exit 1
|
||||
fi
|
||||
geom=$(slurp -b 00000066 -c e91e63ff -w 2) || exit 0
|
||||
img() { grim -g "$geom" -; }
|
||||
if command -v magick >/dev/null; then img() { grim -g "$geom" - | magick png:- -resize 200% -colorspace Gray png:-; }; fi
|
||||
text=$(img | tesseract stdin stdout -l eng+rus --psm 6 2>/dev/null | sed 's/[[:space:]]*$//')
|
||||
[ -z "$text" ] && { notify-send -a ocr "OCR" "nothing recognised"; exit 1; }
|
||||
printf '%s' "$text" | wl-copy
|
||||
notify-send -a ocr "Copied $(printf '%s\n' "$text" | wc -l) line(s)" "$(printf '%s\n' "$text" | head -2)"
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Alt+Tab: jump to the previously focused window (press again to jump back). RAISE_DRY=1 only prints.
|
||||
target=$(hyprctl clients -j | jq -r '
|
||||
[.[] | select(.workspace.name | startswith("special") | not) | select(.focusHistoryID > 0)]
|
||||
| sort_by(.focusHistoryID) | .[0].address // empty')
|
||||
[ -z "$target" ] && exit 0
|
||||
[ -n "$RAISE_DRY" ] && { echo "focus: $target"; exit 0; }
|
||||
hyprctl dispatch "hl.dsp.focus({ window = \"address:$target\" })" >/dev/null
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# raise.sh <class-regex> <command...>
|
||||
# Focus the app if it already has a window (also switches workspace/monitor). Pressing again while
|
||||
# it is focused cycles through its other windows. If it is not running, launch <command>.
|
||||
# RAISE_DRY=1 only prints what would happen.
|
||||
re=$1; shift
|
||||
act=$(hyprctl activewindow -j 2>/dev/null | jq -r '.address // ""')
|
||||
target=$(hyprctl clients -j | jq -r --arg re "$re" --arg act "$act" '
|
||||
[.[] | select(.class | test($re)) | select(.workspace.name | startswith("special") | not)]
|
||||
| sort_by(.focusHistoryID) as $w
|
||||
| if ($w | length) == 0 then ""
|
||||
elif $w[0].address == $act and ($w | length) > 1 then $w[1].address
|
||||
else $w[0].address end')
|
||||
if [ -z "$target" ]; then
|
||||
[ -n "$RAISE_DRY" ] && { echo "launch: $*"; exit 0; }
|
||||
exec "$@"
|
||||
fi
|
||||
[ -n "$RAISE_DRY" ] && { echo "focus: $target"; exit 0; }
|
||||
hyprctl dispatch "hl.dsp.focus({ window = \"address:$target\" })" >/dev/null
|
||||
Executable
+214
@@ -0,0 +1,214 @@
|
||||
#!/bin/bash
|
||||
# Keyboard-driven SSH hub (fzf). Launched by Mod+S in a floating kitty (class ssh-menu).
|
||||
# Enter ssh Alt+T ssh+tmux Alt+F sftp Alt+Y copy addr Alt+K push key
|
||||
# Alt+N new host Alt+E edit Alt+G group Alt+R toggle PROD Tab mark Alt+A/Z all/none
|
||||
# Host metadata: "# group: X" / "# env: prod" comments above Host lines in ~/.ssh/config.
|
||||
S=$(readlink -f "$0")
|
||||
H="python3 -I $(dirname "$S")/ssh_hosts.py"
|
||||
state=${XDG_STATE_HOME:-$HOME/.local/state}/ssh-menu
|
||||
mkdir -p "$state"; touch "$state/history"
|
||||
|
||||
c_ok=$'\e[32m'; c_err=$'\e[31m'; c_dim=$'\e[2m'; c_hi=$'\e[38;2;233;30;99m'; c_off=$'\e[0m'
|
||||
say() { printf '%s\n' "$*"; }
|
||||
ask() { local v; read -e -r -p "$c_hi$1$c_off ${2:+[$2] }" v; printf '%s' "${v:-$2}"; }
|
||||
yes() { local v; read -r -p "$c_hi$1$c_off [Y/n] " v; [[ ! $v =~ ^[nNнН] ]]; }
|
||||
pause(){ read -r -s -n1 -p "${c_dim}press any key${c_off}"; echo; }
|
||||
fz() { fzf --layout=reverse --height=45% --border=rounded --prompt="$1" --pointer='▌' \
|
||||
--color='bg+:#261d1e,fg+:#ffffff,hl:#e91e63,hl+:#ff6398,pointer:#e91e63,prompt:#e91e63,border:#e91e63,info:#8e7a7d' "${@:2}"; }
|
||||
|
||||
# ---------- connect ----------
|
||||
run_ssh() { # run_ssh <target> [remote command...]
|
||||
local t=$1; shift
|
||||
echo "$t" >> "$state/history"
|
||||
# become a normal tiled window once we are a session, not a menu
|
||||
[ "$SSH_MENU" = 1 ] && hyprctl dispatch 'hl.dsp.window.float({ action = "disable" })' >/dev/null 2>&1
|
||||
local env; env=$($H env "$t")
|
||||
if [ "$env" = prod ]; then printf '\e]2;⚠ PROD ssh: %s\a\e]11;#220a0f\a' "$t"
|
||||
else printf '\e]2;ssh: %s\a' "$t"; fi
|
||||
ssh "$t" "$@"; local rc=$?
|
||||
printf '\e]111\a'
|
||||
if [ $rc -ne 0 ]; then
|
||||
printf '\n%sssh %s exited with %s%s (press Enter to close)' "$c_err" "$t" "$rc" "$c_off"; read -r _
|
||||
fi
|
||||
exit $rc
|
||||
}
|
||||
cmd_connect() { local t=${1:-$2}; [ -n "$t" ] && run_ssh "$t"; }
|
||||
cmd_tmux() { run_ssh "$1" -t 'command -v tmux >/dev/null && exec tmux new-session -A -s main || exec $SHELL -l'; }
|
||||
|
||||
# ---------- quick actions ----------
|
||||
cmd_sftp() {
|
||||
local a jump url
|
||||
read -r a jump < <($H addr "$1")
|
||||
# jump hosts: let gvfs go through ssh config by alias; otherwise explicit user@host:port
|
||||
if [ -n "$jump" ]; then url="sftp://$1/"; else url="sftp://$a/"; fi
|
||||
notify-send -a ssh-menu "SFTP" "opening $a …"
|
||||
# detached, so it survives the menu window closing
|
||||
setsid -f nautilus --new-window "$url" >/dev/null 2>&1
|
||||
}
|
||||
cmd_copy() { read -r a _ < <($H addr "$1"); printf %s "$a" | wl-copy; notify-send -a ssh-menu "ssh" "copied $a"; }
|
||||
|
||||
# ---------- keys ----------
|
||||
pick_key() {
|
||||
local pubs sel name
|
||||
mapfile -t pubs < <(ls "$HOME"/.ssh/*.pub 2>/dev/null)
|
||||
sel=$( { printf '%s\n' "${pubs[@]}"; echo "+ generate new ed25519 key"; } | grep -v '^$' | fz 'key ❯ ' --height=30%) || return 1
|
||||
if [[ $sel == "+ generate"* ]]; then
|
||||
name=$(ask "key file name" "id_ed25519_$(date +%y%m)")
|
||||
[ -e "$HOME/.ssh/$name" ] && { say "${c_err}$name exists${c_off}" >&2; return 1; }
|
||||
ssh-keygen -t ed25519 -f "$HOME/.ssh/$name" -C "$USER@$(hostname -s)" >&2 || return 1
|
||||
sel="$HOME/.ssh/$name.pub"
|
||||
fi
|
||||
printf '%s' "$sel"
|
||||
}
|
||||
cmd_keys() {
|
||||
[ $# -eq 0 ] && return
|
||||
local pub ok=() bad=() h
|
||||
pub=$(pick_key) || return
|
||||
say "${c_hi}key:${c_off} $pub ${c_hi}→${c_off} $*"; echo
|
||||
for h in "$@"; do
|
||||
say "${c_hi}── $h${c_off}"
|
||||
if ssh-copy-id -i "$pub" "$h"; then ok+=("$h"); else bad+=("$h"); fi
|
||||
done
|
||||
echo; [ ${#ok[@]} -gt 0 ] && say "${c_ok}✓ ${ok[*]}${c_off}"
|
||||
[ ${#bad[@]} -gt 0 ] && say "${c_err}✗ ${bad[*]}${c_off}"
|
||||
pause
|
||||
}
|
||||
|
||||
# ---------- new host / edit / group ----------
|
||||
cmd_new() {
|
||||
clear; say "${c_hi}── new SSH host ──${c_off} (Enter = default)"; echo
|
||||
local alias hostname user port jump group env key
|
||||
while :; do
|
||||
alias=$(ask "alias (name in the menu)"); [ -z "$alias" ] && return
|
||||
$H has "$alias" && { say "${c_err}already exists${c_off}"; continue; }
|
||||
[[ $alias =~ [[:space:]*?!] ]] && { say "${c_err}no spaces or wildcards${c_off}"; continue; }
|
||||
break
|
||||
done
|
||||
hostname=$(ask "HostName / IP" "$alias")
|
||||
user=$(ask "User" "root")
|
||||
port=$(ask "Port" "22")
|
||||
jump=$(ask "ProxyJump (empty = none)")
|
||||
group=$(ask "group (empty = none)")
|
||||
yes "mark as PROD (red window)?" && env=prod || env=
|
||||
key=$(ask "IdentityFile (empty = default)")
|
||||
echo; say "${c_dim}Host $alias → $user@$hostname:$port ${jump:+via $jump }${group:+[$group] }${env:+PROD}${c_off}"
|
||||
yes "save to ~/.ssh/config?" || return
|
||||
$H add "$alias" "$hostname" "$user" "$port" "$jump" "$group" "$env" "$key" || { pause; return; }
|
||||
say "${c_ok}saved${c_off} (backup in $state/backups)"
|
||||
yes "push your public key to it now?" && cmd_keys "$alias"
|
||||
echo "$alias" > "$state/lastnew"
|
||||
}
|
||||
cmd_edit() {
|
||||
local ed=micro; command -v micro >/dev/null || ed=${EDITOR:-nano}
|
||||
local l; l=$($H line "$1")
|
||||
if [ "$ed" = micro ]; then micro "+$l" ~/.ssh/config; else "$ed" "+$l" ~/.ssh/config; fi
|
||||
}
|
||||
cmd_group() {
|
||||
[ $# -eq 0 ] && return
|
||||
local out rc g h
|
||||
out=$($H groups | fz "group ❯ " --height=40% --print-query \
|
||||
--header "pick a group or type a new name (empty = clear)"); rc=$?
|
||||
[ $rc -gt 1 ] && return # Esc
|
||||
mapfile -t o <<< "$out" # line 1 = query, line 2 = selection (if any)
|
||||
g=${o[1]:-${o[0]}}
|
||||
for h in "$@"; do $H set "$h" group "$g"; done
|
||||
}
|
||||
cmd_envtoggle() { for h in "$@"; do $H toggle-prod "$h"; done; }
|
||||
|
||||
# ---------- several hosts ----------
|
||||
confirm_prod() { # confirm_prod host... ; asks (default: no) only when PROD hosts are involved
|
||||
local prod=() h v
|
||||
for h in "$@"; do [ "$($H env "$h")" = prod ] && prod+=("$h"); done
|
||||
[ ${#prod[@]} -eq 0 ] && return 0
|
||||
say "${c_err}${#prod[@]} of $# hosts are PROD:${c_off} ${prod[*]}"
|
||||
read -r -p "${c_hi}continue?${c_off} [y/N] " v
|
||||
[[ $v =~ ^[yYдД] ]]
|
||||
}
|
||||
cmd_broadcast() { # one tmux pane per host, keyboard input synced (Alt+S toggles)
|
||||
[ $# -eq 0 ] && return
|
||||
confirm_prod "$@" || return
|
||||
local conf; conf="$(dirname "$S")/tmux-broadcast.conf"
|
||||
local T=(tmux -L sshmenu -f "$conf") sess="bc-$(date +%H%M%S)" h first=1
|
||||
for h in "$@"; do
|
||||
echo "$h" >> "$state/history"
|
||||
if [ $first = 1 ]; then "${T[@]}" new-session -d -s "$sess" -x 220 -y 60 "ssh $(printf %q "$h")"; first=0
|
||||
else "${T[@]}" split-window -t "$sess" "ssh $(printf %q "$h")"; "${T[@]}" select-layout -t "$sess" tiled; fi
|
||||
"${T[@]}" select-pane -t "$sess" -T "$h"
|
||||
done
|
||||
"${T[@]}" setw -t "$sess" synchronize-panes on
|
||||
setsid -f kitty --title "broadcast: $# hosts" "${T[@]}" attach -t "$sess" >/dev/null 2>&1
|
||||
}
|
||||
cmd_run() { # run one command on all marked hosts, identical answers merged
|
||||
[ $# -eq 0 ] && return
|
||||
local cmd v
|
||||
set -o history; HISTFILE="$state/cmd_history"; history -r
|
||||
say "${c_dim}run on: $*${c_off}"
|
||||
read -e -r -p "${c_hi}\$${c_off} " cmd; [ -z "$cmd" ] && return
|
||||
history -s "$cmd"; history -w
|
||||
confirm_prod "$@" || return
|
||||
clear
|
||||
$H run "$cmd" "$@" | less -RFX
|
||||
pause
|
||||
}
|
||||
cmd_web() { # open the host's web UI; first time asks for the URL and stores it as "# url:"
|
||||
local url; url=$($H url "$1")
|
||||
if [ -z "$url" ]; then
|
||||
url=$(ask "web UI for $1" "$($H url-default "$1")"); [ -z "$url" ] && return
|
||||
$H set "$1" url "$url"
|
||||
fi
|
||||
setsid -f flatpak run com.google.Chrome "$url" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# ---------- dispatcher ----------
|
||||
case "$1" in
|
||||
connect) shift; cmd_connect "$@"; exit ;;
|
||||
tmux) shift; cmd_tmux "$@"; exit ;;
|
||||
sftp) shift; cmd_sftp "$@"; exit ;;
|
||||
copy) shift; cmd_copy "$@"; exit ;;
|
||||
keys) shift; cmd_keys "$@"; exit ;;
|
||||
new) cmd_new; exit ;;
|
||||
edit) shift; cmd_edit "$@"; exit ;;
|
||||
group) shift; cmd_group "$@"; exit ;;
|
||||
envtoggle) shift; cmd_envtoggle "$@"; exit ;;
|
||||
broadcast) shift; cmd_broadcast "$@"; exit ;;
|
||||
run) shift; cmd_run "$@"; exit ;;
|
||||
web) shift; cmd_web "$@"; exit ;;
|
||||
preview) shift; $H preview "$1"; exit ;;
|
||||
list) $H list; exit ;;
|
||||
live) $H live; exit ;;
|
||||
esac
|
||||
|
||||
# ---------- menu ----------
|
||||
# hotkey table: accent key + dim description, 4 columns
|
||||
key() { printf '\e[1;38;2;248;187;217m%-6s\e[0m\e[38;2;150;135;140m%-15s\e[0m' "$1" "$2"; }
|
||||
hdr=$(
|
||||
key Enter connect; key Alt+T tmux; key Alt+F sftp; key Alt+W web; echo
|
||||
key Alt+Y 'copy addr'; key Alt+N 'new host'; key Alt+E 'edit config'; key Alt+R 'toggle PROD'; echo
|
||||
key Tab mark; key Alt+A 'mark all'; key Alt+Z unmark; key Alt+G group; echo
|
||||
key Alt+B broadcast; key Alt+X 'run cmd'; key Alt+K 'push key'
|
||||
)
|
||||
|
||||
$H list | fzf --ansi --multi --layout=reverse --height=100% --tiebreak=index \
|
||||
--style=full:rounded --padding=1,2 --highlight-line \
|
||||
--prompt=' ' --pointer='▌' --gutter=' ' --marker='✓' --info=inline-right --no-scrollbar \
|
||||
--input-label=' ssh ' --list-label=' hosts ' --header-label=' keys ' --preview-label=' details ' \
|
||||
--ghost='type to filter, or user@host to connect to anything' \
|
||||
--header "$hdr" \
|
||||
--color='bg+:#2a2024,fg+:#ffffff,hl:#e91e63,hl+:#ff6398,pointer:#e91e63,prompt:#e91e63,info:#8e7a7d,marker:#f8bbd9,spinner:#e91e63,ghost:#6b5a5e,gutter:-1' \
|
||||
--color='border:#3a2a2e,list-border:#3a2a2e,header-border:#3a2a2e,preview-border:#3a2a2e,input-border:#e91e63' \
|
||||
--color='label:#e91e63,input-label:#e91e63,list-label:#8e7a7d,header-label:#8e7a7d,preview-label:#8e7a7d,header:-1' \
|
||||
--preview "$S preview {2}" --preview-window='right:32%:wrap' \
|
||||
--bind "start:reload($S live)" \
|
||||
--bind "enter:become($S connect {2} {q})" \
|
||||
--bind "alt-t:become($S tmux {2})" \
|
||||
--bind "alt-f:execute-silent($S sftp {2})+abort" \
|
||||
--bind "alt-y:execute-silent($S copy {2})+abort" \
|
||||
--bind "alt-k:execute($S keys {+2})+reload($S list)" \
|
||||
--bind "alt-n:execute($S new)+reload($S list)+transform-query(cat $state/lastnew 2>/dev/null; rm -f $state/lastnew)" \
|
||||
--bind "alt-e:execute($S edit {2})+reload($S live)" \
|
||||
--bind "alt-g:execute($S group {+2})+reload($S list)" \
|
||||
--bind "alt-r:execute-silent($S envtoggle {+2})+reload($S list)" \
|
||||
--bind "alt-b:execute($S broadcast {+2})+abort" \
|
||||
--bind "alt-x:execute($S run {+2})" \
|
||||
--bind "alt-w:execute($S web {2})+reload($S list)+abort" \
|
||||
--bind "alt-a:select-all,alt-z:deselect-all"
|
||||
Executable
+326
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Helper for ssh-menu.sh: parse and edit ~/.ssh/config.
|
||||
|
||||
Host metadata lives in comments directly above a `Host` line:
|
||||
# group: Proxmox
|
||||
# env: prod
|
||||
Host pm
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import zlib
|
||||
from collections import Counter
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
HOME = os.path.expanduser("~")
|
||||
CFG = os.path.join(HOME, ".ssh", "config")
|
||||
STATE = os.path.join(os.environ.get("XDG_STATE_HOME", HOME + "/.local/state"), "ssh-menu")
|
||||
HIST = os.path.join(STATE, "history")
|
||||
CACHE = os.path.join(STATE, "status.tsv")
|
||||
BACKUPS = os.path.join(STATE, "backups")
|
||||
os.makedirs(BACKUPS, exist_ok=True)
|
||||
|
||||
G, R, Y, D, B, X = "\x1b[32m", "\x1b[31m", "\x1b[33m", "\x1b[2m", "\x1b[1;31m", "\x1b[0m"
|
||||
|
||||
|
||||
def read_lines():
|
||||
try:
|
||||
return open(CFG).read().split("\n")
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
|
||||
def backup():
|
||||
if os.path.exists(CFG):
|
||||
shutil.copy2(CFG, os.path.join(BACKUPS, "config-" + time.strftime("%Y%m%d-%H%M%S")))
|
||||
old = sorted(os.listdir(BACKUPS))[:-20]
|
||||
for f in old:
|
||||
os.remove(os.path.join(BACKUPS, f))
|
||||
|
||||
|
||||
def write_lines(lines):
|
||||
backup()
|
||||
text = "\n".join(lines)
|
||||
if not text.endswith("\n"):
|
||||
text += "\n"
|
||||
with open(CFG, "w") as f:
|
||||
f.write(text)
|
||||
os.chmod(CFG, 0o600)
|
||||
|
||||
|
||||
def parse():
|
||||
hosts, meta = [], {}
|
||||
for i, l in enumerate(read_lines()):
|
||||
m = re.match(r"^\s*#\s*(group|env|url)\s*:\s*(.+?)\s*$", l, re.I)
|
||||
if m:
|
||||
meta[m.group(1).lower()] = m.group(2)
|
||||
continue
|
||||
m = re.match(r"^\s*host\s+(.+?)\s*$", l, re.I)
|
||||
if m:
|
||||
for a in m.group(1).split():
|
||||
if not re.search(r"[*?!]", a):
|
||||
hosts.append(dict(alias=a, group=meta.get("group", ""), env=meta.get("env", ""), url=meta.get("url", ""), line=i + 1))
|
||||
meta = {}
|
||||
elif l.strip() == "" or not l.lstrip().startswith("#"):
|
||||
meta = {}
|
||||
return hosts
|
||||
|
||||
|
||||
def find(alias):
|
||||
return next((h for h in parse() if h["alias"] == alias), None)
|
||||
|
||||
|
||||
def resolve(alias):
|
||||
"""ssh -G -> dict(user, hostname, port, proxyjump, identity)"""
|
||||
info = dict(user="", hostname=alias, port="22", proxyjump="", identity="")
|
||||
try:
|
||||
out = subprocess.run(["ssh", "-G", alias], capture_output=True, text=True, timeout=3).stdout
|
||||
except Exception:
|
||||
return info
|
||||
ids = []
|
||||
for l in out.splitlines():
|
||||
k, _, v = l.partition(" ")
|
||||
if k == "user": info["user"] = v
|
||||
elif k == "hostname": info["hostname"] = v
|
||||
elif k == "port": info["port"] = v
|
||||
elif k == "proxyjump": info["proxyjump"] = v
|
||||
elif k == "identityfile": ids.append(os.path.expanduser(v))
|
||||
info["identity"] = next((p for p in ids if os.path.exists(p)), "")
|
||||
return info
|
||||
|
||||
|
||||
def probe(info):
|
||||
if info["proxyjump"]:
|
||||
return "jump"
|
||||
try:
|
||||
socket.create_connection((info["hostname"], int(info["port"])), timeout=0.6).close()
|
||||
return "up"
|
||||
except Exception:
|
||||
return "down"
|
||||
|
||||
|
||||
def addr(info):
|
||||
return f'{info["user"]}@{info["hostname"]}:{info["port"]}'
|
||||
|
||||
|
||||
def load_cache():
|
||||
c = {}
|
||||
try:
|
||||
for l in open(CACHE):
|
||||
a, ad, st = l.rstrip("\n").split("\t")
|
||||
c[a] = (ad, st)
|
||||
except Exception:
|
||||
pass
|
||||
return c
|
||||
|
||||
|
||||
def uses():
|
||||
try:
|
||||
return Counter(open(HIST).read().split())
|
||||
except FileNotFoundError:
|
||||
return Counter()
|
||||
|
||||
|
||||
PALETTE = [(248, 187, 217), (125, 207, 255), (158, 206, 106), (224, 175, 104), (187, 154, 247), (115, 218, 202)]
|
||||
|
||||
|
||||
def rgb(r, g, b, bold=False):
|
||||
return f"\x1b[{'1;' if bold else ''}38;2;{r};{g};{b}m"
|
||||
|
||||
|
||||
FIXED = {"proxmox": (248, 187, 217), "services": (125, 207, 255), "git": (158, 206, 106), "legacy": (130, 120, 125)}
|
||||
|
||||
|
||||
def group_color(name):
|
||||
if name.lower() in FIXED:
|
||||
return FIXED[name.lower()]
|
||||
return PALETTE[zlib.crc32(name.lower().encode()) % len(PALETTE)]
|
||||
|
||||
|
||||
def group_cell(name, width):
|
||||
if not name:
|
||||
return " " * width
|
||||
return rgb(*group_color(name)) + f"{name:<{width}}" + X
|
||||
|
||||
|
||||
def addr_cell(ad):
|
||||
"""user@host:port with the host highlighted and the rest dimmed"""
|
||||
m = re.match(r"^(.*)@(.*):(\d+)$", ad or "")
|
||||
if not m:
|
||||
return D + (ad or "") + X
|
||||
u, h, p = m.groups()
|
||||
return f"{rgb(120, 108, 112)}{u}@{X}{rgb(205, 195, 200)}{h}{X}{rgb(120, 108, 112)}" + ("" if p == "22" else f":{p}") + X
|
||||
|
||||
|
||||
def cmd_list(live):
|
||||
hosts = parse()
|
||||
cnt = uses()
|
||||
hosts.sort(key=lambda h: (-cnt.get(h["alias"], 0), h["alias"].lower()))
|
||||
if live:
|
||||
def work(h):
|
||||
i = resolve(h["alias"])
|
||||
return h["alias"], addr(i), probe(i)
|
||||
with ThreadPoolExecutor(32) as ex:
|
||||
rows = list(ex.map(work, hosts))
|
||||
with open(CACHE, "w") as f:
|
||||
for a, ad, st in rows:
|
||||
f.write(f"{a}\t{ad}\t{st}\n")
|
||||
cache = {a: (ad, st) for a, ad, st in rows}
|
||||
else:
|
||||
cache = load_cache()
|
||||
dots = {"up": G + "●" + X, "down": "\x1b[1;31m●" + X, "jump": Y + "●" + X}
|
||||
for h in hosts:
|
||||
ad, st = cache.get(h["alias"], ("", "?"))
|
||||
dot = dots.get(st, D + "·" + X)
|
||||
prod = rgb(255, 92, 108, bold=True) + "PROD" + X if h["env"].lower() == "prod" else " "
|
||||
print(f'{dot} {h["alias"]:<24} {group_cell(h["group"], 11)} {prod} {addr_cell(ad)}')
|
||||
|
||||
|
||||
def cmd_preview(alias):
|
||||
lab = lambda t: rgb(120, 108, 112) + f"{t:<7}" + X
|
||||
val = lambda t: rgb(235, 225, 230) + str(t) + X
|
||||
h = find(alias)
|
||||
if not h:
|
||||
print(f"{alias}\n\n{D}not in ~/.ssh/config:\nwill connect as typed{X}")
|
||||
return
|
||||
i = resolve(alias)
|
||||
st = probe(i)
|
||||
col = {"up": (120, 235, 90), "down": (255, 92, 108), "jump": (224, 175, 104)}[st]
|
||||
prod = " " + rgb(255, 92, 108, bold=True) + "PROD" + X if h["env"].lower() == "prod" else ""
|
||||
print(rgb(*col, bold=True) + "● " + alias + X + prod)
|
||||
print(f'{D}{addr(i)}{X}\n')
|
||||
if i["proxyjump"]:
|
||||
print(lab("via") + val(i["proxyjump"]))
|
||||
print(lab("key") + val(os.path.basename(i["identity"]) or "default / agent"))
|
||||
if h["group"]:
|
||||
print(lab("group") + rgb(*group_color(h["group"])) + h["group"] + X)
|
||||
if h["url"]:
|
||||
print(lab("web") + val(h["url"]))
|
||||
print(lab("used") + val(f'{uses().get(alias, 0)}x'))
|
||||
if st != "jump":
|
||||
try:
|
||||
out = subprocess.run(["ping", "-c1", "-W1", i["hostname"]], capture_output=True, text=True, timeout=2).stdout
|
||||
m = re.search(r"time=([\d.]+)", out)
|
||||
print(lab("ping") + (val(m.group(1) + " ms") if m else rgb(255, 92, 108) + "no reply" + X))
|
||||
except Exception:
|
||||
print(lab("ping") + "-")
|
||||
print(f"\n{D}~/.ssh/config:{h['line']}{X}")
|
||||
for l in read_lines()[h["line"] - 1:h["line"] + 12]:
|
||||
if l.strip() == "" or (re.match(r"^\s*host\s", l, re.I) and not l.lower().lstrip().startswith("host " + alias.lower())):
|
||||
break
|
||||
print(D + l + X)
|
||||
|
||||
|
||||
def host_index(lines, alias):
|
||||
for i, l in enumerate(lines):
|
||||
m = re.match(r"^\s*host\s+(.+?)\s*$", l, re.I)
|
||||
if m and alias in m.group(1).split():
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def set_meta(alias, key, value):
|
||||
lines = read_lines()
|
||||
idx = host_index(lines, alias)
|
||||
if idx < 0:
|
||||
sys.exit(f"{alias}: not found")
|
||||
j = idx
|
||||
while j > 0 and lines[j - 1].lstrip().startswith("#"):
|
||||
j -= 1
|
||||
block = [l for l in lines[j:idx] if not re.match(rf"^\s*#\s*{key}\s*:", l, re.I)]
|
||||
if value:
|
||||
block.append(f"# {key}: {value}")
|
||||
lines[j:idx] = block
|
||||
write_lines(lines)
|
||||
|
||||
|
||||
def cmd_add(alias, hostname, user, port, jump, group, env, identity):
|
||||
if find(alias) or re.search(r"[\s*?!]", alias):
|
||||
sys.exit("bad or duplicate alias")
|
||||
lines = read_lines()
|
||||
while lines and lines[-1].strip() == "":
|
||||
lines.pop()
|
||||
lines.append("")
|
||||
if group: lines.append(f"# group: {group}")
|
||||
if env: lines.append(f"# env: {env}")
|
||||
lines.append(f"Host {alias}")
|
||||
lines.append(f" HostName {hostname or alias}")
|
||||
if user: lines.append(f" User {user}")
|
||||
if port and port != "22": lines.append(f" Port {port}")
|
||||
if jump: lines.append(f" ProxyJump {jump}")
|
||||
if identity:
|
||||
lines.append(f" IdentityFile {identity}")
|
||||
lines.append(" IdentitiesOnly yes")
|
||||
write_lines(lines)
|
||||
|
||||
|
||||
def cmd_run(cmd, hosts):
|
||||
"""Run cmd on all hosts in parallel; hosts with identical output are merged into one block."""
|
||||
def one(h):
|
||||
try:
|
||||
p = subprocess.run(["ssh", "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=6", h, cmd],
|
||||
stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=60)
|
||||
return h, p.returncode, (p.stdout + p.stderr).rstrip("\n")
|
||||
except subprocess.TimeoutExpired:
|
||||
return h, -1, "timeout"
|
||||
except Exception as e:
|
||||
return h, -1, str(e)
|
||||
with ThreadPoolExecutor(16) as ex:
|
||||
res = list(ex.map(one, hosts))
|
||||
groups, order = {}, []
|
||||
for h, rc, out in res:
|
||||
k = (rc, out)
|
||||
if k not in groups:
|
||||
groups[k] = []
|
||||
order.append(k)
|
||||
groups[k].append(h)
|
||||
print(f"{D}$ {cmd}{X}\n")
|
||||
for rc, out in order:
|
||||
hs = groups[(rc, out)]
|
||||
note = "" if rc == 0 else f" {D}{'ssh error' if rc in (-1, 255) else f'exit {rc}'}{X}"
|
||||
print(f'{(G if rc == 0 else R)}●{X} {rgb(235, 225, 230, bold=True)}{", ".join(hs)}{X}{note}')
|
||||
for l in out.splitlines():
|
||||
print(" " + l)
|
||||
print()
|
||||
|
||||
|
||||
def url_default(alias):
|
||||
i, h = resolve(alias), find(alias)
|
||||
port = ""
|
||||
if h and h["group"].lower() == "proxmox":
|
||||
port = ":8007" if alias == "pbs" else ":8006"
|
||||
return f'https://{i["hostname"]}{port}'
|
||||
|
||||
|
||||
def main():
|
||||
a = sys.argv[1:]
|
||||
c = a[0] if a else "list"
|
||||
if c == "list": cmd_list(False)
|
||||
elif c == "live": cmd_list(True)
|
||||
elif c == "preview": cmd_preview(a[1])
|
||||
elif c == "addr":
|
||||
i = resolve(a[1]); print(addr(i), "jump" if i["proxyjump"] else "")
|
||||
elif c == "env":
|
||||
h = find(a[1]); print(h["env"] if h else "")
|
||||
elif c == "line":
|
||||
h = find(a[1]); print(h["line"] if h else 1)
|
||||
elif c == "groups":
|
||||
print("\n".join(sorted({h["group"] for h in parse() if h["group"]})))
|
||||
elif c == "url":
|
||||
h = find(a[1]); print(h["url"] if h else "")
|
||||
elif c == "url-default": print(url_default(a[1]))
|
||||
elif c == "run": cmd_run(a[1], a[2:])
|
||||
elif c == "has": sys.exit(0 if find(a[1]) else 1)
|
||||
elif c == "set": set_meta(a[1], a[2], a[3] if len(a) > 3 else "")
|
||||
elif c == "toggle-prod":
|
||||
h = find(a[1])
|
||||
if h: set_meta(a[1], "env", "" if h["env"].lower() == "prod" else "prod")
|
||||
elif c == "add": cmd_add(*a[1:9])
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
# Isolated tmux server for the SSH menu's broadcast windows (tmux -L sshmenu); your ~/.tmux.conf is untouched.
|
||||
set -g status off
|
||||
set -g mouse on
|
||||
set -g escape-time 0
|
||||
set -g allow-set-title off
|
||||
set -g remain-on-exit failed
|
||||
set -g default-terminal "tmux-256color"
|
||||
set -ga terminal-overrides ",*:Tc"
|
||||
set -g pane-border-status top
|
||||
set -g pane-border-lines single
|
||||
set -g pane-border-style "fg=#3a2a2e"
|
||||
set -g pane-active-border-style "fg=#e91e63"
|
||||
set -g pane-border-format " #{pane_title}#{?pane_synchronized, #[fg=#e91e63]sync#[default],} "
|
||||
# Alt+S: typing goes to all panes / only this one. Alt+Z: zoom the current pane.
|
||||
bind -n M-s setw synchronize-panes
|
||||
bind -n M-z resize-pane -Z
|
||||
@@ -0,0 +1,128 @@
|
||||
# ---------- Font ----------
|
||||
font_family family="JetBrains Mono" style="Medium"
|
||||
bold_font family="JetBrains Mono" style="ExtraBold"
|
||||
italic_font family="JetBrains Mono" style="Medium Italic"
|
||||
bold_italic_font family="JetBrains Mono" style="ExtraBold Italic"
|
||||
font_size 13.5
|
||||
# Ligatures (-> => != ===) stay on; they only break when the cursor is on them
|
||||
disable_ligatures cursor
|
||||
adjust_line_height 106%
|
||||
# Nerd Font icons are built into kitty and picked up automatically
|
||||
|
||||
# ---------- Window / glass ----------
|
||||
background_opacity 0.80
|
||||
dynamic_background_opacity yes
|
||||
background_blur 0
|
||||
window_padding_width 12 14
|
||||
hide_window_decorations yes
|
||||
confirm_os_window_close 0
|
||||
remember_window_size no
|
||||
initial_window_width 110c
|
||||
initial_window_height 32c
|
||||
|
||||
# ---------- Cursor ----------
|
||||
cursor_shape beam
|
||||
cursor_beam_thickness 1.8
|
||||
cursor_blink_interval 0.6 ease-in-out
|
||||
cursor_trail 3
|
||||
cursor_trail_decay 0.1 0.35
|
||||
cursor_trail_start_threshold 2
|
||||
|
||||
# ---------- Scrollback ----------
|
||||
scrollback_lines 20000
|
||||
scrollback_pager_history_size 100
|
||||
mouse_hide_wait 2.0
|
||||
copy_on_select clipboard
|
||||
strip_trailing_spaces smart
|
||||
|
||||
# ---------- Tabs ----------
|
||||
tab_bar_edge top
|
||||
tab_bar_style powerline
|
||||
tab_powerline_style round
|
||||
tab_bar_min_tabs 2
|
||||
tab_title_template "{index}: {title[:24]}"
|
||||
active_tab_font_style bold
|
||||
tab_bar_margin_width 8
|
||||
tab_bar_margin_height 6 0
|
||||
|
||||
# ---------- Splits ----------
|
||||
enabled_layouts splits:split_axis=horizontal,stack,tall
|
||||
active_border_color #bb9af7
|
||||
inactive_border_color #3b4261
|
||||
inactive_text_alpha 0.75
|
||||
window_border_width 1pt
|
||||
|
||||
# ---------- URLs ----------
|
||||
url_color #7dcfff
|
||||
url_style curly
|
||||
detect_urls yes
|
||||
underline_hyperlinks hover
|
||||
|
||||
# ---------- Bell ----------
|
||||
enable_audio_bell no
|
||||
visual_bell_duration 0.15 ease-out
|
||||
window_alert_on_bell yes
|
||||
|
||||
# ---------- Colors: Tokyo Night Storm tuned for glass ----------
|
||||
foreground #c0caf5
|
||||
background #11111b
|
||||
selection_foreground #11111b
|
||||
selection_background #bb9af7
|
||||
cursor #ff9e64
|
||||
cursor_text_color #11111b
|
||||
url_color #7dcfff
|
||||
|
||||
active_tab_foreground #11111b
|
||||
active_tab_background #bb9af7
|
||||
inactive_tab_foreground #a9b1d6
|
||||
inactive_tab_background #1f2335
|
||||
tab_bar_background none
|
||||
|
||||
mark1_foreground #11111b
|
||||
mark1_background #ff9e64
|
||||
|
||||
color0 #414868
|
||||
color8 #565f89
|
||||
color1 #f7768e
|
||||
color9 #ff899d
|
||||
color2 #9ece6a
|
||||
color10 #b9f27c
|
||||
color3 #e0af68
|
||||
color11 #ffc777
|
||||
color4 #7aa2f7
|
||||
color12 #8db0ff
|
||||
color5 #bb9af7
|
||||
color13 #c9a9ff
|
||||
color6 #7dcfff
|
||||
color14 #a4daff
|
||||
color7 #a9b1d6
|
||||
color15 #c0caf5
|
||||
|
||||
# ---------- Shortcuts ----------
|
||||
# Plain Ctrl+C / Ctrl+V: Ctrl+C copies when text is selected, otherwise it is the usual interrupt (SIGINT)
|
||||
map ctrl+c copy_or_interrupt
|
||||
map ctrl+v paste_from_clipboard
|
||||
map ctrl+shift+t new_tab_with_cwd
|
||||
map ctrl+shift+enter launch --location=vsplit --cwd=current
|
||||
map ctrl+alt+enter launch --location=hsplit --cwd=current
|
||||
# Tabs: Ctrl+Shift+Left/Right (kitty default). Splits: Ctrl+Alt+arrows
|
||||
map ctrl+shift+left previous_tab
|
||||
map ctrl+shift+right next_tab
|
||||
map ctrl+alt+left neighboring_window left
|
||||
map ctrl+alt+right neighboring_window right
|
||||
map ctrl+alt+up neighboring_window up
|
||||
map ctrl+alt+down neighboring_window down
|
||||
map ctrl+shift+z toggle_layout stack
|
||||
map ctrl+shift+equal change_font_size current +1.0
|
||||
map ctrl+shift+minus change_font_size current -1.0
|
||||
map ctrl+shift+0 change_font_size current 0
|
||||
map ctrl+shift+f show_scrollback
|
||||
map ctrl+shift+o kitten hints --type path --program -
|
||||
map ctrl+shift+e kitten hints --type url
|
||||
map ctrl+shift+u kitten unicode_input
|
||||
map ctrl+shift+backspace set_background_opacity +0.05
|
||||
map ctrl+shift+delete set_background_opacity -0.05
|
||||
|
||||
|
||||
|
||||
term xterm-256color
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# Copy the live config (~/.config) into this repo. Run it, then `git add -A && git commit`.
|
||||
# Only what belongs to the rice is copied; machine-generated files (hypr/dms/, DMS colour caches) are skipped.
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
C=${XDG_CONFIG_HOME:-$HOME/.config}
|
||||
mkdir -p hypr dms/plugins systemd/user kitty
|
||||
rsync -a --delete --exclude 'dms/' --exclude 'hyprlock.conf' --exclude 'hyprpaper.conf' --exclude 'scripts/osd.sh' "$C/hypr/" hypr/
|
||||
rsync -a --delete "$C/dms/plugins/networkEditor/" dms/plugins/networkEditor/
|
||||
cp -f "$C/dms/settings.json" dms/settings.json
|
||||
cp -f "$C/systemd/user/hyprland-session.target" systemd/user/
|
||||
cp -f "$C/kitty/kitty.conf" kitty/kitty.conf
|
||||
git status --short
|
||||
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=Hyprland session
|
||||
Documentation=man:systemd.special(7)
|
||||
BindsTo=graphical-session.target
|
||||
Wants=graphical-session-pre.target
|
||||
After=graphical-session-pre.target
|
||||
Reference in New Issue
Block a user