Compare commits

...

15 Commits

Author SHA1 Message Date
37e8efa795 add battery widget 2026-05-15 18:17:20 +02:00
6847a123d5 update quickshell conf 2026-05-12 12:20:34 +02:00
f28788b71a update nb monitors 2026-04-17 15:13:54 +02:00
5aec12cd1f update zhsenv 2026-04-17 14:34:55 +02:00
6efa5d599c update quickshell conf 2026-04-14 18:13:39 +02:00
9248f9b33f change filemanager 2026-04-12 15:32:24 +02:00
252c88b869 update telescope and treesitter for nvim v0.12 2026-04-10 14:08:56 +02:00
63f99ff01a update bar 2026-04-08 02:21:01 +02:00
586e4b6320 update conf 2026-04-06 01:30:38 +02:00
c2b28df404 add quickshell bar 2026-04-06 01:30:28 +02:00
8d95eeb892 add quit all motion 2026-04-05 13:25:20 +02:00
d0d3fc23e8 fix zoxide integration 2026-04-03 12:24:06 +02:00
98235a8c18 zsh conf updates
- Add lazygit alias `lg`
- Configure fzf reverse search
2026-04-01 15:02:32 +02:00
e645ea1641 Update monitors conf on notebook 2026-04-01 14:41:12 +02:00
7824d27555 remove autojmp 2026-04-01 14:38:52 +02:00
44 changed files with 1559 additions and 79 deletions

View File

@ -11,6 +11,7 @@ default_roles:
# - ssh
- hyprland
- ghostty
- quickshell
temp:
- docker # Container platform
@ -67,4 +68,5 @@ aur_packages:
- pgformatter-git
# - jetbrains-toolbox
home_dir: "/home/johannes"
config_dir: "/home/johannes/.config"

View File

@ -2,7 +2,8 @@
- name: Setup dotfiles configuration
hosts: localhost
connection: local
become: false
become: true
become_user: johannes
vars:
dotfiles_dir: "{{ ansible_env.HOME }}/dotfiles"
# roles:

View File

