Sync config from arch

- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
This commit is contained in:
asepharyana committed 2026-09-23 15:19:12 +07:00
commit 1cdb82a76f
300 files changed
+78143

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
{
"format": "omarchy-config",
"version": 1,
"synced_by": "gladimdim.config-sync"
}
+3
View File
@@ -0,0 +1,3 @@
-- App-specific window rules (dimuat setelah default Omarchy, jadi menang).
-- Obsidian: tiling penuh seperti browser, opacity penuh (tanpa efek kusam default).
o.window("md.obsidian.Obsidian", { tag = "-default-opacity", float = false, opacity = "1 1 override" })
+2
View File
@@ -0,0 +1,2 @@
-- Extra autostart processes.
-- o.launch_on_start("my-service")
+13
View File
@@ -0,0 +1,13 @@
-- config-sync cherry-pick
o.bind("SUPER + C", "Close window", hl.dsp.window.close())
o.bind("SUPER + E", "File manager", { omarchy = "nautilus" })
o.bind("SUPER + Q", "Terminal", { omarchy = "terminal" })
o.bind("SUPER + R", "Omarchy menu", "omarchy-menu toggle")
o.bind("SUPER + SHIFT + C", "Universal copy", universal_clipboard_shortcut("CTRL", "C", "CTRL", "Insert"))
o.bind("SUPER + SHIFT + V", "Universal paste", universal_clipboard_shortcut("CTRL", "V", "SHIFT", "Insert"))
o.bind("SUPER + SHIFT + X", "Universal cut", send_shortcut_once("CTRL", "X"))
hl.unbind("SUPER + SPACE")
hl.unbind("SUPER + V")
hl.unbind("SUPER + W")
hl.unbind("SUPER + X")
+3
View File
@@ -0,0 +1,3 @@
-- Omarchy VAAPI fix: force Intel iHD for video decode (hypr nvidia.lua sets LIBVA=nvidia which breaks Chromium YouTube on hybrid iGPU display)
hl.env("LIBVA_DRIVER_NAME", "iHD")
hl.env("__EGL_VENDOR_LIBRARY_FILENAMES", "/usr/share/glvnd/egl_vendor.d/50_mesa.json")
+35
View File
@@ -0,0 +1,35 @@
-- Learn how to configure Hyprland: https://wiki.hypr.land/Configuring/Start/
-- Omarchy's bootstrap keeps path setup out of this user config.
dofile((os.getenv("OMARCHY_PATH") or "/usr/share/omarchy") .. "/default/hypr/bootstrap.lua")
-- Disable all Omarchy default bindings. Add your own in hypr/bindings.lua.
-- omarchy_default_bindings = false
--
-- Or disable only bindings for Omarchy's preinstalled apps/web apps while
-- keeping core window-manager bindings:
-- omarchy_preinstalled_bindings = false
-- Load Omarchy defaults.
require("default.hypr.omarchy")
-- Put your personal overrides in these files. They're loaded after Omarchy's
-- defaults so package updates can improve the defaults without rewriting your
-- ~/.config/hypr files.
require("hypr.monitors")
require("hypr.input")
require("hypr.bindings")
require("hypr.looknfeel")
require("hypr.autostart")
-- Toggle config flags dynamically.
require("default.hypr.toggles")
-- VAAPI fix: overwrite nvidia env from default.hypr.nvidia
require("hypr.envs")
-- App-specific window rules (tiling/opacity per aplikasi).
require("hypr.apps")
-- Load settings written by OmaSettings (omasettings:managed).
require("hypr.omasettings")
+14
View File
@@ -0,0 +1,14 @@
# Makes hyprsunset do nothing to the screen by default
# Without this, the default applies some tint to the monitor
profile {
time = 07:00
identity = true
}
# To enable auto switch to nightlight, add to your .config/hypr/autostart.lua:
# o.launch_on_start("hyprsunset")
# and use the following:
# profile {
# time = 20:00
# temperature = 4000
# }
+57
View File
@@ -0,0 +1,57 @@
-- Keep only your personal input overrides here. Uncommented settings below
-- replace Omarchy's defaults.
-- Keyboard layout and options.
-- See https://wiki.hypr.land/Configuring/Basics/Variables/#input
-- hl.config({
-- input = {
-- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt.
-- kb_layout = "us,dk,eu",
-- kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle",
--
-- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards).
-- kb_variant = "intl",
--
-- -- Change speed of keyboard repeat.
-- repeat_rate = 40,
-- repeat_delay = 250,
--
-- -- Start with numlock on by default.
-- numlock_by_default = true,
--
-- -- Increase sensitivity for mouse/trackpad (default: 0).
-- sensitivity = 0.35,
--
-- -- Turn off mouse acceleration (default: adaptive).
-- accel_profile = "flat",
--
-- touchpad = {
-- -- Use natural (inverse) scrolling.
-- natural_scroll = true,
--
-- -- Use two-finger clicks for right-click instead of lower-right corner.
-- clickfinger_behavior = true,
--
-- -- Control the speed of your scrolling.
-- scroll_factor = 0.4,
--
-- -- Enable the touchpad while typing.
-- disable_while_typing = false,
--
-- -- Left-click-and-drag with three fingers.
-- drag_3fg = 1,
-- },
-- },
-- })
-- App-specific touchpad scroll speeds.
-- o.window("(Alacritty|kitty|foot)", { scroll_touchpad = 1.5 })
-- o.window("com.mitchellh.ghostty", { scroll_touchpad = 0.2 })
-- Enable touchpad gestures for changing workspaces.
-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Gestures/
-- hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
-- Enable touchpad gestures for moving focus (helpful on scrolling layout).
-- hl.gesture({ fingers = 3, direction = "left", action = function() hl.dispatch(hl.dsp.focus({ direction = "l" })) end })
-- hl.gesture({ fingers = 3, direction = "right", action = function() hl.dispatch(hl.dsp.focus({ direction = "r" })) end })
+50
View File
@@ -0,0 +1,50 @@
-- Change the default Omarchy look'n'feel.
-- https://wiki.hypr.land/Configuring/Basics/Variables/#general
-- hl.config({
-- general = {
-- -- No gaps between windows or borders.
-- gaps_in = 0,
-- gaps_out = 0,
-- border_size = 0,
--
-- -- Change to niri-like side-scrolling layout.
-- layout = "scrolling",
-- },
-- })
-- https://wiki.hypr.land/Configuring/Basics/Variables/#decoration
-- hl.config({
-- decoration = {
-- -- Use round window corners.
-- rounding = 8,
--
-- -- Dim unfocused windows (0.0 = no dim, 1.0 = fully dimmed).
-- dim_inactive = true,
-- dim_strength = 0.15,
-- },
-- })
-- https://wiki.hypr.land/Configuring/Basics/Variables/#animations
-- hl.config({
-- animations = {
-- -- Disable all animations.
-- enabled = false,
-- },
-- })
-- https://wiki.hypr.land/Configuring/Basics/Variables/#layout
-- hl.config({
-- layout = {
-- -- Avoid overly wide single-window layouts on wide screens.
-- single_window_aspect_ratio = { 1, 1 },
-- },
-- })
-- https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/
-- hl.config({
-- scrolling = {
-- -- See only one column per screen instead of two.
-- column_width = 0.97,
-- },
-- })
+4
View File
@@ -0,0 +1,4 @@
-- Generated by OmaSettings (omasettings:managed) — do not edit by hand.
-- Every value here was set from the OmaSettings window; delete a line
-- to hand that setting back to your own config.
+4
View File
@@ -0,0 +1,4 @@
screencopy {
allow_token_by_default = true
custom_picker_binary = hyprland-preview-share-picker
}
+26
View File
@@ -0,0 +1,26 @@
██████████████████████████████████████████████████████
██████████████████████████████████████████████████████
████ ████ ████
████ ████ ████
████ █████████████████████ ████████ ████
████ █████████████████████ ████████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████████████ ████ ████
████████████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ██████████████████████████████████████ ████
████ ██████████████████████████████████████ ████
████ ████ ████
████ ████ ████
█████████████████████████████ ████████████████████
█████████████████████████████ ████████████████████
+10
View File
@@ -0,0 +1,10 @@
▄▄▄
▄█████▄ ▄███████████▄ ▄███████ ▄███████ ▄███████ ▄█ █▄ ▄█ █▄
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▀ ███ ███ ███ ███
███ ███ ███ ███ ███ ▄███▄▄▄███ ▄███▄▄▄██▀ ███ ▄███▄▄▄███▄ ███▄▄▄███
███ ███ ███ ███ ███ ▀███▀▀▀███ ▀███▀▀▀▀ ███ ▀▀███▀▀▀███ ▀▀▀▀▀▀███
███ ███ ███ ███ ███ ███ ███ ██████████ ███ █▄ ███ ███ ▄██ ███
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
▀█████▀ ▀█ ███ █▀ ███ █▀ ███ ███ ███████▀ ███ █▀ ▀█████▀
███ █▀
+30
View File
@@ -0,0 +1,30 @@
{
// Extend the Quickshell Omarchy menu with JSONC.
//
// IDs are object keys. The parent is inferred from the dotted id, so
// "personal.notes" appears under "personal", and "personal" appears on the
// root menu. Reuse an existing id to override/extend it.
//
// Fields:
// icon Nerd Font glyph shown in the icon column.
// label Visible row title.
// action Shell command to run. If omitted, the row is a submenu.
// target Existing submenu id to open. Use for links/aliases.
// provider Runtime provider function/command returning JSON rows.
// aliases alternate `omarchy menu summon <name>` routes; also searchable.
// description Optional subtitle and extra search text.
// when Shell condition; hide row when it fails.
// checked Shell condition; append ✓ when it succeeds.
//
// Examples:
// "personal": {"icon":"","label":"Personal"},
// "personal.notes": {"icon":"󰎞","label":"Notes","action":"omarchy-launch-editor ~/notes"},
// "personal.files": {"icon":"","label":"Files","action":"uwsm-app -- nautilus ~/Documents"},
//
// Only use provider when a provider_name function or command named "name"
// returns JSON rows. Static submenus only need dotted ids.
//
// Example: replace the default About action by reusing the same id. Existing
// fields are kept unless overridden.
// "about": {"icon":"","label":"About","action":"omarchy-launch-or-focus-tui \"zsh -c 'fastfetch; read -k 1'\""},
}
@@ -0,0 +1,10 @@
#!/bin/bash
# This hook is called with the current battery percentage when the low battery
# notification is sent. To put it into use, remove .sample from this file name.
SOUND_FILE="/usr/share/sounds/freedesktop/stereo/dialog-warning.oga"
if omarchy-cmd-present mpv && [[ -f $SOUND_FILE ]]; then
mpv --no-video "$SOUND_FILE" >/dev/null 2>&1
fi
@@ -0,0 +1,7 @@
#!/bin/bash
# This hook is called with the snake-cased name of the font that has just been set.
# To put it into use, remove .sample from this file name.
# Example: Show the name of the font that was just set.
# omarchy-notification-send -u low "New font" "Your new font is $1"
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Show the weather status notification after Omarchy has started.
# To put it into use, remove .sample from this file name.
weather=$(omarchy-weather-status 2>/dev/null) || true
if [[ -n $weather && $weather != "Weather unavailable" ]]; then
omarchy-notification-send -u low "$weather"
fi
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
if omarchy-done ensure voxtype-install-invitation; then
omarchy-notification-send -u critical -g  "Install Dictation with Voxtype" \
"Click to install voice dictation for Omarchy." \
--exec omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install
fi
@@ -0,0 +1,11 @@
#!/bin/bash
set -e
# Omarchy ships no default agent, so invite once rather than picking one. An
# agent already chosen means the invitation has nothing to offer.
if [[ -z $(omarchy-default-agent) ]] && omarchy-done ensure agent-setup-invitation; then
omarchy-notification-send -u critical -g 󰚩 "Set your default agent" \
"Let your favorite agent help with Omarchy." \
--exec omarchy menu summon setup.default.agent
fi
@@ -0,0 +1,12 @@
#!/bin/bash
set -e
# Only invite when there's a reader to use and it isn't set up yet (the lock
# PAM file is the last thing the setup writes on success).
if omarchy-hw-fingerprint && [[ ! -f /etc/pam.d/omarchy-lock-fingerprint ]] &&
omarchy-done ensure fingerprint-setup-invitation; then
omarchy-notification-send -u critical -g 󰈷 "Setup Fingerprint Reader" \
"Enable sudo and unlocking with your fingerprint." \
--exec omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint
fi
@@ -0,0 +1,7 @@
#!/bin/bash
# This hook is called after an Omarchy system update has been performed.
# To put it into use, remove .sample from this file name.
# Example: Show notification after the system has been updated.
# omarchy-notification-send -u low "Update Performed" "Your system is now up to date"
@@ -0,0 +1,24 @@
#!/bin/bash
# This hook is called by `omarchy refresh pacman` AFTER the channel template
# is copied to /etc/pacman.conf and BEFORE `pacman -Syyuu` runs. Use it to
# layer customizations onto the freshly-written pacman.conf so they're
# respected by the upgrade — common cases are adding a custom repository
# (e.g. CachyOS, Chaotic-AUR, an internal company repo) or extra IgnorePkg
# lines.
#
# The hook runs as the invoking user with a warm sudo cache.
#
# To put it into use, remove .sample from this file name.
# Example: add an Include line above [core] for a custom repo.
# Maintain the repo entries in /etc/pacman.d/custom-repos.conf yourself.
CONF=/etc/pacman.conf
SNIPPET=/etc/pacman.d/custom-repos.conf
MARKER="Include = $SNIPPET"
[[ -r $SNIPPET ]] || exit 0
grep -qxF "$MARKER" "$CONF" && exit 0
sudo sed -i "0,/^\[core\]/s||$MARKER\n\n[core]|" "$CONF"
@@ -0,0 +1,7 @@
#!/bin/bash
# This hook is called with the snake-cased name of the theme that has just been set.
# To put it into use, remove .sample from this file name.
# Example: Show the name of the theme that was just set.
# omarchy-notification-send -u low "New theme" "Your new theme is $1"
+90
View File
@@ -0,0 +1,90 @@
{
"version": 1,
"idle": {
"screensaver": 150,
"lock": 300
},
"bar": {
"position": "top",
"transparent": false,
"centerAnchor": "omarchy.clock",
"layout": {
"left": [
{
"id": "omarchy.menu"
},
{
"id": "omarchy.workspaces"
}
],
"center": [
{
"id": "omarchy.indicators"
},
{
"id": "omarchy.clock",
"format": "dddd HH:mm",
"formatAlt": "d MMMM 'W'ww yyyy",
"verticalFormat": "HH\n—\nmm"
},
{
"id": "omarchy.keyboard-layout"
},
{
"id": "omarchy.weather"
},
{
"id": "omarchy.media"
},
{
"id": "omarchy.microphone"
},
{
"id": "omarchy.system-update"
}
],
"right": [
{
"id": "omarchy.tray"
},
{
"id": "io.github.x3me.nexthop"
},
{
"id": "io.github.elevate08.qs-bitwarden-cli"
},
{
"id": "gladimdim.config-sync"
},
{
"id": "omarchy.tailscale"
},
{
"id": "stappmus.activity-monitor"
},
{
"id": "omaplug"
},
{
"id": "omarchy.agents"
},
{
"id": "omarchy.bluetooth"
},
{
"id": "omarchy.network"
},
{
"id": "omarchy.audio"
},
{
"id": "omarchy.monitor"
},
{
"id": "omarchy.power"
}
]
}
},
"plugins": []
}
+2
View File
@@ -0,0 +1,2 @@
[font]
base-size = 9
+1
View File
@@ -0,0 +1 @@
azure-glow
+81
View File
@@ -0,0 +1,81 @@
# Azure Glow Theme
A deep, cyber-ocean aesthetic — shadowy bases lit by bright azure and electric teal highlights. Designed for people who want their desktop to feel like it’s running on pure bioluminescence.
![Azure Glow Theme Preview](./preview.png)
---
## Design Notes
- **True depth**: Midnight backgrounds with subtle gradients
- **Pops of light**: Vivid blues that feel alive in the dark
- **Readable at a glance**: High-contrast, glow-ready palette
- **One vision, everywhere**: Synced colors across terminal, bar, lock screen, and notifications
---
## Color DNA
### Core Shades
| Purpose | Hex | Name |
|-------------|-----------|------------------------|
| Background | `#0a0f14` | Abyss Black-Blue |
| Foreground | `#a9dfff` | Soft Ice Blue |
| Secondary | `#101820` | Deep Graphite |
### Accents
| Role | Hex | Description |
|----------|-----------|--------------------------|
| Primary | `#00bfff` | Neon Azure |
| Secondary| `#00e0ff` | Electric Cyan |
| Success | `#2affd5` | Aqua Mint |
| Warning | `#ffd43b` | Soft Amber |
| Error | `#ff4c4c` | Signal Red |
| Info | `#33d4ff` | Sky Glow |
---
## Terminal Palette
| Color | Normal | Bright |
|----------|-----------|-----------|
| Black | `#101820` | `#1c2833` |
| Red | `#ff4c4c` | `#ff6b6b` |
| Green | `#2affd5` | `#4dffd9` |
| Yellow | `#ffd43b` | `#ffdf70` |
| Blue | `#00bfff` | `#4dcfff` |
| Magenta | `#9a6bff` | `#b594ff` |
| Cyan | `#00e0ff` | `#4deeff` |
| White | `#a9dfff` | `#ffffff` |
---
## What’s Themed
- **Alacritty** — deep ocean backdrop with crisp, glowing colors
- **Hyprland** — azure → cyan gradient active borders
- **Waybar** — cool-tone indicators with subtle luminance
- **Mako** — glowing borders on notifications
- **Btop** — gradients that mimic underwater light rays
- **Neovim** — syntax colors matched to the Azure Glow palette
- **Hyprlock** — soft blue glow lock screen motif
---
## Install
To install this theme, use the `omarchy-theme-install` command:
```bash
omarchy-theme-install https://github.com/Hydradevx/omarchy-azure-glow-theme
```
---
## Wallpaper Ideas
- Minimalist dark blue gradients with glow streaks
- Underwater photography with bright blue plankton
- Futuristic cityscapes at night with cyan lighting
- Abstract lightwave renders
---
**Tip:** Works beautifully with transparent windows and background blur for a real “underwater cyberpunk” vibe.
+37
View File
@@ -0,0 +1,37 @@
[colors.primary]
background = "#0a0f1a" # Deep ocean black-blue
foreground = "#a8dfff" # Icy azure glow
[colors.cursor]
cursor = "#00ccff" # Electric cyan beam
text = "#0a0f1a" # Dark contrast
[colors.normal]
black = "#0d1b26" # Abyssal navy
red = "#0099cc" # Bright cerulean
green = "#00e0b8" # Tropical aqua
yellow = "#33ccff" # Sky flash
blue = "#00aaff" # Bold azure
magenta = "#3399ff" # Deep sky fusion
cyan = "#66e0ff" # Frosted cyan
white = "#cceeff" # Snow drift
[colors.bright]
black = "#123247" # Steel blue shadow
red = "#33ccff" # Neon sky
green = "#33ffdd" # Glacial mint
yellow = "#66ddff" # Arc light
blue = "#33bbff" # Vibrant azure
magenta = "#66b2ff" # Cloud-washed blue
cyan = "#99eeff" # Crystal haze
white = "#ffffff" # Pure white glow
[font]
normal.family = "JetBrainsMono Nerd Font"
size = 11.0
[window]
padding.x = 10
padding.y = 10
opacity = 0.95
+60
View File
@@ -0,0 +1,60 @@
# Azure Glow Theme for Btop
# Electric blues and radiant cyan with deep abyss backgrounds
theme[main_bg]="#0a0a14"
theme[main_fg]="#b4dcff"
theme[title]="#00aaff"
theme[hi_fg]="#d0f0ff"
theme[selected_bg]="#00334d"
theme[selected_fg]="#00ccff"
theme[inactive_fg]="#00334d"
theme[proc_misc]="#b4dcff"
theme[cpu_box]="#00aaff"
theme[mem_box]="#00e5ff"
theme[net_box]="#00ccff"
theme[proc_box]="#0099ff"
theme[div_line]="#00334d"
theme[graph_text]="#66ccff"
# CPU gradients - from deep azure (low) to sky cyan to electric blue (high)
theme[cpu_start]="#0055ff"
theme[cpu_mid]="#00ccff"
theme[cpu_end]="#00f0ff"
# Memory gradients - from icy blue to bright cyan
theme[mem_start]="#66d9ff"
theme[mem_mid]="#00bfff"
theme[mem_end]="#0099cc"
# Network gradients - from bright cyan to aqua
theme[download_start]="#00e5ff"
theme[download_mid]="#00ccff"
theme[download_end]="#0099ff"
theme[upload_start]="#00ccff"
theme[upload_mid]="#00b3ff"
theme[upload_end]="#66ccff"
# Process gradients - vibrant cyan to aqua blue
theme[process_start]="#00e5ff"
theme[process_mid]="#00ccff"
theme[process_end]="#0099ff"
# Temperature gradients - from cool blue to brighter cyan
theme[temp_start]="#66d9ff"
theme[temp_mid]="#00ccff"
theme[temp_end]="#0099ff"
# Battery gradient - cool cyan tones
theme[battery_start]="#00e5ff"
theme[battery_mid]="#00ccff"
theme[battery_end]="#0099ff"
theme[clock]="#d0f0ff"
theme[input_text]="#ffffff"
theme[error_text]="#00ffff"
+4
View File
@@ -0,0 +1,4 @@
general {
col.active_border = rgb(00ccff) rgb(0055ff) 45deg
col.inactive_border = rgb(00334d)
}
+109
View File
@@ -0,0 +1,109 @@
$color = rgba(10,10,20,0.9) # Deep abyss blue
$inner_color = rgba(15,25,45,0.8) # Midnight navy
$outer_color = rgba(0,170,255,1.0) # Neon azure
$font_color = rgba(180,220,255,1.0) # Frosted cyan
$check_color = rgba(0,255,255,1.0) # Electric aqua
$fail_color = rgba(0,120,255,1.0) # Alert blue
$input_color = rgba(0,50,80,0.5) # Deep sea shadow
general {
grace = 0
hide_cursor = true
}
background {
monitor =
path = ~/.config/omarchy/themes/azure-glow/backgrounds/background.png
blur_size = 5
blur_passes = 2
noise = 0.02
brightness = 0.85
}
label {
monitor =
text = $TIME
color = $font_color
font_size = 64
font_family = JetBrainsMono Nerd Font Bold
position = 0, 100
halign = center
valign = center
}
label {
monitor =
text = ~~~ ~~~ ~~~
color = $font_color
font_size = 24
font_family = JetBrainsMono Nerd Font Mono
position = 0, 360
halign = center
valign = center
}
label {
monitor =
text = ~~ ~~ ~~ ~~ ~~ ~~
color = $font_color
font_size = 24
font_family = JetBrainsMono Nerd Font Mono
position = 0, 355
halign = center
valign = center
}
label {
monitor =
text = ~~ ~~ ~~ ~~ ~~ ~~
color = $font_color
font_size = 24
font_family = JetBrainsMono Nerd Font Mono
position = 0, 350
halign = center
valign = center
}
label {
monitor =
text = ~~ ~~ ~~ ~~
color = $font_color
font_size = 24
font_family = JetBrainsMono Nerd Font Mono
position = 0, 345
halign = center
valign = center
}
label {
monitor =
text = ~~ ~~ ~~ ~~ ~~ ~~
color = $font_color
font_size = 24
font_family = JetBrainsMono Nerd Font Mono
position = 0, 340
halign = center
valign = center
}
label {
monitor =
text = ~~ ~~ ~~ ~~ ~~ ~~
color = $font_color
font_size = 24
font_family = JetBrainsMono Nerd Font Mono
position = 0, 335
halign = center
valign = center
}
label {
monitor =
text = ~~~ ~~~ ~~~
color = $font_color
font_size = 24
font_family = JetBrainsMono Nerd Font Mono
position = 0, 330
halign = center
valign = center
}
+1
View File
@@ -0,0 +1 @@
Yaru-blue
+30
View File
@@ -0,0 +1,30 @@
text-color=#B3E5FC
border-color=#40C4FF
background-color=#0A0E14ee
progress-color=#00B0FF
font=Liberation Sans 11
width=420
height=110
padding=10
border-size=2
border-radius=5
anchor=top-right
default-timeout=5000
[urgency=low]
border-color=#80D8FF
[urgency=normal]
border-color=#40C4FF
[urgency=high]
border-color=#00B0FF
background-color=#10293Fee
text-color=#E1F5FE
[app-name=Spotify]
invisible=1
[mode=do-not-disturb]
invisible=1
+80
View File
@@ -0,0 +1,80 @@
-- Azure Glow NVIM Config
return {
{
"LazyVim/LazyVim",
opts = {
colorscheme = function()
-- Azure Glow Colorscheme
local colors = {
bg = "#0A0E14", -- Deep midnight
fg = "#E6F0FF", -- Soft icy white
primary = "#64B5F6", -- Vivid sky blue
secondary = "#00CFFF", -- Electric cyan
success = "#4DD0E1", -- Bright aqua
danger = "#2979FF", -- Intense azure
warning = "#81D4FA", -- Soft light blue
info = "#00E5FF", -- Neon cyan
light = "#B3E5FC", -- Pale baby blue
dark = "#041019", -- Deeper midnight
muted = "#355A66", -- Stormy teal
}
vim.cmd("highlight clear")
vim.cmd("set termguicolors")
vim.api.nvim_set_hl(0, "Normal", { fg = colors.fg, bg = colors.bg })
vim.api.nvim_set_hl(0, "Comment", { fg = colors.muted, italic = true })
vim.api.nvim_set_hl(0, "Constant", { fg = colors.secondary })
vim.api.nvim_set_hl(0, "String", { fg = colors.success })
vim.api.nvim_set_hl(0, "Character", { fg = colors.success })
vim.api.nvim_set_hl(0, "Number", { fg = colors.warning })
vim.api.nvim_set_hl(0, "Boolean", { fg = colors.primary, bold = true })
vim.api.nvim_set_hl(0, "Float", { fg = colors.warning })
vim.api.nvim_set_hl(0, "Identifier", { fg = colors.info })
vim.api.nvim_set_hl(0, "Function", { fg = colors.primary, bold = true })
vim.api.nvim_set_hl(0, "Statement", { fg = colors.danger, bold = true })
vim.api.nvim_set_hl(0, "Conditional", { fg = colors.danger })
vim.api.nvim_set_hl(0, "Repeat", { fg = colors.secondary })
vim.api.nvim_set_hl(0, "Label", { fg = colors.secondary })
vim.api.nvim_set_hl(0, "Operator", { fg = colors.fg })
vim.api.nvim_set_hl(0, "Keyword", { fg = colors.primary, bold = true })
vim.api.nvim_set_hl(0, "Exception", { fg = colors.danger })
vim.api.nvim_set_hl(0, "PreProc", { fg = colors.secondary })
vim.api.nvim_set_hl(0, "Include", { fg = colors.primary })
vim.api.nvim_set_hl(0, "Define", { fg = colors.primary })
vim.api.nvim_set_hl(0, "Macro", { fg = colors.warning })
vim.api.nvim_set_hl(0, "PreCondit", { fg = colors.secondary })
vim.api.nvim_set_hl(0, "Type", { fg = colors.info })
vim.api.nvim_set_hl(0, "StorageClass", { fg = colors.info })
vim.api.nvim_set_hl(0, "Structure", { fg = colors.secondary })
vim.api.nvim_set_hl(0, "Typedef", { fg = colors.secondary })
vim.api.nvim_set_hl(0, "Special", { fg = colors.warning })
vim.api.nvim_set_hl(0, "SpecialChar", { fg = colors.warning })
vim.api.nvim_set_hl(0, "Tag", { fg = colors.info })
vim.api.nvim_set_hl(0, "Delimiter", { fg = colors.fg })
vim.api.nvim_set_hl(0, "SpecialComment",{ fg = colors.muted })
vim.api.nvim_set_hl(0, "Debug", { fg = colors.danger })
-- UI Elements
vim.api.nvim_set_hl(0, "CursorLine", { bg = "#0F141C" })
vim.api.nvim_set_hl(0, "CursorLineNr", { fg = colors.primary, bold = true })
vim.api.nvim_set_hl(0, "LineNr", { fg = colors.muted })
vim.api.nvim_set_hl(0, "Visual", { bg = "#123C56" })
vim.api.nvim_set_hl(0, "Search", { fg = colors.bg, bg = colors.primary })
vim.api.nvim_set_hl(0, "IncSearch", { fg = colors.bg, bg = colors.secondary })
vim.api.nvim_set_hl(0, "Pmenu", { fg = colors.fg, bg = "#0E1620" })
vim.api.nvim_set_hl(0, "PmenuSel", { fg = colors.bg, bg = colors.primary })
vim.api.nvim_set_hl(0, "StatusLine", { fg = colors.fg, bg = "#0E1620" })
vim.api.nvim_set_hl(0, "StatusLineNC", { fg = colors.muted, bg = "#0E1620" })
vim.api.nvim_set_hl(0, "VertSplit", { fg = colors.dark })
vim.api.nvim_set_hl(0, "Title", { fg = colors.primary, bold = true })
vim.api.nvim_set_hl(0, "ErrorMsg", { fg = colors.bg, bg = colors.danger, bold = true })
vim.api.nvim_set_hl(0, "WarningMsg", { fg = colors.bg, bg = colors.warning })
vim.api.nvim_set_hl(0, "MoreMsg", { fg = colors.success })
vim.api.nvim_set_hl(0, "ModeMsg", { fg = colors.primary, bold = true })
end,
},
},
}
+45
View File
@@ -0,0 +1,45 @@
@define-color background-color #0A0E14ee; /* Deep midnight blue with transparency */
@define-color border-color #40C4FF; /* Bright sky blue */
@define-color label #B3E5FC; /* Soft glowing cyan */
@define-color image #B3E5FC; /* Soft glowing cyan */
@define-color progress #00B0FF; /* Rich azure */
window {
background: @background-color;
border: 2px solid @border-color;
border-radius: 10px;
}
#container {
padding: 15px;
}
#image {
color: @image;
font-size: 48px;
}
#label {
color: @label;
font-family: "JetBrainsMono Nerd Font";
font-size: 14px;
font-weight: bold;
margin: 10px 0;
}
progressbar {
background: rgba(27, 42, 58, 0.5);
border-radius: 5px;
min-height: 10px;
}
progressbar > trough {
background: rgba(27, 42, 58, 0.5);
border-radius: 5px;
}
progressbar > trough > progress {
background: linear-gradient(90deg, @progress, @border-color);
border-radius: 5px;
box-shadow: 0 0 10px rgba(64, 196, 255, 0.3);
}
+72
View File
@@ -0,0 +1,72 @@
@define-color selected-text #40C4FF; /* Bright sky blue */
@define-color text #B3E5FC; /* Soft glowing cyan */
@define-color base #0A0E14; /* Deep midnight blue */
@define-color surface #081018; /* Abyssal navy */
@define-color overlay #1B2A3A; /* Dim steel blue */
@define-color muted #4DD0E1; /* Lively aqua */
@define-color subtle #00B0FF; /* Rich azure */
@define-color border #40C4FF; /* Bright sky blue */
* {
font-family: "JetBrainsMono Nerd Font";
}
#window {
background: rgba(10, 14, 20, 0.95);
border: 2px solid @border;
border-radius: 10px;
}
#box {
padding: 20px;
}
#search {
background: @surface;
border: 2px solid @border;
border-radius: 5px;
padding: 10px;
color: @text;
font-size: 14px;
}
#search:focus {
border-color: @selected-text;
box-shadow: 0 0 10px rgba(64, 196, 255, 0.5);
}
#list {
margin-top: 10px;
}
#entry {
padding: 10px;
margin: 5px 0;
background: @surface;
border-radius: 5px;
color: @text;
}
#entry:hover {
background: @overlay;
border-left: 3px solid @subtle;
}
#entry:selected {
background: linear-gradient(90deg, rgba(64, 196, 255, 0.2), rgba(0, 176, 255, 0.2));
border-left: 3px solid @selected-text;
color: @selected-text;
}
#entry #icon {
margin-right: 10px;
}
#entry #name {
font-weight: bold;
}
#entry #description {
color: @muted;
font-size: 12px;
}
+93
View File
@@ -0,0 +1,93 @@
@define-color foreground #B3E5FC; /* Soft glowing cyan */
@define-color background #0A0E14; /* Deep midnight blue */
@define-color primary #40C4FF; /* Bright sky blue */
@define-color secondary #00B0FF; /* Rich azure */
@define-color success #00E5FF; /* Electric cyan */
@define-color danger #FF5252; /* Alert red */
@define-color warning #FFD740; /* Vibrant yellow */
@define-color info #82B1FF; /* Cool periwinkle */
@define-color light #E6F0FF; /* Frost white */
@define-color dark #081018; /* Abyssal navy */
@define-color muted #1B2A3A; /* Dim steel blue */
* {
font-family: "JetBrainsMono Nerd Font", monospace;
font-size: 13px;
min-height: 0;
}
window#waybar {
background: rgba(10, 14, 20, 0.9);
color: @foreground;
border-bottom: 2px solid @primary;
}
#workspaces button {
padding: 0 5px;
background: transparent;
color: @muted;
border-bottom: 3px solid transparent;
}
#workspaces button.active {
background: rgba(64, 196, 255, 0.1);
color: @primary;
border-bottom: 3px solid @primary;
}
#workspaces button:hover {
background: rgba(0, 176, 255, 0.2);
color: @secondary;
}
#cpu, #memory, #temperature, #network, #pulseaudio, #battery, #clock {
padding: 0 10px;
color: @foreground;
}
#cpu {
color: @secondary;
}
#memory {
color: @warning;
}
#temperature {
color: @danger;
}
#temperature.critical {
background: @danger;
color: @background;
}
#network {
color: @info;
}
#pulseaudio {
color: @primary;
}
#battery {
color: @success;
}
#battery.critical:not(.charging) {
background: @danger;
color: @background;
animation: blink 0.5s linear infinite alternate;
}
#clock {
color: @light;
font-weight: bold;
}
@keyframes blink {
to {
background-color: @background;
color: @danger;
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,162 @@
# Changelog
## [1.8.1] - 2026-09-05
### Added
- **Colorized menu-bar icon.** The primary Bitwarden shield can follow the
active Omarchy theme accent, while locked, setup and urgent indicators keep
their status colors.
### Fixed
- **The panel-open underline stays centered under the shield.** The custom
glyph now preserves fractional positioning at non-integer display scales,
while the indicator keeps Omarchy's standard width.
- **The shield no longer renders with coloured edges.** The glyph was drawn
through a different text renderer than the rest of the bar, which left
saturated blue and gold along its edges -- visible against every theme, and
on no other icon. It now uses the renderer Omarchy uses everywhere else. The
centering above is unaffected: both renderers place the painted center on the
same pixel at fractional scales.
## [1.7.1] - 2026-09-04
### Fixed
- **A button could be laid out past the edge of the panel and vanish.** Opening an item while the panel recognised the active window added a fourth button -- **Suggested here** -- to the detail header, and that header was a `Row`: a positioner that can neither shrink a control nor start a second line, so the fourth pushed **Delete** off the panel entirely. It went only once the suggestion was pinned, because "Suggested here" is one character wider than "Suggest here", and that character was theLine truncated
- **The folder, organization and type filters had the same fault and worse odds.** Their labels carry vault names of no fixed length, and the row is centred, so a long folder or organization name spilled off both edges at once -- and it did not take an unusual name: *Unfiled / Personal / Favorites* was already over. The row wraps too, and stays centred while the three fit a line. A name past twenty characters is clipped, with the whole of it still in the tooltip: a wrapping row can move a buttoLine truncated
- The SSH agent's client-routing buttons in Settings could overflow the same way, if the four that share that row were ever shown together.
- **Seven spacers meant to push a control to the right-hand edge were doing nothing at all.** `Item { Layout.fillWidth: true }` is a QtQuick.Layouts instruction, and these sat inside plain `Row`s, which ignore it -- so each laid out at zero width and the control after it stopped short. The countdown beside **VERIFICATION CODE (TOTP)** sat against the heading instead of at the margin, and so did the controls beside **ATTACHMENTS**, **NOTES** and **PASSWORD**. Those rows are `RowLayout`s now, whiLine truncated
## [1.7.0] - 2026-09-03
### Added
- **Centered SSH approval popup** (`sshAgentApprovalPopup`, on by default). Shows SSH unlock and signing requests in a transient card centered on the active screen instead of opening the full Bitwarden panel. Users who prefer prompts in the anchored panel can disable the popup in Settings or config. If the vault is locked, the card presents configured unlock options (PIN, fingerprint, or master password) before transitioning to the signing approval once unlocked. Escape or clicking outside the Line truncated
- **Concurrent SSH request queueing**. Multiple simultaneous SSH requests are sequentially queued in order (up to 4 deep, matching helper capacity) instead of dropping or overwriting in-flight prompts. The approval screen surfaces a `1 of N` queue counter, advances to the next request upon approval or denial, offers a `Deny all (N)` action (`Shift+Escape` in the popup), and automatically purges requests when clients cancel or time out.
- **Card and identity items are fully supported.** Both were already listed and filterable, but opening one showed its name and nothing else -- the fields were parsed and then discarded. A card now shows its cardholder, brand, number, expiry and security code, with the number and code masked until revealed, each independently of the other; an identity shows its name, username, company, email, phone, its SSN, passport and licence numbers, and its address as one copyable block. Empty fields are nLine truncated
- Cards and identities are searchable by whatever the list shows them as: a card by brand, cardholder or last four digits, an identity by name, email, username or company. Deliberately not by the middle of a card number.
- Detail shortcuts cover the new types. `y` copies what an item is for -- the password on a login, the number on a card -- while `n` and `k` reach a card's number and security code directly, and `u` and `c` split into username and email on an identity.
### Changed
- **The settings screen is organised into sections.** It had grown into one undifferentiated scroll of fourteen settings, the SSH agent status block and a row of action buttons. The settings are now grouped under **General**, **Security** and **SSH Agent** headings, with Behavior and Suggestions -- one and two rows each -- merged into General, and the SSH agent's status and client-routing block moved inside the section it describes rather than trailing every group.
- **The section you are reading is named above the scroll area, and stays there as you scroll.** Its own heading in the list yields to it, so nothing is drawn twice, and it clears once you scroll past the last section into the maintenance rows. **Back (Esc)** is pinned alongside it on the right, instead of scrolling out of reach with everything else.
- Scrollbars have a lane of their own throughout the panel rather than floating over the right edge of the content. They were overlays drawn on top of whatever was under them -- toggles and number fields on the settings screen, copy buttons on an item, the ends of elided names in the vault list. Every scrolling view now reserves the same width, so nothing is covered and the right-hand edges line up from screen to screen.
- **Destructive actions have their own section.** **Remove Plugin Data** sat in a row visually identical to **Dependencies**, so the button that clears your keyring entries looked exactly as safe to press as the one that opens a checklist. The action buttons are now split under **MAINTENANCE** and a separated **DANGER ZONE**.
### Fixed
- **Deleting an item no longer holds the panel either.** It cost the same second or two of `bw`, spent on a frozen detail screen, and then re-read the whole vault to learn about the one row that had gone. The row goes immediately and the panel comes back; if the vault refuses, the row returns with the reason.
- **Enter saves the item form**, from any field, so a long item does not have to be scrolled to the bottom to be committed. Not while a folder, organization or collection picker is open, where Enter belongs to the list being picked from.
- **The panel scrolls at its own rate.** Qt moves a view by the platform's wheel-scroll-lines, which suits a full-screen document and crawls in a panel a few hundred pixels tall. Every scrolling view in the panel now moves about twice as far per notch, and all of them at the same rate.
- **Saving no longer holds the panel.** A save costs whatever `bw` costs -- a second or two of CLI startup, vault decryption and a round trip, none of which this plugin can shorten -- and it used to spend all of it on a frozen form. The form now closes as soon as the command is launched and the list shows the item as it will be, its icon replaced by a spinner until the vault answers, at which point the authoritative version takes its place. An item still being saved cannot be edited or deleted,Line truncated
- **Saving an item no longer re-reads the whole vault.** A save was followed by re-listing and re-decrypting every item in order to learn about the one just written. `bw create item` and `bw edit item` both print the item the vault now holds, so the list is brought up to date from that instead. The saved response is a complete decrypted cipher, so it passes through the same strict-JSON and allowlisting stages the item list does before anything reaches the panel; if either stage fails, or the enLine truncated
- **Saving an item is about 2.7 seconds faster.** Every save, folder creation and Send piped its payload through `bw encode`, which base64-encodes stdin and does nothing else -- no vault, no session, no network -- for the price of a full Bitwarden CLI startup. `base64` from coreutils produces byte-identical output in about two milliseconds. The payload still travels in the environment and is still piped rather than interpolated, so nothing about where a password lives has changed.
- Enter on a list row now opens items that have nothing to copy. It still copies the password on a login, and still arms the TOTP follow-up; but on a card, an identity, a note, an SSH key, or a login saved without a password it opens the item instead of doing nothing at all. Enter on the detail screen copies the primary secret -- the password on a login, the number on a card -- which the "Copy password (y / Enter)" tooltip had been promising all along without anything implementing it.
- Transient status and error messages now float at the bottom of the panel instead of changing its measured height, so updates such as a successful unlock no longer shift the active screen down and back up. Errors use the same compact notice surface and can be dismissed in place.
## [1.6.0] - 2026-08-31
### Added
- **Server region** on the login screen: **US** (the default), **EU**, or **Custom**. EU points the CLI at `https://vault.bitwarden.eu`; Custom reveals the server URL field for self-hosted Bitwarden and Vaultwarden. The choice applies to email/password, API key and the interactive terminal login alike, so an EU account no longer has to be told its own server's address. Refs #6.
### Changed
- Dependabot proposes lockfile-only cargo updates, so a bump moves `agent/Cargo.lock` within the bounds `agent/Cargo.toml` already allows and never raises a floor on its own. The crypto crates are coupled -- `ssh-key`, `rsa` and the traits they re-export have to move together or cargo resolves two generations side by side and nothing compiles -- and README's **Dependencies** section records why that upgrade is a manual, all-at-once edit, along with the binary rebuild every accepted bump needs.
## [1.5.0] - 2026-08-31
Opt-in SSH agent. Implements #1.
### Added
- **SSH agent** (`sshAgentEnabled`, off by default). Serves the SSH keys in your vault to `ssh`, Git and `ssh-keygen -Y sign` while the vault is unlocked. Ed25519 and RSA SHA-2, over a socket in `$XDG_RUNTIME_DIR` that only your own UID may use. Private keys live in a separate helper process, never on disk and never in QML, and are dropped on lock, logout and exit.
- **Every signature is approved in the panel**, which names the key, its fingerprint and the program asking. One approval can cover further signatures from the same program and key for `sshAgentApprovalWindowSec` seconds (default 120), so a twenty-commit rebase is one prompt. Live approvals are listed with the time they have left and can be revoked.
- **A cooldown after two unanswered prompts**, five minutes, during which signing is refused without reopening the panel. A banner says so and counts down; **Resume Signing Now** ends it early.
- **Public keys are projected** to `~/.local/share/qs-bitwarden-cli/ssh/*.pub`, public material only, so Git SSH signing has the file paths it requires.
- **Client routing** through one plugin-owned UWSM fragment, written when the agent is enabled and removed when it is disabled, taking effect at the next login. An agent that already owns `SSH_AUTH_SOCK` is named and confirmed before it is replaced; a file this plugin did not write is reported and left alone.
- **`sshAgentUnlockOnDemand`** (off by default) lets an identity listing raise the unlock prompt when the vault is locked with no keys loaded. Signing a key the helper already holds always prompts, with or without it.
- **Remove Plugin Data** on the settings screen clears the keyring entries, learned suggestions and exported public keys in one confirmed action. Your vault is untouched.
- **`sshAgentStatus` diagnostics**: which helper is running, whether its checksum matched, and what the panel believes about client routing.
- The helper ships as a **reproducibly built, checksum-validated binary**, with releases carrying a GitHub build-provenance attestation, an SBOM and a dependency report. Any validation failure disables SSH support alone and leaves the rest of the plugin working; a locally built helper is used as a fallback and says so on a banner.
### Security
- The vault read is split before it reaches the panel: SSH private material goes to the helper over a private FIFO carrying a per-load nonce, and QML receives a sanitized list with it removed.
- A signature is refused unless the vault is unlocked at the epoch its key was loaded under, so a lock racing a load, an approval or a signature cannot leave a key usable.
- Agent forwarding is not supported in this release; a forwarded request is labelled as such in the prompt, because the process it names is not the one that would use the signature.
- `ecdsa` keys are not supported.
## [1.4.1] - 2026-08-31
### Fixed
- The password generator, both Copy password buttons, the generator's Password type and the field-level **Generate...** shortcut wear a key icon again. 1.4.0 replaced all five with the refresh icon: a bulk glyph edit meant to correct one new button rewrote every other use of the same codepoint. Cosmetic only -- no button changed what it does -- and now pinned per button by test rather than by count, since a count moves with exactly this kind of mistake.
## [1.4.0] - 2026-08-30
### Added
- **Two-step method selection.** An account is asked which two-step method it uses before any code is collected, because a code sent without its method is a code the server will reject. The choice goes to Bitwarden on its own first, so a method the account does not have costs a round trip rather than a typed code -- and choosing **Email** is what makes Bitwarden send the email, since `bw` posts it only for a request carrying no token yet. It is asked once per account, not once per login.
- The method that worked is remembered per login address in `twoFactorMethods`, so the question is asked once per account rather than once per login, and two vaults on one machine each keep their own answer. **Change method** on the code screen asks again. A remembered method the account rejects is dropped for that account alone and retried without one.
### Fixed
- Fixes #4: a login on a machine Bitwarden has not seen before now completes in the panel. It used to ask for the emailed code over and over, because `bw login` has no flag for it -- `--code` carries the two-step token, which the device-verification step never reads. The challenge is told apart from a rejected two-step code by the attempt it answers: both say `Code is required.`, but only device verification says it again to a login that already sent a code. The panel then answers bw's prompt dLine truncated
- Two-step login now asks which method an account uses before collecting any code, and never sends a code without it. `bw` only puts the token on the wire when a provider came with it, so `--code` alone makes the request a bare password grant -- and an email provider answers that by issuing a *fresh* code, invalidating the one being submitted. Measured against `bw` 2026.2.0: the same login succeeds with `--method` and returns `Two-step token is invalid.` without it. Authenticator codes survivedLine truncated
- An account with more than one two-step method can log in again. The panel never sent `--method`, which `bw` needs as soon as an account has a choice to make; without it the login failed with `Login failed. No provider selected.` and no way forward.
- A login waiting on an emailed code survives the panel closing. It could not before: closing dropped the master password and the login stage, so going to read the code meant coming back to a blank form. That made both email two-step and new-device verification impossible to complete in the panel -- neither code can be read without leaving it. The login is now held for five minutes, on the wall clock so a suspend counts against it, and reopening lands on the field that was waiting.
- A `bw status` check no longer cancels the login it lands in the middle of. That check takes seconds and answers about the world as it was when it started -- a world where the login had not happened yet -- so it reported `unauthenticated` and the panel acted on it, sending SIGTERM to the login the user had just submitted and clearing the progress indicator on the way past. The button dropped out of "Verifying..." and nothing was shown, which is why a second press was needed. A submitted login Line truncated
- A verification code typed into the panel is no longer discarded on the way out. Typing into a field assigns to its own `text`, which breaks the binding back to the state behind it -- so clearing that state left the field showing the code while the login read an empty value, sent no `--code` at all, and reported back that the code had been rejected. Retyping it repaired the state, which is why a second attempt worked and why the first code had expired by then. Fields and the state behind them Line truncated
- A login no longer has to be submitted twice. Pressing the submit button while the panel was scrubbing the login process's output buffer queued the login against that scrub's exit, and the exit handler returned early for a scrub -- so the queued login was dropped and the click did nothing at all. The next click worked because by then nothing held the process. Both ways the process can end now dispatch whatever was queued, and a scrub is no longer started over a submit that is already waiting.
- A login no longer has to be submitted twice when handing the password to `bw` misses its window. The writer polls for `bw`'s FIFO and gives up if `bw` has not opened it in time, which a cold start after the panel has been closed can outrun; unlock has always re-armed itself there, while login left the button for you to press again. It now retries once on its own, and still reports if the second attempt fails too.
- A vault that has never synced is no longer shown as an empty vault. `bw login` calls its full sync without `allowThrowOnError`, so a sync that fails is swallowed: login still exits 0 and prints a working session, onto a local vault holding no ciphers. The panel now notices `lastSync` is unset on an unlocked vault and syncs once to repair it, which also covers the terminal handoff and a session restored from the keyring.
- An account whose only two-step methods are ones the CLI cannot perform -- a passkey, or Duo -- now says so and points at API key login, instead of reporting a bare `No providers available for this client.`
### Changed
- Every login result is logged with the branch it took, the exit code, and how many bytes came back -- lengths and flags only, never a session, never a code, and stderr through the same sanitiser the panel shows. Read it with `quickshell log -f | grep qs-bitwarden`. A failed login happens on someone else's machine against someone else's account, and this is the difference between a bug report and a guess.
- Email login is three stages where it was two: credentials, the two-step method, then the code. The method is asked once per account and remembered, so only a first login on an account sees the middle stage.
### Security
- A closed panel now holds one thing it did not before: a login stopped on a second factor keeps the master password and its stage for five minutes. That is a deliberate exception to the panel dropping everything on close, and it is what makes an emailed code answerable at all. It is bounded on the wall clock rather than a monotonic timer, so a machine suspended mid-login wakes past it rather than into it; it expires while the panel is closed rather than at the next open; and locking, logging oLine truncated
- The one login that runs with bw's prompts enabled -- new-device verification, the only challenge bw accepts from no flag -- keeps the guarantee `BW_NOINTERACTION` was there for, by answering on a pipe rather than a pty. A pipe ends: measured against the inquirer 8.2.6 bw bundles, a prompt with nothing left to read exits rather than blocking, so an unexpected prompt still ends the login instead of hanging it with the master password loaded. `timeout` covers a bw that never prompts at all. The Line truncated
- Logging out no longer takes the cursor out of the master password field a few seconds later. A logout sets the status itself and then confirms it with `bw status`; that confirmation re-focused the login screen mid-typing, so the rest of the master password was typed into the unmasked email field, which the next submit would have sent as an email address. Focus now moves only onto a screen that does not already hold it. The same fix covers the API key form's client secret and master password.
## [1.3.1] - 2026-08-26
### Fixed
- Fixes #2: ask for a verification code only after Bitwarden requires one, including Bitwarden CLI 2026.2.0's standalone `Code is required.` challenge.
## [1.3.0] - 2026-08-24
### Added
- Authentication prewarming for substantially quicker locked-vault unlocks and logged-out sign-ins.
- Deterministic vault fixture tiers and performance regression coverage from 100 to 5,000 items.
- Visible, compact sync progress while fresh vault data is loading.
### Changed
- Render vault items before deferred folder, organization, and status metadata work.
- Coalesce generator, TOTP, and learned-association work to keep rapid interaction responsive and correct.
- Refresh the fixture screenshots and marketplace preview under the title “Bitwarden Vault Plugin.”
### Security
- Keep authentication secrets out of command arguments and deliver passwords through private runtime FIFOs.
- Scrub process collectors and transient plaintext after use, lock, logout, or cancellation.
- Cancel attachment and generator subprocess groups safely when their owning vault or screen closes.
- Serialize logout with credential writers and verify that session, PIN, and fingerprint credentials are absent from the OS keyring before allowing another login.
- Harden custom-server validation, session handoff, bounded subprocess output, and attachment destination handling.
### Fixed
- Prevent stale asynchronous results from crossing vault generations or mutating a newer session.
- Preserve folder and organization filtering during the faster initial-load sequence.
- Keep generator and TOTP requests correct across rapid option changes, cancellation, scrubbing, and reopen cycles.
@@ -0,0 +1,105 @@
import QtQuick
import qs.Commons
import qs.Ui
import "BitwardenModel.js" as Model
// One labelled, copyable field on the detail screen.
//
// The detail view drew each of these longhand -- a PanelSectionHeader, a
// BorderSurface, a Text and one or two PanelActionButtons, forty lines at a
// time. That was tolerable while only a login had fields worth showing.
// Cards and identities together add sixteen more, and sixteen more copies of
// the same forty lines is how the surfaces drift apart: one row elides and
// the next does not, one masks and the next forgets to.
//
// Empty is not a state worth drawing. `visible` is false when there is no
// value, so a caller can declare every field a type can carry and let the
// sparse ones -- most of an identity, most of the time -- take themselves off
// the screen rather than leaving labelled blanks behind.
Column {
id: root
required property string label
required property string value
required property color foreground
required property string fontFamily
// A value that should not sit in plain sight on a shared screen: a card
// number, a security code, a social security number. Masked until revealed,
// and the reveal is per-field rather than a screen-wide switch.
property bool sensitive: false
property bool revealed: false
// What the flash message calls this once it is on the clipboard.
property string copyLabel: label
// Appended to the copy button's tooltip, e.g. "(n)". Empty when the field
// has no key bound to it.
property string shortcutHint: ""
// The same, for the reveal button. Separate because only one field per item
// is reachable by `v` -- promising it on the others would be a lie, and the
// reveal on each field is independent of every other.
property string revealHint: ""
// The copy button's glyph. Defaults to a plain copy icon; callers pass a
// semantic one where the detail view already had it, so a converted row
// keeps the icon it has always drawn.
property string copyIcon: "󰈙"
signal copyRequested()
signal revealToggled()
readonly property bool masked: root.sensitive && !root.revealed
visible: root.value !== ""
width: parent ? parent.width : 0
spacing: Style.space(4)
PanelSectionHeader { text: root.label.toUpperCase() }
BorderSurface {
width: parent.width
implicitHeight: Style.space(34)
radius: Style.cornerRadius
color: Style.hoverFillFor(root.foreground, Color.accent)
borderSpec: Border.controlSpec("normal", root.foreground, Color.accent)
Row {
anchors.fill: parent
anchors.leftMargin: Style.space(10)
anchors.rightMargin: Style.space(6)
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
text: root.masked ? Model.maskString(root.value) : root.value
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.body
elide: Text.ElideRight
width: parent.width - fieldActions.width - Style.space(10)
}
Row {
id: fieldActions
anchors.verticalCenter: parent.verticalCenter
spacing: Style.space(4)
PanelActionButton {
visible: root.sensitive
iconText: root.revealed ? "󰈉" : "󰈈"
tooltipText: (root.revealed ? "Hide " : "Reveal ") + root.copyLabel.toLowerCase()
+ (root.revealHint === "" ? "" : " (" + root.revealHint + ")")
fontFamily: root.fontFamily
onClicked: root.revealToggled()
}
PanelActionButton {
iconText: root.copyIcon
tooltipText: "Copy " + root.copyLabel.toLowerCase()
+ (root.shortcutHint === "" ? "" : " (" + root.shortcutHint + ")")
fontFamily: root.fontFamily
onClicked: root.copyRequested()
}
}
}
}
}
@@ -0,0 +1,73 @@
import QtQuick
import qs.Commons
import qs.Ui
// One row in an item-form picker: folder, organization, or collection.
//
// `multi` distinguishes the two behaviours. A folder or organization is a
// single choice, so its mark is a tick; a collection is one of several an item
// may belong to, so its mark is a checkbox that reads as toggleable.
BorderSurface {
id: row
property string label: ""
property string glyph: ""
property bool picked: false
property bool multi: false
property color foreground: Color.foreground
property string fontFamily: Style.font.family
signal activated()
implicitHeight: Style.space(28)
radius: Style.cornerRadius
color: picked ? Style.selectedFillFor(foreground, Color.accent)
: (mouse.containsMouse ? Style.hoverFillFor(foreground, Color.accent) : "transparent")
borderSpec: Border.surfaceSpec("menu", "border", picked ? Color.accent : "transparent", picked ? 1 : 0)
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: row.activated()
}
Row {
anchors.fill: parent
anchors.leftMargin: Style.space(9)
anchors.rightMargin: Style.space(9)
spacing: Style.space(8)
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
text: row.glyph
color: row.picked ? Color.accent : Qt.darker(row.foreground, 1.5)
font.family: row.fontFamily
font.pixelSize: Style.font.bodySmall
}
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Style.space(46)
text: row.label
color: row.picked ? Color.accent : row.foreground
font.family: row.fontFamily
font.pixelSize: Style.font.bodySmall
font.bold: row.picked
elide: Text.ElideRight
}
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
visible: row.multi || row.picked
text: row.multi ? (row.picked ? "󰄲" : "󰄱") : "󰄬"
color: row.picked ? Color.accent : Qt.darker(row.foreground, 1.6)
font.family: row.fontFamily
font.pixelSize: Style.font.bodySmall
}
}
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 David Spencer
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.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,581 @@
# qs-bitwarden-cli
Your Bitwarden vault in the **Omarchy** status bar. Search, copy, and manage
every item type without opening a browser.
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-1.8.1-green.svg)](manifest.json)
[![Platform: Omarchy](https://img.shields.io/badge/platform-Omarchy%20%2F%20Hyprland-7c3aed.svg)](https://omarchy.org/)
[![Requires: Bitwarden CLI + jq](https://img.shields.io/badge/requires-bw%20CLI%20%2B%20jq-175ddc.svg)](https://bitwarden.com/help/cli/)
![Bitwarden Vault Plugin preview](preview.png)
Built on Quickshell and the official Bitwarden CLI. Keyboard-first, fast, and
it never writes your vault to a cache of its own.
---
## Install
One command. It clones the plugin, enables it, and puts it in the bar:
```bash
omarchy plugin add https://github.com/Elevate08/qs-bitwarden-cli --enable
```
Nothing else has to be installed first. Open the panel and it tells you what it
still needs -- on a stock Omarchy install that is the Bitwarden CLI and `jq` --
with an **Install** button that hands off to Omarchy's own installer.
To update: `omarchy plugin update io.github.elevate08.qs-bitwarden-cli`
### Sign in
<img src="docs/screenshots/12-login.png" width="420" align="right" alt="Login screen">
Email and password, with the 2FA prompt appearing only when Bitwarden asks for
one. **Server region** picks US, EU, or a custom URL for self-hosted Bitwarden
and Vaultwarden.
Using SSO, a Duo push, or a hardware key? **Launch Terminal** runs `bw login` in
a real terminal so Bitwarden's own prompts handle it, then hands the session
straight back to the panel -- no second login just to get in.
<br clear="all">
---
## The tour
### Your vault, one keystroke away
<img src="docs/screenshots/01-vault-list.png" width="420" align="right" alt="Vault list">
Search by name, username, URL, public key or fingerprint. <kbd>Enter</kbd>
copies the password and arms the TOTP follow-up; press it again within the
window and the live 2FA code replaces it on the clipboard.
Items show their folder and organization inline. The bottom bar filters by
folder, organization and type without leaving the keyboard.
**Suggestions** read the focused window or browser tab and pin the matching
credential to the top, so <kbd>Enter</kbd> is usually the only key you need.
Pick an item once for a site a title cannot match, and it is remembered.
<br clear="all">
<img src="docs/screenshots/06-folder-drawer.png" width="420" align="right" alt="Folder filter drawer">
<kbd>f</kbd>, <kbd>o</kbd> and <kbd>t</kbd> open the folder, organization and
type drawers. Arrows move, <kbd>Enter</kbd> applies, <kbd>Esc</kbd> closes --
and the cursor starts on the option already in effect, so <kbd>Enter</kbd>
never changes anything by accident.
<br clear="all">
### Open an item
<img src="docs/screenshots/02-login-detail.png" width="420" align="right" alt="Login detail">
Username, password and the live TOTP with its countdown, the websites attached
to the item, its notes and any custom fields. Copy any of them with one key.
**Suggest here** pins this item for the app or site in front of you, so it is
offered outright next time rather than inferred.
<br clear="all">
### Every item type, not just logins
<img src="docs/screenshots/04-card-detail.png" width="420" align="right" alt="Card detail">
**Cards** show cardholder, brand, number, expiry and security code. The number
and the code are masked until revealed, and each reveals independently -- an eye
is a statement about the field it sits on.
Search finds a card by brand, cardholder, or last four digits. Deliberately not
by the middle of a number.
<br clear="all">
<img src="docs/screenshots/05-identity-detail.png" width="420" align="right" alt="Identity detail">
**Identities** show name, username, company, email and phone, the social
security, passport and licence numbers, and the address as a single copyable
block rather than seven rows. Empty fields are not drawn, so a sparse identity
stays short.
The three identifiers are masked for the reason a password is, with the
difference that these cannot be rotated afterwards.
<br clear="all">
### Add and edit, without waiting
<img src="docs/screenshots/03-edit-item.png" width="420" align="right" alt="Edit item form">
Create logins, secure notes, cards and identities. <kbd>Enter</kbd> saves from
anywhere in the form, so a long item does not have to be scrolled to the bottom.
Saving and deleting no longer hold the panel. The form closes as the command is
launched and the row shows a spinner until the vault answers. If the vault
refuses, the list goes straight back to what it actually holds and offers to
reopen what you typed.
<br clear="all">
### Generator
<img src="docs/screenshots/08-generator.png" width="420" align="right" alt="Password generator">
Every option the browser extension has -- length, character classes, minimums,
ambiguous characters, or a passphrase with a word count and separator -- with a
live strength meter.
Generation comes from Bitwarden's own generator, not a reimplementation, and
answers in about **2ms** rather than the ~2.9s a fresh `bw generate` costs.
<br clear="all">
### Send
<img src="docs/screenshots/09-sends.png" width="420" align="right" alt="Bitwarden Send">
Share a secret through a link that expires on its own, so a credential need not
live in a chat log. Set a deletion window, a view limit and an optional
password; the link is copied the moment it is created.
<br clear="all">
### SSH agent
<img src="docs/screenshots/13-ssh-approval.png" width="420" align="right" alt="SSH signing approval">
Opt-in. Serves the SSH keys in your vault to `ssh`, Git and `ssh-keygen -Y sign`
while the vault is unlocked, from a helper process that holds the private keys
in memory -- never on disk, never in QML.
Every signature names the key, its fingerprint and the program asking. One
approval can cover a whole rebase; live grants are listed and revocable.
**[Setup, verification and threat model →](docs/ssh-agent.md)**
<br clear="all">
### Settings
<img src="docs/screenshots/10-settings.png" width="420" align="right" alt="Settings screen">
Grouped into **General**, **Security** and **SSH Agent**, with the section you
are reading pinned above the list as you scroll. Destructive actions sit under
their own **DANGER ZONE** heading.
Changes are written to the plugin's entry in `~/.config/omarchy/shell.json`
through `omarchy bar set`, so Omarchy owns the file and the shell hot-reloads.
The General settings include **Colorize menu-bar icon**, which makes the
primary Bitwarden shield follow the active Omarchy theme accent. It is off by
default; lock and setup/error indicators keep their existing status colors.
<br clear="all">
---
## How it compares
What this plugin does, next to the two official Bitwarden clients a Linux user
would otherwise reach for. Checked against Bitwarden's documentation on 2026-12-01.
| | This plugin | Bitwarden CLI | Bitwarden Desktop |
| :--- | :---: | :---: | :---: |
| **Lives in the Omarchy bar** | ✅ | ❌ | ❌ |
| View logins, notes, cards, identities | ✅ | ✅ | ✅ |
| Create / edit logins, notes, cards, identities [^cli-json] | ✅ | ✅ | ✅ |
| View SSH key items | ✅ | ✅ | ✅ |
| Create / import SSH keys [^adr] [^ssh-clients] | ❌ | ❌ | ✅ |
| **SSH agent** [^ssh-desktop] | ✅ | ❌ | ✅ |
| TOTP codes, auto-copied after the password [^totp] | ✅ | ❌ | ❌ |
| Download attachments | ✅ | ✅ | ✅ |
| Bitwarden Send, text | ✅ | ✅ | ✅ |
| Folders, collections, organizations | ✅ | ✅ | ✅ |
| Password / passphrase generator | ✅ | ✅ | ✅ |
| **Unlock with PIN** [^pin] | ✅ | ❌ | ✅ |
| **Unlock with fingerprint** [^fp] [^bio] [^bio-linux] | ✅ | ❌ | ✅ |
| Auto-lock on idle, screen lock, suspend [^cli-lock] [^desk-lock] | ✅ | ❌ | ✅ |
| **Suggests by focused window / browser tab** | ✅ | ❌ | ❌ |
| Self-hosted and Vaultwarden | ✅ | ✅ | ✅ |
| Import / export your vault [^io] | ❌ | ✅ | ✅ |
| Trash: restore a deleted item [^trash] | ❌ | ✅ | ✅ |
| Upload attachments [^attach] | ❌ | ✅ | ✅ |
| File Sends [^filesend] | ❌ | ✅ | ✅ |
| Edit custom fields [^fields] | ❌ | ✅ | ✅ |
| Organization admin: confirm members, approve devices [^orgadmin] | ❌ | ✅ | ❌ |
[^cli-json]: The CLI creates a login by default; other types need the JSON
edited before encoding, as its documentation describes -- "use a
command-line JSON processor like jq to change a `.type=` attribute to
create other item types."
[^adr]: **This plugin** will not. The CLI can encrypt a type-5 item, but
generating a key means putting private material somewhere this plugin has
deliberately kept it out of.
See [ADR 0004](docs/decisions/0004-ssh-key-creation.md).
[^ssh-clients]: Bitwarden documents SSH keys as generated or imported "using
the desktop app, web app, and browser extension", and generation is
Ed25519 only.
[^ssh-desktop]: Bitwarden's SSH agent is a desktop-app feature; the CLI does
not provide one.
[^pin]: PIN unlock is documented for "mobile apps, browser extensions, and
desktop apps".
[^fp]: **This plugin** verifies through the same PAM stack as the Omarchy lock
screen, so it works wherever `omarchy setup security fingerprint` has been
run.
[^bio]: Biometric unlock is documented for the desktop app, browser extensions
and mobile apps -- not the CLI.
[^bio-linux]: On Linux the desktop app's biometric unlock goes through a polkit
agent rather than a fingerprint reader directly.
[^totp]: All three read TOTP codes -- `bw get totp` on the CLI. The check
here is for the follow-up: <kbd>Enter</kbd> copies the password and then
replaces it with the live code a few seconds later, so a login and its
second factor are one keystroke apart.
[^cli-lock]: The CLI has `bw lock`, but no timeout of its own -- a session key
stays valid until something locks it.
[^desk-lock]: The desktop app offers time passed, on system idle, on system
sleep, on system lock and on restart.
[^io]: `bw import` and `bw export` on the CLI; the desktop app has both in its
UI. This plugin has neither -- it reads and writes single items, and a
vault export is a different kind of operation from the one it is for.
[^trash]: A delete here is a delete. Bitwarden keeps deleted items in a trash
for 30 days and both official clients can restore from it (`bw restore`);
this plugin shows no trash and cannot restore.
[^attach]: This plugin downloads attachments but cannot add one. The CLI has
`bw create attachment --file`.
[^filesend]: This plugin creates text Sends only. Both official clients send
files too -- `bw send -f <path>`.
[^fields]: This plugin shows an item's custom fields but does not edit them.
[^orgadmin]: `bw confirm` and `bw device-approval` are CLI features; the
desktop app does not do this either, and it is otherwise the web vault's
job. Listed because the CLI is genuinely ahead of both here.
Sources: [CLI](https://bitwarden.com/help/cli/) ·
[SSH agent](https://bitwarden.com/help/ssh-agent/) ·
[About SSH](https://bitwarden.com/help/about-ssh/) ·
[PIN unlock](https://bitwarden.com/help/unlock-with-pin/) ·
[Biometrics](https://bitwarden.com/help/biometrics/)
---
## Usage & Keyboard Shortcuts
The panel opens with the item list focused, so single-letter shortcuts work straight away. Press <kbd>/</kbd> to type a search.
**While the search box has focus** every letter is search text -- as a text field should behave. Hold <kbd>Alt</kbd> to reach the same shortcuts without leaving the box or disturbing your query; <kbd>↓</kbd> also hands focus back to the list.
### Vault List View (Main Screen)
| Shortcut | Action |
| :--- | :--- |
| <kbd>Enter</kbd> | Copy password (and arm the TOTP follow-up), or open the item when there is no password to copy -- a card, an identity, a note, an SSH key |
| <kbd>Enter</kbd> *(again)* | Copy the TOTP code during the follow-up window |
| <kbd>↑</kbd> / <kbd>↓</kbd> / <kbd>j</kbd> / <kbd>k</kbd> | Move through items, or through an open filter drawer |
| <kbd>/</kbd> | Focus the search box |
| <kbd>Tab</kbd> / <kbd>Shift+Tab</kbd> | Cycle types without opening the drawer |
| <kbd>p</kbd> *(or <kbd>y</kbd>)* | Copy **p**assword |
| <kbd>u</kbd> *(or <kbd>c</kbd>)* | Copy **u**sername / email |
| <kbd>m</kbd> | Copy TOTP **m**ulti-factor code |
| <kbd>w</kbd> | Open the **w**ebsite in your browser |
| <kbd>e</kbd> | Open the detail inspector / **e**dit |
| <kbd>f</kbd> | **F**olders filter |
| <kbd>o</kbd> | **O**rganizations filter |
| <kbd>t</kbd> | **T**ypes filter |
| <kbd>g</kbd> | **G**enerator |
| <kbd>n</kbd> | **N**ew vault item |
| <kbd>s</kbd> | **S**ettings |
| <kbd>r</kbd> | Sync (**r**efresh) |
| <kbd>l</kbd> | **L**ock the vault |
| <kbd>Alt</kbd>+<kbd>s</kbd> | Bitwarden **S**end |
| <kbd>Alt</kbd>+<kbd>,</kbd> | Settings |
| <kbd>Esc</kbd> | Close the filter drawer, clear the search, or close the panel |
`Alt` + any letter above runs the same action from inside the search box. Two are `Alt`-only: <kbd>Alt</kbd>+<kbd>s</kbd> opens **Send** (which has no bare letter, since <kbd>s</kbd> is Settings), and <kbd>Alt</kbd>+<kbd>,</kbd> opens **Settings**, so Settings is still reachable while searching.
### Detail Inspector
| Shortcut | Action |
| :--- | :--- |
| <kbd>Enter</kbd> / <kbd>y</kbd> / <kbd>p</kbd> | Copy what the item is for: the password on a login, the number on a card |
| <kbd>u</kbd> / <kbd>c</kbd> | Copy username; on an identity <kbd>u</kbd> is the username and <kbd>c</kbd> the email |
| <kbd>n</kbd> | Copy a card's **n**umber |
| <kbd>k</kbd> | Copy a card's security code |
| <kbd>m</kbd> | Copy TOTP code |
| <kbd>v</kbd> | Toggle re**v**eal on the item's principal secret -- the password on a login, the number on a card. Every other masked field has its own eye, and each reveals independently |
| <kbd>a</kbd> | Save every **a**ttachment on this item |
| <kbd>e</kbd> | Edit this item |
| <kbd>x</kbd> | Delete this item (asks first) |
| <kbd>b</kbd> / <kbd>q</kbd> / <kbd>Esc</kbd> | Back to the list |
### Filter Drawer (Folders / Organizations / Types)
| Shortcut | Action |
| :--- | :--- |
| <kbd>f</kbd> / <kbd>o</kbd> / <kbd>t</kbd> | Open (or close) that drawer |
| <kbd>↑</kbd> / <kbd>↓</kbd> | Move through the options |
| <kbd>Enter</kbd> | Apply the highlighted option |
| <kbd>Esc</kbd> | Close without changing anything |
The cursor starts on the option already in effect, so <kbd>Enter</kbd> never changes a filter by accident.
### Settings Screen
| Shortcut | Action |
| :--- | :--- |
| <kbd>↑</kbd> / <kbd>↓</kbd> | Move between settings |
| <kbd>←</kbd> / <kbd>→</kbd> | Decrease / increase a number by its step, or switch a toggle off / on |
| <kbd>Enter</kbd> | Flip the highlighted toggle, or open the PIN / fingerprint form |
| <kbd>Esc</kbd> | Back |
### Send Screen
| Shortcut | Action |
| :--- | :--- |
| <kbd>Alt</kbd>+<kbd>s</kbd> | Open Sends |
| <kbd>n</kbd> | New Send |
| <kbd>r</kbd> | Refresh the list |
| <kbd>x</kbd> | Delete the highlighted Send |
| <kbd>Enter</kbd> | Copy the highlighted Send's link |
| <kbd>Esc</kbd> | Back |
---
---
## Optional features
### PIN unlock
Turn on **Unlock with PIN** in the settings screen. You are asked for your master password once (it is needed to encrypt) and for a PIN. Six digits or more is what the screen asks for; four and five are accepted but shown in red with the number of combinations spelled out, so a weak PIN is a decision rather than an accident.
**How it differs from fingerprint unlock.** Fingerprint unlock keeps your master password in the login keyring in the clear, because PAM can only prove presence. A PIN can do better: the master password is encrypted with a key derived from the PIN (PBKDF2-SHA256, 600,000 iterations, salted) and only the ciphertext is stored, so reading the keyring is not by itself enough. A wrong PIN fails decryption, which means correctness needs no stored hash and there is no hash to attack.
**The honest limit.** A short PIN is a small search space, and if the ciphertext leaks, the iteration count is the only thing standing between an attacker and your master password. Five wrong attempts at the panel deletes the stored ciphertext, but that is a UI throttle and does nothing against an offline attack on a copy of the blob. Concretely: 4 digits is 10,000 candidates, which is minutes of offline guessing even at 600,000 PBKDF2 rounds each; 6 digits is 1,000,000, and 8 is 100,000,000. PLine truncated
The stored ciphertext is removed when you turn the setting off, after five wrong attempts, or when the vault rejects the decrypted password (for example after a master password change).
### Fingerprint unlock
Set `fingerprintUnlock` to `true` to unlock the vault with a finger instead of your master password.
**Requirements**
- A fingerprint reader with at least one enrolled finger, configured through `omarchy setup security fingerprint`. The plugin verifies all of this itself (`/etc/pam.d/omarchy-lock-fingerprint`, `fprintd-list`) and silently stays hidden when any part is missing.
- A running, unlocked OS keyring, as used by `rememberSession`. Omarchy ships libsecret itself, so there is nothing to install for this.
**How it works**
1. Switch **Unlock with fingerprint** on in the settings screen. It asks for your master password once -- the same way setting a PIN does -- and stores it in the login keyring under `service=qs-bitwarden-cli, account=master_password`.
2. On every later lock, opening the panel arms the reader. A verified fingerprint releases the stored password to `bw unlock`; the password field remains available as a fallback at all times.
3. Unlocking with your master password afterwards refreshes the stored copy, so changing your master password does not silently strand the enrolment.
**Security trade-off -- read before enabling**
PAM can prove that you are present, but it cannot produce your Bitwarden master password, and `bw unlock` accepts nothing else. Fingerprint unlock therefore keeps your master password in the OS login keyring and treats a verified fingerprint as the gate on reading it back. This is the same trade the official Bitwarden desktop client makes for its own biometric unlock, and it means **anyone who can read your unlocked login keyring can read your master password**. It is off by default and worth lLine truncated
The stored password is removed when you turn the setting off, press **Forget Fingerprint** on the locked screen, log out of the account, or when the vault rejects it (for example after a master password change, which then prompts you for the new one).
### SSH agent
Off by default. See **[docs/ssh-agent.md](docs/ssh-agent.md)** for setup, the
provenance check, and what the agent does and does not protect.
---
## Bar placement
`--enable` already puts the widget in the bar. To move it:
```bash
omarchy bar move io.github.elevate08.qs-bitwarden-cli --section right
```
Settings are editable from the panel's own settings screen, or directly in
`~/.config/omarchy/shell.json`. Each setting lives **inline on the bar entry**,
not in a separate block:
```json
{
"bar": {
"layout": {
"right": [
{
"id": "io.github.elevate08.qs-bitwarden-cli",
"autoLockMinutes": 15,
"lockOnScreenLock": true,
"lockOnSuspend": true,
"clearClipboardSec": 30,
"rememberSession": true,
"fingerprintUnlock": false,
"sshAgentEnabled": false,
"sshAgentApprovalPopup": true
}
]
}
}
}
```
## Global hotkey
To toggle the Bitwarden panel with a keyboard shortcut (e.g. `SUPER + CTRL + /`), add the binding to `~/.config/hypr/bindings.lua`:
```lua
o.bind("SUPER + CTRL + SLASH", "Bitwarden vault", "omarchy-shell io.github.elevate08.qs-bitwarden-cli toggle")
```
Apply changes by restarting the shell:
```bash
omarchy restart shell
```
---
## Configuration Reference
The following settings are read from the plugin's own entry in the
`bar.layout` array of `~/.config/omarchy/shell.json` -- inline alongside its
`id`, as shown above. The panel's settings screen writes them for you via
`omarchy bar set`, so editing the file by hand is optional:
| Key | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `autoLockMinutes` | `number` | `15` | Minutes of inactivity before automatically locking the vault (`0` to disable). Range `0`-`1440`; out of range is clamped and an unreadable value falls back to `15`. |
| `clearClipboardSec` | `number` | `30` | Seconds before automatically clearing copied secrets from the clipboard (`0` to disable). Range `0`-`300`; out of range is clamped and an unreadable value falls back to `30`. |
| `lockOnScreenLock` | `boolean` | `true` | Lock the vault as soon as the screen locks, rather than waiting out `autoLockMinutes`. Reads the Omarchy lock screen's own state, so it follows a manual lock and an idle lock alike. A shell without the lock plugin simply never reports a lock; it is never read as one. |
| `lockOnSuspend` | `boolean` | `true` | Lock the vault when the machine is going to sleep, so no unlocked session key is left in the suspended machine's memory. Holds a `delay` sleep inhibitor for about a second so the lock finishes first. Needs `gdbus` (glib2) and `systemd-inhibit`; without them the setting is simply inert. |
| `rememberSession` | `boolean` | `true` | Persist session token in OS keyring (`secret-tool`) while unlocked. Survives a shell restart, never a reboot -- see the note above. |
| `autoCopyTotpSec` | `number` | `3` | Seconds after password copy to automatically replace clipboard with TOTP code (`0` to disable). Range `0`-`30`; out of range is clamped and an unreadable value falls back to `3`. |
| `closeOnCopy` | `boolean` | `true` | Automatically close panel on Enter copy so target application receives focus immediately. |
| `suggestOnOpen` | `boolean` | `true` | Automatically suggest matching vault items for the active window or browser tab on open. |
| `fingerprintUnlock` | `boolean` | `false` | Unlock the vault with an enrolled fingerprint. Stores your master password in the OS login keyring -- see [Optional: Fingerprint Unlock](#fingerprint-unlock). |
| `pinUnlock` | `boolean` | `false` | Unlock with a numeric PIN. Stores the master password encrypted under a PIN-derived key -- see [Optional: PIN Unlock](#pin-unlock). |
| `sshAgentEnabled` | `boolean` | `false` | Serve your vault's SSH keys to `ssh`, Git and signing while the vault is unlocked. Starts a helper process and a socket under `$XDG_RUNTIME_DIR`; private keys stay in that helper and are dropped on lock -- see [SSH Agent](docs/ssh-agent.md). |
| `sshAgentUnlockOnDemand` | `boolean` | `false` | Let an identity listing raise the unlock prompt when the vault is locked and no keys have been loaded yet, instead of answering with an empty list. Signing a key the helper already knows always raises the prompt, with or without this. Off by default: `ssh` asks the agent for identities on every connection, so this raises the configured approval surface on the first `ssh` after every login. |
| `sshAgentApprovalPopup` | `boolean` | `true` | Show SSH unlock and signing requests in a transient card in the middle of the screen instead of opening the anchored panel. Disable to show prompts in the panel. Multiple concurrent requests are queued sequentially with a "1 of N" counter and "Deny all" option. Escape and outside click deny. |
| `sshAgentApprovalWindowSec` | `number` | `120` | How long one approval keeps covering further signatures from the same program with the same key. Range `0`-`900`; `0` asks every time. Held in memory only and dropped on lock, logout or exit. |
One further key, `twoFactorMethods`, is written to the same entry but is not a
setting you configure. It records which two-step method last logged each
account in, keyed by login address -- `{"you@example.com": 0}`, where `0` is
authenticator, `1` email and `3` YubiKey -- so an account with more than one
method is asked only once, and two vaults on one machine each keep their own
answer. **Change method** on the code screen asks again and rewrites it. Entries
are capped at ten accounts, and anything unreadable is treated as not
remembered, which costs that account one extra prompt.
Learned suggestions are stored separately in `~/.local/state/qs-bitwarden-cli/associations.json`. Delete that file to reset everything the panel has learned; logging out deletes it for you.
---
---
## IPC & Scripting Interface
You can control and query the Bitwarden plugin from the terminal, scripts, or window manager bindings. The form is `omarchy-shell <target> <method>`:
```bash
# Show, hide, or toggle the popup panel
omarchy-shell io.github.elevate08.qs-bitwarden-cli open
omarchy-shell io.github.elevate08.qs-bitwarden-cli close
omarchy-shell io.github.elevate08.qs-bitwarden-cli toggle
# Jump straight to a screen
omarchy-shell io.github.elevate08.qs-bitwarden-cli settings # -> "settings"
omarchy-shell io.github.elevate08.qs-bitwarden-cli setup # -> "setup" (dependency wizard)
# Lock the vault immediately
omarchy-shell io.github.elevate08.qs-bitwarden-cli lock # -> "locked"
# Sync with Bitwarden
omarchy-shell io.github.elevate08.qs-bitwarden-cli sync # -> "syncing"
# Query vault state
omarchy-shell io.github.elevate08.qs-bitwarden-cli status # -> "unlocked" | "locked" | "unauthenticated"
```
`open`, `close` and `toggle` return nothing; the rest echo the state they moved to.
Omarchy's shell-level dispatcher also toggles any plugin, and works equally well for a keybinding:
```bash
omarchy-shell shell toggle io.github.elevate08.qs-bitwarden-cli
```
Only `toggle` exists at that level, though -- `omarchy-shell shell open|close <id>` answers `Function not found`, and `omarchy-shell shell call <id> <method> '{}'` answers `unknown`. Use the plugin-target form above for everything other than toggling.
The same calls work through Quickshell directly, which is useful when `omarchy-shell` is not on `PATH`:
```bash
qs -p /usr/share/omarchy/shell/shell.qml ipc call io.github.elevate08.qs-bitwarden-cli status
```
---
---
## Dependencies
The helper's crates are updated by Dependabot under `versioning-strategy:
lockfile-only`, so a proposed bump only ever moves `agent/Cargo.lock` within
the bounds `agent/Cargo.toml` already allows. Crossing a major is a manual
edit, on purpose: several of the version floors in that manifest exist to keep
one copy of the RustCrypto traits in the graph, and Dependabot raising them
one at a time is what broke the build in PR #11.
**The crypto stack moves together or not at all.** `ssh-key` and `rsa`
re-export the trait generation their callers must match, and `agent/src/lib.rs`
calls those traits directly -- `Verifier::verify`, `try_sign`,
`pkcs1v15::SigningKey`. Bump one crate without the others and cargo resolves
two versions side by side, at which point the traits stop unifying and
nothing compiles. The coupled set is `ssh-key`, `ssh-encoding`,
`ed25519-dalek`, `rsa`, `signature`, `sha2`/`digest`, `zeroize` and
`rand_core`; Dependabot groups them under `crypto` for the same reason.
As of 2026-08-31 that upgrade is gated upstream: `ssh-key` is at `0.7.0-rc.11`
and `rsa` at `0.10.0-rc.18`, both still pre-release, and the stable releases
still pin the older generation. When they land, raise every crate in the set
in one commit and expect real source changes, not just a manifest edit.
Nothing will prompt you -- there are no `ignore` conditions to trip, because
cargo's own semver rules already hold `0.10` back from `0.11`.
Every accepted bump, major or not, changes the shipped bytes and so needs the
binary rebuilt in the same change -- see the `needs-binary-rebuild` label:
```bash
gh pr checkout <n>
./scripts/build-agent.sh # re-enters the digest-pinned image
git commit -am "deps: rebuild the agent binary" && git push
```
CI never does this for you. `--compare-tracked` proves the committed bytes are
what the committed source builds; it cannot tell you whether that source is
trustworthy, and a malicious crate builds just as reproducibly as an honest
one. Reading the `Cargo.lock` diff before you commit the binary is the only
check that covers that, which is why the rebuild stays a human step.
---
---
## More
- **[Features in detail](docs/features.md)** -- every feature and why it works the way it does.
- **[SSH agent](docs/ssh-agent.md)** -- setup, verification, threat model.
- **[Uninstall](docs/uninstall.md)** -- including what to clear before removing the plugin.
- **[Development](docs/development.md)** -- linting and the test suite.
- **[Decisions](docs/decisions/)** -- the arguments that were had once and should not drift.
---
## License
MIT -- see [LICENSE](LICENSE).
@@ -0,0 +1,253 @@
import QtQuick
import qs.Commons
import qs.Ui
import "BitwardenModel.js" as Model
// The SSH agent's own settings sections, lifted out of Panel.qml so that file
// is not the only place this feature can be read.
//
// Two separate things, deliberately drawn apart. The top half is what the
// feature is doing; the bottom half is whether the user's terminals will
// reach it. Neither one gates the other. The approval screen lives with the
// other screens in Panel.qml, because that is what it is.
//
// `panel` is the Panel root: this section reads its vault and agent state and
// calls back into it for every action. Nothing here holds state of its own.
Column {
id: section
required property var panel
// The bar's foreground and font family, not the global theme's -- the same
// values the rest of the panel draws with. PanelSectionHeader and Text both
// default to the globals, so every text element here states them.
component SshSectionHeader: PanelSectionHeader {
textFormat: Text.PlainText
foreground: section.panel.fg
fontFamily: section.panel.fontFamily
}
component SshCaption: Text {
textFormat: Text.PlainText
width: parent ? parent.width : 0
color: section.panel.dim
font.family: section.panel.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
visible: panel.sshUiAvailable
width: parent.width
spacing: Style.space(6)
Item { width: parent.width; height: Style.space(10) }
SshSectionHeader {
text: "SSH AGENT STATUS"
}
Row {
width: parent.width
spacing: Style.space(8)
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
text: panel.sshAgentSetup.state === "enabled"
? (panel.sshAgentSetup.busy ? "󰔟" : "󰄬")
: (panel.sshAgentSetup.state === "error" ? "󰀪" : "󰅘")
color: panel.sshAgentSetup.state === "error"
? panel.urgent
: (panel.sshAgentSetup.state === "enabled" && !panel.sshAgentSetup.busy ? Color.accent : panel.dim)
font.family: panel.fontFamily
font.pixelSize: Style.font.body
}
SshCaption {
width: parent.width - Style.space(30)
text: panel.sshAgentSetup.message
color: panel.sshAgentSetup.state === "error" ? panel.urgent : panel.dim
}
}
// Which helper is running. A developer with a local build and a
// user on a release see the same panel otherwise, and confusing
// the two wastes an afternoon.
SshCaption {
visible: panel.sshAgentHelper.source !== ""
text: "Using " + Model.sshAgentHelperSourceLabel(panel.sshAgentHelper.source)
+ (panel.sshAgentHelper.checksum === "match" ? " (checksum verified)" : "")
color: panel.sshAgentHelper.source === "development" ? panel.urgent : panel.dim
}
// Why the feature is unavailable, when it is. These are the
// failures a real clone produces: a stale binary, a dropped file
// mode, an LFS placeholder.
SshCaption {
visible: panel.sshAgentEnabled && panel.sshAgentHelper.message !== ""
text: panel.sshAgentHelper.message
color: panel.urgent
}
// The helper's own version, once it has said hello. Non-secret,
// and the quickest way to tell a stale bundled binary apart from
// a working one.
SshCaption {
visible: panel.sshAgentVersion !== ""
text: "Helper version " + panel.sshAgentVersion
}
// Routing is the thing most likely to be missing when the agent looks
// healthy and SSH still does not use it. Said here because this is the
// block a user reads first, and decided by the routing file rather than by
// this session's SSH_AUTH_SOCK -- see sshAgentRoutingNotice for why.
SshCaption {
visible: panel.sshAgentSetup.state === "enabled" && !panel.sshAgentSetup.busy
&& panel.sshRoutingNotice.text !== ""
text: panel.sshRoutingNotice.text
color: panel.sshRoutingNotice.urgent ? panel.urgent : panel.dim
}
Item { width: parent.width; height: Style.space(10) }
SshSectionHeader {
text: "CLIENT ROUTING"
}
SshCaption {
text: panel.sshRouting.message
color: panel.sshRouting.state === "matches" ? panel.dim : panel.fg
}
// The check the user runs in the terminal they actually use --
// which is the only place the answer is authoritative.
Text {
textFormat: Text.PlainText
width: parent.width
text: " " + panel.sshRouting.terminalCheck
color: Color.accent
font.family: panel.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WrapAnywhere
}
SshCaption {
text: panel.uwsmFragment.message
}
// Replacing the session's primary agent is a real decision, so the
// conflict is stated and confirmed rather than absorbed by the
// first click.
SshCaption {
visible: panel.uwsmConfirmPending
text: "This will make Bitwarden your session's SSH agent at the next login, replacing "
+ (panel.sshRouting.owner !== "" ? panel.sshRouting.owner : "the one you have now")
+ ". Continue?"
color: panel.urgent
}
SshCaption {
visible: panel.uwsmFlash !== ""
text: panel.uwsmFlash
color: panel.fg
}
// A Flow, because which of these four are showing is decided by the routing
// state: the idle pair and the confirming pair are each narrow enough, but
// nothing in a Row enforces that, and a Row answers a set that is too wide by
// laying the last button out past the panel edge rather than wrapping it.
Flow {
width: parent.width
spacing: Style.space(8)
Button {
visible: !panel.uwsmConfirmPending && panel.uwsmFragment.state !== "managed"
text: "Route SSH Clients Here"
iconText: "󰌘"
tooltipText: "Write " + Model.uwsmFragmentDisplayPath() + " so the next login points SSH clients at this agent"
fontFamily: panel.fontFamily
fontSize: Style.font.bodySmall
enabled: !panel.uwsmBusy
onClicked: panel.beginUwsmSetup()
}
Button {
visible: panel.uwsmConfirmPending
text: "Yes, Replace It"
iconText: "󰄬"
fontFamily: panel.fontFamily
fontSize: Style.font.bodySmall
enabled: !panel.uwsmBusy
onClicked: panel.beginUwsmSetup()
}
Button {
visible: panel.uwsmConfirmPending
text: "Cancel"
iconText: "󰅘"
fontFamily: panel.fontFamily
fontSize: Style.font.bodySmall
onClicked: panel.cancelUwsmSetup()
}
Button {
visible: !panel.uwsmConfirmPending && panel.uwsmFragment.removable
text: "Remove Routing File"
iconText: "󰩹"
tooltipText: "Delete " + Model.uwsmFragmentDisplayPath()
fontFamily: panel.fontFamily
fontSize: Style.font.bodySmall
enabled: !panel.uwsmBusy
onClicked: panel.removeUwsmFragment()
}
}
Item {
visible: panel.sshGrants.length > 0
width: parent.width
height: visible ? Style.space(10) : 0
}
SshSectionHeader {
visible: panel.sshGrants.length > 0
text: "ACTIVE APPROVALS"
}
// Every live grant, with the process it belongs to and what is
// left of it. A grant is a window in which signing happens with
// no prompt, so it has to be visible and revocable while it runs.
Repeater {
model: panel.sshGrants
delegate: Row {
required property var modelData
width: parent.width
spacing: Style.space(8)
SshCaption {
width: parent.width - Style.space(110)
text: modelData.keyName + " · "
+ modelData.processName
+ " · " + modelData.remainingLabel
}
Button {
anchors.verticalCenter: parent.verticalCenter
text: "Revoke"
iconText: "󰩹"
fontFamily: panel.fontFamily
fontSize: Style.font.caption
onClicked: panel.revokeSshGrant(modelData.grantId)
}
}
}
Button {
visible: panel.sshGrants.length > 1
text: "Revoke All Approvals"
iconText: "󰩹"
tooltipText: "Drop every live approval; the next signature asks again"
fontFamily: panel.fontFamily
fontSize: Style.font.bodySmall
onClicked: panel.revokeAllSshGrants()
}
}
@@ -0,0 +1,163 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Ui
// A transient, centered SSH authorization surface. The full-screen layer
// window supplies the scrim, outside-click denial, and keyboard focus; only
// the compact card is visible. It is tied to the bar widget's screen but not
// positioned relative to the bar, so the plugin otherwise stays out of sight.
PanelWindow {
id: popup
required property var panel
required property Item anchorItem
readonly property bool open: panel.sshAgentApprovalPopup && (panel.sshPrompt !== null || panel.sshUnlockRequest !== null)
property bool focusPrimed: false
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
readonly property int cardWidth: Math.max(1, Math.min(Style.space(460), width - Style.gapsOut * 2))
readonly property int cardHeight: Math.max(1, Math.min(
content.implicitHeight + card.contentTopInset + card.contentBottomInset,
height - Style.gapsOut * 2))
function beginFocusPrime() {
if (open && backingWindowVisible) focusPrimeTimer.restart()
}
function refocus() {
if (!open) return
Qt.callLater(function() {
if (!popup.open) return
if (popup.panel.sshPrompt) approvalScreen.focusDefault()
else unlockScreen.focusDefault()
})
}
screen: anchorItem && anchorItem.QsWindow.window ? anchorItem.QsWindow.window.screen : null
visible: open
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "qs-bitwarden-ssh-approval"
WlrLayershell.layer: WlrLayer.Overlay
// Prime focus briefly so keyboard-summoned requests reliably receive it,
// then settle to OnDemand so another monitor is not pointer-blocked.
WlrLayershell.keyboardFocus: open
? (focusPrimed ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive)
: WlrKeyboardFocus.None
anchors {
top: true
bottom: true
left: true
right: true
}
onBackingWindowVisibleChanged: beginFocusPrime()
onOpenChanged: {
if (open) {
focusPrimed = false
beginFocusPrime()
refocus()
} else {
focusPrimeTimer.stop()
focusPrimed = false
}
}
Connections {
target: popup.panel
function onSshPromptChanged() { popup.refocus() }
function onSshUnlockRequestChanged() { popup.refocus() }
function onStatusChanged() { popup.refocus() }
}
Timer {
id: focusPrimeTimer
interval: 75
repeat: false
onTriggered: {
popup.focusPrimed = true
popup.refocus()
}
}
Rectangle {
anchors.fill: parent
color: Color.menu.scrim
}
MouseArea {
anchors.fill: parent
onClicked: popup.panel.denySshRequest()
}
BorderSurface {
id: card
width: popup.cardWidth
height: popup.cardHeight
anchors.centerIn: parent
radius: Style.cornerRadius
color: Color.popups.background
borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border,
Math.max(1, Style.space(2)))
padding: Style.spacing.panelPadding
// Swallow clicks on unused card space; only a click outside the card is a
// denial. Interactive children declared below remain above this catcher.
MouseArea { anchors.fill: parent; onClicked: {} }
Item {
id: keyScope
anchors.fill: parent
anchors.topMargin: card.contentTopInset
anchors.rightMargin: card.contentRightInset
anchors.bottomMargin: card.contentBottomInset
anchors.leftMargin: card.contentLeftInset
focus: popup.open
Keys.priority: Keys.BeforeItem
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
if (!(event.modifiers & ~Qt.KeypadModifier)) {
popup.panel.denySshRequest()
event.accepted = true
} else if (event.modifiers & Qt.ShiftModifier) {
popup.panel.denyAllSshRequests()
event.accepted = true
}
}
}
Flickable {
id: scroller
anchors.fill: parent
contentWidth: width
contentHeight: content.implicitHeight
clip: true
flickableDirection: Flickable.VerticalFlick
boundsBehavior: Flickable.StopAtBounds
interactive: contentHeight > height
Column {
id: content
width: scroller.width
SshUnlockScreen {
id: unlockScreen
panel: popup.panel
active: popup.open && popup.panel.sshPrompt === null
}
SshApprovalScreen {
id: approvalScreen
panel: popup.panel
active: popup.open && popup.panel.sshPrompt !== null
}
}
}
}
}
}
@@ -0,0 +1,210 @@
import QtQuick
import qs.Commons
import qs.Ui
import "BitwardenModel.js" as Model
// SCREEN: SSH signing approval.
//
// The one place a signature is authorised. It states what the companion
// verified -- the requesting user -- and is explicit that everything else
// about the process is context rather than identity.
//
// `panel` is the Panel root. This screen holds no state: it draws the pending
// request and calls back for the answer.
Column {
id: screen
required property var panel
property bool active: panel.activeScreen === "sshApproval"
// A signing decision should never open with an affirmative action focused.
// Both the anchored panel and the centered popup can call this after their
// window receives keyboard focus.
function focusDefault() {
if (screen.active && screen.visible) denyButton.forceActiveFocus()
}
// The bar's foreground and font family rather than the global theme's, the
// same as every other text element in this panel. Text defaults to AutoText,
// so the format is stated even where the string is constant today.
component SshSectionHeader: PanelSectionHeader {
textFormat: Text.PlainText
foreground: screen.panel.fg
fontFamily: screen.panel.fontFamily
}
component SshCaption: Text {
textFormat: Text.PlainText
width: parent ? parent.width : 0
color: screen.panel.dim
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
visible: active && panel.sshPrompt !== null
width: parent.width
spacing: Style.space(12)
PanelSeparator {
visible: !screen.panel.sshAgentApprovalPopup
width: parent.width
}
Row {
width: parent.width
spacing: Style.space(8)
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
text: "󰌆"
color: Color.accent
font.family: panel.fontFamily
font.pixelSize: Style.font.body
}
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
text: "SSH signing request"
color: panel.fg
font.family: panel.fontFamily
font.pixelSize: Style.font.body
}
Item { width: Math.max(0, parent.width - Style.space(panel.sshPendingCount > 1 ? 290 : 230)); height: 1 }
Text {
textFormat: Text.PlainText
visible: panel.sshPendingCount > 1
anchors.verticalCenter: parent.verticalCenter
text: "1 of " + panel.sshPendingCount
color: Color.accent
font.family: panel.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
}
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
text: panel.sshPromptRemainingSec + "s left"
color: panel.sshPromptRemainingSec <= 5 ? panel.urgent : panel.dim
font.family: panel.fontFamily
font.pixelSize: Style.font.caption
}
}
// Forwarding is rejected in v1. If one ever reaches here it is
// called out rather than shown as ordinary context, because the
// process named would not be the one using the signature.
SshCaption {
visible: panel.sshPrompt && panel.sshPrompt.forwardedWarning !== ""
text: panel.sshPrompt ? panel.sshPrompt.forwardedWarning : ""
color: panel.urgent
}
SshCaption {
visible: panel.sshAgentLoadActive
text: Model.sshAgentLoadingNote()
}
SshSectionHeader {
text: "KEY"
}
Text {
textFormat: Text.PlainText
width: parent.width
text: panel.sshPrompt ? panel.sshPrompt.keyName : ""
color: panel.fg
font.family: panel.fontFamily
font.pixelSize: Style.font.body
wrapMode: Text.WordWrap
}
// The fingerprint is the value worth checking, so it is shown whole
// rather than elided.
SshCaption {
text: panel.sshPrompt ? panel.sshPrompt.fingerprint : ""
wrapMode: Text.WrapAnywhere
}
SshSectionHeader {
text: "REQUESTED BY"
}
Text {
textFormat: Text.PlainText
width: parent.width
text: panel.sshPrompt
? panel.sshPrompt.processName
: ""
color: panel.fg
font.family: panel.fontFamily
font.pixelSize: Style.font.body
wrapMode: Text.WordWrap
}
SshCaption {
text: panel.sshPrompt ? panel.sshPrompt.processPath : ""
wrapMode: Text.WrapAnywhere
}
SshCaption {
text: panel.sshPrompt ? panel.sshPrompt.provenanceNote : ""
}
PanelSeparator {
visible: !screen.panel.sshAgentApprovalPopup
width: parent.width
}
// Deny leads, and nothing is activated by a bare Enter: a stray
// keypress must not be able to sign.
Row {
width: parent.width
spacing: Style.space(8)
Button {
id: denyButton
text: "Deny (Esc)"
iconText: "󰅘"
fontFamily: panel.fontFamily
fontSize: Style.font.bodySmall
focusable: true
onClicked: panel.denySshRequest()
}
Button {
visible: panel.sshPendingCount > 1
text: "Deny all (" + panel.sshPendingCount + ")"
iconText: "󰅙"
fontFamily: panel.fontFamily
fontSize: Style.font.bodySmall
focusable: true
onClicked: panel.denyAllSshRequests()
}
Button {
text: "Approve once"
iconText: "󰄬"
fontFamily: panel.fontFamily
fontSize: Style.font.bodySmall
focusable: true
onClicked: panel.approveSshRequest(0)
}
}
Button {
visible: panel.sshPrompt && panel.sshPrompt.grantOffered
text: panel.sshPrompt ? panel.sshPrompt.grantLabel : ""
iconText: "󰔟"
tooltipText: "Sign further requests from this same program with this key, without asking again, until the window expires"
fontFamily: panel.fontFamily
fontSize: Style.font.bodySmall
focusable: true
onClicked: panel.approveSshRequest(panel.sshPrompt ? panel.sshPrompt.grantSeconds : 0)
}
}
@@ -0,0 +1,332 @@
import QtQuick
import qs.Commons
import qs.Ui
import "BitwardenModel.js" as Model
// The first step of an SSH request when the vault is locked. Uses the same
// layout, pulsing fingerprint animation, and unlock controls as the panel's
// unlock screen, while keeping the SSH request context visible.
Column {
id: screen
required property var panel
property bool active: false
visible: active && panel.sshUnlockRequest !== null
width: parent ? parent.width : 0
spacing: Style.space(12)
onVisibleChanged: if (!visible && eyeBtnUnlock) eyeBtnUnlock.revealed = false
onActiveChanged: if (!active && eyeBtnUnlock) eyeBtnUnlock.revealed = false
component UnlockCaption: Text {
textFormat: Text.PlainText
width: parent ? parent.width : 0
color: screen.panel.dim
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
function focusDefault() {
if (!screen.active || !screen.visible) return
screen.panel.prepareUnlock()
if (screen.panel.fingerprintReady) screen.panel.startFingerprintUnlock()
Qt.callLater(function() {
if (!screen.active || screen.panel.status !== "locked") return
if (screen.panel.pinReady) pinField.forceActiveFocus()
else passwordField.forceActiveFocus()
})
}
// Centered header matching Panel.qml Screen 2
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.space(6)
Text {
id: fingerprintIcon
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter
text: screen.panel.fingerprintScanning ? "󰈷" : "󰌋"
color: screen.panel.fingerprintScanning ? Color.accent : screen.panel.fg
opacity: 0.85
font.family: screen.panel.fontFamily
font.pixelSize: Style.space(38)
SequentialAnimation on opacity {
running: screen.panel.fingerprintScanning
loops: Animation.Infinite
NumberAnimation { to: 0.35; duration: 700; easing.type: Easing.InOutQuad }
NumberAnimation { to: 0.95; duration: 700; easing.type: Easing.InOutQuad }
onStopped: fingerprintIcon.opacity = 0.85
}
}
Text {
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter
text: screen.panel.status === "unlocked"
? "Loading SSH keys"
: (screen.panel.fingerprintReady ? "Unlock Vault" : "Enter Master Password")
color: screen.panel.fg
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.title
font.bold: true
}
Text {
textFormat: Text.PlainText
visible: screen.panel.userEmail !== ""
anchors.horizontalCenter: parent.horizontalCenter
text: screen.panel.userEmail
color: screen.panel.dim
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.bodySmall
}
}
UnlockCaption {
text: {
var request = screen.panel.sshUnlockRequest
var prefix = "Vault needs to be unlocked first: "
if (!request) return "Vault needs to be unlocked first."
if (request.keyName !== "") {
return prefix + request.keyName + " is needed by " + request.processName + "."
}
return prefix + request.processName + " is asking which SSH keys are available."
}
horizontalAlignment: Text.AlignHCenter
color: screen.panel.fg
}
UnlockCaption {
text: "Unlocking only loads the key. You will still approve the signing request separately."
horizontalAlignment: Text.AlignHCenter
}
// Fingerprint status / prompt
Text {
textFormat: Text.PlainText
visible: screen.panel.fingerprintMessage !== ""
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: screen.panel.fingerprintMessage
color: screen.panel.fingerprintScanning ? Color.accent : screen.panel.dim
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.bodySmall
wrapMode: Text.WordWrap
}
// Offered when fingerprint unlock is on but nothing is stored yet
Text {
textFormat: Text.PlainText
visible: screen.panel.fingerprintUnlock && screen.panel.fingerprintAvailable && !screen.panel.fingerprintStored
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: "󰈷 Unlock once with your master password to enable fingerprint unlock."
color: screen.panel.dim
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
// Checking / keys loading into helper indicator
Rectangle {
visible: screen.panel.status === "checking"
|| (screen.panel.status === "unlocked" && screen.panel.sshAgentLoadActive)
width: parent.width
height: loadingText.implicitHeight + Style.space(20)
radius: Style.cornerRadius
color: Util.alpha(Color.popups.text, 0.06)
Text {
id: loadingText
textFormat: Text.PlainText
anchors.centerIn: parent
width: parent.width - Style.space(24)
text: screen.panel.status === "checking"
? "Checking vault status..."
: Model.sshAgentLoadingNote()
color: screen.panel.fg
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.bodySmall
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
// PIN entry, offered above the password field when one is set
Column {
visible: screen.panel.status === "locked" && screen.panel.pinReady
width: parent.width
spacing: Style.space(8)
Text {
textFormat: Text.PlainText
text: "PIN"
color: screen.panel.dim
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
}
Row {
width: parent.width
spacing: Style.space(8)
TextField {
id: pinField
width: parent.width - pinUnlockBtn.width - Style.space(8)
placeholderText: "Enter your PIN..."
password: true
text: screen.panel.pinEntry
onTextChanged: screen.panel.pinEntry = text.replace(/[^0-9]/g, "")
onAccepted: screen.panel.submitPinUnlock()
enabled: !screen.panel.pinBusy && !screen.panel.isUnlocking
}
Button {
id: pinUnlockBtn
text: screen.panel.pinBusy ? "Checking..." : "Unlock"
iconText: screen.panel.pinBusy ? "󰑐" : "󰌿"
iconSpinning: screen.panel.pinBusy
selected: true
accent: Color.accent
fontFamily: screen.panel.fontFamily
focusable: true
enabled: !screen.panel.pinBusy && !screen.panel.isUnlocking
onClicked: screen.panel.submitPinUnlock()
}
}
Text {
textFormat: Text.PlainText
visible: screen.panel.pinError !== ""
width: parent.width
text: screen.panel.pinError
color: screen.panel.urgent
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.bodySmall
wrapMode: Text.WordWrap
}
Text {
textFormat: Text.PlainText
text: "or use your master password below"
color: screen.panel.dim
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.caption
}
}
// Fingerprint / Password column matching Panel.qml
Column {
visible: screen.panel.status === "locked"
width: parent.width
spacing: Style.space(10)
Button {
visible: screen.panel.fingerprintReady
width: parent.width
text: screen.panel.fingerprintScanning ? "Waiting for fingerprint..." : "Unlock with Fingerprint"
iconText: "󰈷"
selected: true
accent: Color.accent
fontFamily: screen.panel.fontFamily
focusable: true
enabled: !screen.panel.isUnlocking && !screen.panel.fingerprintScanning
onClicked: screen.panel.startFingerprintUnlock()
}
Row {
width: parent.width
spacing: Style.space(8)
TextField {
id: passwordField
width: parent.width - eyeBtnUnlock.width - Style.space(8)
placeholderText: "Master password..."
password: !eyeBtnUnlock.revealed
text: screen.panel.masterPassword
onTextChanged: screen.panel.masterPassword = text
onActiveFocusChanged: if (activeFocus) screen.panel.prepareUnlock()
onAccepted: screen.panel.unlockVault()
enabled: !screen.panel.isUnlocking
}
Button {
id: eyeBtnUnlock
property bool revealed: false
iconText: revealed ? "󰈉" : "󰈈"
tooltipText: revealed ? "Hide password" : "Show password"
fontFamily: screen.panel.fontFamily
focusable: true
onClicked: revealed = !revealed
}
}
Button {
width: parent.width
text: screen.panel.isUnlocking ? "Unlocking..." : "Unlock Vault"
iconText: screen.panel.isUnlocking ? "󰑐" : "󰌋"
iconSpinning: screen.panel.isUnlocking
selected: true
accent: Color.accent
fontFamily: screen.panel.fontFamily
focusable: true
enabled: !screen.panel.isUnlocking
onClicked: screen.panel.unlockVault()
}
}
UnlockCaption {
visible: screen.panel.errorMessage !== ""
text: screen.panel.errorMessage
color: screen.panel.urgent
horizontalAlignment: Text.AlignHCenter
}
UnlockCaption {
visible: screen.panel.status === "unauthenticated"
text: "Sign in from the Bitwarden panel before using vault SSH keys."
color: screen.panel.urgent
horizontalAlignment: Text.AlignHCenter
}
Row {
width: parent.width
spacing: Style.space(8)
Button {
text: "Not now (Esc)"
iconText: "󰅘"
fontFamily: screen.panel.fontFamily
fontSize: Style.font.bodySmall
focusable: true
onClicked: screen.panel.denySshRequest()
}
Button {
visible: screen.panel.sshUnlockPendingCount > 1
text: "Deny all (" + screen.panel.sshUnlockPendingCount + ")"
iconText: "󰅙"
fontFamily: screen.panel.fontFamily
fontSize: Style.font.bodySmall
focusable: true
onClicked: screen.panel.denyAllSshRequests()
}
Item { width: Math.max(0, parent.width - Style.space(screen.panel.sshUnlockPendingCount > 1 ? 280 : 160)); height: 1 }
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
text: screen.panel.sshPromptRemainingSec + "s left"
color: screen.panel.sshPromptRemainingSec <= 5
? screen.panel.urgent : screen.panel.dim
font.family: screen.panel.fontFamily
font.pixelSize: Style.font.caption
}
}
}
@@ -0,0 +1,127 @@
import QtQuick
import qs.Commons
import qs.Ui
// A transient panel-local notice. It anchors to its parent as an overlay and
// deliberately reports no height to the parent's content layout.
BorderSurface {
id: root
required property string statusMessage
required property string errorMessage
required property bool statusSuppressed
required property color foreground
required property color surfaceColor
required property color accentColor
required property color urgentColor
required property string fontFamily
readonly property bool showsError: root.errorMessage !== ""
readonly property bool showsStatus: root.statusMessage !== "" && !root.statusSuppressed
readonly property bool shown: showsError || showsStatus
readonly property color tone: showsError ? root.urgentColor : root.accentColor
// Optional recovery offered alongside an error. Empty means none.
property string actionLabel: ""
signal errorDismissed()
signal actionRequested()
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.space(10)
width: Math.min(parent.width - Style.space(20), Style.space(390))
implicitHeight: noticeRow.implicitHeight + Style.space(16)
z: 20
visible: opacity > 0
enabled: shown
opacity: shown ? 1 : 0
color: root.surfaceColor
radius: Style.cornerRadius
borderSpec: Border.surfaceSpec("menu", "border", root.tone, 1)
Accessible.role: Accessible.AlertMessage
Accessible.name: (root.showsError ? "Needs attention: " : "Status: ") + noticeMessage.text
Accessible.ignored: !root.shown
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutQuad }
}
// Consume pointer presses on the floating surface so covered controls
// cannot be activated through it.
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
}
Row {
id: noticeRow
anchors.fill: parent
anchors.margins: Style.space(8)
spacing: Style.space(8)
Text {
textFormat: Text.PlainText
id: noticeIcon
anchors.verticalCenter: parent.verticalCenter
text: root.showsError ? "󰅚" : "󰋼"
color: root.tone
font.family: root.fontFamily
font.pixelSize: Style.font.body
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - noticeIcon.implicitWidth - Style.space(8)
- (dismissNoticeButton.visible
? dismissNoticeButton.implicitWidth + Style.space(8)
: 0)
spacing: Style.space(2)
Text {
textFormat: Text.PlainText
width: parent.width
text: root.showsError ? "NEEDS ATTENTION" : "STATUS"
color: root.tone
font.family: root.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
}
Text {
textFormat: Text.PlainText
id: noticeMessage
width: parent.width
text: root.showsError ? root.errorMessage : root.statusMessage
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
wrapMode: Text.Wrap
}
}
// An error the user can do something about carries the doing with it. A
// failed save is the case this exists for: the message says the vault
// refused it, and the button is the way back to what was typed.
PanelActionButton {
id: noticeActionButton
visible: root.showsError && root.actionLabel !== ""
anchors.verticalCenter: parent.verticalCenter
iconText: "󰑌"
tooltipText: root.actionLabel
fontFamily: root.fontFamily
onClicked: root.actionRequested()
}
PanelActionButton {
id: dismissNoticeButton
visible: root.showsError
anchors.verticalCenter: parent.verticalCenter
iconText: "󰅖"
tooltipText: "Dismiss message"
fontFamily: root.fontFamily
onClicked: root.errorDismissed()
}
}
}
@@ -0,0 +1,37 @@
import QtQuick
// Wheel scrolling for a Flickable, at a rate chosen here rather than inherited.
//
// Qt moves a Flickable by the platform's wheel-scroll-lines, which is tuned for
// a full-screen document and is a crawl in a panel three hundred pixels tall:
// several turns of the wheel to cross one screen of settings. The step below is
// a multiple of that, applied identically everywhere so no two views in the
// panel scroll at different speeds.
//
// Placed inside the Flickable it drives, which it takes as its target. It
// accepts the event, so the Flickable's own slower handling does not also run.
WheelHandler {
id: root
required property Flickable view
// Pixels per wheel notch. A notch is 120 eighths-of-a-degree. Roughly twice
// what Qt would move on its own -- enough that a screenful is a couple of
// turns rather than half a dozen, and not so much that a notch overshoots
// the thing being scrolled to. One number, one place; every view reads it.
property real step: 90
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: function(event) {
var notches = event.angleDelta.y / 120
if (notches === 0) return
// A touchpad sends many small deltas rather than whole notches, and
// multiplying those the same way overshoots wildly. Scale the fractional
// part as it comes; only whole notches get the full step.
var limit = Math.max(0, root.view.contentHeight - root.view.height)
var next = root.view.contentY - notches * root.step
root.view.contentY = Math.max(0, Math.min(limit, next))
event.accepted = true
}
}
Loaded 50 of 300 files, more files were not shown because too many files have changed in this diff. Show more