quickshell

This commit is contained in:
Acid
2026-04-09 02:08:41 -04:00
parent 554014f8b4
commit 8a64176960
10 changed files with 1270 additions and 236 deletions
+4 -3
View File
@@ -4,16 +4,17 @@ fi
ZSH_DISABLE_COMPFIX="true" #root comp
ZSH_THEME="powerlevel10k/powerlevel10k"
#
# If you come from bash you might have to change your $PATH.
# export PATH=$HOME/bin:/usr/local/bin:$PATH
export ZSH="$HOME/.oh-my-zsh"
export ZSH_COMPDUMP=$ZSH/cache/.zcompdump-$HOST
export EDITOR='nvim'
export VISUAL='nvim'
export PATH="$HOME/scripts:$PATH"
export PATH="$HOME/.local/bin:$PATH"
export PATH="$HOME/.cargo/bin:$PATH"
export PATH="$HOME/go/bin/:$PATH"
export PYTHONUNBUFFERED=1
export QT_QPA_PLATFORMTHEME=qt5ct
export MANPAGER="sh -c 'awk '\''{ gsub(/\x1B\[[0-9;]*m/, \"\", \$0); gsub(/.\x08/, \"\", \$0); print }'\'' | bat -p -lman'"
+5
View File
@@ -137,6 +137,11 @@ windowrule = float on, size 413 689, match:class ^(io.bassi.Amberol)$
windowrule = float on, match:class ^(qalculate-gtk)$, match:title ^(Qalculate!)$
# windowrule = tile on, workspace 10, match:class ^(affinity.exe)$
# QuickShell
# windowrule = float on, match:title ^(qs-volume)$
# windowrule = no_shadow on, match:title ^(qs-volume)$
# windowrule = border_size 0, match:title ^(qs-volume)$
$mainMod = SUPER
bind = $mainMod, R, exec, $terminal
+13 -10
View File
@@ -1,8 +1,8 @@
{
"LuaSnip": { "branch": "master", "commit": "642b0c595e11608b4c18219e93b88d7637af27bc" },
"LuaSnip": { "branch": "master", "commit": "a62e1083a3cfe8b6b206e7d3d33a51091df25357" },
"barbar.nvim": { "branch": "master", "commit": "539d73def39c9172b4d4d769f14090e08f37b29d" },
"buffer-vacuum": { "branch": "main", "commit": "917986d31cce6501c929ab6088c0887beff56b69" },
"catppuccin": { "branch": "main", "commit": "16408d203973da7e43fab87c6f9d3bf7fa98fa8b" },
"catppuccin": { "branch": "main", "commit": "426dbebe06b5c69fd846ceb17b42e12f890aedf1" },
"cmp-buffer": { "branch": "main", "commit": "b74fab3656eea9de20a9b8116afa3cfc4ec09657" },
"cmp-nvim-lsp": { "branch": "main", "commit": "cbc7b02bb99fae35cb42f514762b89b5126651ef" },
"cmp-nvim-lua": { "branch": "main", "commit": "e3a22cb071eb9d6508a156306b102c45cd2d573d" },
@@ -13,29 +13,32 @@
"indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" },
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
"local-highlight.nvim": { "branch": "master", "commit": "bdf0e15df9164b507cb9e9ba781ba29f2dd3f4a6" },
"lualine.nvim": { "branch": "master", "commit": "74114f0df664f14d7c228945693ba68a3b70a794" },
"lualine.nvim": { "branch": "master", "commit": "f5d2a8570f8b736ddb9bb4be504355bcd6e15ec8" },
"markdown-preview.nvim": { "branch": "master", "commit": "a923f5fc5ba36a3b17e289dc35dc17f66d0548ee" },
"mason.nvim": { "branch": "main", "commit": "44d1e90e1f66e077268191e3ee9d2ac97cc18e65" },
"mason.nvim": { "branch": "main", "commit": "b03fb0f20bc1d43daf558cda981a2be22e73ac42" },
"neo-tree.nvim": { "branch": "v3.x", "commit": "84c75e7a7e443586f60508d12fc50f90d9aee14e" },
"none-ls-extras.nvim": { "branch": "main", "commit": "c6fa39ac52814182c05552cb5d3750cae23ff0f0" },
"none-ls.nvim": { "branch": "main", "commit": "6ad251ef07dd4ba0cc725a5553cc76e9923f1a4a" },
"none-ls-extras.nvim": { "branch": "main", "commit": "14fa31ce8c0268a3b2c9cc14979ecf771982d433" },
"none-ls.nvim": { "branch": "main", "commit": "7f9301e416533b5d74e2fb3b1ce5059eeaed748b" },
"nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" },
"nvim-autopairs": { "branch": "master", "commit": "59bce2eef357189c3305e25bc6dd2d138c1683f5" },
"nvim-cmp": { "branch": "main", "commit": "a1d504892f2bc56c2e79b65c6faded2fd21f3eca" },
"nvim-colorizer.lua": { "branch": "master", "commit": "c8c2c19436d33165ec34fa90c975bc0a739fe3fb" },
"nvim-lspconfig": { "branch": "master", "commit": "9ccd58a7949091c0cc2777d4e92a45a209c808c1" },
"nvim-colorizer.lua": { "branch": "master", "commit": "5cfe7fffbd01e17b3c1e14af85d5febdef88bd8c" },
"nvim-lspconfig": { "branch": "master", "commit": "c588db330592fa477a70d2fee6ba20a57194bdc3" },
"nvim-treesitter": { "branch": "master", "commit": "cf12346a3414fa1b06af75c79faebe7f76df080a" },
"nvim-treesitter-textobjects": { "branch": "master", "commit": "5ca4aaa6efdcc59be46b95a3e876300cfead05ef" },
"nvim-ts-autotag": { "branch": "main", "commit": "8e1c0a389f20bf7f5b0dd0e00306c1247bda2595" },
"nvim-web-devicons": { "branch": "master", "commit": "40e9d5a6cc3db11b309e39593fc7ac03bb844e38" },
"nvim-web-devicons": { "branch": "master", "commit": "95b7a002d5dba1a42eb58f5fac5c565a485eefd0" },
"oil.nvim": { "branch": "master", "commit": "0fcc83805ad11cf714a949c98c605ed717e0b83e" },
"plenary.nvim": { "branch": "master", "commit": "b9fd5226c2f76c951fc8ed5923d85e4de065e509" },
"rainbow-delimiters.nvim": { "branch": "master", "commit": "50080ed0f44dbc18ae13b81278a01b951a06127a" },
"rainbow-delimiters.nvim": { "branch": "master", "commit": "aab6caaffd79b8def22ec4320a5344f7c42f58d2" },
"ruffer": { "branch": "plugin", "commit": "5248604605bf1039147f3f29dcd46aa296557e9c" },
"telescope-ui-select.nvim": { "branch": "master", "commit": "6e51d7da30bd139a6950adf2a47fda6df9fa06d2" },
"telescope.nvim": { "branch": "master", "commit": "a0bbec21143c7bc5f8bb02e0005fa0b982edc026" },
"undotree": { "branch": "master", "commit": "6fa6b57cda8459e1e4b2ca34df702f55242f4e4d" },
"vim-commentary": { "branch": "master", "commit": "64a654ef4a20db1727938338310209b6a63f60c9" },
"vim-dadbod": { "branch": "master", "commit": "6d1d41da4873a445c5605f2005ad2c68c99d8770" },
"vim-dadbod-completion": { "branch": "master", "commit": "a8dac0b3cf6132c80dc9b18bef36d4cf7a9e1fe6" },
"vim-dadbod-ui": { "branch": "master", "commit": "07e92e22114cc5b1ba4938d99897d85b58e20475" },
"vim-devicons": { "branch": "master", "commit": "71f239af28b7214eebb60d4ea5bd040291fb7e33" },
"vim-floaterm": { "branch": "master", "commit": "0ab5eb8135dc884bc543a819ac7033c15e72a76b" },
"vim-signature": { "branch": "master", "commit": "6bc3dd1294a22e897f0dcf8dd72b85f350e306bc" },
+77 -72
View File
@@ -1,80 +1,85 @@
local cmp = require'cmp'
local cmp = require("cmp")
local kind_icons = {
Text = "",
Method = "",
Function = "",
Constructor = "",
Field = "",
Variable = "",
Class = " ",
Interface = "",
Module = "",
Property = "",
Unit = "",
Value = " ",
Enum = "",
Keyword = " ",
Snippet = "",
Color = " ",
File = " ",
Reference = "",
Folder = " ",
EnumMember = "",
Constant = " ",
Struct = "",
Event = " ",
Operator = " ",
TypeParameter = " ",
Text = "",
Method = "",
Function = "",
Constructor = "",
Field = "",
Variable = "",
Class = "",
Interface = "",
Module = "",
Property = "",
Unit = "",
Value = "",
Enum = "",
Keyword = "",
Snippet = "",
Color = "",
File = "",
Reference = "",
Folder = "",
EnumMember = "",
Constant = "",
Struct = "",
Event = "",
Operator = "",
TypeParameter = "",
}
cmp.setup({
snippet = {
expand = function(args)
require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
end,
},
formatting = {
format = function(_, vim_item)
vim_item.kind = (kind_icons[vim_item.kind] or '') .. vim_item.kind
return vim_item
end,
},
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
mapping = {
['<Down>'] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Select }),
['<A-j>'] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Select }),
['<Up>'] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Select }),
['<A-k>'] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Select }),
['<A-d>'] = cmp.mapping.scroll_docs(-4),
['<A-f>'] = cmp.mapping.scroll_docs(4),
['<A-Space>'] = cmp.mapping.complete(),
['<A-e>'] = cmp.mapping.close(),
['<CR>'] = cmp.mapping.confirm({
behavior = cmp.ConfirmBehavior.Insert,
select = true,
}),
snippet = {
expand = function(args)
require("luasnip").lsp_expand(args.body) -- For `luasnip` users.
end,
},
formatting = {
format = function(_, vim_item)
vim_item.kind = (kind_icons[vim_item.kind] or "") .. vim_item.kind
return vim_item
end,
},
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
mapping = {
["<Down>"] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Select }),
["<A-j>"] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Select }),
["<Up>"] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Select }),
["<A-k>"] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Select }),
["<A-d>"] = cmp.mapping.scroll_docs(-4),
["<A-f>"] = cmp.mapping.scroll_docs(4),
["<A-Space>"] = cmp.mapping.complete(),
["<A-e>"] = cmp.mapping.close(),
["<CR>"] = cmp.mapping.confirm({
behavior = cmp.ConfirmBehavior.Insert,
select = true,
}),
['<Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
else
fallback()
end
end, { 'i', 's' }),
["<Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
else
fallback()
end
end, { "i", "s" }),
},
},
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'luasnip' }, -- For luasnip users.
{ name = 'path' }
},
{
{ name = 'buffer' },
})
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" }, -- For luasnip users.
{ name = "path" },
}, {
{ name = "buffer" },
}),
})
--dadbod
cmp.setup.filetype({ "sql" }, {
sources = {
{ name = "vim-dadbod-completion" },
{ name = "buffer" },
},
})
+160 -149
View File
@@ -1,191 +1,202 @@
-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
if vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
{ out, "WarningMsg" },
{ "\nPress any key to exit..." },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
if vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
{ out, "WarningMsg" },
{ "\nPress any key to exit..." },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
end
vim.opt.rtp:prepend(lazypath)
local plugins = {
{ "neovim/nvim-lspconfig" },
{ "neovim/nvim-lspconfig" },
-- mine
{
'acidburnmonkey/ruffer',
config = function()
require('ruffer').setup()
end,
},
-- mine
{
"acidburnmonkey/ruffer",
config = function()
require("ruffer").setup()
end,
},
-- Terminal floating window
{ "voldikss/vim-floaterm" },
-- Terminal floating window
{ "voldikss/vim-floaterm" },
-- Indent guides
{
"lukas-reineke/indent-blankline.nvim",
main = "ibl",
opts = {},
},
-- Indent guides
{
"lukas-reineke/indent-blankline.nvim",
main = "ibl",
opts = {},
},
-- Color highlighting
{ "catgoose/nvim-colorizer.lua"},
-- Color highlighting
{ "catgoose/nvim-colorizer.lua" },
-- Undo tree viewer
{ "mbbill/undotree" },
-- Undo tree viewer
{ "mbbill/undotree" },
-- Devicons
{ "nvim-tree/nvim-web-devicons", opts = {} },
-- Devicons
{ "nvim-tree/nvim-web-devicons", opts = {} },
-- Neo-tree file explorer
{
"nvim-neo-tree/neo-tree.nvim",
branch = "v3.x",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-tree/nvim-web-devicons",
"MunifTanjim/nui.nvim",
},
lazy = true,
},
-- Neo-tree file explorer
{
"nvim-neo-tree/neo-tree.nvim",
branch = "v3.x",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-tree/nvim-web-devicons",
"MunifTanjim/nui.nvim",
},
lazy = true,
},
-- Telescope fuzzy finder
{
'nvim-telescope/telescope.nvim', tag = '0.1.8',
dependencies = { 'nvim-lua/plenary.nvim' }
},
-- Telescope fuzzy finder
{
"nvim-telescope/telescope.nvim",
tag = "0.1.8",
dependencies = { "nvim-lua/plenary.nvim" },
},
-- Treesitter
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
},
{ "nvim-treesitter/nvim-treesitter-textobjects" },
-- Treesitter
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
},
{ "nvim-treesitter/nvim-treesitter-textobjects" },
-- Mason package manager
{
"mason-org/mason.nvim",
opts = {},
},
-- Mason package manager
{
"mason-org/mason.nvim",
opts = {}
},
-- LuaSnip engine
{
"L3MON4D3/LuaSnip",
build = "make install_jsregexp",
},
-- LuaSnip snippets
{ "rafamadriz/friendly-snippets" },
-- LuaSnip engine
{
"L3MON4D3/LuaSnip",
build = "make install_jsregexp",
},
-- LuaSnip snippets
{ "rafamadriz/friendly-snippets" },
-- Autopairs
{
"windwp/nvim-autopairs",
event = "InsertEnter",
},
-- Autopairs
{
'windwp/nvim-autopairs',
event = "InsertEnter",
},
-- Commenting utility
{ "tpope/vim-commentary" },
-- Commenting utility
{ "tpope/vim-commentary" },
-- Catppuccin theme
{ "catppuccin/nvim", name = "catppuccin", priority = 1000 },
-- Catppuccin theme
{ "catppuccin/nvim",
name = "catppuccin",
priority = 1000 },
-- Surround plugin
{ "tpope/vim-surround" },
-- Surround plugin
{ "tpope/vim-surround" },
-- Formatter/Linter integration
{ "nvimtools/none-ls.nvim", dependencies = {
"nvimtools/none-ls-extras.nvim",
} },
-- Formatter/Linter integration
{ "nvimtools/none-ls.nvim" ,
dependencies = {
"nvimtools/none-ls-extras.nvim"}
} ,
-- Treesitter auto-closing tags
{ "windwp/nvim-ts-autotag" },
-- Treesitter auto-closing tags
{ "windwp/nvim-ts-autotag" },
-- Signature marks
{ "kshenoy/vim-signature" },
-- Signature marks
{ "kshenoy/vim-signature" },
-- Oil file explorer
{
"stevearc/oil.nvim",
lazy = false,
},
-- Oil file explorer
{
'stevearc/oil.nvim',
lazy = false
},
-- Rainbow delimiters
{ "HiPhish/rainbow-delimiters.nvim" },
-- Rainbow delimiters
{ "HiPhish/rainbow-delimiters.nvim" },
-- Completion engine
{ "hrsh7th/nvim-cmp" },
{ "hrsh7th/cmp-buffer" },
{ "hrsh7th/cmp-path" },
{ "saadparwaiz1/cmp_luasnip" },
{ "hrsh7th/cmp-nvim-lsp" },
{ "hrsh7th/cmp-nvim-lua" },
-- Completion engine
{ "hrsh7th/nvim-cmp" },
{ "hrsh7th/cmp-buffer" },
{ "hrsh7th/cmp-path" },
{ "saadparwaiz1/cmp_luasnip" },
{ "hrsh7th/cmp-nvim-lsp" },
{ "hrsh7th/cmp-nvim-lua" },
-- Lualine
{ "nvim-lualine/lualine.nvim", dependencies = { "nvim-tree/nvim-web-devicons" } },
-- Lualine
{ 'nvim-lualine/lualine.nvim',
dependencies = { 'nvim-tree/nvim-web-devicons' } },
-- File icons
{ "ryanoasis/vim-devicons" },
-- File icons
{ "ryanoasis/vim-devicons" },
-- barbar
{
"romgrk/barbar.nvim",
dependencies = {
"nvim-tree/nvim-web-devicons",
},
init = function()
vim.g.barbar_auto_setup = false -- disable auto-setup
end,
},
-- barbar
{
'romgrk/barbar.nvim',
dependencies = {
'nvim-tree/nvim-web-devicons'
},
init = function()
vim.g.barbar_auto_setup = false -- disable auto-setup
end
},
-- highlight
{
"tzachar/local-highlight.nvim",
},
-- highlight
{
'tzachar/local-highlight.nvim',
},
-- buffer vacuum
{
"ChuufMaster/buffer-vacuum",
opts = {
max_buffers = 2,
count_pinned_buffers = false,
enable_messages = false,
},
},
-- buffer vacuum
{
'ChuufMaster/buffer-vacuum',
opts = {
max_buffers = 2,
count_pinned_buffers = false,
enable_messages = false,
},
},
-- telescope plugin
{ "nvim-telescope/telescope-ui-select.nvim" },
-- telescope plugin
{'nvim-telescope/telescope-ui-select.nvim' },
-- harpoon
{ "ThePrimeagen/harpoon", lazy = false },
-- harpoon
{'ThePrimeagen/harpoon',lazy=false},
--MarkdownPreview
{
"iamcco/markdown-preview.nvim",
cmd = { "MarkdownPreviewToggle", "MarkdownPreview", "MarkdownPreviewStop" },
build = "cd app && npm install",
init = function()
vim.g.mkdp_filetypes = { "markdown" }
end,
ft = { "markdown" },
},
--MarkdownPreview
{
"iamcco/markdown-preview.nvim",
cmd = { "MarkdownPreviewToggle", "MarkdownPreview", "MarkdownPreviewStop" },
build = "cd app && npm install",
init = function()
vim.g.mkdp_filetypes = { "markdown" }
end,
ft = { "markdown" },
},
-- Dadbod
{
"kristijanhusak/vim-dadbod-ui",
dependencies = {
{ "tpope/vim-dadbod", lazy = true },
{ "kristijanhusak/vim-dadbod-completion", ft = { "sql", "mysql", "plsql" }, lazy = true },
},
cmd = {
"DBUI",
"DBUIToggle",
"DBUIAddConnection",
"DBUIFindBuffer",
},
init = function()
vim.g.db_ui_use_nerd_fonts = 1
end,
},
}
local opts = {}
require('lazy').setup(plugins, opts)
require("lazy").setup(plugins, opts)
+880
View File
@@ -0,0 +1,880 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Window
import QtQuick.Effects
import Quickshell
import Quickshell.Io
Item {
id: window
focus: true
// -------------------------------------------------------------------------
// SHORTCUTS & AUDIO
// -------------------------------------------------------------------------
Shortcut {
sequence: "Tab"
onActivated: {
if (window.activeTab === "outputs") window.activeTab = "inputs";
else if (window.activeTab === "inputs") window.activeTab = "apps";
else window.activeTab = "outputs";
}
}
// -------------------------------------------------------------------------
// COLORS (Catppuccin Mocha)
// -------------------------------------------------------------------------
readonly property color base: "#1e1e2e"
readonly property color mantle: "#181825"
readonly property color crust: "#11111b"
readonly property color text: "#cdd6f4"
readonly property color subtext0: "#a6adc8"
readonly property color overlay0: "#6c7086"
readonly property color overlay1: "#7f849c"
readonly property color surface0: "#313244"
readonly property color surface1: "#45475a"
readonly property color surface2: "#585b70"
readonly property color mauve: "#cba6f7"
readonly property color pink: "#f5c2e7"
readonly property color red: "#f38ba8"
readonly property color maroon: "#eba0ac"
readonly property color peach: "#fab387"
readonly property color yellow: "#f9e2af"
readonly property color green: "#a6e3a1"
readonly property color teal: "#94e2d5"
readonly property color sapphire: "#74c7ec"
readonly property color blue: "#89b4fa"
// -------------------------------------------------------------------------
// STATE & CONFIG
// -------------------------------------------------------------------------
readonly property string scriptsDir: Qt.resolvedUrl(".").toString().replace(/^file:\/\//, "")
// Dynamic height: 25(top margin) + 150(hero) + 20(spacing) + 54(tabs) + 20(spacing) + list + 25(bottom margin)
readonly property real listH: Math.max(220, Math.min(600, contentList.contentHeight))
implicitHeight: 294 + listH
property string activeTab: "outputs" // outputs, inputs, apps
onActiveTabChanged: updateHeroData()
readonly property color tabColor: {
if (activeTab === "outputs") return window.blue;
if (activeTab === "inputs") return window.mauve;
return window.green;
}
property real globalOrbitAngle: 0
NumberAnimation on globalOrbitAngle {
from: 0; to: Math.PI * 2; duration: 90000; loops: Animation.Infinite; running: true
}
// Top Orb Active State Links
property string activeId: ""
property string activeName: "No Device"
property string activeDesc: ""
property int activeVol: 0
property bool activeMute: false
property string activeIcon: "󰓃"
// Models
ListModel { id: outputsModel }
ListModel { id: inputsModel }
ListModel { id: appsModel }
property var draggingNodes: ({})
property bool draggingMaster: false
Timer { id: syncDelay; interval: 600; onTriggered: { window.draggingNodes = ({}); window.draggingMaster = false; } }
// -------------------------------------------------------------------------
// CACHING & DATA LOGIC
// -------------------------------------------------------------------------
function processAudioJson(textData) {
if (!textData) return;
try {
let data = JSON.parse(textData);
syncModel(outputsModel, data.outputs || []);
syncModel(inputsModel, data.inputs || []);
syncModel(appsModel, data.apps || []);
updateHeroData();
} catch(e) {}
}
function updateHeroData() {
let targetModel = (window.activeTab === "inputs") ? inputsModel : outputsModel;
let foundDefault = false;
for (let i = 0; i < targetModel.count; i++) {
let d = targetModel.get(i);
if (d.is_default) {
window.activeId = d.id;
window.activeName = d.description;
window.activeDesc = d.name;
window.activeIcon = d.icon;
if (!window.draggingMaster) {
window.activeVol = d.volume;
window.activeMute = d.mute;
}
foundDefault = true;
break;
}
}
// Fallback if no default is found
if (!foundDefault && targetModel.count > 0) {
let d = targetModel.get(0);
window.activeId = d.id;
window.activeName = d.description;
window.activeDesc = d.name;
window.activeIcon = d.icon;
if (!window.draggingMaster) {
window.activeVol = d.volume;
window.activeMute = d.mute;
}
}
}
function syncModel(listModel, dataArray) {
for (let i = listModel.count - 1; i >= 0; i--) {
let id = listModel.get(i).id;
let found = false;
for (let j = 0; j < dataArray.length; j++) {
if (id === dataArray[j].id) { found = true; break; }
}
if (!found) listModel.remove(i);
}
for (let i = 0; i < dataArray.length; i++) {
let d = dataArray[i];
let foundIdx = -1;
for (let j = i; j < listModel.count; j++) {
if (listModel.get(j).id === d.id) { foundIdx = j; break; }
}
let obj = {
id: d.id, name: d.name, description: d.description,
volume: d.volume, mute: d.mute, is_default: d.is_default, icon: d.icon
};
if (foundIdx === -1) {
listModel.insert(i, obj);
} else {
if (foundIdx !== i) listModel.move(foundIdx, i, 1);
for (let key in obj) {
if (key === "volume" && window.draggingNodes[obj.id]) continue;
if (listModel.get(i)[key] !== obj[key]) {
listModel.setProperty(i, key, obj[key]);
}
}
}
}
}
Process {
id: audioPoller
command: ["python3", window.scriptsDir + "/get_audio_state.py"]
running: true
stdout: StdioCollector {
onStreamFinished: {
processAudioJson(this.text.trim());
}
}
}
Timer {
interval: 1000; running: true; repeat: true; triggeredOnStart: true;
onTriggered: audioPoller.running = true
}
// -------------------------------------------------------------------------
// ANIMATIONS
// -------------------------------------------------------------------------
property real introMain: 0
property real introHeader: 0
property real introContent: 0
ParallelAnimation {
running: true
NumberAnimation { target: window; property: "introMain"; from: 0; to: 1.0; duration: 800; easing.type: Easing.OutExpo }
SequentialAnimation {
PauseAnimation { duration: 100 }
NumberAnimation { target: window; property: "introHeader"; from: 0; to: 1.0; duration: 700; easing.type: Easing.OutBack; easing.overshoot: 1.2 }
}
SequentialAnimation {
PauseAnimation { duration: 200 }
NumberAnimation { target: window; property: "introContent"; from: 0; to: 1.0; duration: 800; easing.type: Easing.OutExpo }
}
}
// -------------------------------------------------------------------------
// UI LAYOUT
// -------------------------------------------------------------------------
Item {
anchors.fill: parent
scale: 0.95 + (0.05 * introMain)
opacity: introMain
transform: Translate { y: 20 * (1 - introMain) }
Rectangle {
anchors.fill: parent
radius: 20
color: window.base
border.color: window.surface0
border.width: 1
clip: true
// Rotating Background Blobs
Rectangle {
width: parent.width * 0.8; height: width; radius: width / 2
x: (parent.width / 2 - width / 2) + Math.cos(window.globalOrbitAngle * 2) * 150
y: (parent.height / 2 - height / 2) + Math.sin(window.globalOrbitAngle * 2) * 100
opacity: 0.06
color: window.tabColor
Behavior on color { ColorAnimation { duration: 800 } }
}
Rectangle {
width: parent.width * 0.9; height: width; radius: width / 2
x: (parent.width / 2 - width / 2) + Math.sin(window.globalOrbitAngle * 1.5) * -150
y: (parent.height / 2 - height / 2) + Math.cos(window.globalOrbitAngle * 1.5) * -100
opacity: 0.04
color: Qt.lighter(window.tabColor, 1.3)
Behavior on color { ColorAnimation { duration: 800 } }
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 25
spacing: 20
// ==========================================
// HERO ORB & MASTER SLIDER (TOP SECTION)
// ==========================================
Item {
Layout.fillWidth: true
Layout.preferredHeight: 150
opacity: introHeader
transform: Translate { y: 30 * (1.0 - introHeader) }
RowLayout {
anchors.fill: parent
spacing: 25
// 1. The Orb
Item {
Layout.preferredWidth: 130
Layout.preferredHeight: 130
scale: masterOrbMa.pressed ? 0.95 : (masterOrbMa.containsMouse ? 1.05 : 1.0)
Behavior on scale { NumberAnimation { duration: 400; easing.type: Easing.OutBack } }
// Outermost border pulse ring
Rectangle {
anchors.centerIn: parent
width: parent.width + 15
height: width
radius: width / 2
color: "transparent"
border.color: window.activeMute ? window.red : window.tabColor
border.width: 3
z: -2
property real pulseOp: 0.0
property real pulseSc: 1.0
opacity: window.activeMute ? 0.0 : pulseOp
scale: pulseSc
Timer {
interval: 45
running: parent.opacity > 0.01 || !window.activeMute
repeat: true
onTriggered: {
var time = Date.now() / 1000;
parent.pulseOp = 0.3 + Math.sin(time * 2.5) * 0.15;
parent.pulseSc = 1.02 + Math.cos(time * 3.0) * 0.02;
}
}
}
// Solid pulsing background ring
Rectangle {
anchors.centerIn: parent
width: parent.width + 40
height: width
radius: width / 2
color: window.activeMute ? window.red : window.tabColor
opacity: window.activeMute ? 0.3 : 0.15
z: -1
Behavior on color { ColorAnimation { duration: 300 } }
SequentialAnimation on scale {
loops: Animation.Infinite; running: true
NumberAnimation { to: masterOrbMa.containsMouse ? 1.15 : 1.1; duration: masterOrbMa.containsMouse ? 800 : 2000; easing.type: Easing.InOutSine }
NumberAnimation { to: 1.0; duration: masterOrbMa.containsMouse ? 800 : 2000; easing.type: Easing.InOutSine }
}
}
// Core Shadow
MultiEffect {
source: centralCore
anchors.fill: centralCore
shadowEnabled: true
shadowColor: "#000000"
shadowOpacity: 0.5
shadowBlur: 1.2
shadowVerticalOffset: 6
z: -1
}
// Core Rectangle
Rectangle {
id: centralCore
anchors.fill: parent
radius: width / 2
color: window.base
border.color: window.activeMute ? window.red : Qt.lighter(window.tabColor, 1.1)
border.width: 2
clip: true
Behavior on border.color { ColorAnimation { duration: 300 } }
// Volume Wave Fill
Canvas {
id: orbWave
anchors.fill: parent
property real wavePhase: 0.0
NumberAnimation on wavePhase {
running: window.activeVol > 0 && window.activeVol < 100
loops: Animation.Infinite
from: 0; to: Math.PI * 2; duration: 1200
}
onWavePhaseChanged: requestPaint()
Connections {
target: window
function onActiveVolChanged() { orbWave.requestPaint() }
function onActiveMuteChanged() { orbWave.requestPaint() }
function onTabColorChanged() { orbWave.requestPaint() }
}
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
if (window.activeVol <= 0) return;
var fillRatio = window.activeVol / 100.0;
var r = width / 2;
var fillY = height * (1.0 - fillRatio);
ctx.save();
// 1. Establish the circular clipping mask
ctx.beginPath();
ctx.arc(r, r, r, 0, 2 * Math.PI);
ctx.clip();
// 2. Draw the actual wave filling
ctx.beginPath();
ctx.moveTo(0, fillY);
if (fillRatio < 0.99) {
var waveAmp = 8 * Math.sin(fillRatio * Math.PI);
var cp1y = fillY + Math.sin(wavePhase) * waveAmp;
var cp2y = fillY + Math.cos(wavePhase + Math.PI) * waveAmp;
ctx.bezierCurveTo(width * 0.33, cp2y, width * 0.66, cp1y, width, fillY);
ctx.lineTo(width, height);
ctx.lineTo(0, height);
} else {
ctx.lineTo(width, 0);
ctx.lineTo(width, height);
ctx.lineTo(0, height);
}
ctx.closePath();
// Vibrant gradient matching the network orb
var grad = ctx.createLinearGradient(0, 0, 0, height);
if (window.activeMute) {
grad.addColorStop(0, Qt.lighter(window.red, 1.15).toString());
grad.addColorStop(1, window.red.toString());
} else {
grad.addColorStop(0, Qt.lighter(window.tabColor, 1.15).toString());
grad.addColorStop(1, window.tabColor.toString());
}
ctx.fillStyle = grad;
ctx.globalAlpha = 1.0;
ctx.fill();
ctx.restore();
}
}
// Dual-Layer Text for contrast clipping
// 1. Base Text (Visible when empty)
Text {
anchors.centerIn: parent
font.family: "JetBrains Mono"
font.weight: Font.Black
font.pixelSize: 32
color: window.activeMute ? window.red : window.text
text: window.activeMute ? "MUTE" : window.activeVol + "%"
Behavior on color { ColorAnimation { duration: 200 } }
}
// 2. Clipped Text (Dark text that reveals over the wave fill dynamically)
Item {
id: waveClipItem
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
// Calculate the exact wave offset at the center of the orb using the Bezier formula
property real fillRatio: window.activeVol / 100.0
property real waveAmp: fillRatio < 0.99 ? 8 * Math.sin(fillRatio * Math.PI) : 0
property real waveCenterOffset: 0.375 * waveAmp * (Math.sin(orbWave.wavePhase) - Math.cos(orbWave.wavePhase))
property real baseClipHeight: parent.height * fillRatio
height: Math.min(parent.height, Math.max(0, baseClipHeight - waveCenterOffset))
clip: true
visible: window.activeVol > 0
Text {
x: waveClipItem.width / 2 - width / 2
y: (centralCore.height / 2) - (height / 2) - (centralCore.height - waveClipItem.height)
font.family: "JetBrains Mono"
font.weight: Font.Black
font.pixelSize: 32
color: window.crust
text: window.activeMute ? "MUTE" : window.activeVol + "%"
}
}
}
MouseArea {
id: masterOrbMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let type = window.activeTab === "inputs" ? "source" : "sink";
Quickshell.execDetached(["bash", window.scriptsDir + "/audio_control.sh", "toggle-mute", type, window.activeId]);
audioPoller.running = true;
}
}
}
// 2. Details & Slider
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
ColumnLayout {
spacing: 2
Text {
Layout.fillWidth: true; elide: Text.ElideRight
font.family: "JetBrains Mono"; font.weight: Font.Black; font.pixelSize: 20
color: window.text
text: window.activeName
}
Text {
Layout.fillWidth: true; elide: Text.ElideRight
font.family: "JetBrains Mono"; font.pixelSize: 13
color: window.subtext0
text: window.activeTab === "apps" ? "Master Output Volume" : window.activeDesc
}
}
Item { Layout.fillHeight: true } // spacer
RowLayout {
Layout.fillWidth: true
spacing: 15
// Slider
Item {
Layout.fillWidth: true
height: 24
Timer {
id: masterCmdThrottle
interval: 50
property int targetPct: -1
onTriggered: {
if (targetPct >= 0) {
let type = window.activeTab === "inputs" ? "source" : "sink";
if (targetPct > 0 && window.activeMute) {
Quickshell.execDetached(["bash", window.scriptsDir + "/audio_control.sh", "toggle-mute", type, window.activeId]);
}
Quickshell.execDetached(["bash", window.scriptsDir + "/audio_control.sh", "set-volume", type, window.activeId, targetPct]);
targetPct = -1;
}
}
}
Rectangle {
anchors.fill: parent; radius: 12
color: "#0dffffff"; border.color: "#1affffff"; border.width: 1
clip: true
Rectangle {
height: parent.height
width: parent.width * (Math.min(100, window.activeVol) / 100)
radius: 12
opacity: window.activeMute ? 0.3 : (masterSliderMa.containsMouse ? 1.0 : 0.85)
Behavior on opacity { NumberAnimation { duration: 200 } }
Behavior on width { enabled: !window.draggingMaster; NumberAnimation { duration: 300; easing.type: Easing.OutQuint } }
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: window.activeMute ? window.surface2 : window.tabColor; Behavior on color { ColorAnimation{duration: 300} } }
GradientStop { position: 1.0; color: window.activeMute ? Qt.lighter(window.surface2, 1.15) : Qt.lighter(window.tabColor, 1.25); Behavior on color { ColorAnimation{duration: 300} } }
}
}
}
MouseArea {
id: masterSliderMa
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
onPressed: (mouse) => { syncDelay.stop(); window.draggingMaster = true; updateVol(mouse.x); }
onPositionChanged: (mouse) => { if (pressed) updateVol(mouse.x); }
onReleased: { syncDelay.restart(); audioPoller.running = true; }
function updateVol(mx) {
let pct = Math.max(0, Math.min(100, Math.round((mx / width) * 100)));
window.activeVol = pct; // Instant visual feedback on orb
masterCmdThrottle.targetPct = pct;
if (!masterCmdThrottle.running) masterCmdThrottle.start();
}
}
}
}
}
}
}
// ==========================================
// TABS
// ==========================================
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 54
radius: 14
color: "#0dffffff"
border.color: "#1affffff"
border.width: 1
opacity: introHeader
transform: Translate { y: 20 * (1.0 - introHeader) }
Rectangle {
width: (parent.width - 2) / 3
height: parent.height - 2
y: 1
radius: 10
x: {
if (window.activeTab === "outputs") return 1;
if (window.activeTab === "inputs") return width + 1;
return (width * 2) + 1;
}
Behavior on x { NumberAnimation { duration: 500; easing.type: Easing.OutBack; easing.overshoot: 1.1 } }
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: window.tabColor; Behavior on color { ColorAnimation { duration: 400 } } }
GradientStop { position: 1.0; color: Qt.lighter(window.tabColor, 1.15); Behavior on color { ColorAnimation { duration: 400 } } }
}
}
RowLayout {
anchors.fill: parent
spacing: 0
Repeater {
model: ListModel {
ListElement { tabId: "outputs"; icon: "󰓃"; label: "Outputs" }
ListElement { tabId: "inputs"; icon: "󰍬"; label: "Inputs" }
ListElement { tabId: "apps"; icon: "󰎆"; label: "Streams" }
}
delegate: Item {
Layout.fillWidth: true
Layout.fillHeight: true
RowLayout {
anchors.centerIn: parent
spacing: 8
Text {
font.family: "Iosevka Nerd Font"; font.pixelSize: 18
color: window.activeTab === tabId ? window.crust : (tabMa.containsMouse ? window.text : window.subtext0)
text: icon
Behavior on color { ColorAnimation { duration: 200 } }
}
Text {
font.family: "JetBrains Mono"; font.weight: Font.Black; font.pixelSize: 13
color: window.activeTab === tabId ? window.crust : (tabMa.containsMouse ? window.text : window.subtext0)
text: label
Behavior on color { ColorAnimation { duration: 200 } }
}
}
MouseArea {
id: tabMa
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
onClicked: {
window.activeTab = tabId;
}
}
}
}
}
}
// ==========================================
// LIST VIEW CONTENT
// ==========================================
Item {
Layout.fillWidth: true
Layout.fillHeight: true
opacity: introContent
transform: Translate { y: 20 * (1.0 - introContent) }
ListView {
id: contentList
anchors.fill: parent
spacing: 12
clip: true
boundsBehavior: Flickable.StopAtBounds
// Elegant sliding transitions when models rearrange
add: Transition {
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 400; easing.type: Easing.OutQuint }
NumberAnimation { property: "scale"; from: 0.9; to: 1; duration: 400; easing.type: Easing.OutBack }
}
displaced: Transition {
SpringAnimation { property: "y"; spring: 3; damping: 0.2; mass: 0.2 }
}
model: {
if (window.activeTab === "outputs") return outputsModel;
if (window.activeTab === "inputs") return inputsModel;
return appsModel;
}
Item {
width: contentList.width; height: contentList.height
visible: contentList.count === 0
ColumnLayout {
anchors.centerIn: parent
spacing: 10
Text { Layout.alignment: Qt.AlignHCenter; font.family: "Iosevka Nerd Font"; font.pixelSize: 32; color: window.surface2; text: "󰖁" }
Text { Layout.alignment: Qt.AlignHCenter; font.family: "JetBrains Mono"; font.pixelSize: 14; color: window.overlay0; text: "No active streams" }
}
}
delegate: Rectangle {
id: delegateRoot
width: contentList.width
// Staggered Intro Animation Timer
property bool isLoaded: false
Timer {
running: true
interval: 40 + (index * 40)
onTriggered: delegateRoot.isLoaded = true
}
// Intro transforms
opacity: isLoaded ? 1.0 : 0.0
transform: Translate { y: isLoaded ? 0 : 15 }
Behavior on opacity { NumberAnimation { duration: 500; easing.type: Easing.OutQuint } }
Behavior on transform { NumberAnimation { duration: 500; easing.type: Easing.OutQuint } }
// Dynamic Height: The active hero element collapses its bottom slider row
property bool isActiveNode: model.is_default && window.activeTab !== "apps"
height: isActiveNode ? 60 : 100
Behavior on height { NumberAnimation { duration: 400; easing.type: Easing.OutQuint } }
radius: 14
property bool isHovered: cardMa.containsMouse && !isActiveNode
color: isActiveNode ? window.tabColor : (isHovered ? "#0affffff" : "#05ffffff")
border.color: isActiveNode ? window.tabColor : "#1affffff"
border.width: isActiveNode ? 2 : 1
Behavior on border.color { ColorAnimation { duration: 300 } }
Behavior on color { ColorAnimation { duration: 300 } }
// Full card selection listener
MouseArea {
id: cardMa
anchors.fill: parent
hoverEnabled: window.activeTab !== "apps"
cursorShape: window.activeTab !== "apps" ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (window.activeTab !== "apps" && !model.is_default) {
let type = window.activeTab === "outputs" ? "sink" : "source";
Quickshell.execDetached(["bash", window.scriptsDir + "/audio_control.sh", "set-default", type, model.name]);
audioPoller.running = true;
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.topMargin: 12
anchors.bottomMargin: isActiveNode ? 12 : 16 // Prevent slider crowding bottom bounds
spacing: 12
// Top row: Text info and Icon
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
font.family: "Iosevka Nerd Font"; font.pixelSize: 22
color: isActiveNode ? window.crust : window.text
Behavior on color { ColorAnimation { duration: 200 } }
text: {
if (window.activeTab === "inputs") return "󰍬";
if (window.activeTab === "apps") return "󰎆";
if (model.description.toLowerCase().indexOf("headset") !== -1 || model.description.toLowerCase().indexOf("headphones") !== -1) return "󰋎";
return "󰓃";
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Text {
Layout.fillWidth: true; elide: Text.ElideRight
font.family: "JetBrains Mono"; font.weight: Font.Bold; font.pixelSize: 14
color: isActiveNode ? window.crust : window.text
text: model.description
}
Text {
Layout.fillWidth: true; elide: Text.ElideRight
font.family: "JetBrains Mono"; font.pixelSize: 11
color: isActiveNode ? Qt.darker(window.crust, 1.5) : window.subtext0
text: isActiveNode ? "Active Default" : model.name
}
}
}
// Bottom row: Custom Slider & Mute (Hides if it's the active node)
RowLayout {
Layout.fillWidth: true
spacing: 15
visible: !isActiveNode
opacity: isActiveNode ? 0.0 : 1.0
Behavior on opacity { NumberAnimation { duration: 200 } }
Rectangle {
Layout.preferredWidth: 32; Layout.preferredHeight: 32; radius: 16
color: muteMa.containsMouse ? "#1affffff" : "transparent"
border.color: muteMa.containsMouse ? (model.mute ? window.overlay0 : window.tabColor) : "transparent"
Behavior on color { ColorAnimation { duration: 150 } }
Text {
anchors.centerIn: parent
font.family: "Iosevka Nerd Font"; font.pixelSize: 18
color: model.mute ? window.overlay0 : window.subtext0
text: model.mute || model.volume === 0 ? "󰖁" : (model.volume > 50 ? "󰕾" : "󰖀")
Behavior on color { ColorAnimation { duration: 200 } }
}
MouseArea {
id: muteMa
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
onClicked: {
let type = "sink";
if (window.activeTab === "inputs") type = "source";
if (window.activeTab === "apps") type = "sink-input";
Quickshell.execDetached(["bash", window.scriptsDir + "/audio_control.sh", "toggle-mute", type, model.id]);
audioPoller.running = true;
}
}
}
// Local Slider
Item {
Layout.fillWidth: true
height: 14 // Slightly thinner than master slider for hierarchy
Timer {
id: volCmdThrottle
interval: 50
property int targetPct: -1
onTriggered: {
if (targetPct >= 0) {
let type = "sink";
if (window.activeTab === "inputs") type = "source";
if (window.activeTab === "apps") type = "sink-input";
if (targetPct > 0 && model.mute) {
Quickshell.execDetached(["bash", window.scriptsDir + "/audio_control.sh", "toggle-mute", type, model.id]);
}
Quickshell.execDetached(["bash", window.scriptsDir + "/audio_control.sh", "set-volume", type, model.id, targetPct]);
targetPct = -1;
}
}
}
Rectangle {
anchors.fill: parent; radius: 7
color: "#0dffffff"; border.color: "#1affffff"; border.width: 1
clip: true
Rectangle {
height: parent.height
width: parent.width * (Math.min(100, model.volume) / 100)
radius: 7
// Heavily dimmed if muted, slightly dimmed if background node
opacity: model.mute ? 0.3 : (volSliderMa.containsMouse ? 0.7 : 0.4)
Behavior on opacity { NumberAnimation { duration: 200 } }
Behavior on width { enabled: !window.draggingNodes[model.id]; NumberAnimation { duration: 300; easing.type: Easing.OutQuint } }
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: model.mute ? window.surface2 : window.tabColor; Behavior on color { ColorAnimation { duration: 300 } } }
GradientStop { position: 1.0; color: model.mute ? Qt.lighter(window.surface2, 1.15) : Qt.lighter(window.tabColor, 1.25); Behavior on color { ColorAnimation { duration: 300 } } }
}
}
}
MouseArea {
id: volSliderMa
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
onPressed: (mouse) => { syncDelay.stop(); window.draggingNodes[model.id] = true; updateVol(mouse.x); }
onPositionChanged: (mouse) => { if (pressed) updateVol(mouse.x); }
onReleased: { syncDelay.restart(); audioPoller.running = true; }
function updateVol(mx) {
let pct = Math.max(0, Math.min(100, Math.round((mx / width) * 100)));
let targetList = window.activeTab === "outputs" ? outputsModel : (window.activeTab === "inputs" ? inputsModel : appsModel);
for (let i = 0; i < targetList.count; i++) {
if (targetList.get(i).id === model.id) {
targetList.setProperty(i, "volume", pct);
break;
}
}
volCmdThrottle.targetPct = pct;
if (!volCmdThrottle.running) volCmdThrottle.start();
}
}
}
Text {
Layout.preferredWidth: 35
font.family: "JetBrains Mono"; font.weight: Font.Bold; font.pixelSize: 12
color: window.subtext0
text: model.volume + "%"
horizontalAlignment: Text.AlignRight
}
}
}
}
}
}
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import QtQuick
import Quickshell
import Quickshell.Io
FloatingWindow {
id: root
title: "qs-volume"
objectName: "quickshellWidget"
color: "transparent"
visible: true
implicitWidth: 480
implicitHeight: volumePopup.implicitHeight
VolumePopup {
id: volumePopup
anchors.fill: parent
}
Shortcut {
sequence: "Escape"
onActivated: Qt.quit()
}
}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
ACTION=$1
TYPE=$2
ID=$3
VAL=$4
case $ACTION in
set-volume)
# Type should be 'sink', 'source', or 'sink-input'
pactl set-$TYPE-volume "$ID" "$VAL%"
;;
toggle-mute)
pactl set-$TYPE-mute "$ID" toggle
;;
set-default)
# For setting defaults, we need to use the name rather than the index
pactl set-default-$TYPE "$ID"
;;
esac
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import subprocess
import json
import sys
def run_cmd(cmd):
try:
return subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL).decode('utf-8')
except:
return "[]"
def parse_pactl(output):
try:
return json.loads(output)
except:
return []
def get_valid_string(*args):
"""Safely return the first valid string that isn't 'null' or empty."""
for arg in args:
if arg and str(arg).strip().lower() not in ["null", "none", ""]:
return str(arg)
return ""
def get_data():
sinks = parse_pactl(run_cmd("pactl -f json list sinks"))
sources = parse_pactl(run_cmd("pactl -f json list sources"))
sink_inputs = parse_pactl(run_cmd("pactl -f json list sink-inputs"))
# Get defaults
try:
info = parse_pactl(run_cmd("pactl -f json info"))
default_sink = info.get("default_sink_name", "")
default_source = info.get("default_source_name", "")
except:
default_sink = ""
default_source = ""
def format_node(n, is_default=False, is_app=False):
# Extract volume gracefully
vol = 0
if "volume" in n and isinstance(n["volume"], dict):
if "front-left" in n["volume"]:
vol = int(n["volume"]["front-left"].get("value_percent", "0%").strip("%"))
elif "mono" in n["volume"]:
vol = int(n["volume"]["mono"].get("value_percent", "0%").strip("%"))
props = n.get("properties", {})
if is_app:
display_name = get_valid_string(props.get("application.name"), props.get("application.process.binary"), "Unknown App")
sub_desc = get_valid_string(props.get("media.name"), props.get("window.title"), props.get("media.role"), "Audio Stream")
else:
display_name = get_valid_string(props.get("device.description"), n.get("name"), "Unknown Device")
sub_desc = get_valid_string(n.get("name"), "Unknown")
icon = get_valid_string(props.get("application.icon_name"), props.get("device.icon_name"), "audio-card")
return {
"id": str(n.get("index")),
"name": sub_desc,
"description": display_name,
"volume": vol,
"mute": bool(n.get("mute", False)),
"is_default": bool(is_default),
"icon": icon
}
# Filter out empty apps/system sounds
apps = []
for s in sink_inputs:
props = s.get("properties", {})
if props.get("application.id") != "org.PulseAudio.pavucontrol":
apps.append(format_node(s, is_app=True))
out = {
"outputs": [format_node(s, s.get("name") == default_sink) for s in sinks],
"inputs": [format_node(s, s.get("name") == default_source) for s in sources],
"apps": apps
}
print(json.dumps(out))
if __name__ == "__main__":
get_data()
+2 -2
View File
@@ -79,8 +79,8 @@
]
},
"scroll-step": 5,
"on-click": "pamixer -t",
"on-click-right": "pavucontrol"
"on-click": "quickshell -p ~/.config/quickshell/audio/audioWidget.qml > /dev/null 2>&1",
"on-click-right": "pamixer -t"
},
"tray": {