@ -14,7 +14,7 @@ source = ./workspaces.conf
# See https://wiki.hypr.land/Configuring/Keywords/
$terminal = ghostty
$fileManager = thunar
$fileManager = ghostty -e yazi
$menu = env XDG_CURRENT_DESKTOP=Hyprland wofi --show drun
#################
@ -25,7 +25,8 @@ $menu = env XDG_CURRENT_DESKTOP=Hyprland wofi --show drun
# Or execute your favorite apps at launch like this:
# exec-once = waybar
exec-once = /usr/lib/hyprpolkitagent/hyprpolkitagent
exec-once = hyprpanel
# exec-once = hyprpanel
exec-once = quickshell
exec-once = dunst
exec-once = firefox
exec-once = hyprpaper
@ -45,7 +46,7 @@ debug:disable_logs = false
# See https://wiki.hypr.land/Configuring/Environment-variables/
env = XCURSOR_SIZE,24
env = HYPRCURSOR_SIZE,24
env = QT_QPA_PLATFORM,waylandl;xcb
env = QT_QPA_PLATFORM,wayland
env = QT_QPA_PLATFORMTHEME,qt5ct
env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1
env = QT_AUTO_SCREEN_SCALE_FACTOR,1
@ -90,18 +91,13 @@ env = _JAVA_AWT_WM_NONREPARENTING, 1
# https://wiki.hypr.land/Configuring/Variables/#general
general {
gaps_in = 5
gaps_out = 20
gaps_out = 10
border_size = 2
border_size = 3
# https://wiki.hypr.land/Configuring/Variables/#variable-types for info about colors
col.active_border = rgba(33ccffee) rgba(00ff99ee) 45deg
col.active_border = rgba(9b1a1aee) rgba(c94040ee) 45deg
col.inactive_border = rgba(595959aa)
# Active: cosmic purple -> neon cyan gradient
# col.active_border = rgba(7f5af0ee) rgba(00d1ffee) 45deg
#
# # Inactive: deep space slate
# col.inactive_border = rgba(0c0e14aa)
# Set to true enable resizing windows by clicking and dragging on borders and gaps
resize_on_border = false
@ -200,7 +196,7 @@ animations {
# See https://wiki.hypr.land/Configuring/Dwindle-Layout/ for more
dwindle {
pseudotile = true # Master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below
# pseudotile = true # Master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below
preserve_split = true # You probably want this
}
@ -334,6 +330,13 @@ windowrule {
suppress_event = maximize
}
xwayland {
enabled = true
use_nearest_neighbor = true
force_zero_scaling = false
create_abstract_socket = false
}
# Fix some dragging issues with XWayland
windowrule {
name = fix-xwayland-drags

View File

@ -1,23 +1,23 @@
preload = /home/johannes/Pictures/Wallpapers/wallhaven-5g22q5.png
preload = /home/johannes/Pictures/Wallpapers/japan-artistic-3840x2160-25406.jpg
wallpaper {
monitor = DP-1
path =/home/johannes/Pictures/Wallpapers/wallhaven-5g22q5.png
path =/home/johannes/Pictures/Wallpapers/japan-artistic-3840x2160-25406.jpg
# fitmode = cover
}
wallpaper {
monitor = DP-2
path =/home/johannes/Pictures/Wallpapers/wallhaven-5g22q5.png
path =/home/johannes/Pictures/Wallpapers/japan-artistic-3840x2160-25406.jpg
# fitmode = cover
}
wallpaper {
monitor = HDMI-A-1
path =/home/johannes/Pictures/Wallpapers/wallhaven-5g22q5.png
path =/home/johannes/Pictures/Wallpapers/japan-artistic-3840x2160-25406.jpg
# fitmode = cover
}
wallpaper {
monitor = HDMI-A-2
path =/home/johannes/Pictures/Wallpapers/wallhaven-5g22q5.png
path =/home/johannes/Pictures/Wallpapers/japan-artistic-3840x2160-25406.jpg
# fitmode = cover
}

View File

@ -0,0 +1,3 @@
# Generated by nwg-displays on 2026-04-10 at 16:05:43. Do not edit manually.
monitor=eDP-1,2880x1800@90.0,0x0,1.4999999999999996

View File

@ -1,7 +1,7 @@
# Generated by nwg-displays on 2026-02-28 at 11:47:04. Do not edit manually.
# Generated by nwg-displays on 2026-04-20 at 01:17:12. Do not edit manually.
monitor=DP-1,2560x1440@165.0,6626x876,1.0
monitor=DP-2,2560x1440@165.0,4066x876,1.0
monitor=HDMI-A-1,disable
monitor=HDMI-A-2,1920x1080@60.0,9186x396,1.0
monitor=HDMI-A-2,transform,3
monitor=HDMI-A-1,disable

View File

@ -41,5 +41,8 @@
"bar.battery.label": false,
"bar.battery.hideLabelWhenFull": true,
"theme.bar.buttons.battery.enableBorder": false,
"theme.osd.enable": true
"theme.osd.enable": true,
"theme.bar.buttons.y_margins": "0.2em",
"theme.bar.dropdownGap": "2.9em",
"theme.bar.outer_spacing": "1.6em"
}

View File

@ -15,32 +15,31 @@
path: "{{ config_dir }}/hypr"
state: directory
mode: '0755'
owner: johannes
group: johannes
- name: Symlink hyprland config files
file:
src: "{{ role_path }}/files/hypr/{{ item }}"
dest: "{{ config_dir }}/hypr/{{ item }}"
src: "{{ role_path }}/files/hypr/{{ conf_file }}"
dest: "{{ config_dir }}/hypr/{{ conf_file }}"
state: link
force: true
loop:
- hyprland.conf
- hyprlock.conf
- hyprpaper.conf
# - workspaces.conf
- scripts
loop_control:
loop_var: conf_file
- name: Symlink host-specific monitors.conf
file:
src: "{{ role_path }}/files/hypr/monitors_{{ ansible_hostname }}.conf"
src: "{{ role_path }}/files/hypr/monitors_{{ ansible_facts['hostname'] }}.conf"
dest: "{{ config_dir }}/hypr/monitors.conf"
state: link
force: true
- name: Symlink host-specific workspaces.conf
file:
src: "{{ role_path }}/files/hypr/workspaces_{{ ansible_hostname }}.conf"
src: "{{ role_path }}/files/hypr/workspaces_{{ ansible_facts['hostname'] }}.conf"
dest: "{{ config_dir }}/hypr/workspaces.conf"
state: link
force: true

View File

@ -1 +1 @@
vim.opt_local.shiftwidth = 4
vim.opt_local.shiftwidth = 2

View File

@ -1,5 +1,5 @@
{
"LuaSnip": { "branch": "master", "commit": "5a1e39223db9a0498024a77b8441169d260c8c25" },
"LuaSnip": { "branch": "master", "commit": "642b0c595e11608b4c18219e93b88d7637af27bc" },
"alpha-nvim": { "branch": "main", "commit": "a9d8fb72213c8b461e791409e7feabb74eb6ce73" },
"auto-session": { "branch": "main", "commit": "62437532b38495551410b3f377bcf4aaac574ebe" },
"bufdelete.nvim": { "branch": "master", "commit": "f6bcea78afb3060b198125256f897040538bcb81" },
@ -12,28 +12,29 @@
"dressing.nvim": { "branch": "master", "commit": "2d7c2db2507fa3c4956142ee607431ddb2828639" },
"flutter-tools.nvim": { "branch": "main", "commit": "677cc07c16e8b89999108d2ebeefcfc5f539b73c" },
"friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" },
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
"lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" },
"lazygit.nvim": { "branch": "main", "commit": "a04ad0dbc725134edbee3a5eea29290976695357" },
"lspkind.nvim": { "branch": "master", "commit": "c7274c48137396526b59d86232eabcdc7fed8a32" },
"lualine.nvim": { "branch": "master", "commit": "47f91c416daef12db467145e16bed5bbfe00add8" },
"mason-lspconfig.nvim": { "branch": "main", "commit": "a676ab7282da8d651e175118bcf54483ca11e46d" },
"lualine.nvim": { "branch": "master", "commit": "8811f3f3f4dc09d740c67e9ce399e7a541e2e5b2" },
"mason-lspconfig.nvim": { "branch": "main", "commit": "63a3c6a80538de1003373a619e29aeda27809ad3" },
"mason-tool-installer.nvim": { "branch": "main", "commit": "443f1ef8b5e6bf47045cb2217b6f748a223cf7dc" },
"mason.nvim": { "branch": "main", "commit": "44d1e90e1f66e077268191e3ee9d2ac97cc18e65" },
"monokai-nightasty.nvim": { "branch": "main", "commit": "8be5e1c6e1d59873505e81b161e923264dfa5c1a" },
"none-ls-extras.nvim": { "branch": "main", "commit": "c6fa39ac52814182c05552cb5d3750cae23ff0f0" },
"none-ls.nvim": { "branch": "main", "commit": "c9317c2a8629d4e39e7cf47be74cb67f3ab37cda" },
"mason.nvim": { "branch": "main", "commit": "b03fb0f20bc1d43daf558cda981a2be22e73ac42" },
"monokai-nightasty.nvim": { "branch": "main", "commit": "1e9b92006782a1217d0a7a871b871768f1cbf5ed" },
"none-ls-extras.nvim": { "branch": "main", "commit": "14fa31ce8c0268a3b2c9cc14979ecf771982d433" },
"none-ls.nvim": { "branch": "main", "commit": "7f9301e416533b5d74e2fb3b1ce5059eeaed748b" },
"nvim-autopairs": { "branch": "master", "commit": "59bce2eef357189c3305e25bc6dd2d138c1683f5" },
"nvim-cmp": { "branch": "main", "commit": "da88697d7f45d16852c6b2769dc52387d1ddc45f" },
"nvim-cmp": { "branch": "main", "commit": "a1d504892f2bc56c2e79b65c6faded2fd21f3eca" },
"nvim-lsp-file-operations": { "branch": "master", "commit": "b9c795d3973e8eec22706af14959bc60c579e771" },
"nvim-lspconfig": { "branch": "master", "commit": "1d13d2b0df9a0a02904c76d7ad6810f71d404406" },
"nvim-lspconfig": { "branch": "master", "commit": "fb5fa30626ae10f7f79f740059d3769993936ecb" },
"nvim-surround": { "branch": "main", "commit": "61319d4bd1c5e336e197defa15bd104c51f0fb29" },
"nvim-tree.lua": { "branch": "master", "commit": "b3772adec8db61ba9098c5624a0823a77be3a23d" },
"nvim-treesitter": { "branch": "master", "commit": "42fc28ba918343ebfd5565147a42a26580579482" },
"nvim-tree.lua": { "branch": "master", "commit": "509962f21ab7289d8dcd28568af539be39a8c01e" },
"nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" },
"nvim-ts-autotag": { "branch": "main", "commit": "8e1c0a389f20bf7f5b0dd0e00306c1247bda2595" },
"nvim-web-devicons": { "branch": "master", "commit": "d7462543c9e366c0d196c7f67a945eaaf5d99414" },
"nvim-web-devicons": { "branch": "master", "commit": "95b7a002d5dba1a42eb58f5fac5c565a485eefd0" },
"plenary.nvim": { "branch": "master", "commit": "b9fd5226c2f76c951fc8ed5923d85e4de065e509" },
"telescope-fzf-native.nvim": { "branch": "main", "commit": "6fea601bd2b694c6f2ae08a6c6fab14930c60e2c" },
"telescope.nvim": { "branch": "0.1.x", "commit": "a0bbec21143c7bc5f8bb02e0005fa0b982edc026" },
"telescope.nvim": { "branch": "master", "commit": "7ef4d6dccb78ee71e552bbd866176762ad328afa" },
"vim-floaterm": { "branch": "master", "commit": "0ab5eb8135dc884bc543a819ac7033c15e72a76b" },
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
}

