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:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"format": "omarchy-config",
|
||||
"version": 1,
|
||||
"synced_by": "gladimdim.config-sync"
|
||||
}
|
||||
@@ -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" })
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Extra autostart processes.
|
||||
-- o.launch_on_start("my-service")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
|
||||
# }
|
||||
@@ -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 })
|
||||
@@ -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,
|
||||
-- },
|
||||
-- })
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
screencopy {
|
||||
allow_token_by_default = true
|
||||
custom_picker_binary = hyprland-preview-share-picker
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
██████████████████████████████████████████████████████
|
||||
██████████████████████████████████████████████████████
|
||||
████ ████ ████
|
||||
████ ████ ████
|
||||
████ █████████████████████ ████████ ████
|
||||
████ █████████████████████ ████████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████████████ ████ ████
|
||||
████████████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ██████████████████████████████████████ ████
|
||||
████ ██████████████████████████████████████ ████
|
||||
████ ████ ████
|
||||
████ ████ ████
|
||||
█████████████████████████████ ████████████████████
|
||||
█████████████████████████████ ████████████████████
|
||||
@@ -0,0 +1,10 @@
|
||||
▄▄▄
|
||||
▄█████▄ ▄███████████▄ ▄███████ ▄███████ ▄███████ ▄█ █▄ ▄█ █▄
|
||||
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
|
||||
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▀ ███ ███ ███ ███
|
||||
███ ███ ███ ███ ███ ▄███▄▄▄███ ▄███▄▄▄██▀ ███ ▄███▄▄▄███▄ ███▄▄▄███
|
||||
███ ███ ███ ███ ███ ▀███▀▀▀███ ▀███▀▀▀▀ ███ ▀▀███▀▀▀███ ▀▀▀▀▀▀███
|
||||
███ ███ ███ ███ ███ ███ ███ ██████████ ███ █▄ ███ ███ ▄██ ███
|
||||
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
|
||||
▀█████▀ ▀█ ███ █▀ ███ █▀ ███ ███ ███████▀ ███ █▀ ▀█████▀
|
||||
███ █▀
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[font]
|
||||
base-size = 9
|
||||
@@ -0,0 +1 @@
|
||||
azure-glow
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,4 @@
|
||||
general {
|
||||
col.active_border = rgb(00ccff) rgb(0055ff) 45deg
|
||||
col.inactive_border = rgb(00334d)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Yaru-blue
|
||||
@@ -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
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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
File diff suppressed because one or more lines are too long
@@ -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
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user