diff --git a/bin/omarchy-capture-screenrecording b/bin/omarchy-capture-screenrecording index 75a5c207..a466cffd 100755 --- a/bin/omarchy-capture-screenrecording +++ b/bin/omarchy-capture-screenrecording @@ -251,6 +251,8 @@ stop_screenrecording() { toggle_screenrecording_indicator() { pkill -RTMIN+8 waybar + OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} + quickshell ipc -p "$OMARCHY_PATH/default/quickshell/bar" call bar refreshScreenRecording >/dev/null 2>&1 || true } screenrecording_active() { diff --git a/bin/omarchy-font-current b/bin/omarchy-font-current index ead8c3a3..fb032e9b 100755 --- a/bin/omarchy-font-current +++ b/bin/omarchy-font-current @@ -3,4 +3,28 @@ # omarchy:summary=Show current monospace font # omarchy:examples=omarchy font current -grep -oP 'font-family:\s*["'\'']?\K[^;"'\'']+' ~/.config/waybar/style.css | head -n1 +bar_config="$HOME/.config/omarchy/bar.json" + +if [[ -f $bar_config ]]; then + font_family=$(python3 - "$bar_config" <<'PY' +import json +import sys + +try: + with open(sys.argv[1]) as file: + data = json.load(file) +except (FileNotFoundError, json.JSONDecodeError): + data = {} + +font = data.get("fontFamily") if isinstance(data, dict) else None +if font: + print(font) +PY +) +fi + +if [[ -n $font_family ]]; then + printf '%s\n' "$font_family" +else + grep -oP 'font-family:\s*["'\'']?\K[^;"'\'']+' ~/.config/waybar/style.css | head -n1 +fi diff --git a/bin/omarchy-font-set b/bin/omarchy-font-set index 44bb9833..a62b7b8c 100755 --- a/bin/omarchy-font-set +++ b/bin/omarchy-font-set @@ -28,6 +28,26 @@ if [[ -n $font_name ]]; then sed -i "s/font_family = .*/font_family = $font_name/g" ~/.config/hypr/hyprlock.conf sed -i "s/font-family: .*/font-family: '$font_name';/g" ~/.config/waybar/style.css + mkdir -p "$HOME/.config/omarchy" + python3 - "$HOME/.config/omarchy/bar.json" "$font_name" <<'PY' +import json +import sys + +path, font_name = sys.argv[1], sys.argv[2] +try: + with open(path) as file: + data = json.load(file) + if not isinstance(data, dict): + data = {} +except (FileNotFoundError, json.JSONDecodeError): + data = {} + +data["fontFamily"] = font_name + +with open(path, "w") as file: + json.dump(data, file, indent=2) + file.write("\n") +PY sed -i "s/font-family: .*/font-family: '$font_name';/g" ~/.config/swayosd/style.css xmlstarlet ed -L \ -u '//match[@target="pattern"][test/string="monospace"]/edit[@name="family"]/string' \ @@ -35,6 +55,10 @@ if [[ -n $font_name ]]; then ~/.config/fontconfig/fonts.conf omarchy-restart-waybar + OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} + if quickshell list -p "$OMARCHY_PATH/default/quickshell/bar" 2>/dev/null | grep -q '^Instance '; then + omarchy-restart-bar + fi omarchy-restart-swayosd if pgrep -x ghostty; then diff --git a/bin/omarchy-menu b/bin/omarchy-menu index 6f372be2..25f56a3b 100755 --- a/bin/omarchy-menu +++ b/bin/omarchy-menu @@ -218,7 +218,7 @@ show_toggle_menu() { *Nightlight*) omarchy-toggle-nightlight ;; *Idle*) omarchy-toggle-idle ;; *Notifications*) omarchy-toggle-notification-silencing ;; - *Bar*) omarchy-toggle-waybar ;; + *Bar*) omarchy-toggle-bar ;; *Layout*) omarchy-hyprland-workspace-layout-toggle ;; *Ratio*) omarchy-hyprland-window-single-square-aspect-toggle ;; *Gaps*) omarchy-hyprland-window-gaps-toggle ;; @@ -271,7 +271,7 @@ show_hardware_touchpad_haptics_menu() { } show_style_menu() { - case $(menu "Style" "󰸌 Theme\n󰟵 Unlock\n Font\n Background\n󰍜 Waybar\n󰘇 Corners\n Hyprland\n󱄄 Screensaver\n About") in + case $(menu "Style" "󰸌 Theme\n󰟵 Unlock\n Font\n Background\n󰍜 Bar\n󰘇 Corners\n Hyprland\n󱄄 Screensaver\n About") in *Theme*) show_theme_menu ;; *Unlock*) omarchy-launch-walker -m menus:omarchyunlocks --width 800 --minheight 400 ;; @@ -279,7 +279,7 @@ show_style_menu() { *Background*) show_background_menu ;; *Corners*) show_style_corners_menu ;; *Hyprland*) open_in_editor "$(hypr_config_file looknfeel)" ;; - *Waybar*) show_waybar_position_menu ;; + *Bar*) show_bar_position_menu ;; *Screensaver*) show_screensaver_menu ;; *About*) show_about_menu ;; *) show_main_menu ;; @@ -294,12 +294,12 @@ show_style_corners_menu() { esac } -show_waybar_position_menu() { - case $(menu "Waybar Position" "󰁝 Top\n󰁅 Bottom\n󰁍 Left\n󰁔 Right") in - *Top*) omarchy-style-waybar-position top ;; - *Bottom*) omarchy-style-waybar-position bottom ;; - *Left*) omarchy-style-waybar-position left ;; - *Right*) omarchy-style-waybar-position right ;; +show_bar_position_menu() { + case $(menu "Bar Position" "󰁝 Top\n󰁅 Bottom\n󰁍 Left\n󰁔 Right") in + *Top*) omarchy-style-bar-position top ;; + *Bottom*) omarchy-style-bar-position bottom ;; + *Left*) omarchy-style-bar-position left ;; + *Right*) omarchy-style-bar-position right ;; *) show_style_menu ;; esac } @@ -489,14 +489,14 @@ show_setup_default_editor_menu() { } show_setup_config_menu() { - case $(menu "Setup" " Hyprland\n Hypridle\n Hyprlock\n Hyprsunset\n Swayosd\n󰌧 Walker\n󰍜 Waybar\n󰞅 XCompose") in + case $(menu "Setup" " Hyprland\n Hypridle\n Hyprlock\n Hyprsunset\n Swayosd\n󰌧 Walker\n󰍜 Bar\n󰞅 XCompose") in *Hyprland*) open_in_editor "$(hypr_config_file hyprland)" ;; *Hypridle*) open_in_editor ~/.config/hypr/hypridle.conf && omarchy-restart-hypridle ;; *Hyprlock*) open_in_editor ~/.config/hypr/hyprlock.conf ;; *Hyprsunset*) open_in_editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset ;; *Swayosd*) open_in_editor ~/.config/swayosd/config.toml && omarchy-restart-swayosd ;; *Walker*) open_in_editor ~/.config/walker/config.toml && omarchy-restart-walker ;; - *Waybar*) open_in_editor ~/.config/waybar/config.jsonc && omarchy-restart-waybar ;; + *Bar*) open_in_editor ~/.config/omarchy/bar.json && omarchy-restart-bar ;; *XCompose*) open_in_editor ~/.XCompose && omarchy-restart-xcompose ;; *) show_setup_menu ;; esac @@ -813,19 +813,19 @@ show_update_channel_menu() { esac } show_update_process_menu() { - case $(menu "Restart" " Hypridle\n Hyprsunset\n󰎟 Mako\n Swayosd\n󰌧 Walker\n󰍜 Waybar") in + case $(menu "Restart" " Hypridle\n Hyprsunset\n󰎟 Mako\n Swayosd\n󰌧 Walker\n󰍜 Bar") in *Hypridle*) omarchy-restart-hypridle ;; *Hyprsunset*) omarchy-restart-hyprsunset ;; *Mako*) omarchy-restart-mako ;; *Swayosd*) omarchy-restart-swayosd ;; *Walker*) omarchy-restart-walker ;; - *Waybar*) omarchy-restart-waybar ;; + *Bar*) omarchy-restart-bar ;; *) show_update_menu ;; esac } show_update_config_menu() { - case $(menu "Use default config" " Hyprland\n Hypridle\n Hyprlock\n Hyprsunset\n󱣴 Plymouth\n Swayosd\n Tmux\n󰌧 Walker\n󰍜 Waybar") in + case $(menu "Use default config" " Hyprland\n Hypridle\n Hyprlock\n Hyprsunset\n󱣴 Plymouth\n Swayosd\n Tmux\n󰌧 Walker\n󰍜 Bar") in *Hyprland*) present_terminal omarchy-refresh-hyprland ;; *Hypridle*) present_terminal omarchy-refresh-hypridle ;; *Hyprlock*) present_terminal omarchy-refresh-hyprlock ;; @@ -834,7 +834,7 @@ show_update_config_menu() { *Swayosd*) present_terminal omarchy-refresh-swayosd ;; *Tmux*) present_terminal omarchy-refresh-tmux ;; *Walker*) present_terminal omarchy-refresh-walker ;; - *Waybar*) present_terminal omarchy-refresh-waybar ;; + *Bar*) present_terminal omarchy-refresh-bar ;; *) show_update_menu ;; esac } diff --git a/bin/omarchy-refresh-bar b/bin/omarchy-refresh-bar new file mode 100755 index 00000000..8d57fbb9 --- /dev/null +++ b/bin/omarchy-refresh-bar @@ -0,0 +1,17 @@ +#!/bin/bash + +# omarchy:summary=Reset bar user overrides to Omarchy defaults +# omarchy:examples=omarchy refresh bar + +set -e + +OMARCHY_PATH="${OMARCHY_PATH:-$HOME/.local/share/omarchy}" +DEFAULT_CONFIG="$OMARCHY_PATH/config/omarchy/bar.json" + +if [[ ! -f $DEFAULT_CONFIG ]]; then + echo "Missing default bar config at $DEFAULT_CONFIG" >&2 + exit 1 +fi + +omarchy-refresh-config omarchy/bar.json +omarchy-restart-bar diff --git a/bin/omarchy-refresh-config b/bin/omarchy-refresh-config index 0e46925a..a4733803 100755 --- a/bin/omarchy-refresh-config +++ b/bin/omarchy-refresh-config @@ -18,8 +18,9 @@ USAGE fi # Backup the destination file (with timestamp) to avoid clobbering (Ex: hyprlock.conf.bak.1753817951) +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} user_config_file="${HOME}/.config/$config_file" -default_config_file="${HOME}/.local/share/omarchy/config/$config_file" +default_config_file="$OMARCHY_PATH/config/$config_file" backup_config_file="$user_config_file.bak.$(date +%s)" mkdir -p "$(dirname "$user_config_file")" diff --git a/bin/omarchy-restart-bar b/bin/omarchy-restart-bar new file mode 100755 index 00000000..b1302f56 --- /dev/null +++ b/bin/omarchy-restart-bar @@ -0,0 +1,10 @@ +#!/bin/bash + +# omarchy:summary=Restart the bar +# omarchy:examples=omarchy restart bar + +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} +CONFIG_DIR="$OMARCHY_PATH/default/quickshell/bar" + +quickshell kill -p "$CONFIG_DIR" >/dev/null 2>&1 || true +setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$CONFIG_DIR" >/dev/null 2>&1 & diff --git a/bin/omarchy-style-bar-position b/bin/omarchy-style-bar-position new file mode 100755 index 00000000..1e84d745 --- /dev/null +++ b/bin/omarchy-style-bar-position @@ -0,0 +1,39 @@ +#!/bin/bash + +# omarchy:summary=Set bar position +# omarchy:args= +# omarchy:examples=omarchy-style-bar-position top | omarchy style bar position bottom + +set -e + +CONFIG_FILE="$HOME/.config/omarchy/bar.json" +position=$1 + +if [[ ! $position =~ ^(top|bottom|left|right)$ ]]; then + echo "Usage: omarchy-style-bar-position top|bottom|left|right" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$CONFIG_FILE")" +python3 - "$CONFIG_FILE" "$position" <<'PY' +import json +import os +import sys + +path, position = sys.argv[1], sys.argv[2] +try: + with open(path) as file: + data = json.load(file) + if not isinstance(data, dict): + data = {} +except (FileNotFoundError, json.JSONDecodeError): + data = {} + +data["position"] = position + +with open(path, "w") as file: + json.dump(data, file, indent=2) + file.write("\n") +PY + +omarchy-restart-bar diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index 8dafed8b..bc26b126 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -53,6 +53,10 @@ fi if pgrep -x waybar >/dev/null; then omarchy-restart-waybar fi +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} +if quickshell list -p "$OMARCHY_PATH/default/quickshell/bar" 2>/dev/null | grep -q '^Instance '; then + omarchy-restart-bar +fi omarchy-restart-swayosd omarchy-restart-terminal omarchy-restart-hyprctl diff --git a/bin/omarchy-toggle-bar b/bin/omarchy-toggle-bar new file mode 100755 index 00000000..572b9aad --- /dev/null +++ b/bin/omarchy-toggle-bar @@ -0,0 +1,15 @@ +#!/bin/bash + +# omarchy:summary=Toggle bar visibility +# omarchy:examples=omarchy toggle bar + +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} +CONFIG_DIR="$OMARCHY_PATH/default/quickshell/bar" + +omarchy-toggle bar-off + +if quickshell list -p "$CONFIG_DIR" 2>/dev/null | grep -q '^Instance '; then + quickshell kill -p "$CONFIG_DIR" >/dev/null 2>&1 || true +else + uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$CONFIG_DIR" >/dev/null 2>&1 & +fi diff --git a/bin/omarchy-toggle-idle b/bin/omarchy-toggle-idle index 622f1ab4..b81ab84b 100755 --- a/bin/omarchy-toggle-idle +++ b/bin/omarchy-toggle-idle @@ -11,3 +11,5 @@ else fi pkill -RTMIN+9 waybar +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} +quickshell ipc -p "$OMARCHY_PATH/default/quickshell/bar" call bar refreshIndicators >/dev/null 2>&1 || true diff --git a/bin/omarchy-toggle-notification-silencing b/bin/omarchy-toggle-notification-silencing index 5e327ec2..8045814f 100755 --- a/bin/omarchy-toggle-notification-silencing +++ b/bin/omarchy-toggle-notification-silencing @@ -11,3 +11,5 @@ else fi pkill -RTMIN+10 waybar +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} +quickshell ipc -p "$OMARCHY_PATH/default/quickshell/bar" call bar refreshIndicators >/dev/null 2>&1 || true diff --git a/bin/omarchy-update-available-reset b/bin/omarchy-update-available-reset index d1e2ca4c..282c1e14 100755 --- a/bin/omarchy-update-available-reset +++ b/bin/omarchy-update-available-reset @@ -1,6 +1,8 @@ #!/bin/bash -# omarchy:summary=Ensure Waybar icon offering the available update is removed +# omarchy:summary=Ensure the status bar icon offering the available update is removed pkill -RTMIN+7 waybar +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} +quickshell ipc -p "$OMARCHY_PATH/default/quickshell/bar" call bar refreshUpdate >/dev/null 2>&1 || true exit 0 diff --git a/bin/omarchy-voxtype-install b/bin/omarchy-voxtype-install index 32a1153e..cdcfe22a 100755 --- a/bin/omarchy-voxtype-install +++ b/bin/omarchy-voxtype-install @@ -21,5 +21,9 @@ if gum confirm "Install Voxtype + AI model (~150MB) to enable dictation?"; then voxtype setup systemd omarchy-restart-waybar + OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} + if quickshell list -p "$OMARCHY_PATH/default/quickshell/bar" 2>/dev/null | grep -q '^Instance '; then + omarchy-restart-bar + fi notify-send " Voxtype Dictation Ready" "Hold F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000 fi diff --git a/bin/omarchy-voxtype-model b/bin/omarchy-voxtype-model index 00ccb426..65619584 100755 --- a/bin/omarchy-voxtype-model +++ b/bin/omarchy-voxtype-model @@ -6,3 +6,7 @@ set -e omarchy-launch-floating-terminal-with-presentation "voxtype setup model" omarchy-restart-waybar +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} +if quickshell list -p "$OMARCHY_PATH/default/quickshell/bar" 2>/dev/null | grep -q '^Instance '; then + omarchy-restart-bar +fi diff --git a/config/omarchy/bar.json b/config/omarchy/bar.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/config/omarchy/bar.json @@ -0,0 +1 @@ +{} diff --git a/default/hypr/autostart.lua b/default/hypr/autostart.lua index 7eed5fbb..1e3ead8a 100644 --- a/default/hypr/autostart.lua +++ b/default/hypr/autostart.lua @@ -1,7 +1,7 @@ hl.on("hyprland.start", function() hl.exec_cmd("uwsm-app -- hypridle") hl.exec_cmd("uwsm-app -- mako") - hl.exec_cmd("! omarchy-toggle-enabled waybar-off && uwsm-app -- waybar") + hl.exec_cmd("! omarchy-toggle-enabled bar-off && uwsm-app -- env OMARCHY_PATH=$HOME/.local/share/omarchy quickshell -p $HOME/.local/share/omarchy/default/quickshell/bar") hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem") hl.exec_cmd("uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill") hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1") diff --git a/default/hypr/bindings/utilities.lua b/default/hypr/bindings/utilities.lua index cb75f755..87ff6ba7 100644 --- a/default/hypr/bindings/utilities.lua +++ b/default/hypr/bindings/utilities.lua @@ -11,11 +11,11 @@ hl.bind("SUPER + K", hl.dsp.exec_cmd("omarchy-menu-keybindings"), { description hl.bind("SUPER + ALT + K", hl.dsp.exec_cmd("omarchy-menu-tmux-keybindings"), { description = "Show Tmux key bindings" }) hl.bind("XF86Calculator", hl.dsp.exec_cmd("gnome-calculator"), { description = "Calculator" }) -hl.bind("SUPER + SHIFT + SPACE", hl.dsp.exec_cmd("omarchy-toggle-waybar"), { description = "Toggle top bar" }) -hl.bind("SUPER + SHIFT + CTRL + UP", hl.dsp.exec_cmd("omarchy-style-waybar-position top"), { description = "Move Waybar to top" }) -hl.bind("SUPER + SHIFT + CTRL + DOWN", hl.dsp.exec_cmd("omarchy-style-waybar-position bottom"), { description = "Move Waybar to bottom" }) -hl.bind("SUPER + SHIFT + CTRL + LEFT", hl.dsp.exec_cmd("omarchy-style-waybar-position left"), { description = "Move Waybar to left" }) -hl.bind("SUPER + SHIFT + CTRL + RIGHT", hl.dsp.exec_cmd("omarchy-style-waybar-position right"), { description = "Move Waybar to right" }) +hl.bind("SUPER + SHIFT + SPACE", hl.dsp.exec_cmd("omarchy-toggle-bar"), { description = "Toggle top bar" }) +hl.bind("SUPER + SHIFT + CTRL + UP", hl.dsp.exec_cmd("omarchy-style-bar-position top"), { description = "Move bar to top" }) +hl.bind("SUPER + SHIFT + CTRL + DOWN", hl.dsp.exec_cmd("omarchy-style-bar-position bottom"), { description = "Move bar to bottom" }) +hl.bind("SUPER + SHIFT + CTRL + LEFT", hl.dsp.exec_cmd("omarchy-style-bar-position left"), { description = "Move bar to left" }) +hl.bind("SUPER + SHIFT + CTRL + RIGHT", hl.dsp.exec_cmd("omarchy-style-bar-position right"), { description = "Move bar to right" }) hl.bind("SUPER + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-menu background"), { description = "Background switcher" }) hl.bind("SUPER + SHIFT + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-menu theme"), { description = "Theme menu" }) hl.bind("SUPER + BACKSPACE", hl.dsp.exec_cmd("omarchy-hyprland-window-transparency-toggle"), { description = "Toggle window transparency" }) diff --git a/default/quickshell/bar/README.md b/default/quickshell/bar/README.md new file mode 100644 index 00000000..ed844bc8 --- /dev/null +++ b/default/quickshell/bar/README.md @@ -0,0 +1,108 @@ +# Omarchy bar + +This is the Quickshell implementation of the Omarchy status bar. + +- `shell.qml` is Omarchy-owned bar engine code. Users should not edit it directly. +- `bar-defaults.json` is the Omarchy-owned default layout and module settings. +- User overrides live in `~/.config/omarchy/bar.json` and are merged over defaults at runtime. +- `omarchy-style-bar-position` updates only the user override file. +- Wi-Fi/LAN state is polled from the active route and NetworkManager so the QML can mirror Waybar's network icons. +- Tooltips use a small Quickshell `PopupWindow`, not Qt Quick Controls' clipped default tooltip. + +Example user override: + +```json +{ + "position": "right", + "layout": { + "left": ["omarchy", "workspaces"], + "center": ["clock", "weather", "update"], + "right": ["tray", "bluetooth", "network", "audio", "cpu", "battery"] + }, + "modules": { + "clock": { + "format": "HH:mm", + "formatAlt": "dd MMMM 'W'ww yyyy", + "verticalFormat": "HH\n—\nmm" + } + } +} +``` + +`centerAnchor` defaults to `clock`. When the anchor module is present in the `center` list, it is pinned to the exact center of the bar and modules before/after it flank that anchor. If the anchor is not in the center list, the center list is centered as a group. + +Available built-in modules: `omarchy`, `workspaces`, `clock`, `weather`, `update`, `voxtype`, `screenRecording`, `idle`, `notifications`, `tray`, `bluetooth`, `network`, `audio`, `cpu`, `battery`. + +## Custom modules + +Add a module name to a layout list, then define it under `modules` in `~/.config/omarchy/bar.json`. + +For simple text/JSON output, use a command module: + +```json +{ + "layout": { + "right": ["tray", "vpn", "audio", "cpu"] + }, + "modules": { + "vpn": { + "type": "command", + "exec": "~/.config/omarchy/bar/scripts/vpn-status", + "interval": 5, + "tooltip": "VPN", + "onClick": "nm-connection-editor" + } + } +} +``` + +The command may print plain text or Waybar-style JSON, for example: + +```json +{"text":"󰌆","tooltip":"Work VPN","class":"active"} +``` + +For advanced custom UI, use QML: + +```json +{ + "layout": { + "right": ["gpu", "audio", "cpu"] + }, + "modules": { + "gpu": { + "type": "qml" + } + } +} +``` + +Then create `~/.config/omarchy/bar/modules/gpu.qml`. If you want to store it elsewhere, add a `source` path. + +Custom QML modules should be an `Item` with `implicitWidth` and `implicitHeight`. They may optionally define these properties, which the bar fills after loading: + +```qml +import QtQuick + +Item { + property var bar + property string moduleName + property var settings + + implicitWidth: 28 + implicitHeight: bar ? bar.barSize : 26 + + Text { + anchors.centerIn: parent + text: "GPU" + color: bar ? bar.foreground : "white" + font.family: bar ? bar.fontFamily : "monospace" + font.pixelSize: 12 + } + + MouseArea { + anchors.fill: parent + onClicked: if (bar) bar.run("omarchy-launch-or-focus-tui btop") + } +} +``` diff --git a/default/quickshell/bar/bar-defaults.json b/default/quickshell/bar/bar-defaults.json new file mode 100644 index 00000000..546bb1dc --- /dev/null +++ b/default/quickshell/bar/bar-defaults.json @@ -0,0 +1,17 @@ +{ + "position": "top", + "fontFamily": "JetBrainsMono Nerd Font", + "centerAnchor": "clock", + "layout": { + "left": ["omarchy", "workspaces"], + "center": ["clock", "weather", "update", "voxtype", "screenRecording", "idle", "notifications"], + "right": ["tray", "bluetooth", "network", "audio", "cpu", "battery"] + }, + "modules": { + "clock": { + "format": "dddd HH:mm", + "formatAlt": "dd MMMM 'W'ww yyyy", + "verticalFormat": "HH\n—\nmm" + } + } +} diff --git a/default/quickshell/bar/shell.qml b/default/quickshell/bar/shell.qml new file mode 100644 index 00000000..1c1c3a14 --- /dev/null +++ b/default/quickshell/bar/shell.qml @@ -0,0 +1,1512 @@ +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Hyprland +import Quickshell.Io +import Quickshell.Services.Pipewire +import Quickshell.Services.SystemTray +import Quickshell.Services.UPower +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +ShellRoot { + id: root + + property string home: Quickshell.env("HOME") + property string omarchyPath: Quickshell.env("OMARCHY_PATH") || (home + "/.local/share/omarchy") + property string omarchyConfigDir: home + "/.config/omarchy" + property var builtinBarConfig: ({ + position: "top", + fontFamily: "JetBrainsMono Nerd Font", + centerAnchor: "clock", + layout: { + left: ["omarchy", "workspaces"], + center: ["clock", "weather", "update", "voxtype", "screenRecording", "idle", "notifications"], + right: ["tray", "bluetooth", "network", "audio", "cpu", "battery"] + }, + modules: { + clock: { + format: "dddd HH:mm", + formatAlt: "dd MMMM 'W'ww yyyy", + verticalFormat: "HH\n—\nmm" + } + } + }) + property var defaultBarConfig: builtinBarConfig + property var userBarConfig: ({}) + property var layoutConfig: builtinBarConfig.layout + property var moduleConfig: builtinBarConfig.modules + property string centerAnchor: "clock" + property int barConfigSerial: 0 + property string position: "top" + property string fontFamily: "JetBrainsMono Nerd Font" + property color foreground: "#cacccc" + property color background: "#101315" + property color urgent: "#a55555" + property string weatherText: "" + property string weatherClass: "" + property bool updateAvailable: false + property string voxtypeIcon: "" + property string voxtypeClass: "" + property string screenRecordingText: "" + property string screenRecordingTooltip: "" + property string idleText: "" + property string idleTooltip: "" + property string notificationSilencingText: "" + property string notificationSilencingTooltip: "" + property int audioVolumePercent: -1 + property string networkKind: "disconnected" + property string networkLabel: "" + property int networkSignal: -1 + property string networkFrequency: "" + property bool clockAlt: false + property var tooltipTarget: null + property string tooltipText: "" + property bool tooltipShown: false + + readonly property bool vertical: position === "left" || position === "right" + readonly property int barSize: vertical ? 28 : 26 + + function normalizePosition(value) { + var next = String(value || "").trim() + return /^(top|bottom|left|right)$/.test(next) ? next : "top" + } + + function shellQuote(value) { + return "'" + String(value).replace(/'/g, "'\\''") + "'" + } + + function fileUrl(path) { + return "file://" + path.split("/").map(encodeURIComponent).join("/") + } + + function commandWithOmarchyPath(command) { + return "OMARCHY_PATH=" + shellQuote(omarchyPath) + " " + command + } + + function isPlainObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value) + } + + function cloneConfig(value) { + if (Array.isArray(value)) { + var arrayCopy = [] + for (var i = 0; i < value.length; i++) + arrayCopy.push(cloneConfig(value[i])) + return arrayCopy + } + + if (isPlainObject(value)) { + var objectCopy = {} + for (var key in value) + objectCopy[key] = cloneConfig(value[key]) + return objectCopy + } + + return value + } + + function mergeConfig(base, override) { + var result = cloneConfig(base || {}) + if (!isPlainObject(override)) return result + + for (var key in override) { + if (isPlainObject(result[key]) && isPlainObject(override[key])) + result[key] = mergeConfig(result[key], override[key]) + else + result[key] = cloneConfig(override[key]) + } + + return result + } + + function parseConfig(raw, label) { + var text = String(raw || "").trim() + if (!text) return {} + + try { + var parsed = JSON.parse(text) + return isPlainObject(parsed) ? parsed : {} + } catch (error) { + console.warn("Failed to parse " + label + ": " + error) + return {} + } + } + + function applyBarConfig() { + var config = mergeConfig(defaultBarConfig, userBarConfig) + + position = normalizePosition(config.position) + fontFamily = String(config.fontFamily || "JetBrainsMono Nerd Font") + centerAnchor = String(config.centerAnchor || "clock") + layoutConfig = isPlainObject(config.layout) ? config.layout : builtinBarConfig.layout + moduleConfig = isPlainObject(config.modules) ? config.modules : {} + barConfigSerial++ + } + + function loadDefaultBarConfig(raw) { + defaultBarConfig = mergeConfig(builtinBarConfig, parseConfig(raw, "bar defaults")) + applyBarConfig() + } + + function loadUserBarConfig(raw) { + userBarConfig = parseConfig(raw, "bar config") + applyBarConfig() + } + + function layoutModules(region) { + var serial = barConfigSerial + var modules = layoutConfig ? layoutConfig[region] : null + return Array.isArray(modules) ? modules : [] + } + + function moduleSettings(name) { + var serial = barConfigSerial + var settings = moduleConfig ? moduleConfig[String(name)] : null + return isPlainObject(settings) ? settings : {} + } + + function moduleString(name, key, fallback) { + var value = moduleSettings(name)[key] + return value === undefined || value === null ? fallback : String(value) + } + + function moduleIndex(modules, name) { + if (!Array.isArray(modules)) return -1 + + for (var i = 0; i < modules.length; i++) { + if (String(modules[i]) === name) + return i + } + + return -1 + } + + function modulesBefore(modules, name) { + var index = moduleIndex(modules, name) + return index <= 0 ? [] : modules.slice(0, index) + } + + function modulesAfter(modules, name) { + var index = moduleIndex(modules, name) + return index === -1 ? [] : modules.slice(index + 1) + } + + function builtinModuleComponent(name) { + switch (String(name)) { + case "omarchy": return omarchyModuleComponent + case "workspaces": return workspacesModuleComponent + case "clock": return clockModuleComponent + case "weather": return weatherModuleComponent + case "update": return updateModuleComponent + case "voxtype": return voxtypeModuleComponent + case "screenRecording": + case "screenrecording": return screenRecordingModuleComponent + case "idle": return idleModuleComponent + case "notifications": + case "notificationSilencing": return notificationsModuleComponent + case "tray": return trayModuleComponent + case "bluetooth": return bluetoothModuleComponent + case "network": return networkModuleComponent + case "audio": + case "pulseaudio": return audioModuleComponent + case "cpu": return cpuModuleComponent + case "battery": return batteryModuleComponent + default: return null + } + } + + function expandPath(path) { + var value = String(path || "") + if (value === "") return "" + if (value.indexOf("~/") === 0) return home + value.substring(1) + if (value.indexOf("$HOME/") === 0) return home + value.substring(5) + return value + } + + function customModuleSafeName(name) { + var value = String(name || "") + return value !== "" && value.indexOf("..") === -1 && value[0] !== "/" + } + + function customModuleType(name) { + var settings = moduleSettings(name) + var type = String(settings.type || "") + if (type) return type + if (settings.exec) return "command" + if (settings.source) return "qml" + return "" + } + + function customModuleSource(name) { + var settings = moduleSettings(name) + var source = settings.source ? expandPath(settings.source) : "" + if (!source && customModuleSafeName(name)) + source = omarchyConfigDir + "/bar/modules/" + String(name) + ".qml" + + return source ? fileUrl(source) : "" + } + + function networkCommand() { + return [ + "route_device=$(ip route get 1.1.1.1 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i == \"dev\") { print $(i + 1); exit } }')", + "connection_name() { command -v nmcli >/dev/null 2>&1 && nmcli -t -f GENERAL.CONNECTION device show \"$1\" 2>/dev/null | awk -F: '$1 == \"GENERAL.CONNECTION\" { print $2; exit }'; }", + "wifi_details() { command -v nmcli >/dev/null 2>&1 && nmcli -t -f IN-USE,SIGNAL,FREQ device wifi list --rescan no 2>/dev/null | awk -F: '$1 == \"*\" { print $2 \"\\t\" $3; exit }'; }", + "if [[ -n $route_device && ! -d /sys/class/net/$route_device/wireless ]]; then", + " label=$(connection_name \"$route_device\"); label=${label:-$route_device}", + " printf 'ethernet\\t%s\\t\\t\\n' \"$label\"", + "elif [[ -n $route_device ]]; then", + " label=$(connection_name \"$route_device\"); label=${label:-$route_device}", + " details=$(wifi_details)", + " signal=${details%%$'\\t'*}", + " frequency=${details#*$'\\t'}", + " [[ $frequency == \"$details\" ]] && frequency=\"\"", + " printf 'wifi\\t%s\\t%s\\t%s\\n' \"$label\" \"$signal\" \"$frequency\"", + "else", + " printf 'disconnected\\t\\t\\t\\n'", + "fi" + ].join("\n") + } + + function run(command) { + if (!command) return + + launcher.command = ["bash", "-lc", command] + launcher.startDetached() + } + + function runProcess(process) { + if (!process.running) + process.running = true + } + + function showTooltip(target, text) { + if (!text) return + + tooltipTarget = target + tooltipText = text + tooltipShown = false + tooltipTimer.restart() + } + + function hideTooltip(target) { + if (tooltipTarget !== target) return + + tooltipTimer.stop() + tooltipTarget = null + tooltipText = "" + tooltipShown = false + } + + function parseModuleJson(raw) { + var text = String(raw || "").trim() + if (!text) return {} + + var lines = text.split("\n") + try { + return JSON.parse(lines[lines.length - 1]) + } catch (error) { + return { text: text } + } + } + + function loadTheme(raw) { + var lines = String(raw || "").split("\n") + for (var i = 0; i < lines.length; i++) { + var match = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*["']?(#[0-9A-Fa-f]{6})/) + if (!match) continue + + if (match[1] === "foreground") foreground = match[2] + else if (match[1] === "background") background = match[2] + else if (match[1] === "red") urgent = match[2] + } + } + + function updateWeather(raw) { + var data = parseModuleJson(raw) + weatherText = data.text || "" + weatherClass = data.class || "" + } + + function updateIndicator(name, raw) { + var data = parseModuleJson(raw) + var text = data.text || "" + var tooltip = data.tooltip || "" + + if (name === "screenRecording") { + screenRecordingText = text + screenRecordingTooltip = tooltip + } else if (name === "idle") { + idleText = text + idleTooltip = tooltip + } else if (name === "notifications") { + notificationSilencingText = text + notificationSilencingTooltip = tooltip + } + } + + function updateVoxtype(raw) { + var data = parseModuleJson(raw) + var state = data.alt || data.class || "idle" + + voxtypeClass = state + if (state === "recording") voxtypeIcon = "󰍬" + else if (state === "transcribing") voxtypeIcon = "󰔟" + else voxtypeIcon = "" + } + + function updateNetwork(raw) { + var parts = String(raw || "disconnected\t\t\t").replace(/\r?\n+$/, "").split("\t") + networkKind = parts[0] || "disconnected" + networkLabel = parts[1] || "" + networkSignal = parts[2] ? parseInt(parts[2], 10) : -1 + networkFrequency = parts[3] || "" + } + + function refreshWeather() { + runProcess(weatherProc) + } + + function refreshUpdate() { + runProcess(updateProc) + } + + function refreshScreenRecording() { + runProcess(screenRecordingProc) + } + + function refreshIndicators() { + refreshScreenRecording() + runProcess(idleProc) + runProcess(notificationSilencingProc) + } + + function refreshNetwork() { + runProcess(networkProc) + } + + function updateAudioVolume(raw) { + var volume = parseInt(String(raw || "").trim(), 10) + audioVolumePercent = isNaN(volume) ? -1 : Math.max(0, volume) + } + + function refreshAudio() { + runProcess(audioProc) + } + + function workspaceById(id) { + var values = Hyprland.workspaces.values + for (var i = 0; i < values.length; i++) { + if (values[i].id === id) + return values[i] + } + + return null + } + + function workspaceIds() { + var ids = [1, 2, 3, 4, 5] + var values = Hyprland.workspaces.values + + for (var i = 0; i < values.length; i++) { + var id = values[i].id + if (id > 0 && id <= 10 && ids.indexOf(id) === -1) + ids.push(id) + } + + ids.sort(function(left, right) { return left - right }) + return ids + } + + function activeTrayItemCount() { + var count = 0 + var values = SystemTray.items.values + + for (var i = 0; i < values.length; i++) { + if (values[i].status !== Status.Passive) + count++ + } + + return count + } + + function batteryIcon() { + var device = UPower.displayDevice + if (!device || !device.isPresent) return "" + + var chargingIcons = ["󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋", "󰂅"] + var defaultIcons = ["󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹"] + var index = Math.max(0, Math.min(9, Math.floor(device.percentage / 10))) + + if (device.state === UPowerDeviceState.FullyCharged) return "󰂅" + if (!UPower.onBattery && device.state !== UPowerDeviceState.Charging) return "" + if (device.state === UPowerDeviceState.Charging) return chargingIcons[index] + return defaultIcons[index] + } + + function audioIcon() { + var sink = Pipewire.defaultAudioSink + if (!sink || !sink.audio) return "" + if (sink.audio.muted) return "" + + var props = sink.properties || {} + var sinkText = String([ + sink.name, + sink.description, + sink.nickname, + props["device.icon-name"], + props["device.product.name"], + props["node.name"] + ].join(" ")).toLowerCase() + + if (sinkText.indexOf("headphone") !== -1 || sinkText.indexOf("headset") !== -1) + return "" + + var volume = sink.audio.volume + if (!isFinite(volume) && audioVolumePercent >= 0) + volume = audioVolumePercent / 100 + if (!isFinite(volume)) return "" + if (volume < 0.34) return "" + if (volume < 0.67) return "" + return "" + } + + function bluetoothIcon() { + var adapter = Bluetooth.defaultAdapter + if (!adapter) return "" + if (!adapter.enabled) return "󰂲" + + var devices = Bluetooth.devices.values + for (var i = 0; i < devices.length; i++) { + if (devices[i].connected) + return "󰂱" + } + + return "" + } + + function networkIcon() { + if (networkKind === "wifi") { + var icons = ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"] + var index = Math.max(0, Math.min(4, Math.ceil(networkSignal / 20) - 1)) + return icons[index] + } + + if (networkKind === "ethernet") return "󰀂" + return "󰤮" + } + + function networkTooltip() { + if (networkKind === "wifi") { + var frequency = parseFloat(networkFrequency) + var frequencyText = frequency > 0 ? " (" + (frequency / 1000).toFixed(1) + " GHz)" : "" + return (networkLabel || "Wi-Fi") + frequencyText + } + + if (networkKind === "ethernet") return "Connected" + return "Disconnected" + } + + function audioTooltip() { + var sink = Pipewire.defaultAudioSink + if (sink && sink.audio) { + var volume = sink.audio.volume + if (isFinite(volume)) + return "Playing at " + Math.round(volume * 100) + "%" + } + + if (audioVolumePercent >= 0) + return "Playing at " + audioVolumePercent + "%" + + return "Audio" + } + + function bluetoothTooltip() { + var count = 0 + var devices = Bluetooth.devices.values + + for (var i = 0; i < devices.length; i++) { + if (devices[i].connected) + count++ + } + + return "Devices connected: " + count + } + + function batteryTooltip() { + var device = UPower.displayDevice + if (!device || !device.isPresent) return "" + + var direction = UPower.onBattery ? "↓" : "↑" + return Math.round(device.changeRate) + "W" + direction + " " + Math.round(device.percentage) + "%" + } + + function trayIconSource(icon) { + var value = String(icon || "") + var marker = "?path=" + var markerIndex = value.indexOf(marker) + if (markerIndex === -1) return value + + var name = value.substring(0, markerIndex).split("/").pop() + var iconPath = value.substring(markerIndex + marker.length).split("&")[0] + return fileUrl(iconPath + "/hicolor/16x16/status/" + name + ".png") + } + + function trayTooltip(item) { + return item.tooltipTitle || item.title || item.id || "" + } + + function focusWorkspace(id) { + root.run("hyprctl dispatch " + shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })")) + } + + function clockText() { + if (clockAlt) + return Qt.formatDateTime(systemClock.date, moduleString("clock", "formatAlt", "dd MMMM 'W'ww yyyy")) + + if (vertical) + return Qt.formatDateTime(systemClock.date, moduleString("clock", "verticalFormat", "HH\n—\nmm")) + + return Qt.formatDateTime(systemClock.date, moduleString("clock", "format", "dddd HH:mm")) + } + + SystemClock { + id: systemClock + precision: SystemClock.Minutes + } + + Process { id: launcher } + + Timer { + id: tooltipTimer + interval: 400 + onTriggered: root.tooltipShown = true + } + + FileView { + path: root.omarchyPath + "/default/quickshell/bar/bar-defaults.json" + watchChanges: true + printErrors: false + onLoaded: root.loadDefaultBarConfig(text()) + onFileChanged: { + reload() + root.loadDefaultBarConfig(text()) + } + } + + FileView { + path: root.omarchyConfigDir + "/bar.json" + watchChanges: true + printErrors: false + onLoaded: root.loadUserBarConfig(text()) + onFileChanged: { + reload() + root.loadUserBarConfig(text()) + } + } + + FileView { + path: root.home + "/.config/omarchy/current/theme/colors.toml" + watchChanges: true + printErrors: false + onLoaded: root.loadTheme(text()) + onFileChanged: { + reload() + root.loadTheme(text()) + } + } + + Process { + id: weatherProc + command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/default/waybar/weather.sh"))] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.updateWeather(text) + } + } + + Timer { + interval: 60000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.refreshWeather() + } + + Process { + id: updateProc + command: ["bash", "-lc", root.commandWithOmarchyPath("omarchy-update-available")] + stdout: StdioCollector { waitForEnd: true } + onExited: function(exitCode) { + root.updateAvailable = exitCode === 0 + } + } + + Timer { + interval: 21600000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.refreshUpdate() + } + + Process { + id: voxtypeProc + command: ["bash", "-lc", "omarchy-voxtype-status"] + running: true + stdout: SplitParser { + onRead: function(data) { + root.updateVoxtype(data) + } + } + } + + Process { + id: screenRecordingProc + command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/default/waybar/indicators/screen-recording.sh"))] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.updateIndicator("screenRecording", text) + } + } + + Process { + id: idleProc + command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/default/waybar/indicators/idle.sh"))] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.updateIndicator("idle", text) + } + } + + Process { + id: notificationSilencingProc + command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/default/waybar/indicators/notification-silencing.sh"))] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.updateIndicator("notifications", text) + } + } + + Timer { + interval: 2000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.refreshIndicators() + } + + Process { + id: networkProc + command: ["bash", "-lc", root.commandWithOmarchyPath(root.networkCommand())] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.updateNetwork(text) + } + } + + Timer { + interval: 3000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.refreshNetwork() + } + + Process { + id: audioProc + command: ["bash", "-lc", "if command -v pamixer >/dev/null; then pamixer --get-volume; elif command -v wpctl >/dev/null; then wpctl get-volume @DEFAULT_AUDIO_SINK@ | awk '{ print int($2 * 100) }'; fi"] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.updateAudioVolume(text) + } + } + + Timer { + interval: 2000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.refreshAudio() + } + + IpcHandler { + target: "bar" + + function refreshUpdate(): void { + root.refreshUpdate() + } + + function refreshScreenRecording(): void { + root.refreshScreenRecording() + } + + function refreshIndicators(): void { + root.refreshIndicators() + } + } + + Variants { + model: Quickshell.screens + + delegate: Component { + BarPanel { + required property var modelData + + screen: modelData + } + } + } + + component BarPanel: PanelWindow { + id: barWindow + + anchors { + top: root.position === "top" || root.vertical + bottom: root.position === "bottom" || root.vertical + left: root.position === "left" || !root.vertical + right: root.position === "right" || !root.vertical + } + + implicitWidth: root.vertical ? root.barSize : 0 + implicitHeight: root.vertical ? 0 : root.barSize + color: root.background + WlrLayershell.namespace: "omarchy-bar" + WlrLayershell.layer: WlrLayer.Top + + Loader { + anchors.fill: parent + sourceComponent: root.vertical ? verticalBar : horizontalBar + } + + PopupWindow { + id: tooltipWindow + + visible: root.tooltipShown && root.tooltipTarget !== null && root.tooltipText !== "" + color: "transparent" + implicitWidth: tooltipBubble.implicitWidth + implicitHeight: tooltipBubble.implicitHeight + + anchor { + id: tooltipAnchor + window: barWindow + adjustment: PopupAdjustment.Slide + edges: Edges.Top | Edges.Left + gravity: Edges.Bottom | Edges.Right + rect.width: 1 + rect.height: 1 + + onAnchoring: { + var target = root.tooltipTarget + if (!target) return + + var popupWidth = tooltipWindow.implicitWidth + var popupHeight = tooltipWindow.implicitHeight + var localX = target.width / 2 - popupWidth / 2 + var localY = target.height + 6 + + if (root.position === "bottom") { + localY = -popupHeight - 6 + } else if (root.position === "left") { + localX = target.width + 6 + localY = target.height / 2 - popupHeight / 2 + } else if (root.position === "right") { + localX = -popupWidth - 6 + localY = target.height / 2 - popupHeight / 2 + } + + var point = barWindow.contentItem.mapFromItem(target, localX, localY) + tooltipAnchor.rect.x = Math.round(point.x) + tooltipAnchor.rect.y = Math.round(point.y) + } + } + + Rectangle { + id: tooltipBubble + implicitWidth: tooltipLabel.implicitWidth + 20 + implicitHeight: tooltipLabel.implicitHeight + 14 + color: root.background + border.color: root.foreground + border.width: 1 + radius: 0 + opacity: 0.97 + + Text { + id: tooltipLabel + anchors.centerIn: parent + text: root.tooltipText + color: root.foreground + font.family: root.fontFamily + font.pixelSize: 12 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + + Component { + id: horizontalBar + + Item { + anchors.fill: parent + + CenterModules { anchors.fill: parent } + + LeftModules { + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + } + + RightModules { + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Component { + id: verticalBar + + Item { + anchors.fill: parent + + CenterModules { anchors.fill: parent } + + LeftModules { + anchors.top: parent.top + anchors.topMargin: 8 + anchors.horizontalCenter: parent.horizontalCenter + } + + RightModules { + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Component { id: emptyModuleComponent; Item { implicitWidth: 0; implicitHeight: 0; visible: false } } + Component { id: omarchyModuleComponent; OmarchyModule {} } + Component { id: workspacesModuleComponent; WorkspacesModule {} } + Component { id: clockModuleComponent; ClockModule {} } + Component { id: weatherModuleComponent; WeatherModule {} } + Component { id: updateModuleComponent; UpdateModule {} } + Component { id: voxtypeModuleComponent; VoxtypeModule {} } + Component { id: screenRecordingModuleComponent; ScreenRecordingModule {} } + Component { id: idleModuleComponent; IdleModule {} } + Component { id: notificationsModuleComponent; NotificationsModule {} } + Component { id: trayModuleComponent; TrayModule {} } + Component { id: bluetoothModuleComponent; BluetoothModule {} } + Component { id: networkModuleComponent; NetworkModule {} } + Component { id: audioModuleComponent; AudioModule {} } + Component { id: cpuModuleComponent; CpuModule {} } + Component { id: batteryModuleComponent; BatteryModule {} } + + component LeftModules: ModuleList { + modules: root.layoutModules("left") + } + + component RightModules: ModuleList { + modules: root.layoutModules("right") + } + + component CenterModules: Item { + id: centerRoot + + property var modules: root.layoutModules("center") + readonly property bool hasAnchor: root.moduleIndex(modules, root.centerAnchor) !== -1 + + Loader { + anchors.fill: parent + sourceComponent: root.vertical ? verticalCenterModules : horizontalCenterModules + } + + Component { + id: horizontalCenterModules + + Item { + anchors.fill: parent + + ModuleList { + visible: !centerRoot.hasAnchor + modules: centerRoot.modules + anchors.centerIn: parent + } + + ModuleList { + visible: centerRoot.hasAnchor + modules: root.modulesBefore(centerRoot.modules, root.centerAnchor) + anchors.right: centerAnchorModule.left + anchors.verticalCenter: centerAnchorModule.verticalCenter + } + + ModuleSlot { + id: centerAnchorModule + visible: centerRoot.hasAnchor + moduleName: root.centerAnchor + anchors.centerIn: parent + } + + ModuleList { + visible: centerRoot.hasAnchor + modules: root.modulesAfter(centerRoot.modules, root.centerAnchor) + anchors.left: centerAnchorModule.right + anchors.verticalCenter: centerAnchorModule.verticalCenter + } + } + } + + Component { + id: verticalCenterModules + + Item { + anchors.fill: parent + + ModuleList { + visible: !centerRoot.hasAnchor + modules: centerRoot.modules + anchors.centerIn: parent + } + + ModuleList { + visible: centerRoot.hasAnchor + modules: root.modulesBefore(centerRoot.modules, root.centerAnchor) + anchors.bottom: centerAnchorModule.top + anchors.horizontalCenter: centerAnchorModule.horizontalCenter + } + + ModuleSlot { + id: centerAnchorModule + visible: centerRoot.hasAnchor + moduleName: root.centerAnchor + anchors.centerIn: parent + } + + ModuleList { + visible: centerRoot.hasAnchor + modules: root.modulesAfter(centerRoot.modules, root.centerAnchor) + anchors.top: centerAnchorModule.bottom + anchors.horizontalCenter: centerAnchorModule.horizontalCenter + } + } + } + } + + component ModuleList: Loader { + id: moduleListRoot + + property var modules: [] + + visible: modules.length > 0 + sourceComponent: root.vertical ? verticalModuleList : horizontalModuleList + width: item ? item.implicitWidth : 0 + height: item ? item.implicitHeight : 0 + + Component { + id: horizontalModuleList + + Row { + spacing: 0 + + Repeater { + model: moduleListRoot.modules + + ModuleSlot { + required property var modelData + moduleName: String(modelData) + } + } + } + } + + Component { + id: verticalModuleList + + Column { + spacing: 0 + + Repeater { + model: moduleListRoot.modules + + ModuleSlot { + required property var modelData + moduleName: String(modelData) + } + } + } + } + } + + component ModuleSlot: Item { + id: slot + + required property string moduleName + readonly property string customType: root.customModuleType(moduleName) + readonly property var builtinComponent: customType ? null : root.builtinModuleComponent(moduleName) + readonly property bool qmlCustom: customType === "qml" + readonly property bool commandCustom: customType === "command" + readonly property var activeItem: qmlCustom ? qmlLoader.item : componentLoader.item + + implicitWidth: activeItem && activeItem.visible ? activeItem.implicitWidth : 0 + implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0 + width: implicitWidth + height: implicitHeight + + Loader { + id: componentLoader + active: !slot.qmlCustom + sourceComponent: slot.builtinComponent || (slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent) + anchors.fill: parent + } + + Loader { + id: qmlLoader + active: slot.qmlCustom + source: slot.qmlCustom ? root.customModuleSource(slot.moduleName) : "" + anchors.fill: parent + onLoaded: { + if (item && "bar" in item) item.bar = root + if (item && "moduleName" in item) item.moduleName = slot.moduleName + if (item && "settings" in item) item.settings = root.moduleSettings(slot.moduleName) + } + } + + Component { + id: customCommandModuleComponent + CustomCommandModule { moduleName: slot.moduleName } + } + } + + component CustomCommandModule: ModuleButton { + id: customRoot + + required property string moduleName + property var settings: root.moduleSettings(moduleName) + property string outputText: "" + property string outputTooltip: "" + property bool outputActive: false + + function setting(name, fallback) { + var value = settings ? settings[name] : undefined + return value === undefined || value === null ? fallback : value + } + + function update(raw) { + var data = root.parseModuleJson(raw) + var klass = data.class || data.alt || "" + + outputText = data.text || String(raw || "").trim() + outputTooltip = data.tooltip || String(setting("tooltip", "")) + outputActive = klass === "active" || (Array.isArray(klass) && klass.indexOf("active") !== -1) + } + + text: outputText || String(setting("text", "")) + tooltipText: outputTooltip || String(setting("tooltip", "")) + active: outputActive + keepSpace: setting("keepSpace", false) === true + horizontalMargin: Number(setting("horizontalMargin", 7.5)) + verticalPadding: Number(setting("verticalPadding", 6)) + fontSize: Number(setting("fontSize", 12)) + + onPressed: function(button) { + var command = "" + if (button === Qt.RightButton) + command = String(setting("onRightClick", "")) + else if (button === Qt.MiddleButton) + command = String(setting("onMiddleClick", "")) + else + command = String(setting("onClick", "")) + + if (command) root.run(command) + } + + Process { + id: customProc + command: ["bash", "-lc", root.commandWithOmarchyPath(String(customRoot.setting("exec", "")))] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: customRoot.update(text) + } + } + + Timer { + interval: Math.max(1, Number(customRoot.setting("interval", 5))) * 1000 + running: String(customRoot.setting("exec", "")) !== "" + repeat: true + triggeredOnStart: true + onTriggered: root.runProcess(customProc) + } + } + + component ModuleButton: Item { + id: buttonRoot + + property string text: "" + property bool active: false + property bool keepSpace: false + property string fontFamily: root.fontFamily + property real fontSize: 12 + property real horizontalMargin: 7.5 + property real verticalPadding: 6 + property real textRotation: 0 + property real fixedWidth: -1 + property real fixedHeight: -1 + property string tooltipText: "" + + signal pressed(int button) + signal wheelMoved(int delta) + + visible: text !== "" || keepSpace + opacity: text === "" ? 0 : 1 + implicitWidth: fixedWidth > 0 ? fixedWidth : (root.vertical ? root.barSize : Math.max(12, label.implicitWidth + horizontalMargin * 2)) + implicitHeight: fixedHeight > 0 ? fixedHeight : (root.vertical ? Math.max(12, label.implicitHeight + verticalPadding * 2) : root.barSize) + + Text { + id: label + anchors.centerIn: parent + text: buttonRoot.text + color: buttonRoot.active ? root.urgent : root.foreground + font.family: buttonRoot.fontFamily + font.pixelSize: buttonRoot.fontSize + rotation: buttonRoot.textRotation + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + hoverEnabled: true + onEntered: root.showTooltip(buttonRoot, buttonRoot.tooltipText) + onExited: root.hideTooltip(buttonRoot) + onClicked: function(mouse) { buttonRoot.pressed(mouse.button) } + onWheel: function(wheel) { buttonRoot.wheelMoved(wheel.angleDelta.y) } + } + } + + component IndicatorModule: ModuleButton { + fontSize: 10 + horizontalMargin: 5 + verticalPadding: 5 + } + + component OmarchyModule: ModuleButton { + text: "\ue900" + fontFamily: "omarchy" + horizontalMargin: 7.5 + tooltipText: "Omarchy Menu\n\nSuper + Alt + Space" + onPressed: function(button) { + if (button === Qt.RightButton) root.run("xdg-terminal-exec") + else root.run("env -u OMARCHY_PATH " + root.shellQuote(root.home + "/.local/share/omarchy/bin/omarchy-menu")) + } + } + + component WorkspacesModule: GridLayout { + columns: root.vertical ? 1 : root.workspaceIds().length + columnSpacing: root.vertical ? 0 : 3 + rowSpacing: root.vertical ? 3 : 0 + + Repeater { + model: root.workspaceIds() + + ModuleButton { + required property int modelData + + property var workspace: root.workspaceById(modelData) + property bool occupied: workspace !== null && workspace.toplevels.values.length > 0 + property bool focused: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === modelData + + text: focused ? "󱓻" : (modelData === 10 ? "0" : String(modelData)) + opacity: occupied || modelData <= 5 || focused ? 1 : 0.5 + horizontalMargin: 6 + verticalPadding: 6 + fixedWidth: root.vertical ? root.barSize : 24 + fixedHeight: root.barSize + onPressed: function() { root.focusWorkspace(modelData) } + } + } + } + + component ClockModule: ModuleButton { + text: root.clockText() + horizontalMargin: 8.75 + verticalPadding: 8.75 + onPressed: function(button) { + if (button === Qt.RightButton) root.run("omarchy-launch-floating-terminal-with-presentation omarchy-tz-select") + else root.clockAlt = !root.clockAlt + } + } + + component WeatherModule: ModuleButton { + text: root.weatherText + active: root.weatherClass === "active" + keepSpace: false + horizontalMargin: 7.5 + onPressed: function() { root.run("omarchy-notification-send \"$(omarchy-weather-status)\"") } + } + + component UpdateModule: ModuleButton { + text: root.updateAvailable ? "" : "" + fontSize: 10 + tooltipText: root.updateAvailable ? "Omarchy update available" : "" + onPressed: function() { root.run("omarchy-launch-floating-terminal-with-presentation omarchy-update") } + } + + component VoxtypeModule: ModuleButton { + text: root.voxtypeIcon + active: root.voxtypeClass === "recording" + tooltipText: root.voxtypeClass + onPressed: function(button) { + if (button === Qt.RightButton) root.run("omarchy-voxtype-config") + else root.run("omarchy-voxtype-model") + } + } + + component ScreenRecordingModule: IndicatorModule { + text: root.screenRecordingText + active: root.screenRecordingText !== "" + tooltipText: root.screenRecordingTooltip + onPressed: function() { root.run("omarchy-capture-screenrecording") } + } + + component IdleModule: IndicatorModule { + text: root.idleText + active: root.idleText !== "" + tooltipText: root.idleTooltip + onPressed: function() { root.run("omarchy-toggle-idle") } + } + + component NotificationsModule: IndicatorModule { + text: root.notificationSilencingText + active: root.notificationSilencingText !== "" + tooltipText: root.notificationSilencingTooltip + onPressed: function() { root.run("omarchy-toggle-notification-silencing") } + } + + component TrayModule: Item { + id: trayRoot + + property bool expanded: false + readonly property int activeItems: root.activeTrayItemCount() + readonly property int drawerExtent: activeItems > 0 ? activeItems * 16 + (activeItems - 1) * 17 : 0 + // Match Waybar's group/tray-expander drawer transition-duration. + readonly property int animationDuration: 600 + property real revealProgress: expanded ? 1 : 0 + readonly property real revealExtent: drawerExtent * revealProgress + + Behavior on revealProgress { + NumberAnimation { duration: trayRoot.animationDuration; easing.type: Easing.OutCubic } + } + + containmentMask: QtObject { + function contains(point: point): bool { + if (root.vertical) + return point.x >= 0 && point.x <= trayRoot.width && point.y >= trayRoot.drawerExtent - trayRoot.revealExtent && point.y <= trayRoot.height + + return point.x >= trayRoot.drawerExtent - trayRoot.revealExtent && point.x <= trayRoot.width && point.y >= 0 && point.y <= trayRoot.height + } + } + + visible: activeItems > 0 + clip: false + implicitWidth: root.vertical ? root.barSize : trayContent.implicitWidth + implicitHeight: root.vertical ? trayContent.implicitHeight : root.barSize + + Loader { + id: trayContent + anchors.fill: parent + sourceComponent: root.vertical ? verticalTray : horizontalTray + } + + HoverHandler { + onHoveredChanged: trayRoot.expanded = hovered + } + + Component { + id: horizontalTray + + Item { + id: horizontalTrayRoot + + implicitWidth: expandIcon.implicitWidth + trayRoot.drawerExtent + implicitHeight: root.barSize + + ModuleButton { + id: expandIcon + width: implicitWidth + height: implicitHeight + x: trayRoot.drawerExtent - trayRoot.revealExtent + text: "" + horizontalMargin: 9 + verticalPadding: 6 + + } + + Item { + id: trayClip + x: expandIcon.width + anchors.verticalCenter: parent.verticalCenter + width: trayRoot.drawerExtent + height: root.barSize + clip: true + + Row { + id: trayIcons + x: trayRoot.drawerExtent - trayRoot.revealExtent + anchors.verticalCenter: parent.verticalCenter + spacing: 17 + layer.enabled: true + + Repeater { + model: SystemTray.items + TrayItem {} + } + } + } + + } + } + + Component { + id: verticalTray + + Item { + id: verticalTrayRoot + + implicitWidth: root.barSize + implicitHeight: expandIcon.implicitHeight + trayRoot.drawerExtent + + ModuleButton { + id: expandIcon + width: implicitWidth + height: implicitHeight + y: trayRoot.drawerExtent - trayRoot.revealExtent + text: "" + textRotation: 90 + horizontalMargin: 9 + verticalPadding: 6 + + } + + Item { + id: trayClip + y: expandIcon.height + anchors.horizontalCenter: parent.horizontalCenter + width: root.barSize + height: trayRoot.drawerExtent + clip: true + + Column { + id: trayIcons + y: trayRoot.drawerExtent - trayRoot.revealExtent + anchors.horizontalCenter: parent.horizontalCenter + spacing: 17 + layer.enabled: true + + Repeater { + model: SystemTray.items + TrayItem {} + } + } + } + + } + } + } + + component TrayItem: Item { + id: trayItemRoot + + required property var modelData + + visible: modelData.status !== Status.Passive + implicitWidth: visible ? 16 : 0 + implicitHeight: visible ? 16 : 0 + + IconImage { + anchors.centerIn: parent + implicitSize: 12 + width: 12 + height: 12 + source: root.trayIconSource(trayItemRoot.modelData.icon) + } + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + hoverEnabled: true + onEntered: root.showTooltip(trayItemRoot, root.trayTooltip(modelData)) + onExited: root.hideTooltip(trayItemRoot) + onClicked: function(mouse) { + if (mouse.button === Qt.RightButton && trayItemRoot.modelData.hasMenu) { + var point = trayItemRoot.QsWindow.contentItem.mapFromItem(trayItemRoot, mouse.x, mouse.y) + trayItemRoot.modelData.display(trayItemRoot.QsWindow.window, point.x, point.y) + } else if (mouse.button === Qt.MiddleButton) { + trayItemRoot.modelData.secondaryActivate() + } else { + trayItemRoot.modelData.activate() + } + } + onWheel: function(wheel) { + trayItemRoot.modelData.scroll(wheel.angleDelta.y, false) + } + } + } + + component BluetoothModule: ModuleButton { + text: root.bluetoothIcon() + horizontalMargin: 8.5 + visible: Bluetooth.defaultAdapter !== null + tooltipText: root.bluetoothTooltip() + onPressed: function() { root.run("omarchy-launch-bluetooth") } + } + + component NetworkModule: ModuleButton { + text: root.networkIcon() + horizontalMargin: 6.5 + tooltipText: root.networkTooltip() + onPressed: function() { root.run("OMARCHY_PATH=" + root.shellQuote(root.omarchyPath) + " PATH=" + root.shellQuote(root.omarchyPath + "/bin:" + (Quickshell.env("PATH") || "")) + " " + root.shellQuote(root.omarchyPath + "/bin/omarchy-launch-wifi")) } + } + + component AudioModule: ModuleButton { + text: root.audioIcon() + tooltipText: root.audioTooltip() + onPressed: function(button) { + if (button === Qt.RightButton) root.run("pamixer -t") + else root.run("omarchy-launch-audio") + } + onWheelMoved: function(delta) { + if (delta > 0) root.run("pamixer -i 5") + else if (delta < 0) root.run("pamixer -d 5") + root.refreshAudio() + } + } + + component CpuModule: ModuleButton { + text: "󰍛" + onPressed: function(button) { + if (button === Qt.RightButton) root.run("alacritty") + else root.run("omarchy-launch-or-focus-tui btop") + } + } + + component BatteryModule: ModuleButton { + property var device: UPower.displayDevice + + text: root.batteryIcon() + visible: device !== null && device.isPresent && device.percentage > 0 + active: device !== null && device.percentage <= 20 && UPower.onBattery + tooltipText: root.batteryTooltip() + onPressed: function(button) { + if (button === Qt.RightButton) root.run("omarchy-notification-send \"$(omarchy-battery-status)\"") + else root.run("omarchy-menu power") + } + } +}