View File

@ -20,6 +20,10 @@ keymap.set('n', 'j', 'gj', opts)
keymap.set('n', '<UP>', 'gk', opts)
keymap.set('n', '<DOWN>', 'gj', opts)
-- bind tabswitching to Tab and Shift-Tab
keymap.set('n', '<Tab>', ':tabnext<CR>', opts)
keymap.set('n', '<S-Tab>', ':tabprev<CR>', opts)
-- COMMANDS
-- print current working directory

View File

@ -1,5 +1,7 @@
local opt = vim.opt
vim.filetype.add({ extension = { mdx = 'mdx' } })
-- numbers
opt.relativenumber = true
opt.number = true

View File

@ -1,4 +1,4 @@
local lazypath = vim.fn.stdpath('data') .. 'lazy/lazy.nvim'
local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim'
-- bootstrap lazy
if not (vim.uv or vim.loop).fs_stat(lazypath) then

View File

@ -59,6 +59,7 @@ return {
keymap.set('n', '<leader>sl', '<cmd>AutoSession search<cr>', opts)
keymap.set('n', '<leader>sr', session_restore, opts)
keymap.set('n', '<leader>sd', '<cmd>AutoSession delete<cr>', opts)
keymap.set('n', '<leader>qa', '<cmd>qa<cr>', opts)
local function named_save()
vim.ui.input({ prompt = 'Enter Session name: ' }, function(input)
@ -70,12 +71,14 @@ return {
local wk = require('which-key')
wk.add({
{ '<leader>s', group = 'auto-session' },
{ '<leader>q', group = 'quit' },
{ '<leader>ss', desc = 'SessionSave' },
{ '<leader>sS', desc = 'NamedSessionSave' },
{ '<leader>sq', desc = 'Save and Quit' },
{ '<leader>sl', desc = 'SessionList (SessionSearch)'},
{ '<leader>sr', desc = 'SessionRestore'},
{ '<leader>sd', desc = 'SessionDelete'},
{ '<leader>qa', desc = 'Quit All'},
})
end,
}

View File

@ -85,7 +85,16 @@ return {
-- Explicitly enable all mason-managed servers.
-- This is belt-and-suspenders alongside mason-lspconfig's automatic_enable,
-- ensuring servers start regardless of mason-lspconfig version.
vim.lsp.enable({ 'lua_ls', 'html', 'cssls', 'bashls', 'ts_ls', 'eslint', 'tailwindcss' })
vim.lsp.enable({ 'lua_ls', 'html', 'cssls', 'bashls', 'ts_ls', 'eslint', 'tailwindcss', 'mdx_analyzer' })
-- mdx_analyzer: needs typescript SDK path to find tsserverlibrary.js
vim.lsp.config('mdx_analyzer', {
init_options = {
typescript = {
tsdk = vim.fn.stdpath('data') .. '/mason/packages/typescript-language-server/node_modules/typescript/lib',
},
},
})
-- hyprls: not managed by mason, enable manually
vim.lsp.config('hyprls', {
@ -96,5 +105,11 @@ return {
end,
})
vim.lsp.enable('hyprls')
vim.lsp.config('qmlls', {
cmd = { 'qmlls6' },
})
vim.lsp.enable('qmlls')
end,
}

View File

@ -1,6 +1,5 @@
return {
'nvim-telescope/telescope.nvim',
branch = '0.1.x',
'nvim-telescope/telescope.nvim', -- 0.1.x dropped; master has nvim 0.12 compat (ft_to_lang removal)
dependencies = {
'nvim-lua/plenary.nvim',
{ "nvim-telescope/telescope-fzf-native.nvim", build = "make" },

View File

@ -1,23 +1,18 @@
return {
"nvim-treesitter/nvim-treesitter",
branch = "main", -- master is frozen; main required for nvim 0.12+
event = { "BufReadPre", "BufNewFile" },
build = ":TSUpdate",
dependencies = {
"windwp/nvim-ts-autotag",
},
config = function()
local treesitter = require('nvim-treesitter.configs')
treesitter.setup({
require('nvim-treesitter').setup({
auto_install = true,
sync_install = false,
highlight = {
enable = true,
additional_vim_regex_highlighting = false,
},
prefer_git = true, -- tarball downloads fail for some parsers on 0.12
highlight = { enable = true },
indent = { enable = true },
autotag = { enable = true },
ensure_installed = {
install = {
'json',
'javascript',
'typescript',
@ -35,17 +30,10 @@ return {
'python',
'gitignore',
'c',
},
ignore_install = {},
incremental_selection = {
enable = true,
keymaps = {
init_selection = '<C-Space>',
node_incremental = '<C-Space>',
scope_incremental = false,
node_decremental = '<bs>',
},
'mdx',
},
})
require('nvim-ts-autotag').setup()
end,
}

View File

@ -3,6 +3,8 @@
package:
name: neovim
state: present
become: yes
become_user: root
- name: Symlink neovim
file:

View File

@ -0,0 +1,140 @@
import Quickshell
import QtQuick
import "../components"
PanelWindow {
id: root
property var modelData
screen: modelData
anchors {
top: true
left: true
right: true
}
implicitHeight: 40
color: Theme.bg
readonly property string screenName: modelData.name
property string activePopup: ""
readonly property int bw: Theme.borderWidth // 2
readonly property int pad: Theme.enclosureMargin // 3
// ── Bar bottom border ─────────────────────────────────────────────
Rectangle {
anchors { bottom: parent.bottom; left: parent.left; right: parent.right }
height: root.bw
color: Theme.border
}
// ── Bar content ───────────────────────────────────────────────────
Item {
anchors {
fill: parent
leftMargin: 8
rightMargin: 8
topMargin: root.bw
bottomMargin: root.bw
}
Workspaces {
anchors { left: parent.left; verticalCenter: parent.verticalCenter }
screenName: root.screenName
}
MusicPlayer {
id: musicChip
anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
onClicked: root.activePopup = root.activePopup === "controls" ? "" : "controls"
}
Row {
id: rightRow
anchors { right: parent.right; verticalCenter: parent.verticalCenter }
spacing: 8
SysTray {
anchors.verticalCenter: parent.verticalCenter
barWindow: root
}
Battery {
anchors.verticalCenter: parent.verticalCenter
}
NetworkStatus {
anchors.verticalCenter: parent.verticalCenter
}
VolumeControl {
anchors.verticalCenter: parent.verticalCenter
onClickedLeft: root.activePopup = root.activePopup === "mixer" ? "" : "mixer"
}
Clock {
id: clockDisp
anchors.verticalCenter: parent.verticalCenter
onClicked: root.activePopup = root.activePopup === "calendar" ? "" : "calendar"
}
}
}
// ── Music controls popup ──────────────────────────────────────────
PopoutWindow {
popupName: "controls"
activePopup: root.activePopup
anchor.window: root
anchor.rect.y: root.implicitHeight - root.bw - Theme.radius
anchor.rect.x: Math.round((root.width - implicitWidth) / 2)
implicitWidth: Math.max(musicChip.implicitWidth + 2 * root.pad,
ctrlContent.implicitWidth + 2 * root.bw)
implicitHeight: ctrlContent.implicitHeight + root.bw + Theme.radius
MusicPlayerControls {
id: ctrlContent
anchors { left: parent.left; right: parent.right }
player: musicChip.player
}
}
// ── Calendar popup ────────────────────────────────────────────────
PopoutWindow {
popupName: "calendar"
activePopup: root.activePopup
anchor.window: root
anchor.rect.y: root.implicitHeight - root.bw - Theme.radius
readonly property real pw: Math.max(rightRow.width + 2 * root.pad,
calContent.implicitWidth + 2 * root.bw)
anchor.rect.x: root.width - pw
implicitWidth: pw
implicitHeight: calContent.implicitHeight + root.bw + Theme.radius
CalendarContent {
id: calContent
anchors { left: parent.left; right: parent.right }
now: clockDisp.now
}
}
// ── Volume mixer popup ────────────────────────────────────────────
PopoutWindow {
popupName: "mixer"
activePopup: root.activePopup
anchor.window: root
anchor.rect.y: root.implicitHeight - root.bw - Theme.radius
readonly property real pw: Math.max(rightRow.width + 2 * root.pad,
mixContent.implicitWidth + 2 * root.bw)
anchor.rect.x: root.width - pw
implicitWidth: pw
implicitHeight: mixContent.implicitHeight + root.bw + Theme.radius
VolumeMixerContent {
id: mixContent
anchors { left: parent.left; right: parent.right }
}
}
}

View File

@ -0,0 +1,84 @@
import Quickshell
import Quickshell.Io
import QtQuick
import "../components"
Item {
id: root
implicitWidth: hasBattery ? row.implicitWidth + 10 : 0
implicitHeight: 24
visible: hasBattery
property bool hasBattery: false
property int percent: 0
property string status: "Unknown" // "Charging", "Discharging", "Full", "Unknown"
// \uf0e7 = bolt (charging), \uf240-\uf244 = FA battery full…empty
readonly property string battIcon: {
if (status === "Charging" || status === "Full") return "\uf0e7"
if (percent <= 10) return "\uf244"
if (percent <= 25) return "\uf243"
if (percent <= 50) return "\uf242"
if (percent <= 75) return "\uf241"
return "\uf240"
}
readonly property bool isLow: percent <= 20 && status === "Discharging"
Timer {
interval: 30000
running: true
repeat: true
triggeredOnStart: true
onTriggered: battProc.running = true
}
Process {
id: battProc
// Find first BAT* supply; print "NONE" if none exists
command: ["sh", "-c",
"BAT=$(ls /sys/class/power_supply/ 2>/dev/null | grep -m1 '^BAT'); " +
"[ -z \"$BAT\" ] && echo NONE && exit 0; " +
"echo \"$(cat /sys/class/power_supply/$BAT/capacity):$(cat /sys/class/power_supply/$BAT/status)\""
]
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
const line = data.trim()
if (line === "" || line === "NONE") {
root.hasBattery = false
return
}
const sep = line.indexOf(":")
if (sep < 0) return
root.percent = parseInt(line.substring(0, sep)) || 0
root.status = line.substring(sep + 1)
root.hasBattery = true
}
}
onExited: running = false
}
Row {
id: row
anchors.centerIn: parent
spacing: 5
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.battIcon
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 14
color: root.status === "Charging" ? Theme.accent
: root.isLow ? "#cc3333"
: Theme.text
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.percent + "%"
font.pixelSize: 11
color: root.isLow ? "#cc3333" : Theme.textDim
}
}
}

View File

@ -0,0 +1,139 @@
import QtQuick
import "../components"
// Calendar grid a plain Item so it can live inside an expanding section border.
Item {
id: root
property var now: new Date()
property int viewYear: now.getFullYear()
property int viewMonth: now.getMonth() // 0-based
readonly property var monthNames: [
"January","February","March","April","May","June",
"July","August","September","October","November","December"
]
readonly property int daysInMonth: new Date(viewYear, viewMonth + 1, 0).getDate()
// First weekday of month: Mon=0 … Sun=6
readonly property int startOffset: (new Date(viewYear, viewMonth, 1).getDay() + 6) % 7
readonly property int totalCells: Math.ceil((startOffset + daysInMonth) / 7) * 7
readonly property int todayYear: now.getFullYear()
readonly property int todayMonth: now.getMonth()
readonly property int todayDay: now.getDate()
// All widths derived from this constant — never from parent.width —
// to avoid Column polish loops when the popup is wider than the grid.
readonly property int cellW: 28
readonly property int cellGap: 2
readonly property int gridW: 7 * cellW + 6 * cellGap // 208
implicitWidth: gridW + 24
implicitHeight: calLayout.implicitHeight + 16
Column {
id: calLayout
// Centre the fixed-width grid inside the (potentially wider) popup.
// No left+right anchors → implicitWidth stays at gridW, no resize loop.
anchors { top: parent.top; topMargin: 8; horizontalCenter: parent.horizontalCenter }
spacing: 6
// Month navigation — all child widths are explicit constants
Row {
spacing: 0
Text {
text: ""
color: Theme.text
font.pixelSize: 16
width: 20
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.viewMonth === 0) { root.viewMonth = 11; root.viewYear-- }
else root.viewMonth--
}
}
}
Text {
text: root.monthNames[root.viewMonth] + " " + root.viewYear
color: Theme.text
font.pixelSize: 13
font.bold: true
width: root.gridW - 40 // fixed constant, not parent.width
horizontalAlignment: Text.AlignHCenter
}
Text {
text: ""
color: Theme.text
font.pixelSize: 16
width: 20
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.viewMonth === 11) { root.viewMonth = 0; root.viewYear++ }
else root.viewMonth++
}
}
}
}
// Weekday headers
Row {
spacing: root.cellGap
Repeater {
model: ["Mo","Tu","We","Th","Fr","Sa","Su"]
Text {
text: modelData
color: Theme.textDim
font.pixelSize: 10
width: root.cellW
horizontalAlignment: Text.AlignHCenter
}
}
}
// Day grid
Grid {
columns: 7
spacing: root.cellGap
Repeater {
model: root.totalCells
delegate: Item {
width: root.cellW
height: 22
readonly property int dayNum: index - root.startOffset + 1
readonly property bool valid:
index >= root.startOffset &&
index < root.startOffset + root.daysInMonth
readonly property bool isToday:
valid &&
root.viewYear === root.todayYear &&
root.viewMonth === root.todayMonth &&
dayNum === root.todayDay
Rectangle {
anchors.fill: parent
radius: 3
color: isToday ? Theme.accent : "transparent"
visible: isToday
}
Text {
anchors.centerIn: parent
text: parent.valid ? String(parent.dayNum) : ""
color: parent.isToday ? Theme.text : Theme.textDim
font.pixelSize: 11
font.bold: parent.isToday
}
}
}
}
}
}

View File

@ -0,0 +1,47 @@
import QtQuick
import "../components"
Item {
id: root
signal clicked
implicitWidth: timeLabel.implicitWidth + 16
implicitHeight: 24
property var now: new Date()
Timer {
interval: 1000
running: true
repeat: true
onTriggered: root.now = new Date()
}
readonly property string display: {
const d = now
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
const dd = String(d.getDate()).padStart(2, "0")
const mm = String(d.getMonth() + 1).padStart(2, "0")
const yyyy = d.getFullYear()
const h = String(d.getHours()).padStart(2, "0")
const m = String(d.getMinutes()).padStart(2, "0")
const s = String(d.getSeconds()).padStart(2, "0")
return days[d.getDay()] + " " + dd + "." + mm + "." + yyyy + " " + h + ":" + m + ":" + s
}
Text {
id: timeLabel
anchors.centerIn: parent
text: root.display
color: Theme.text
font.pixelSize: 12
font.family: "JetBrainsMono Nerd Font Mono"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}

View File

@ -0,0 +1,38 @@
import Quickshell.Services.Mpris
import QtQuick
import "../components"
Item {
id: root
signal clicked
// Pick the first playing player, fall back to first available
readonly property var player: {
const vals = Mpris.players.values
for (const p of vals) {
if (p.isPlaying) return p
}
return vals.length > 0 ? vals[0] : null
}
visible: player !== null
implicitWidth: player ? Math.min(240, titleLabel.implicitWidth + 20) : 0
implicitHeight: 24
Text {
id: titleLabel
anchors.centerIn: parent
width: parent.width - 20
text: root.player?.trackTitle ?? ""
color: Theme.text
font.pixelSize: 12
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
anchors.fill: parent
onClicked: root.clicked()
}
}

View File

@ -0,0 +1,151 @@
import Quickshell.Services.Mpris
import QtQuick
import QtQuick.Layouts
import "../components"
// Plain Item lives inside the expanding center section border in Bar.qml.
Item {
id: root
required property var player
implicitWidth: 280
implicitHeight: layout.implicitHeight + 28
// Keep the progress binding live while playing, pause while seeking.
FrameAnimation {
running: (root.player?.isPlaying ?? false) && !progressArea.pressed
onTriggered: root.player.positionChanged()
}
ColumnLayout {
id: layout
anchors { fill: parent; margins: 14 }
spacing: 8
// Track info
Text {
text: root.player?.trackTitle ?? ""
color: Theme.text
font.pixelSize: 13
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
text: root.player?.trackArtist ?? ""
color: Theme.textDim
font.pixelSize: 11
Layout.fillWidth: true
elide: Text.ElideRight
visible: (root.player?.trackArtist ?? "") !== ""
Layout.topMargin: -4
}
// Progress bar
Item {
visible: root.player?.positionSupported && root.player?.lengthSupported
&& (root.player?.length ?? 0) > 0
Layout.fillWidth: true
height: 12
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 3
radius: 2
color: Theme.progressTrack
Rectangle {
width: root.player?.length > 0
? parent.width * (root.player.position / root.player.length)
: 0
height: parent.height
radius: parent.radius
color: Theme.accent
}
}
MouseArea {
id: progressArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function seek(mx) {
if (!(root.player?.canSeek)) return
const ratio = Math.max(0, Math.min(1, mx / width))
root.player.position = ratio * (root.player?.length ?? 0)
}
onClicked: seek(mouseX)
onPositionChanged: if (pressed) seek(mouseX)
}
}
// Media controls
RowLayout {
Layout.fillWidth: true
spacing: 0
Item { Layout.fillWidth: true }
ControlButton {
icon: "⏮"
iconSize: 18
enabled: root.player?.canGoPrevious ?? false
onActivated: root.player.previous()
}
Item { width: 8 }
ControlButton {
icon: root.player?.isPlaying ? "⏸" : "▶"
iconSize: 22
enabled: true
onActivated: root.player?.togglePlaying()
}
Item { width: 8 }
ControlButton {
icon: "⏭"
iconSize: 18
enabled: root.player?.canGoNext ?? false
onActivated: root.player.next()
}
Item { Layout.fillWidth: true }
}
}
component ControlButton: Rectangle {
id: btn
required property string icon
required property int iconSize
required property bool enabled
signal activated
implicitWidth: label.implicitWidth + 16
implicitHeight: label.implicitHeight + 10
radius: Theme.radius
color: btnArea.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : 'transparent'
Behavior on color { ColorAnimation { duration: 80 } }
Text {
id: label
anchors.centerIn: parent
text: btn.icon
font.pixelSize: btn.iconSize
color: btn.enabled ? Theme.text : Theme.textDisabled
}
MouseArea {
id: btnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: if (btn.enabled) btn.activated()
}
}
}

View File

@ -0,0 +1,86 @@
import Quickshell
import Quickshell.Io
import QtQuick
import "../components"
Item {
id: root
implicitWidth: row.implicitWidth + 10
implicitHeight: 24
property string connType: "none" // "wifi", "ethernet", "none"
property string connName: ""
// accumulate results here, flush to displayed props on exit
property string _pendingType: "none"
property string _pendingName: ""
// \uf1eb = FA wifi, \uf0e8 = FA sitemap (wired proxy), \uf127 = FA chain-broken
readonly property string netIcon:
connType === "wifi" ? "\uf1eb" :
connType === "ethernet" ? "\uf0e8" : "\uf127"
function parseLine(line) {
const idx1 = line.indexOf(":")
if (idx1 < 0) return
const idx2 = line.indexOf(":", idx1 + 1)
if (idx2 < 0) return
const type = line.substring(0, idx1)
const state = line.substring(idx1 + 1, idx2)
const conn = line.substring(idx2 + 1).trim()
if (state === "connected" && (type === "wifi" || type === "ethernet")) {
if (root._pendingType === "none" || type === "wifi") {
root._pendingType = type
root._pendingName = conn
}
}
}
Timer {
interval: 5000
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
root._pendingType = "none"
root._pendingName = ""
netProc.running = true
}
}
Process {
id: netProc
command: ["nmcli", "-t", "-f", "TYPE,STATE,CONNECTION", "dev"]
stdout: SplitParser {
splitMarker: "\n"
onRead: data => root.parseLine(data)
}
onExited: {
root.connType = root._pendingType
root.connName = root._pendingName
running = false
}
}
Row {
id: row
anchors.centerIn: parent
spacing: 5
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.netIcon
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 14
color: root.connType !== "none" ? Theme.text : Theme.textDim
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.connName.length > 16 ? root.connName.substring(0, 15) + "…" : root.connName
font.pixelSize: 11
color: Theme.textDim
visible: root.connName !== ""
}
}
}

View File

@ -0,0 +1,162 @@
import Quickshell
import QtQuick
import QtQuick.Shapes
import "../components"
PopupWindow {
id: self
property string popupName: ""
property string activePopup: ""
visible: false
color: "transparent"
default property alias content: contentSlot.data
readonly property int bw: Theme.borderWidth
readonly property real r: Theme.radius
readonly property bool _open: activePopup === popupName
on_OpenChanged: {
if (_open) {
hideTimer.stop();
self.visible = true;
fade.opacity = 1.0;
} else {
fade.opacity = 0.0;
hideTimer.start();
}
}
Timer {
id: hideTimer
interval: 200
onTriggered: self.visible = false
}
Item {
id: fade
anchors.fill: parent
opacity: 0.0
Behavior on opacity {
NumberAnimation {
duration: 180
easing.type: Easing.InOutCubic
}
}
// Background + border in one Shape so fill matches stroke exactly
Shape {
id: pbs
anchors.fill: parent
ShapePath {
strokeWidth: self.bw
strokeColor: Theme.border
fillColor: Theme.bg
// Start flush at top-left, hidden under bar
startX: 0
startY: self.r
// Left side down to bottom-left corner
PathLine {
x: 0
y: pbs.height - self.r
}
// Bottom-left corner — Counterclockwise for outward curve
PathArc {
x: self.r
y: pbs.height
radiusX: self.r
radiusY: self.r
direction: PathArc.Counterclockwise
}
// Bottom edge
PathLine {
x: pbs.width - self.r
y: pbs.height
}
// Bottom-right corner — Counterclockwise for outward curve
PathArc {
x: pbs.width
y: pbs.height - self.r
radiusX: self.r
radiusY: self.r
direction: PathArc.Counterclockwise
}
// Right side up, flush to top, hidden under bar
PathLine {
x: pbs.width
y: self.r
}
}
}
// // Top-left concave corner
// Shape {
// x: 0
// y: 0
// width: self.r
// height: self.r
// ShapePath {
// fillColor: Theme.bg
// strokeColor: "transparent"
// startX: 0
// startY: 0
// PathLine {
// x: self.r
// y: 0
// }
// PathArc {
// x: 0
// y: self.r
// radiusX: self.r
// radiusY: self.r
// direction: PathArc.Clockwise
// }
// }
// }
//
// // Top-right concave corner
// Shape {
// x: parent.width - self.r
// y: 0
// width: self.r
// height: self.r
// ShapePath {
// fillColor: Theme.bg
// strokeColor: "transparent"
// startX: self.r
// startY: 0
// PathLine {
// x: 0
// y: 0
// }
// PathArc {
// x: self.r
// y: self.r
// radiusX: self.r
// radiusY: self.r
// direction: PathArc.Counterclockwise
// }
// }
// }
Item {
id: contentSlot
anchors {
top: parent.top
topMargin: self.r + self.bw
left: parent.left
leftMargin: self.bw
right: parent.right
rightMargin: self.bw
}
}
}
}

View File

@ -0,0 +1,65 @@
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import QtQuick
import "../components"
Item {
id: root
required property var barWindow
implicitWidth: trayRow.width
implicitHeight: 24
Row {
id: trayRow
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Repeater {
model: SystemTray.items
delegate: Item {
id: trayDelegate
required property var modelData
width: 24
height: 24
// Hover highlight
Rectangle {
anchors.fill: parent
radius: Theme.radius
color: hoverArea.containsMouse ? Qt.rgba(1,1,1,0.08) : "transparent"
Behavior on color { ColorAnimation { duration: 80 } }
}
TrayIcon {
anchors.centerIn: parent
icon: trayDelegate.modelData.icon
size: 16
}
MouseArea {
id: hoverArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
const item = trayDelegate.modelData
const wantsMenu = mouse.button === Qt.RightButton || item.onlyMenu
if (wantsMenu && item.hasMenu) {
// display() needs the quickshell PanelWindow (not QQuickWindow).
// mapToItem(null) gives scene/window-local coordinates.
const pos = trayDelegate.mapToItem(null, 0, trayDelegate.height)
item.display(root.barWindow, Math.round(pos.x), Math.round(pos.y))
} else if (!item.onlyMenu) {
item.activate()
}
}
}
}
}
}
}

View File

@ -0,0 +1,74 @@
import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts
import "../components"
Item {
id: root
implicitWidth: chip.implicitWidth + 10
implicitHeight: 24
signal clickedLeft
PwObjectTracker {
objects: Pipewire.nodes.values
}
function safeVolume(node) {
if (!node || !node.ready || !node.audio) return 0
const v = node.audio.volume
return (v !== undefined && !isNaN(v)) ? v : 0
}
function setVolume(node, v) {
if (!node || !node.ready || !node.audio) return
node.audio.volume = Math.max(0, Math.min(1, v))
}
function volIcon(vol, muted) {
if (muted) return "\uf6a9"
if (vol > 0.6) return "\uf028"
if (vol > 0.2) return "\uf027"
return "\uf026"
}
readonly property var defaultSink: Pipewire.defaultAudioSink
readonly property real defaultVolume: safeVolume(defaultSink)
readonly property bool defaultMuted: defaultSink?.audio?.muted ?? false
Row {
id: chip
anchors.centerIn: parent
spacing: 5
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.volIcon(root.defaultVolume, root.defaultMuted)
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 14
color: root.defaultMuted ? Theme.textDim : Theme.text
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.defaultMuted ? "mute" : Math.round(root.defaultVolume * 100) + "%"
font.pixelSize: 11
color: Theme.textDim
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (root.defaultSink?.ready && root.defaultSink?.audio)
root.defaultSink.audio.muted = !root.defaultSink.audio.muted
} else {
root.clickedLeft()
}
}
onWheel: wheel => {
if (!root.defaultSink?.ready) return
root.setVolume(root.defaultSink, root.defaultVolume + wheel.angleDelta.y / 120 * 0.05)
}
}
}

View File

@ -0,0 +1,143 @@
import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts
import "../components"
// Volume mixer a plain Item so it can live inside an expanding section border.
Item {
id: root
implicitWidth: 300
implicitHeight: mixerCol.implicitHeight + 24
PwObjectTracker {
objects: Pipewire.nodes.values
}
function safeVolume(node) {
if (!node || !node.ready || !node.audio) return 0
const v = node.audio.volume
return (v !== undefined && !isNaN(v)) ? v : 0
}
function setVolume(node, v) {
if (!node || !node.ready || !node.audio) return
node.audio.volume = Math.max(0, Math.min(1, v))
}
function volIcon(vol, muted) {
if (muted) return "\uf6a9"
if (vol > 0.6) return "\uf028"
if (vol > 0.2) return "\uf027"
return "\uf026"
}
ColumnLayout {
id: mixerCol
anchors { fill: parent; margins: 12 }
spacing: 4
Text {
text: "Output Devices"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.bottomMargin: 2
}
Repeater {
model: Pipewire.nodes
delegate: NodeRow {
required property var modelData
Layout.fillWidth: true
node: modelData
visible: modelData.isSink && !modelData.isStream && modelData.ready
}
}
Text {
text: "Applications"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.topMargin: 6
Layout.bottomMargin: 2
}
Repeater {
model: Pipewire.nodes
delegate: NodeRow {
required property var modelData
Layout.fillWidth: true
node: modelData
visible: modelData.isStream && !modelData.isSink && modelData.ready
&& !(modelData.description ?? "").toLowerCase().includes("monitor")
}
}
}
component NodeRow: RowLayout {
id: row
required property var node
spacing: 8
implicitHeight: 28
Text {
text: root.volIcon(root.safeVolume(row.node), row.node.audio?.muted ?? false)
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 13
color: (row.node.audio?.muted ?? false) ? Theme.textDim : Theme.text
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (row.node.ready && row.node.audio)
row.node.audio.muted = !row.node.audio.muted
}
}
}
Text {
text: row.node.description || row.node.nickname || row.node.name || "?"
font.pixelSize: 11
color: Theme.textDim
Layout.preferredWidth: 90
elide: Text.ElideRight
}
Item {
Layout.fillWidth: true
implicitHeight: 16
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 3
radius: 2
color: Theme.progressTrack
Rectangle {
width: parent.width * Math.min(1, root.safeVolume(row.node))
height: parent.height
radius: parent.radius
color: Theme.accent
}
}
MouseArea {
anchors { fill: parent; topMargin: -4; bottomMargin: -4 }
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function seek(mx) { root.setVolume(row.node, mx / width) }
onClicked: seek(mouseX)
onPositionChanged: if (pressed) seek(mouseX)
}
}
Text {
text: Math.round(root.safeVolume(row.node) * 100) + "%"
font.pixelSize: 10
color: Theme.textDim
Layout.preferredWidth: 30
horizontalAlignment: Text.AlignRight
}
}
}

View File

@ -0,0 +1,70 @@
import Quickshell
import Quickshell.Hyprland
// import Quickshell.Widgets
import QtQuick
import "../components"
Rectangle {
id: root
required property var workspace
required property string screenName
visible: workspace?.lastIpcObject?.monitor === screenName
implicitWidth: Math.max(32, iconsRow.implicitWidth + 14)
implicitHeight: 18
radius: 6
color: workspace?.active ? Theme.accent : 'transparent'
Row {
id: iconsRow
anchors.centerIn: parent
spacing: 3
Repeater {
model: root.workspace?.toplevels
delegate: Item {
id: iconItem
required property var modelData
implicitWidth: 16
implicitHeight: 16
property string appClass: ""
Component.onCompleted: {
var cls = modelData?.lastIpcObject?.["class"] ?? ""
if (cls !== "") {
appClass = cls
} else if (modelData) {
modelData.lastIpcObjectChanged.connect(function() {
var c = iconItem.modelData?.lastIpcObject?.["class"] ?? ""
if (c !== "") iconItem.appClass = c
})
Qt.callLater(Hyprland.refreshToplevels)
}
}
TrayIcon {
anchors.fill: parent
icon: iconItem.appClass
size: 16
}
}
}
// Workspace number shown when no windows are open
Text {
visible: (root.workspace?.toplevels?.values?.length ?? 0) === 0
text: root.workspace?.id ?? ""
color: root.workspace?.active ? Theme.text : Theme.textDim
font.pixelSize: 11
}
}
MouseArea {
anchors.fill: parent
onClicked: Hyprland.dispatch("workspace " + root.workspace.id)
}
}

View File

@ -0,0 +1,22 @@
pragma ComponentBehavior: Bound
import Quickshell.Hyprland
import QtQuick
Row {
id: root
required property string screenName
spacing: 4
Repeater {
id: workspacesRepeater
model: Hyprland.workspaces
property string screenName: root.screenName
delegate: WorkspaceButton {
required property var modelData
workspace: modelData
screenName: workspacesRepeater.screenName
}
}
}

View File

@ -0,0 +1,13 @@
Bar 1.0 Bar.qml
Battery 1.0 Battery.qml
PopoutWindow 1.0 PopoutWindow.qml
CalendarContent 1.0 CalendarContent.qml
Clock 1.0 Clock.qml
MusicPlayer 1.0 MusicPlayer.qml
MusicPlayerControls 1.0 MusicPlayerControls.qml
NetworkStatus 1.0 NetworkStatus.qml
SysTray 1.0 SysTray.qml
VolumeControl 1.0 VolumeControl.qml
VolumeMixerContent 1.0 VolumeMixerContent.qml
Workspaces 1.0 Workspaces.qml
WorkspaceButton 1.0 WorkspaceButton.qml

View File

@ -0,0 +1,11 @@
import QtQuick
import Quickshell.Widgets
import "."
WrapperRectangle {
radius: Theme.radius
color: 'transparent'
border.color: Theme.border
border.width: Theme.borderWidth
margin: Theme.enclosureMargin
}

View File

@ -0,0 +1,19 @@
pragma Singleton
import QtQuick
QtObject {
// Core palette
readonly property color bg: '#D9000000' // bar background (semi-transparent)
readonly property color bgPopup: '#000000' // popup background (fully opaque)
readonly property color accent: '#9B1A1A'
readonly property color border: '#FFFFFF'
readonly property color text: '#FFFFFF'
readonly property color textDim: '#888888'
readonly property color textDisabled: '#444444'
readonly property color progressTrack: '#333333'
// Shape / sizing
readonly property int radius: 16
readonly property int borderWidth: 2
readonly property int enclosureMargin: 3
}

View File

@ -0,0 +1,103 @@
// import QtQuick
// import Quickshell.Widgets
//
// Item {
// id: root
//
// property string icon: ""
// property int size: 16
//
// implicitWidth: size
// implicitHeight: size
//
// readonly property url resolvedSource: {
// if (!icon) return ""
// if (icon.startsWith("/") || icon.startsWith("file://") || icon.startsWith("image://"))
// return icon
// if (icon.includes("?path=")) {
// const [name, path] = icon.split("?path=")
// const baseName = name.slice(name.lastIndexOf("/") + 1)
// return `file://${path}/${baseName}`
// }
// return `image://icon/${icon}`
// }
//
// readonly property bool isIconTheme: resolvedSource.toString().startsWith("image://icon/")
// readonly property bool hasSource: resolvedSource.toString() !== ""
//
// IconImage {
// anchors.fill: parent
// source: root.resolvedSource
// visible: root.hasSource && root.isIconTheme
// asynchronous: true
// }
//
// Image {
// anchors.fill: parent
// source: root.hasSource && !root.isIconTheme ? root.resolvedSource : ""
// visible: root.hasSource && !root.isIconTheme
// asynchronous: true
// fillMode: Image.PreserveAspectFit
// }
// }
// pragma ComponentBehavior: Bound
import QtQuick
// import Quickshell
import Quickshell.Widgets
Item {
id: root
property string icon: ""
property int size: 16
implicitWidth: size
implicitHeight: size
readonly property url resolvedSource: {
if (!icon) return ""
if (icon.startsWith("/") || icon.startsWith("file://") || icon.startsWith("image://"))
return icon
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=")
const baseName = name.slice(name.lastIndexOf("/") + 1)
return `file://${path}/${baseName}`
}
// Use Quickshell's icon image provider instead of iconPath()
return `image://icon/${icon}`
}
Loader {
anchors.fill: parent
// asynchronous: true
sourceComponent: root.resolvedSource && root.resolvedSource.toString() !== ""
? root.resolvedSource.toString().startsWith("image://icon/")
? iconComponent
: imageComponent
: null
}
Component {
id: iconComponent
IconImage {
source: root.resolvedSource
asynchronous: true
// mipmap: true
}
}
Component {
id: imageComponent
Image {
source: root.resolvedSource
asynchronous: true
// mipmap: true
fillMode: Image.PreserveAspectFit
}
}
}

View File

@ -0,0 +1,3 @@
singleton Theme 1.0 Theme.qml
Enclosure 1.0 Enclosure.qml
TrayIcon 1.0 TrayIcon.qml

View File

@ -0,0 +1,13 @@
//@ pragma UseQApplication
import Quickshell
import "bar"
ShellRoot {
Variants {
model: Quickshell.screens
Bar { }
// // modelData injected by Variants; Bar.qml binds screen: modelData internally
// }
}
}

View File

@ -0,0 +1,51 @@
---
- name: Check if quickshell path is a symlink
stat:
path: "{{ config_dir }}/quickshell"
register: quickshell_stat
- name: Remove existing quickshell symlink if present
file:
path: "{{ config_dir }}/quickshell"
state: absent
when: quickshell_stat.stat.exists and quickshell_stat.stat.islnk
- name: Create quickshell config directories
file:
path: "{{ item }}"
state: directory
mode: '0755'
loop:
- "{{ config_dir }}/quickshell"
- "{{ config_dir }}/quickshell/bar"
- "{{ config_dir }}/quickshell/components"
- name: Symlink quickshell config files
file:
src: "{{ role_path }}/files/{{ item.src }}"
dest: "{{ config_dir }}/quickshell/{{ item.dest }}"
state: link
force: true
loop:
# Root
- { src: shell.qml, dest: shell.qml }
# Shared components module
- { src: components/Theme.qml, dest: components/Theme.qml }
- { src: components/Enclosure.qml, dest: components/Enclosure.qml }
- { src: components/TrayIcon.qml, dest: components/TrayIcon.qml }
- { src: components/qmldir, dest: components/qmldir }
# Bar module (imported as "bar" in shell.qml)
- { src: bar/Bar.qml, dest: bar/Bar.qml }
- { src: bar/Battery.qml, dest: bar/Battery.qml }
- { src: bar/CalendarContent.qml, dest: bar/CalendarContent.qml }
- { src: bar/Clock.qml, dest: bar/Clock.qml }
- { src: bar/MusicPlayer.qml, dest: bar/MusicPlayer.qml }
- { src: bar/MusicPlayerControls.qml, dest: bar/MusicPlayerControls.qml }
- { src: bar/NetworkStatus.qml, dest: bar/NetworkStatus.qml }
- { src: bar/PopoutWindow.qml, dest: bar/PopoutWindow.qml }
- { src: bar/SysTray.qml, dest: bar/SysTray.qml }
- { src: bar/VolumeControl.qml, dest: bar/VolumeControl.qml }
- { src: bar/VolumeMixerContent.qml, dest: bar/VolumeMixerContent.qml }
- { src: bar/Workspaces.qml, dest: bar/Workspaces.qml }
- { src: bar/WorkspaceButton.qml, dest: bar/WorkspaceButton.qml }
- { src: bar/qmldir, dest: bar/qmldir }

View File

@ -4,12 +4,14 @@
update_cache: yes
when: ansible_facts.os_family == "Archlinux"
become: yes
become_user: root
- name: Install system packages
package:
name: "{{ system_packages }}"
state: present
become: yes
become_user: root
- name: Ensure .config directory exists
file:

8
roles/zsh/files/.zshenv Normal file
View File

@ -0,0 +1,8 @@
export PATH="/home/johannes/.local/bin:$PATH"
# pnpm
export PNPM_HOME="$HOME/.local/share/pnpm"
case ":$PATH:" in
*":$PNPM_HOME:"*) ;;
*) export PATH="$PNPM_HOME:$PATH" ;;
esac
# pnpm end

View File

@ -52,3 +52,7 @@ alias drun='docker run -it --network=host --device=/dev/kfd --device=/dev/dri --
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/gcr/ssh"
bindkey '^H' backward-kill-word
bindkey '^R' fzf-history-widget
eval "$(zoxide init zsh)"

View File

@ -18,3 +18,7 @@ rm() {
print "⚠️ To use real rm, use /bin/rm"
trash-put "$@"
}
alias lg='lazygit'
alias tt='taskwarrior-tui'

View File

@ -1 +1 @@
source /usr/share/autojump/autojump.zsh
source <(fzf --zsh)

View File

@ -3,22 +3,25 @@
package:
name: zsh
state: present
become: yes
become_user: root
- name: Install oh-my-zsh
shell: |
sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended
args:
creates: "{{ ansible_facts.env.HOME }}/.oh-my-zsh"
creates: "{{ home_dir }}/.oh-my-zsh"
- name: Check for custom zshrc
stat:
path: "{{ role_path }}/files/.zshrc"
register: custom_zshrc
- name: Symlink custom zshrc
- name: Symlink .zshenv
file:
src: "{{ role_path }}/files/.zshrc"
dest: "{{ ansible_facts.env.HOME }}/.zshrc"
src: "{{ role_path }}/files/.zshenv"
dest: "{{ home_dir }}/.zshenv"
state: link
force: yes
- name: Symlink .zshrc
file:
src: "{{ role_path }}/files/.zshrc"
dest: "{{ home_dir }}/.zshrc"
state: link
force: yes
when: custom_zshrc.stat.exists