From 8abef18b680fcfb9bf3e7f419d98b0e630bb4736 Mon Sep 17 00:00:00 2001 From: SamriddhVerma Date: Sun, 5 Jul 2026 23:16:16 +0530 Subject: [PATCH] init --- hypr/hyprland.conf.bak | 355 ++++++++++++++ hypr/hyprland.lua | 367 +++++++++++++++ hypr/hyprland.lua.bak | 116 +++++ nvim/.gitignore | 8 + nvim/.neoconf.json | 15 + nvim/LICENSE | 201 ++++++++ nvim/README.md | 4 + nvim/init.lua | 2 + nvim/lazy-lock.json | 45 ++ nvim/lazyvim.json | 26 ++ nvim/lua/config/autocmds.lua | 8 + nvim/lua/config/keymaps.lua | 3 + nvim/lua/config/lazy.lua | 53 +++ nvim/lua/config/options.lua | 3 + nvim/lua/plugins/example.lua | 197 ++++++++ nvim/stylua.toml | 3 + quickshell/modules/AudioMixer.qml | 58 +++ quickshell/modules/Bar.qml | 72 +++ quickshell/modules/BrightnessOSD.qml | 135 ++++++ quickshell/modules/MixerEntry.qml | 53 +++ quickshell/modules/OSD.qml | 121 +++++ quickshell/modules/components/Battery.qml | 211 +++++++++ .../modules/components/BluetoothControl.qml | 216 +++++++++ quickshell/modules/components/Clock.qml | 231 +++++++++ quickshell/modules/components/Desktop.qml | 224 +++++++++ .../modules/components/NetworkControl.qml | 442 ++++++++++++++++++ quickshell/modules/components/Sound.qml | 23 + quickshell/shell.qml | 20 + 28 files changed, 3212 insertions(+) create mode 100644 hypr/hyprland.conf.bak create mode 100644 hypr/hyprland.lua create mode 100644 hypr/hyprland.lua.bak create mode 100644 nvim/.gitignore create mode 100644 nvim/.neoconf.json create mode 100644 nvim/LICENSE create mode 100644 nvim/README.md create mode 100644 nvim/init.lua create mode 100644 nvim/lazy-lock.json create mode 100644 nvim/lazyvim.json create mode 100644 nvim/lua/config/autocmds.lua create mode 100644 nvim/lua/config/keymaps.lua create mode 100644 nvim/lua/config/lazy.lua create mode 100644 nvim/lua/config/options.lua create mode 100644 nvim/lua/plugins/example.lua create mode 100644 nvim/stylua.toml create mode 100644 quickshell/modules/AudioMixer.qml create mode 100644 quickshell/modules/Bar.qml create mode 100644 quickshell/modules/BrightnessOSD.qml create mode 100644 quickshell/modules/MixerEntry.qml create mode 100644 quickshell/modules/OSD.qml create mode 100644 quickshell/modules/components/Battery.qml create mode 100644 quickshell/modules/components/BluetoothControl.qml create mode 100644 quickshell/modules/components/Clock.qml create mode 100644 quickshell/modules/components/Desktop.qml create mode 100644 quickshell/modules/components/NetworkControl.qml create mode 100644 quickshell/modules/components/Sound.qml create mode 100644 quickshell/shell.qml diff --git a/hypr/hyprland.conf.bak b/hypr/hyprland.conf.bak new file mode 100644 index 0000000..c20b03d --- /dev/null +++ b/hypr/hyprland.conf.bak @@ -0,0 +1,355 @@ +# ####################################################################################### +# AUTOGENERATED HYPRLAND CONFIG. +# EDIT THIS CONFIG ACCORDING TO THE WIKI INSTRUCTIONS. +# ####################################################################################### + + + +# This is an example Hyprland config file. +# Refer to the wiki for more information. +# https://wiki.hypr.land/Configuring/ + +# Please note not all available settings / options are set here. +# For a full list, see the wiki + +# You can split this configuration into multiple files +# Create your files separately and then link them to this file like this: +# source = ~/.config/hypr/myColors.conf + + +################ +### MONITORS ### +################ + +# See https://wiki.hypr.land/Configuring/Monitors/ +monitor=,preferred,auto,1 + + +################### +### MY PROGRAMS ### +################### + +# See https://wiki.hypr.land/Configuring/Keywords/ + +# Set programs that you use +$terminal = kitty +$fileManager = thunar +$menu = fuzzel + + +################# +### AUTOSTART ### +################# + +# Autostart necessary processes (like notifications daemons, status bars, etc.) +# Or execute your favorite apps at launch like this: + +# exec-once = $terminal +# exec-once = nm-applet & +# exec-once = waybar & hyprpaper & firefox +exec-once = mpvpaper -o "no-audio --loop" '*' ~/wallpaper.mp4 +exec-once = qs + +############################# +### ENVIRONMENT VARIABLES ### +############################# + +# See https://wiki.hypr.land/Configuring/Environment-variables/ + +env = XCURSOR_SIZE,24 +env = HYPRCURSOR_SIZE,24 + + +################### +### PERMISSIONS ### +################### + +# See https://wiki.hypr.land/Configuring/Permissions/ +# Please note permission changes here require a Hyprland restart and are not applied on-the-fly +# for security reasons + +# ecosystem { +# enforce_permissions = 1 +# } + +# permission = /usr/(bin|local/bin)/grim, screencopy, allow +# permission = /usr/(lib|libexec|lib64)/xdg-desktop-portal-hyprland, screencopy, allow +# permission = /usr/(bin|local/bin)/hyprpm, plugin, allow + + +##################### +### LOOK AND FEEL ### +##################### + +# Refer to https://wiki.hypr.land/Configuring/Variables/ + +# https://wiki.hypr.land/Configuring/Variables/#general +general { + gaps_in = 0 + gaps_out = 0 + + border_size = 0 + + # https://wiki.hypr.land/Configuring/Variables/#variable-types for info about colors + col.active_border = rgba(33ccffee) rgba(00ff99ee) 45deg + col.inactive_border = rgba(595959aa) + + # Set to true enable resizing windows by clicking and dragging on borders and gaps + resize_on_border = false + + # Please see https://wiki.hypr.land/Configuring/Tearing/ before you turn this on + allow_tearing = false + + layout = dwindle +} + +# https://wiki.hypr.land/Configuring/Variables/#decoration +decoration { + rounding = 0 + rounding_power = 0 + + # Change transparency of focused and unfocused windows + active_opacity = 1.0 + inactive_opacity = 1.0 + + shadow { + enabled = true + range = 10 + render_power = 4 + color = rgba(1a1a1aee) + } + + # https://wiki.hypr.land/Configuring/Variables/#blur + blur { + enabled = true + size = 3 + passes = 1 + brightness = 0.40 + vibrancy = 0.1696 + } +} + +# https://wiki.hypr.land/Configuring/Variables/#animations +animations { + enabled = yes, please :) + + # Default curves, see https://wiki.hypr.land/Configuring/Animations/#curves + # NAME, X0, Y0, X1, Y1 + bezier = easeOutQuint, 0.23, 1, 0.32, 1 + bezier = easeInOutCubic, 0.65, 0.05, 0.36, 1 + bezier = linear, 0, 0, 1, 1 + bezier = almostLinear, 0.5, 0.5, 0.75, 1 + bezier = quick, 0.15, 0, 0.1, 1 + + # Default animations, see https://wiki.hypr.land/Configuring/Animations/ + # NAME, ONOFF, SPEED, CURVE, [STYLE] + animation = global, 1, 10, default + animation = border, 1, 5.39, easeOutQuint + animation = windows, 1, 4.79, easeOutQuint + animation = windowsIn, 1, 4.1, easeOutQuint, popin 87% + animation = windowsOut, 1, 1.49, linear, popin 87% + animation = fadeIn, 1, 1.73, almostLinear + animation = fadeOut, 1, 1.46, almostLinear + animation = fade, 1, 3.03, quick + animation = layers, 1, 3.81, easeOutQuint + animation = layersIn, 1, 4, easeOutQuint, fade + animation = layersOut, 1, 1.5, linear, fade + animation = fadeLayersIn, 1, 1.79, almostLinear + animation = fadeLayersOut, 1, 1.39, almostLinear + animation = workspaces, 1, 1.94, almostLinear, fade + animation = workspacesIn, 1, 1.21, almostLinear, fade + animation = workspacesOut, 1, 1.94, almostLinear, fade + animation = zoomFactor, 1, 7, quick +} + +# Ref https://wiki.hypr.land/Configuring/Workspace-Rules/ +# "Smart gaps" / "No gaps when only" +# uncomment all if you wish to use that. +# workspace = w[tv1], gapsout:0, gapsin:0 +# workspace = f[1], gapsout:0, gapsin:0 +# windowrule { +# name = no-gaps-wtv1 +# match:float = false +# match:workspace = w[tv1] +# +# border_size = 0 +# rounding = 0 +# } +# +# windowrule { +# name = no-gaps-f1 +# match:float = false +# match:workspace = f[1] +# +# border_size = 0 +# rounding = 0 +# } + +# See https://wiki.hypr.land/Configuring/Dwindle-Layout/ for more +dwindle { + preserve_split = true # You probably want this +} + +# See https://wiki.hypr.land/Configuring/Master-Layout/ for more +master { + new_status = master +} + +# https://wiki.hypr.land/Configuring/Variables/#misc +misc { + force_default_wallpaper = -1 # Set to 0 or 1 to disable the anime mascot wallpapers + disable_hyprland_logo = false # If true disables the random hyprland logo / anime girl background. :( +} + + +############# +### INPUT ### +############# + +# https://wiki.hypr.land/Configuring/Variables/#input +input { + kb_layout = us + kb_variant = + kb_model = + kb_options = + kb_rules = + + follow_mouse = 1 + + sensitivity = 0 # -1.0 - 1.0, 0 means no modification. + + touchpad { + natural_scroll = false + } +} + +# See https://wiki.hypr.land/Configuring/Gestures +gesture = 3, horizontal, workspace + +# Example per-device config +# See https://wiki.hypr.land/Configuring/Keywords/#per-device-input-configs for more +device { + name = epic-mouse-v1 + sensitivity = -0.5 +} + + +################### +### KEYBINDINGS ### +################### + +# See https://wiki.hypr.land/Configuring/Keywords/ +$mainMod = SUPER # Sets "Windows" key as main modifier + +# Screenshot binds +bind = , Print, exec, grim -g "$(slurp)" - | swappy -f - +bind = $mainMod, Print, exec, grim -g "$(slurp -d)" - | wl-copy + +# Example binds, see https://wiki.hypr.land/Configuring/Binds/ for more +bind = $mainMod, Q, exec, $terminal +bind = $mainMod, C, killactive, +bind = $mainMod, M, exec, command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch exit +bind = $mainMod, E, exec, $fileManager +bind = $mainMod, V, togglefloating, +bind = $mainMod, R, exec, $menu +bind = $mainMod, P, pseudo, # dwindle +bind = $mainMod, J, layoutmsg, togglesplit # dwindle + +# Move focus with mainMod + arrow keys +bind = $mainMod, left, movefocus, l +bind = $mainMod, right, movefocus, r +bind = $mainMod, up, movefocus, u +bind = $mainMod, down, movefocus, d + +# Switch workspaces with mainMod + [0-9] +bind = $mainMod, 1, workspace, 1 +bind = $mainMod, 2, workspace, 2 +bind = $mainMod, 3, workspace, 3 +bind = $mainMod, 4, workspace, 4 +bind = $mainMod, 5, workspace, 5 +bind = $mainMod, 6, workspace, 6 +bind = $mainMod, 7, workspace, 7 +bind = $mainMod, 8, workspace, 8 +bind = $mainMod, 9, workspace, 9 +bind = $mainMod, 0, workspace, 10 + +# Move active window to a workspace with mainMod + SHIFT + [0-9] +bind = $mainMod SHIFT, 1, movetoworkspace, 1 +bind = $mainMod SHIFT, 2, movetoworkspace, 2 +bind = $mainMod SHIFT, 3, movetoworkspace, 3 +bind = $mainMod SHIFT, 4, movetoworkspace, 4 +bind = $mainMod SHIFT, 5, movetoworkspace, 5 +bind = $mainMod SHIFT, 6, movetoworkspace, 6 +bind = $mainMod SHIFT, 7, movetoworkspace, 7 +bind = $mainMod SHIFT, 8, movetoworkspace, 8 +bind = $mainMod SHIFT, 9, movetoworkspace, 9 +bind = $mainMod SHIFT, 0, movetoworkspace, 10 + +# Example special workspace (scratchpad) +bind = $mainMod, S, togglespecialworkspace, magic +bind = $mainMod SHIFT, S, movetoworkspace, special:magic + +# Custom bind for fanboost +bind = , XF86Launch2, exec, sudo /usr/local/bin/fanboost.sh + +# Scroll through existing workspaces with mainMod + scroll +bind = $mainMod, mouse_down, workspace, e+1 +bind = $mainMod, mouse_up, workspace, e-1 + +# Move/resize windows with mainMod + LMB/RMB and dragging +bindm = $mainMod, mouse:272, movewindow +bindm = $mainMod, mouse:273, resizewindow + +# Laptop multimedia keys for volume and LCD brightness +bindel = ,XF86AudioRaiseVolume, exec, wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ +bindel = ,XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- +bindel = ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle +bindel = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle +bindel = ,XF86MonBrightnessUp, exec, brightnessctl -e4 -n2 set 5%+ +bindel = ,XF86MonBrightnessDown, exec, brightnessctl -e4 -n2 set 5%- + +# Requires playerctl +bindl = , XF86AudioNext, exec, playerctl next +bindl = , XF86AudioPause, exec, playerctl play-pause +bindl = , XF86AudioPlay, exec, playerctl play-pause +bindl = , XF86AudioPrev, exec, playerctl previous + +############################## +### WINDOWS AND WORKSPACES ### +############################## + +# See https://wiki.hypr.land/Configuring/Window-Rules/ for more +# See https://wiki.hypr.land/Configuring/Workspace-Rules/ for workspace rules + +# Example windowrules that are useful + +windowrule { + # Ignore maximize requests from all apps. You'll probably like this. + name = suppress-maximize-events + match:class = .* + + suppress_event = maximize +} + +windowrule { + # Fix some dragging issues with XWayland + name = fix-xwayland-drags + match:class = ^$ + match:title = ^$ + match:xwayland = true + match:float = true + match:fullscreen = false + match:pin = false + + no_focus = true +} + +# Hyprland-run windowrule +windowrule { + name = move-hyprland-run + + match:class = hyprland-run + + move = 20 monitor_h-120 + float = yes +} diff --git a/hypr/hyprland.lua b/hypr/hyprland.lua new file mode 100644 index 0000000..cefa45e --- /dev/null +++ b/hypr/hyprland.lua @@ -0,0 +1,367 @@ +------------------ +---- MONITORS ---- +------------------ + +-- See https://wiki.hypr.land/Configuring/Basics/Monitors/ +hl.monitor({ + output = "", + mode = "preferred", + position = "auto", + scale = "1", +}) + + +--------------------- +---- MY PROGRAMS ---- +--------------------- + +-- Set programs that you use +local terminal = "kitty" +local fileManager = "thunar" +local menu = "fuzzel" + + +------------------- +---- AUTOSTART ---- +------------------- + +-- See https://wiki.hypr.land/Configuring/Basics/Autostart/ + +-- Autostart necessary processes (like notifications daemons, status bars, etc.) +-- Or execute your favorite apps at launch like this: +-- +hl.on("hyprland.start", function() + hl.exec_cmd("prime-run mpvpaper -o \"no-audio --loop\" '*' ~/wallpaper.mp4") + hl.exec_cmd("qs") + hl.exec_cmd("kitty --class sticky-notes sh -c 'while true; do nvim ~/notes.md; sleep 0.3; done'") +end) + + +------------------------------- +---- ENVIRONMENT VARIABLES ---- +------------------------------- + +-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Environment-variables/ + +hl.env("XCURSOR_SIZE", "24") +hl.env("HYPRCURSOR_SIZE", "24") + + +----------------------- +----- PERMISSIONS ----- +----------------------- + +-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Permissions/ +-- Please note permission changes here require a Hyprland restart and are not applied on-the-fly +-- for security reasons + +-- hl.config({ +-- ecosystem = { +-- enforce_permissions = true, +-- }, +-- }) + +-- hl.permission("/usr/(bin|local/bin)/grim", "screencopy", "allow") +-- hl.permission("/usr/(lib|libexec|lib64)/xdg-desktop-portal-hyprland", "screencopy", "allow") +-- hl.permission("/usr/(bin|local/bin)/hyprpm", "plugin", "allow") + + +----------------------- +---- LOOK AND FEEL ---- +----------------------- + +-- Refer to https://wiki.hypr.land/Configuring/Basics/Variables/ +hl.config({ + general = { + gaps_in = 0, + gaps_out = 0, + + border_size = 0, + + col = { + active_border = { colors = { "rgba(33ccffee)", "rgba(00ff99ee)" }, angle = 45 }, + inactive_border = "rgba(595959aa)", + }, + + -- Set to true to enable resizing windows by clicking and dragging on borders and gaps + resize_on_border = true, + + -- Please see https://wiki.hypr.land/Configuring/Advanced-and-Cool/Tearing/ before you turn this on + allow_tearing = false, + + layout = "dwindle", + }, + + decoration = { + rounding = 0, + rounding_power = 0, + + -- Change transparency of focused and unfocused windows + active_opacity = 1.0, + inactive_opacity = 1.0, + + shadow = { + enabled = true, + range = 4, + render_power = 3, + color = 0xee1a1a1a, + }, + + blur = { + enabled = true, + size = 3, + passes = 1, + vibrancy = 0.1696, + }, + }, + + animations = { + enabled = true, + }, +}) + +-- Default curves and animations, see https://wiki.hypr.land/Configuring/Advanced-and-Cool/Animations/ +hl.curve("easeOutQuint", { type = "bezier", points = { { 0.23, 1 }, { 0.32, 1 } } }) +hl.curve("easeInOutCubic", { type = "bezier", points = { { 0.65, 0.05 }, { 0.36, 1 } } }) +hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } }) +hl.curve("almostLinear", { type = "bezier", points = { { 0.5, 0.5 }, { 0.75, 1 } } }) +hl.curve("quick", { type = "bezier", points = { { 0.15, 0 }, { 0.1, 1 } } }) + +-- Default springs +hl.curve("easy", { type = "spring", mass = 1, stiffness = 71.2633, dampening = 15.8273644 }) + +hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" }) +hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" }) +hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" }) +hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "easy", style = "popin 87%" }) +hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" }) +hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" }) +hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" }) +hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" }) +hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" }) +hl.animation({ leaf = "layersIn", enabled = true, speed = 4, bezier = "easeOutQuint", style = "fade" }) +hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "linear", style = "fade" }) +hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.79, bezier = "almostLinear" }) +hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.39, bezier = "almostLinear" }) +hl.animation({ leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "workspacesIn", enabled = true, speed = 1.21, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "workspacesOut", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "quick" }) + +-- Ref https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/ +-- "Smart gaps" / "No gaps when only" +-- uncomment all if you wish to use that. +-- hl.workspace_rule({ workspace = "w[tv1]", gaps_out = 0, gaps_in = 0 }) +-- hl.workspace_rule({ workspace = "f[1]", gaps_out = 0, gaps_in = 0 }) +-- hl.window_rule({ +-- name = "no-gaps-wtv1", +-- match = { float = false, workspace = "w[tv1]" }, +-- border_size = 0, +-- rounding = 0, +-- }) +-- hl.window_rule({ +-- name = "no-gaps-f1", +-- match = { float = false, workspace = "f[1]" }, +-- border_size = 0, +-- rounding = 0, +-- }) + +-- See https://wiki.hypr.land/Configuring/Layouts/Dwindle-Layout/ for more +hl.config({ + dwindle = { + preserve_split = true, -- You probably want this + }, +}) + +-- See https://wiki.hypr.land/Configuring/Layouts/Master-Layout/ for more +hl.config({ + master = { + new_status = "master", + }, +}) + +-- See https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/ for more +hl.config({ + scrolling = { + fullscreen_on_one_column = true, + }, +}) + +---------------- +---- MISC ---- +---------------- + +hl.config({ + misc = { + force_default_wallpaper = -1, -- Set to 0 or 1 to disable the anime mascot wallpapers + disable_hyprland_logo = false, -- If true disables the random hyprland logo / anime girl background. :( + }, +}) + + +--------------- +---- INPUT ---- +--------------- + +hl.config({ + input = { + kb_layout = "us", + kb_variant = "", + kb_model = "", + kb_options = "", + kb_rules = "", + + follow_mouse = 1, + + sensitivity = 0, -- -1.0 - 1.0, 0 means no modification. + + touchpad = { + natural_scroll = true, + }, + }, +}) + +hl.gesture({ + fingers = 3, + direction = "horizontal", + action = "workspace" +}) + +-- Example per-device config +-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Devices/ for more +hl.device({ + name = "epic-mouse-v1", + sensitivity = -0.5, +}) + + +--------------------- +---- KEYBINDINGS ---- +--------------------- + +local mainMod = "SUPER" -- Sets "Windows" key as main modifier + +-- Example binds, see https://wiki.hypr.land/Configuring/Basics/Binds/ for more + +hl.bind("Print", hl.dsp.exec_cmd("grim -g \"$(slurp)\" - | wl-copy")) + + +hl.bind(mainMod .. " + Q", hl.dsp.exec_cmd(terminal)) +local closeWindowBind = hl.bind(mainMod .. " + C", hl.dsp.window.close()) +-- closeWindowBind:set_enabled(false) +hl.bind(mainMod .. " + M", + hl.dsp.exec_cmd("command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch 'hl.dsp.exit()'")) +hl.bind(mainMod .. " + E", hl.dsp.exec_cmd(fileManager)) +hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" })) +hl.bind(mainMod .. " + R", hl.dsp.exec_cmd(menu)) +hl.bind(mainMod .. " + P", hl.dsp.window.pseudo()) +hl.bind(mainMod .. " + J", hl.dsp.layout("togglesplit")) -- dwindle only + +-- Move focus with mainMod + arrow keys +hl.bind(mainMod .. " + left", hl.dsp.focus({ direction = "left" })) +hl.bind(mainMod .. " + right", hl.dsp.focus({ direction = "right" })) +hl.bind(mainMod .. " + up", hl.dsp.focus({ direction = "up" })) +hl.bind(mainMod .. " + down", hl.dsp.focus({ direction = "down" })) + +-- Switch workspaces with mainMod + [0-9] +-- Move active window to a workspace with mainMod + SHIFT + [0-9] +for i = 1, 10 do + local key = i % 10 -- 10 maps to key 0 + hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i })) + hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i })) +end + +-- Example special workspace (scratchpad) +hl.bind(mainMod .. " + S", hl.dsp.workspace.toggle_special("magic")) +hl.bind(mainMod .. " + SHIFT + S", hl.dsp.window.move({ workspace = "special:magic" })) + +-- Sticky notes special workspace +hl.bind(mainMod .. " + grave", hl.dsp.workspace.toggle_special("notes")) +hl.bind(mainMod .. " + SHIFT + grave", hl.dsp.window.move({ workspace = "special:notes" })) + +-- Scroll through existing workspaces with mainMod + scroll +hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" })) +hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" })) + +-- Move/resize windows with mainMod + LMB/RMB and dragging +hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) +hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) + +-- Laptop multimedia keys for volume and LCD brightness +hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"), + { locked = true, repeating = true }) +hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), + { locked = true, repeating = true }) +hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), + { locked = true, repeating = true }) +hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), + { locked = true, repeating = true }) +hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true }) +hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true }) + +-- Requires playerctl +hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true }) +hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) +hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) +hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true }) + + +-------------------------------- +---- WINDOWS AND WORKSPACES ---- +-------------------------------- + +-- See https://wiki.hypr.land/Configuring/Basics/Window-Rules/ +-- and https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/ + +-- Example window rules that are useful + +local suppressMaximizeRule = hl.window_rule({ + -- Ignore maximize requests from all apps. You'll probably like this. + name = "suppress-maximize-events", + match = { class = ".*" }, + + suppress_event = "maximize", +}) +-- suppressMaximizeRule:set_enabled(false) + +hl.window_rule({ + -- Fix some dragging issues with XWayland + name = "fix-xwayland-drags", + match = { + class = "^$", + title = "^$", + xwayland = true, + float = true, + fullscreen = false, + pin = false, + }, + + no_focus = true, +}) + +-- Layer rules also return a handle. +-- local overlayLayerRule = hl.layer_rule({ +-- name = "no-anim-overlay", +-- match = { namespace = "^my-overlay$" }, +-- no_anim = true, +-- }) +-- overlayLayerRule:set_enabled(false) + +-- Hyprland-run windowrule +hl.window_rule({ + name = "move-hyprland-run", + match = { class = "hyprland-run" }, + + move = "20 monitor_h-120", + float = true, +}) + +-- Sticky notes: floating, centered note pinned to special:notes workspace +hl.window_rule({ + name = "sticky-notes-workspace", + match = { class = "sticky-notes" }, + workspace = "special:notes", + float = true, + size = "640 720", + center = true, +}) diff --git a/hypr/hyprland.lua.bak b/hypr/hyprland.lua.bak new file mode 100644 index 0000000..4facdab --- /dev/null +++ b/hypr/hyprland.lua.bak @@ -0,0 +1,116 @@ +local terminal = "kitty" +local fileManager = "thunar" +local menu = "fuzzel" +local mainMod = "SUPER" + +-- MONITORS +hl.monitor({ + output = "", + mode = "preferred", + position = "auto", + scale = 1, +}) + +-- AUTOSTART +hl.on("hyprland.start", function() + hl.exec_cmd('mpvpaper -o "no-audio --loop" "*" ~/wallpaper.mp4') + hl.exec_cmd("qs") +end) + +-- ENV +hl.env("XCURSOR_SIZE", "24") +hl.env("HYPRCURSOR_SIZE", "24") + +-- CONFIG +hl.config({ + general = { + gaps_in = 0, + gaps_out = 0, + border_size = 0, + resize_on_border = false, + allow_tearing = false, + layout = "dwindle", + + col = { + active_border = { + colors = { "rgba(33ccffee)", "rgba(00ff99ee)" }, + angle = 45, + }, + inactive_border = "rgba(595959aa)", + }, + }, + + decoration = { + rounding = 0, + rounding_power = 0, + active_opacity = 1.0, + inactive_opacity = 1.0, + + shadow = { + enabled = true, + range = 10, + render_power = 4, + color = "rgba(1a1a1aee)" + }, + + blur = { + enabled = true, + size = 3, + passes = 1, + brightness = 0.40, + vibrancy = 0.1696 + } + }, + + dwindle = { + preserve_split = true + }, + + master = { + new_status = "master" + }, + + input = { + kb_layout = "us", + follow_mouse = 1, + sensitivity = 0, + + touchpad = { + natural_scroll = false + } + }, + + misc = { + force_default_wallpaper = -1, + disable_hyprland_logo = false + } +}) + +-- GESTURES +-- hl.raw("gesture = 3, horizontal, workspace") + +-- KEYBINDS +hl.bind(mainMod .. " + Q", hl.action.exec(terminal)) +hl.bind(mainMod .. " + E", hl.action.exec(fileManager)) +hl.bind(mainMod .. " + R", hl.action.exec(menu)) +hl.bind(mainMod .. " + C", hl.dsp.window.close()) +hl.bind(mainMod .. " + V", hl.dsp.window.toggle_floating()) + +for i = 1, 10 do + local ws = tostring(i == 10 and 0 or i) + local target = tostring(i) + + hl.bind(mainMod .. " + " .. ws, + hl.action.dispatch("workspace " .. target)) + + hl.bind(mainMod .. " + SHIFT + " .. ws, + hl.action.dispatch("movetoworkspace " .. target)) +end + +-- MEDIA KEYS +hl.raw('bindel = ,XF86AudioRaiseVolume, exec, wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+') +hl.raw('bindel = ,XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-') +hl.raw('bindel = ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle') + +-- WINDOW RULES +hl.raw('windowrule = suppressevent maximize,class:.*') diff --git a/nvim/.gitignore b/nvim/.gitignore new file mode 100644 index 0000000..cc5457a --- /dev/null +++ b/nvim/.gitignore @@ -0,0 +1,8 @@ +tt.* +.tests +doc/tags +debug +.repro +foo.* +*.log +data diff --git a/nvim/.neoconf.json b/nvim/.neoconf.json new file mode 100644 index 0000000..7c48087 --- /dev/null +++ b/nvim/.neoconf.json @@ -0,0 +1,15 @@ +{ + "neodev": { + "library": { + "enabled": true, + "plugins": true + } + }, + "neoconf": { + "plugins": { + "lua_ls": { + "enabled": true + } + } + } +} diff --git a/nvim/LICENSE b/nvim/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/nvim/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/nvim/README.md b/nvim/README.md new file mode 100644 index 0000000..185280b --- /dev/null +++ b/nvim/README.md @@ -0,0 +1,4 @@ +# 💤 LazyVim + +A starter template for [LazyVim](https://github.com/LazyVim/LazyVim). +Refer to the [documentation](https://lazyvim.github.io/installation) to get started. diff --git a/nvim/init.lua b/nvim/init.lua new file mode 100644 index 0000000..2514f9e --- /dev/null +++ b/nvim/init.lua @@ -0,0 +1,2 @@ +-- bootstrap lazy.nvim, LazyVim and your plugins +require("config.lazy") diff --git a/nvim/lazy-lock.json b/nvim/lazy-lock.json new file mode 100644 index 0000000..58bbcfd --- /dev/null +++ b/nvim/lazy-lock.json @@ -0,0 +1,45 @@ +{ + "LazyVim": { "branch": "main", "commit": "c10948c50b18fae7f256433afdef09e432410480" }, + "SchemaStore.nvim": { "branch": "main", "commit": "6ff1f21b2e2b77ec59f7433ce2d9fbc052d908ac" }, + "blink-copilot": { "branch": "main", "commit": "7ad8209b2f880a2840c94cdcd80ab4dc511d4f39" }, + "blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" }, + "bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" }, + "catppuccin": { "branch": "main", "commit": "e068ab5f8261f23f6f71ffd8791ae40315b77b9c" }, + "conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" }, + "copilot.lua": { "branch": "master", "commit": "d521d3959f81f4fa58870ed329754f1dd5057511" }, + "flash.nvim": { "branch": "main", "commit": "fcea7ff883235d9024dc41e638f164a450c14ca2" }, + "friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" }, + "gitsigns.nvim": { "branch": "main", "commit": "eb60cc7b94c46005237fd34170d76f3a089a90aa" }, + "grug-far.nvim": { "branch": "main", "commit": "c69859c1d5427ab5fc7ed12380ab521b4e336691" }, + "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, + "lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" }, + "lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" }, + "markdown-preview.nvim": { "branch": "master", "commit": "a923f5fc5ba36a3b17e289dc35dc17f66d0548ee" }, + "mason-lspconfig.nvim": { "branch": "main", "commit": "47059d71b42d74b0a1e9f61c1d99d301039c3b5b" }, + "mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" }, + "mini.ai": { "branch": "main", "commit": "cb20f298ebf5ae91924cd0c6c310712de2ef4086" }, + "mini.icons": { "branch": "main", "commit": "24dbea2195c477e57d581215839a6ab915f34b14" }, + "mini.pairs": { "branch": "main", "commit": "fd150ac39b78e6a2286f5138e472b7dc7eba43b9" }, + "noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" }, + "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, + "nvim-jdtls": { "branch": "master", "commit": "6e9d953f0b82bccdb834cfde0e893f3119c22592" }, + "nvim-lint": { "branch": "master", "commit": "a219b2c9e5b4765e5c845aba119dad55806fcaf1" }, + "nvim-lspconfig": { "branch": "master", "commit": "d224a1920728ba129880efc700d4a0180ac4ecbb" }, + "nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" }, + "nvim-treesitter-textobjects": { "branch": "main", "commit": "851e865342e5a4cb1ae23d31caf6e991e1c99f1e" }, + "nvim-ts-autotag": { "branch": "main", "commit": "88c1453db4ba7dd24131086fe51fdf74e587d275" }, + "persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" }, + "plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" }, + "render-markdown.nvim": { "branch": "main", "commit": "f422cb5c6855f150e2ddcfaf44e7157b98b34f6a" }, + "snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" }, + "todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" }, + "tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" }, + "trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" }, + "ts-comments.nvim": { "branch": "main", "commit": "a59d6092213447450191122c9346f309161504cb" }, + "venv-selector.nvim": { "branch": "main", "commit": "cc4bb3975de8835291f9bb45889e96c6b2795fc4" }, + "vim-dadbod": { "branch": "master", "commit": "6d1d41da4873a445c5605f2005ad2c68c99d8770" }, + "vim-dadbod-completion": { "branch": "master", "commit": "a8dac0b3cf6132c80dc9b18bef36d4cf7a9e1fe6" }, + "vim-dadbod-ui": { "branch": "master", "commit": "afd07819d8efcefc3317205b855ad4e3513b0011" }, + "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }, + "yanky.nvim": { "branch": "main", "commit": "020c22d0cfa60358569e49d1d7b91630f175aef0" } +} diff --git a/nvim/lazyvim.json b/nvim/lazyvim.json new file mode 100644 index 0000000..8331f58 --- /dev/null +++ b/nvim/lazyvim.json @@ -0,0 +1,26 @@ +{ + "extras": [ + "lazyvim.plugins.extras.ai.copilot", + "lazyvim.plugins.extras.coding.yanky", + "lazyvim.plugins.extras.formatting.prettier", + "lazyvim.plugins.extras.lang.dart", + "lazyvim.plugins.extras.lang.docker", + "lazyvim.plugins.extras.lang.git", + "lazyvim.plugins.extras.lang.java", + "lazyvim.plugins.extras.lang.json", + "lazyvim.plugins.extras.lang.kotlin", + "lazyvim.plugins.extras.lang.markdown", + "lazyvim.plugins.extras.lang.prisma", + "lazyvim.plugins.extras.lang.python", + "lazyvim.plugins.extras.lang.sql", + "lazyvim.plugins.extras.lang.tailwind", + "lazyvim.plugins.extras.lang.toml", + "lazyvim.plugins.extras.lang.typescript", + "lazyvim.plugins.extras.util.dot" + ], + "install_version": 8, + "news": { + "NEWS.md": "11866" + }, + "version": 8 +} \ No newline at end of file diff --git a/nvim/lua/config/autocmds.lua b/nvim/lua/config/autocmds.lua new file mode 100644 index 0000000..4221e75 --- /dev/null +++ b/nvim/lua/config/autocmds.lua @@ -0,0 +1,8 @@ +-- Autocmds are automatically loaded on the VeryLazy event +-- Default autocmds that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/autocmds.lua +-- +-- Add any additional autocmds here +-- with `vim.api.nvim_create_autocmd` +-- +-- Or remove existing autocmds by their group name (which is prefixed with `lazyvim_` for the defaults) +-- e.g. vim.api.nvim_del_augroup_by_name("lazyvim_wrap_spell") diff --git a/nvim/lua/config/keymaps.lua b/nvim/lua/config/keymaps.lua new file mode 100644 index 0000000..2c134f7 --- /dev/null +++ b/nvim/lua/config/keymaps.lua @@ -0,0 +1,3 @@ +-- Keymaps are automatically loaded on the VeryLazy event +-- Default keymaps that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/keymaps.lua +-- Add any additional keymaps here diff --git a/nvim/lua/config/lazy.lua b/nvim/lua/config/lazy.lua new file mode 100644 index 0000000..d73bfa1 --- /dev/null +++ b/nvim/lua/config/lazy.lua @@ -0,0 +1,53 @@ +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 +end +vim.opt.rtp:prepend(lazypath) + +require("lazy").setup({ + spec = { + -- add LazyVim and import its plugins + { "LazyVim/LazyVim", import = "lazyvim.plugins" }, + -- import/override with your plugins + { import = "plugins" }, + }, + defaults = { + -- By default, only LazyVim plugins will be lazy-loaded. Your custom plugins will load during startup. + -- If you know what you're doing, you can set this to `true` to have all your custom plugins lazy-loaded by default. + lazy = false, + -- It's recommended to leave version=false for now, since a lot the plugin that support versioning, + -- have outdated releases, which may break your Neovim install. + version = false, -- always use the latest git commit + -- version = "*", -- try installing the latest stable version for plugins that support semver + }, + install = { colorscheme = { "tokyonight", "habamax" } }, + checker = { + enabled = true, -- check for plugin updates periodically + notify = false, -- notify on update + }, -- automatically check for plugin updates + performance = { + rtp = { + -- disable some rtp plugins + disabled_plugins = { + "gzip", + -- "matchit", + -- "matchparen", + -- "netrwPlugin", + "tarPlugin", + "tohtml", + "tutor", + "zipPlugin", + }, + }, + }, +}) diff --git a/nvim/lua/config/options.lua b/nvim/lua/config/options.lua new file mode 100644 index 0000000..3ea1454 --- /dev/null +++ b/nvim/lua/config/options.lua @@ -0,0 +1,3 @@ +-- Options are automatically loaded before lazy.nvim startup +-- Default options that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/options.lua +-- Add any additional options here diff --git a/nvim/lua/plugins/example.lua b/nvim/lua/plugins/example.lua new file mode 100644 index 0000000..17f53d6 --- /dev/null +++ b/nvim/lua/plugins/example.lua @@ -0,0 +1,197 @@ +-- since this is just an example spec, don't actually load anything here and return an empty spec +-- stylua: ignore +if true then return {} end + +-- every spec file under the "plugins" directory will be loaded automatically by lazy.nvim +-- +-- In your plugin files, you can: +-- * add extra plugins +-- * disable/enabled LazyVim plugins +-- * override the configuration of LazyVim plugins +return { + -- add gruvbox + { "ellisonleao/gruvbox.nvim" }, + + -- Configure LazyVim to load gruvbox + { + "LazyVim/LazyVim", + opts = { + colorscheme = "gruvbox", + }, + }, + + -- change trouble config + { + "folke/trouble.nvim", + -- opts will be merged with the parent spec + opts = { use_diagnostic_signs = true }, + }, + + -- disable trouble + { "folke/trouble.nvim", enabled = false }, + + -- override nvim-cmp and add cmp-emoji + { + "hrsh7th/nvim-cmp", + dependencies = { "hrsh7th/cmp-emoji" }, + ---@param opts cmp.ConfigSchema + opts = function(_, opts) + table.insert(opts.sources, { name = "emoji" }) + end, + }, + + -- change some telescope options and a keymap to browse plugin files + { + "nvim-telescope/telescope.nvim", + keys = { + -- add a keymap to browse plugin files + -- stylua: ignore + { + "fp", + function() require("telescope.builtin").find_files({ cwd = require("lazy.core.config").options.root }) end, + desc = "Find Plugin File", + }, + }, + -- change some options + opts = { + defaults = { + layout_strategy = "horizontal", + layout_config = { prompt_position = "top" }, + sorting_strategy = "ascending", + winblend = 0, + }, + }, + }, + + -- add pyright to lspconfig + { + "neovim/nvim-lspconfig", + ---@class PluginLspOpts + opts = { + ---@type lspconfig.options + servers = { + -- pyright will be automatically installed with mason and loaded with lspconfig + pyright = {}, + }, + }, + }, + + -- add tsserver and setup with typescript.nvim instead of lspconfig + { + "neovim/nvim-lspconfig", + dependencies = { + "jose-elias-alvarez/typescript.nvim", + init = function() + require("lazyvim.util").lsp.on_attach(function(_, buffer) + -- stylua: ignore + vim.keymap.set( "n", "co", "TypescriptOrganizeImports", { buffer = buffer, desc = "Organize Imports" }) + vim.keymap.set("n", "cR", "TypescriptRenameFile", { desc = "Rename File", buffer = buffer }) + end) + end, + }, + ---@class PluginLspOpts + opts = { + ---@type lspconfig.options + servers = { + -- tsserver will be automatically installed with mason and loaded with lspconfig + tsserver = {}, + }, + -- you can do any additional lsp server setup here + -- return true if you don't want this server to be setup with lspconfig + ---@type table + setup = { + -- example to setup with typescript.nvim + tsserver = function(_, opts) + require("typescript").setup({ server = opts }) + return true + end, + -- Specify * to use this function as a fallback for any server + -- ["*"] = function(server, opts) end, + }, + }, + }, + + -- for typescript, LazyVim also includes extra specs to properly setup lspconfig, + -- treesitter, mason and typescript.nvim. So instead of the above, you can use: + { import = "lazyvim.plugins.extras.lang.typescript" }, + + -- add more treesitter parsers + { + "nvim-treesitter/nvim-treesitter", + opts = { + ensure_installed = { + "bash", + "html", + "javascript", + "json", + "lua", + "markdown", + "markdown_inline", + "python", + "query", + "regex", + "tsx", + "typescript", + "vim", + "yaml", + }, + }, + }, + + -- since `vim.tbl_deep_extend`, can only merge tables and not lists, the code above + -- would overwrite `ensure_installed` with the new value. + -- If you'd rather extend the default config, use the code below instead: + { + "nvim-treesitter/nvim-treesitter", + opts = function(_, opts) + -- add tsx and treesitter + vim.list_extend(opts.ensure_installed, { + "tsx", + "typescript", + }) + end, + }, + + -- the opts function can also be used to change the default opts: + { + "nvim-lualine/lualine.nvim", + event = "VeryLazy", + opts = function(_, opts) + table.insert(opts.sections.lualine_x, { + function() + return "😄" + end, + }) + end, + }, + + -- or you can return new options to override all the defaults + { + "nvim-lualine/lualine.nvim", + event = "VeryLazy", + opts = function() + return { + --[[add your custom lualine config here]] + } + end, + }, + + -- use mini.starter instead of alpha + { import = "lazyvim.plugins.extras.ui.mini-starter" }, + + -- add jsonls and schemastore packages, and setup treesitter for json, json5 and jsonc + { import = "lazyvim.plugins.extras.lang.json" }, + + -- add any tools you want to have installed below + { + "williamboman/mason.nvim", + opts = { + ensure_installed = { + "stylua", + "shellcheck", + "shfmt", + "flake8", + }, + }, + }, +} diff --git a/nvim/stylua.toml b/nvim/stylua.toml new file mode 100644 index 0000000..5d6c50d --- /dev/null +++ b/nvim/stylua.toml @@ -0,0 +1,3 @@ +indent_type = "Spaces" +indent_width = 2 +column_width = 120 \ No newline at end of file diff --git a/quickshell/modules/AudioMixer.qml b/quickshell/modules/AudioMixer.qml new file mode 100644 index 0000000..804941c --- /dev/null +++ b/quickshell/modules/AudioMixer.qml @@ -0,0 +1,58 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire +import "./" + +LazyLoader { + required property bool visible + + active: visible + + ShellRoot { + + PanelWindow { + // match the system theme background color + color: contentItem.palette.active.window + + ScrollView { + anchors.fill: parent + contentWidth: availableWidth + + ColumnLayout { + anchors.fill: parent + + anchors.margins: 10 + + // get a list of nodes that output to the default sink + PwNodeLinkTracker { + id: linkTracker + node: Pipewire.defaultAudioSink + } + + MixerEntry { + node: Pipewire.defaultAudioSink + } + + Rectangle { + Layout.fillWidth: true + color: palette.active.text + implicitHeight: 1 + } + + Repeater { + model: linkTracker.linkGroups + + MixerEntry { + required property PwLinkGroup modelData + // Each link group contains a source and a target. + // Since the target is the default sink, we want the source. + node: modelData.source + } + } + } + } + } + } +} diff --git a/quickshell/modules/Bar.qml b/quickshell/modules/Bar.qml new file mode 100644 index 0000000..a353e73 --- /dev/null +++ b/quickshell/modules/Bar.qml @@ -0,0 +1,72 @@ +import QtQuick +import QtQuick.Layouts + +import Quickshell +import Quickshell.Hyprland + +import "./components" + +PanelWindow { + id: root + + // WlrLayershell.layer: WlrLayer.Bottom + + Component.onCompleted: { + if (this.WlrLayershell != null) { + this.WlrLayershell.layer = WlrLayer.Bottom; + } + } + + anchors { + top: true + left: true + right: true + } + + implicitHeight: 30 + color: '#28000000' + + // Shared popup manager: only one click-popup (network / bluetooth / clock) + // may be open at a time. Setting `current` to a name opens that popup and + // implicitly closes any other. + QtObject { + id: popups + + property string current: "" + + function toggle(name: string): void { + current = current === name ? "" : name; + } + } + + RowLayout { + + spacing: 12 + + anchors { + fill: parent + leftMargin: 16 + rightMargin: 16 + } + + Desktop {} + + Rectangle { + Layout.fillWidth: true + } + + NetworkControl { + popups: popups + } + + BluetoothControl { + popups: popups + } + + Battery {} + + Clock { + popups: popups + } + } +} diff --git a/quickshell/modules/BrightnessOSD.qml b/quickshell/modules/BrightnessOSD.qml new file mode 100644 index 0000000..32b259a --- /dev/null +++ b/quickshell/modules/BrightnessOSD.qml @@ -0,0 +1,135 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io + +Scope { + id: root + + // Backlight device under /sys/class/backlight, auto-detected at startup. + property string device: "" + // Current brightness as a 0..1 fraction of max_brightness. + property real brightness: 0 + // Suppresses the OSD for the very first reading (startup, not a real change). + property bool initialised: false + property bool shouldShowOsd: false + + // Nerd Font glyph matching the brightness level (matches config icon style). + function brightnessGlyph(): string { + return root.brightness < 0.5 ? "󰃞" : "󰃠"; + } + + // Pick the first backlight device present on the machine. + Process { + running: true + command: ["sh", "-c", "ls -1 /sys/class/backlight/ | head -n1"] + stdout: StdioCollector { + onStreamFinished: root.device = text.trim() + } + } + + // max_brightness is static; a single blocking read is fine. + FileView { + id: maxBrightness + path: root.device ? `/sys/class/backlight/${root.device}/max_brightness` : "" + blockLoading: true + printErrors: false + onLoaded: root.updateBrightness() + } + + // brightness changes whenever brightnessctl (Hyprland keybinds) writes it. + // inotify fires on sysfs writes, so watchChanges is enough - no polling. + FileView { + id: curBrightness + path: root.device ? `/sys/class/backlight/${root.device}/brightness` : "" + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: root.updateBrightness() + } + + function updateBrightness(): void { + const max = parseInt(maxBrightness.text()); + const cur = parseInt(curBrightness.text()); + if (isNaN(max) || isNaN(cur) || max <= 0) + return; + + const value = Math.max(0, Math.min(1, cur / max)); + const changed = Math.abs(value - root.brightness) > 0.0001; + root.brightness = value; + + if (root.initialised && changed) { + root.shouldShowOsd = true; + hideTimer.restart(); + } + root.initialised = true; + } + + Timer { + id: hideTimer + interval: 1000 + onTriggered: root.shouldShowOsd = false + } + + LazyLoader { + active: root.shouldShowOsd + + PanelWindow { + anchors.bottom: true + margins.bottom: screen.height / 5 + exclusiveZone: 0 + + implicitWidth: 400 + implicitHeight: 50 + color: "transparent" + + // An empty click mask prevents the window from blocking mouse events. + mask: Region {} + + Rectangle { + anchors.fill: parent + radius: height / 2 + color: "#80000000" + + RowLayout { + anchors { + fill: parent + leftMargin: 10 + rightMargin: 15 + } + + Text { + Layout.alignment: Qt.AlignVCenter + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 24 + color: "white" + text: root.brightnessGlyph() + } + + Rectangle { + // Stretches to fill all left-over space + Layout.fillWidth: true + + implicitHeight: 10 + radius: 20 + color: "#50ffffff" + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + color: "white" + implicitWidth: parent.width * root.brightness + radius: parent.radius + } + } + } + } + } + } +} diff --git a/quickshell/modules/MixerEntry.qml b/quickshell/modules/MixerEntry.qml new file mode 100644 index 0000000..98f9789 --- /dev/null +++ b/quickshell/modules/MixerEntry.qml @@ -0,0 +1,53 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell.Services.Pipewire + +ColumnLayout { + required property PwNode node + + // bind the node so we can read its properties + PwObjectTracker { + objects: [node] + } + + RowLayout { + Image { + visible: source != "" + source: { + const icon = node.properties["application.icon-name"] ?? "audio-volume-high-symbolic"; + return `image://icon/${icon}`; + } + + sourceSize.width: 20 + sourceSize.height: 20 + } + + Label { + text: { + // application.name -> description -> name + const app = node.properties["application.name"] ?? (node.description != "" ? node.description : node.name); + const media = node.properties["media.name"]; + return media != undefined ? `${app} - ${media}` : app; + } + } + + Button { + text: node.audio.muted ? "unmute" : "mute" + onClicked: node.audio.muted = !node.audio.muted + } + } + + RowLayout { + Label { + Layout.preferredWidth: 50 + text: `${Math.floor(node.audio.volume * 100)}%` + } + + Slider { + Layout.fillWidth: true + value: node.audio.volume + onValueChanged: node.audio.volume = value + } + } +} diff --git a/quickshell/modules/OSD.qml b/quickshell/modules/OSD.qml new file mode 100644 index 0000000..865d8f7 --- /dev/null +++ b/quickshell/modules/OSD.qml @@ -0,0 +1,121 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire + +Scope { + id: root + + // Pick a Nerd Font glyph matching the current level / mute state, matching + // the glyph-based icon style used elsewhere in this config (e.g. Battery). + function volumeGlyph(): string { + const sink = Pipewire.defaultAudioSink; + if (!sink || !sink.audio || sink.audio.muted) + return "󰝟"; // muted + + const v = sink.audio.volume; + if (v <= 0.0) + return "󰝟"; // muted / zero + if (v < 0.34) + return "󰕿"; // low + if (v < 0.67) + return "󰖀"; // medium + return "󰕾"; // high + } + + // Bind the pipewire node so its volume will be tracked + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + Connections { + target: Pipewire.defaultAudioSink?.audio + + function onVolumesChanged() { + root.shouldShowOsd = true; + hideTimer.restart(); + } + + function onMutedChanged() { + root.shouldShowOsd = true; + hideTimer.restart(); + } + } + + property bool shouldShowOsd: false + + Timer { + id: hideTimer + interval: 1000 + onTriggered: root.shouldShowOsd = false + } + + // The OSD window will be created and destroyed based on shouldShowOsd. + // PanelWindow.visible could be set instead of using a loader, but using + // a loader will reduce the memory overhead when the window isn't open. + LazyLoader { + active: root.shouldShowOsd + + PanelWindow { + // Since the panel's screen is unset, it will be picked by the compositor + // when the window is created. Most compositors pick the current active monitor. + + anchors.bottom: true + margins.bottom: screen.height / 5 + exclusiveZone: 0 + + implicitWidth: 400 + implicitHeight: 50 + color: "transparent" + + // An empty click mask prevents the window from blocking mouse events. + mask: Region {} + + Rectangle { + anchors.fill: parent + radius: height / 2 + color: "#80000000" + + RowLayout { + anchors { + fill: parent + leftMargin: 10 + rightMargin: 15 + } + + Text { + Layout.alignment: Qt.AlignVCenter + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 24 + color: "white" + text: root.volumeGlyph() + } + + Rectangle { + // Stretches to fill all left-over space + Layout.fillWidth: true + + implicitHeight: 10 + radius: 20 + color: "#50ffffff" + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + // Dim the fill while muted as an extra visual cue. + color: (Pipewire.defaultAudioSink?.audio.muted ?? false) ? "#80ffffff" : "white" + implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0) + radius: parent.radius + } + } + } + } + } + } +} diff --git a/quickshell/modules/components/Battery.qml b/quickshell/modules/components/Battery.qml new file mode 100644 index 0000000..83960ad --- /dev/null +++ b/quickshell/modules/components/Battery.qml @@ -0,0 +1,211 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts + +import Quickshell +import Quickshell.Services.UPower + +Item { + id: root + + readonly property var battery: UPower.displayDevice + readonly property bool charging: !UPower.onBattery + + implicitHeight: label.implicitHeight + implicitWidth: label.implicitWidth + + function fmtTime(seconds: real): string { + if (!seconds || seconds <= 0) + return "\u2014"; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + return h > 0 ? `${h}h ${m}m` : `${m}m`; + } + + function batteryGlyph(): string { + if (!root.battery) + return "\u{F0091}"; + const percent = Math.round(root.battery.percentage * 100); + if (root.charging) + return "\u{F0084}"; + if (percent > 80) + return "\u{F0079}"; + if (percent > 60) + return "\u{F0080}"; + if (percent > 40) + return "\u{F007F}"; + if (percent > 20) + return "\u{F007E}"; + return "\u{F007A}"; + } + + function stateText(s: int): string { + switch (s) { + case UPowerDeviceState.Charging: + return "Charging"; + case UPowerDeviceState.Discharging: + return "Discharging"; + case UPowerDeviceState.Empty: + return "Empty"; + case UPowerDeviceState.FullyCharged: + return "Fully charged"; + case UPowerDeviceState.PendingCharge: + return "Pending charge"; + case UPowerDeviceState.PendingDischarge: + return "Pending discharge"; + default: + return "Unknown"; + } + } + + MouseArea { + id: mousearea + anchors.fill: parent + hoverEnabled: true + + Text { + id: label + anchors.centerIn: parent + font.family: "JetBrainsMono Nerd Font" + color: "#cdd6f4" + text: { + if (!root.battery) + return "No Battery"; + return root.batteryGlyph() + " " + Math.round(root.battery.percentage * 100) + "%"; + } + } + } + + PopupWindow { + id: popup + + visible: mousearea.containsMouse && root.battery + + anchor { + item: label + edges: Edges.Bottom + rect { + y: 30 + } + } + + color: "transparent" + implicitWidth: 270 + implicitHeight: card.height + + Rectangle { + id: card + + width: 270 + height: col.implicitHeight + 24 + radius: 16 + color: "#f21e1e2e" + border.color: "#45475a" + border.width: 1 + + ColumnLayout { + id: col + + anchors { + left: parent.left + right: parent.right + top: parent.top + margins: 12 + } + + spacing: 8 + + RowLayout { + Layout.fillWidth: true + spacing: 12 + + Text { + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 30 + color: root.charging ? "#a6e3a1" : "#89b4fa" + text: root.batteryGlyph() + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + Text { + color: "#cdd6f4" + font.pixelSize: 22 + font.bold: true + text: root.battery ? Math.round(root.battery.percentage * 100) + "%" : "\u2014" + } + + Text { + color: root.charging ? "#a6e3a1" : "#a6adc8" + font.pixelSize: 12 + text: root.battery ? root.stateText(root.battery.state) : "" + } + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 1 + color: "#313244" + } + + InfoRow { + label: "Power draw" + value: root.battery ? Math.abs(root.battery.changeRate).toFixed(1) + " W" : "\u2014" + } + + InfoRow { + visible: root.charging + label: "Time to full" + value: root.battery ? root.fmtTime(root.battery.timeToFull) : "\u2014" + } + + InfoRow { + visible: !root.charging + label: "Time to empty" + value: root.battery ? root.fmtTime(root.battery.timeToEmpty) : "\u2014" + } + + InfoRow { + label: "Energy" + value: root.battery ? root.battery.energy.toFixed(1) + " / " + root.battery.energyCapacity.toFixed(1) + " Wh" : "\u2014" + } + + InfoRow { + visible: root.battery && root.battery.healthSupported + label: "Health" + value: root.battery ? Math.round(root.battery.healthPercentage) + "%" : "\u2014" + } + } + } + } + + component InfoRow: RowLayout { + id: infoRow + + property string label: "" + property string value: "" + + Layout.fillWidth: true + spacing: 8 + + Text { + color: "#a6adc8" + font.pixelSize: 12 + text: infoRow.label + } + + Item { + Layout.fillWidth: true + } + + Text { + color: "#cdd6f4" + font.pixelSize: 12 + text: infoRow.value + } + } +} diff --git a/quickshell/modules/components/BluetoothControl.qml b/quickshell/modules/components/BluetoothControl.qml new file mode 100644 index 0000000..f465578 --- /dev/null +++ b/quickshell/modules/components/BluetoothControl.qml @@ -0,0 +1,216 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Hyprland + +Item { + id: root + + property var popups: null + readonly property bool expanded: popups ? popups.current === "bluetooth" : false + readonly property var adapter: Bluetooth.defaultAdapter + readonly property bool enabled: adapter?.enabled ?? false + + // Reactive: reading each device's `connected` inside the binding makes it + // re-evaluate whenever any device connects/disconnects. + readonly property bool anyConnected: { + const ds = Bluetooth.devices?.values ?? []; + return ds.some(d => d.connected); + } + + implicitWidth: icon.implicitWidth + implicitHeight: 30 + + Text { + id: icon + anchors.centerIn: parent + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 17 + color: root.enabled ? (root.anyConnected ? "#a6e3a1" : "#89b4fa") : "#6c7086" + text: !root.enabled ? "\u{F00B2}" : (root.anyConnected ? "\u{F00B1}" : "\u{F00AF}") + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: if (root.popups) root.popups.toggle("bluetooth") + } + + LazyLoader { + active: root.expanded + + PanelWindow { + id: win + anchors.top: true + anchors.right: true + margins.top: 6 + margins.right: 8 + implicitWidth: 340 + implicitHeight: card.implicitHeight + color: "transparent" + exclusiveZone: 0 + + HyprlandFocusGrab { + active: true + windows: [win] + onCleared: if (root.popups) root.popups.current = "" + } + + Rectangle { + id: card + width: parent.width + implicitHeight: col.implicitHeight + 24 + radius: 16 + color: "#f21e1e2e" + border.color: "#45475a" + border.width: 1 + + ColumnLayout { + id: col + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 12 + spacing: 10 + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 16 + color: "#cdd6f4" + text: "\u{F00AF} Bluetooth" + } + + Item { Layout.fillWidth: true } + + Pill { + visible: root.enabled + active: root.adapter?.discovering ?? false + text: (root.adapter?.discovering ?? false) ? "Scanning…" : "Scan" + onClicked: { + if (root.adapter) + root.adapter.discovering = !root.adapter.discovering; + } + } + + Pill { + active: root.enabled + text: root.enabled ? "On" : "Off" + onClicked: { + if (root.adapter) + root.adapter.enabled = !root.enabled; + } + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 1 + color: "#313244" + } + + Text { + visible: !root.enabled + Layout.fillWidth: true + color: "#6c7086" + font.pixelSize: 13 + text: "Bluetooth is off" + } + + Text { + visible: root.enabled && (Bluetooth.devices?.values.length ?? 0) === 0 + Layout.fillWidth: true + color: "#6c7086" + font.pixelSize: 13 + text: "No devices found — tap Scan" + } + + Repeater { + model: root.enabled ? (Bluetooth.devices?.values ?? []) : [] + + delegate: RowLayout { + id: drow + + required property var modelData + readonly property bool busy: modelData.state === BluetoothDeviceState.Connecting + || modelData.state === BluetoothDeviceState.Disconnecting + + Layout.fillWidth: true + spacing: 8 + + Text { + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 16 + color: drow.modelData.connected ? "#a6e3a1" : "#cdd6f4" + text: "\u{F00AF}" + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + Text { + Layout.fillWidth: true + elide: Text.ElideRight + color: "#cdd6f4" + font.pixelSize: 13 + text: drow.modelData.name || drow.modelData.address + } + + Text { + visible: drow.modelData.connected + color: "#a6e3a1" + font.pixelSize: 11 + text: drow.modelData.batteryAvailable + ? "Connected · " + Math.round(drow.modelData.battery * 100) + "%" + : "Connected" + } + } + + Pill { + active: drow.modelData.connected + text: drow.busy ? "…" : (drow.modelData.connected ? "Disconnect" : "Connect") + onClicked: drow.modelData.connected = !drow.modelData.connected + } + } + } + } + } + } + } + + component Pill: Rectangle { + id: pill + + property string text: "" + property bool active: false + signal clicked + + implicitWidth: pillLabel.implicitWidth + 18 + implicitHeight: pillLabel.implicitHeight + 8 + radius: height / 2 + color: pill.active ? "#89b4fa" : "#313244" + + Text { + id: pillLabel + anchors.centerIn: parent + text: pill.text + color: pill.active ? "#11111b" : "#cdd6f4" + font.pixelSize: 12 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: pill.clicked() + } + } +} diff --git a/quickshell/modules/components/Clock.qml b/quickshell/modules/components/Clock.qml new file mode 100644 index 0000000..dd43e61 --- /dev/null +++ b/quickshell/modules/components/Clock.qml @@ -0,0 +1,231 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Hyprland + +Item { + id: root + + property var popups: null + readonly property bool expanded: popups ? popups.current === "clock" : false + + property var now: new Date() + property int viewYear: now.getFullYear() + property int viewMonth: now.getMonth() + + implicitWidth: label.implicitWidth + implicitHeight: 30 + + function shiftMonth(delta: int): void { + let m = root.viewMonth + delta; + let y = root.viewYear; + if (m < 0) { + m = 11; + y -= 1; + } else if (m > 11) { + m = 0; + y += 1; + } + root.viewMonth = m; + root.viewYear = y; + } + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: root.now = new Date() + } + + // Snap the calendar back to the current month each time it's opened. + onExpandedChanged: { + if (expanded) { + root.viewYear = root.now.getFullYear(); + root.viewMonth = root.now.getMonth(); + } + } + + Text { + id: label + anchors.centerIn: parent + color: "#cdd6f4" + text: Qt.formatDateTime(root.now, "hh:mm:ss dd MMM") + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: if (root.popups) root.popups.toggle("clock") + } + + LazyLoader { + active: root.expanded + + PanelWindow { + id: win + anchors.top: true + anchors.right: true + margins.top: 6 + margins.right: 8 + implicitWidth: 300 + implicitHeight: card.implicitHeight + color: "transparent" + exclusiveZone: 0 + + HyprlandFocusGrab { + active: true + windows: [win] + onCleared: if (root.popups) root.popups.current = "" + } + + Rectangle { + id: card + width: parent.width + implicitHeight: col.implicitHeight + 24 + radius: 16 + color: "#f21e1e2e" + border.color: "#45475a" + border.width: 1 + + ColumnLayout { + id: col + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 12 + spacing: 10 + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + Text { + color: "#89b4fa" + font.pixelSize: 26 + font.bold: true + text: Qt.formatDateTime(root.now, "dddd") + } + + Text { + color: "#a6adc8" + font.pixelSize: 13 + text: Qt.formatDateTime(root.now, "dd MMMM yyyy \u00B7 hh:mm:ss") + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 1 + color: "#313244" + } + + RowLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + Layout.fillWidth: true + color: "#cdd6f4" + font.pixelSize: 14 + font.bold: true + text: grid.title + } + + NavBtn { + glyph: "\u{F0141}" + onClicked: root.shiftMonth(-1) + } + + NavBtn { + glyph: "\u{F0142}" + onClicked: root.shiftMonth(1) + } + } + + DayOfWeekRow { + Layout.fillWidth: true + locale: grid.locale + + delegate: Text { + required property string shortName + + horizontalAlignment: Text.AlignHCenter + color: "#6c7086" + font.pixelSize: 11 + font.bold: true + text: shortName + } + } + + MonthGrid { + id: grid + + Layout.fillWidth: true + month: root.viewMonth + year: root.viewYear + spacing: 2 + + delegate: Item { + id: cell + + required property var model + + readonly property bool isToday: model.today + readonly property bool inMonth: model.month === grid.month + + implicitWidth: 36 + implicitHeight: 30 + + Rectangle { + anchors.centerIn: parent + width: 26 + height: 26 + radius: 13 + color: cell.isToday ? "#89b4fa" : "transparent" + } + + Text { + anchors.centerIn: parent + text: cell.model.day + font.pixelSize: 12 + color: cell.isToday ? "#11111b" : (cell.inMonth ? "#cdd6f4" : "#585b70") + } + } + } + } + } + } + } + + component NavBtn: Rectangle { + id: navBtn + + property string glyph: "" + signal clicked + + implicitWidth: 26 + implicitHeight: 26 + radius: 13 + color: navMouse.containsMouse ? "#313244" : "transparent" + + Text { + anchors.centerIn: parent + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 16 + color: "#cdd6f4" + text: navBtn.glyph + } + + MouseArea { + id: navMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: navBtn.clicked() + } + } +} diff --git a/quickshell/modules/components/Desktop.qml b/quickshell/modules/components/Desktop.qml new file mode 100644 index 0000000..aeb9d8a --- /dev/null +++ b/quickshell/modules/components/Desktop.qml @@ -0,0 +1,224 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +RowLayout { + id: root + + spacing: 4 + + readonly property int maxSlots: 20 + readonly property var focusedWs: Hyprland.focusedWorkspace + + // Show every regular workspace up to the highest occupied/focused one, plus + // one spare empty slot (the "+1"). e.g. 1 busy -> show 1,2 ; 3 busy -> 1..4. + readonly property int slotCount: { + let hi = 0; + const vals = Hyprland.workspaces.values; + for (let i = 0; i < vals.length; i++) { + const w = vals[i]; + if (w.id > 0 && w.id > hi) + hi = w.id; + } + const f = Hyprland.focusedWorkspace; + if (f && f.id > hi) + hi = f.id; + return Math.min(root.maxSlots, hi + 1); + } + + // Existing special workspaces (id < 0). + readonly property var specialList: { + const out = []; + const vals = Hyprland.workspaces.values; + for (let i = 0; i < vals.length; i++) { + if (vals[i].id < 0) + out.push(vals[i]); + } + return out; + } + + function wsFor(id: int): var { + const vals = Hyprland.workspaces.values; + for (let i = 0; i < vals.length; i++) { + if (vals[i].id === id) + return vals[i]; + } + return null; + } + + // Hyprland runs in Lua dispatch mode here, so dispatchers take a Lua + // expression. Shell out to hyprctl with the raw Lua form. + Process { + id: dispatcher + } + + function focusWorkspace(id: int): void { + dispatcher.exec(["hyprctl", "dispatch", "hl.dsp.focus({ workspace = " + id + " })"]); + } + + function toggleSpecial(name: string): void { + dispatcher.exec(["hyprctl", "dispatch", "hl.dsp.workspace.toggle_special(\"" + name + "\")"]); + } + + // Quickshell's workspace `active`/`focused` don't reflect whether a special + // is currently shown on a monitor, so we track it ourselves. Map of + // monitorName -> shown special id (0 = none). A special chip is "active" + // when its id appears here. + property var shownSpecials: ({}) + + function isSpecialShown(id: int): bool { + const m = root.shownSpecials; + for (const k in m) { + if (m[k] === id) + return true; + } + return false; + } + + Connections { + target: Hyprland + + function onRawEvent(event: var): void { + if (event.name !== "activespecialv2") + return; + // data: ",," (id/name empty when hidden) + const parts = event.data.split(","); + const mon = parts[parts.length - 1]; + const id = parts[0] === "" ? 0 : parseInt(parts[0]); + const next = Object.assign({}, root.shownSpecials); + next[mon] = id; + root.shownSpecials = next; + } + } + + // Seed the shown-special state at startup (no event has fired yet). + Process { + id: seedProc + running: true + command: ["hyprctl", "-j", "monitors"] + stdout: StdioCollector { + id: seedOut + onStreamFinished: { + try { + const mons = JSON.parse(seedOut.text); + const next = ({}); + for (const m of mons) { + const sw = m.specialWorkspace; + next[m.name] = sw ? sw.id : 0; + } + root.shownSpecials = next; + } catch (e) { + // ignore malformed output + } + } + } + } + + // ---- Regular workspaces ---- + Repeater { + model: root.slotCount + + delegate: Rectangle { + id: chip + + required property int index + readonly property int wsId: index + 1 + readonly property var ws: root.wsFor(wsId) + readonly property bool active: root.focusedWs ? root.focusedWs.id === wsId : false + readonly property bool urgent: ws ? ws.urgent : false + // A non-focused workspace only persists in Hyprland while occupied. + readonly property bool occupied: ws !== null && !active + + implicitWidth: Math.max(22, txt.implicitWidth + 12) + implicitHeight: 22 + radius: 11 + color: chip.urgent ? "#f38ba8" + : chip.active ? "#89b4fa" + : (mouse.containsMouse ? "#313244" : "transparent") + + Behavior on color { + ColorAnimation { duration: 120 } + } + + Text { + id: txt + anchors.centerIn: parent + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 13 + font.bold: chip.active + text: chip.wsId + color: (chip.active || chip.urgent) ? "#11111b" + : chip.occupied ? "#cdd6f4" + : "#585b70" + } + + MouseArea { + id: mouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.focusWorkspace(chip.wsId) + } + } + } + + // ---- Separator (only when specials exist) ---- + Rectangle { + visible: root.specialList.length > 0 + implicitWidth: 1 + implicitHeight: 14 + color: "#45475a" + Layout.leftMargin: 2 + Layout.rightMargin: 2 + } + + // ---- Special workspaces ---- + Repeater { + model: root.specialList + + delegate: Rectangle { + id: spc + + required property var modelData + readonly property bool active: root.isSpecialShown(modelData.id) + readonly property bool urgent: modelData.urgent + readonly property string shortName: { + const n = modelData.name || ""; + return n.startsWith("special:") ? n.slice(8) : n; + } + + implicitWidth: Math.max(22, slabel.implicitWidth + 12) + implicitHeight: 22 + radius: 11 + color: spc.urgent ? "#f38ba8" + : spc.active ? "#cba6f7" + : (smouse.containsMouse ? "#313244" : "transparent") + + Behavior on color { + ColorAnimation { duration: 120 } + } + + Text { + id: slabel + anchors.centerIn: parent + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 13 + font.bold: spc.active + text: "\u{F04CE}" + (spc.shortName ? " " + spc.shortName : "") + color: (spc.active || spc.urgent) ? "#11111b" : "#cba6f7" + } + + MouseArea { + id: smouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.toggleSpecial(spc.shortName) + } + } + } +} diff --git a/quickshell/modules/components/NetworkControl.qml b/quickshell/modules/components/NetworkControl.qml new file mode 100644 index 0000000..362969c --- /dev/null +++ b/quickshell/modules/components/NetworkControl.qml @@ -0,0 +1,442 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +Item { + id: root + + property var popups: null + readonly property bool expanded: popups ? popups.current === "network" : false + property bool wifiEnabled: true + property var networks: [] + property string activeSsid: "" + property int activeSignal: 0 + property var knownSsids: ({}) + property string statusMsg: "" + property string pendingSsid: "" + + implicitWidth: icon.implicitWidth + implicitHeight: 30 + + function secured(sec) { + return !!sec && sec.trim().length > 0; + } + + function signalGlyph(s) { + if (s >= 80) return "\u{F0928}"; + if (s >= 55) return "\u{F0925}"; + if (s >= 30) return "\u{F0922}"; + return "\u{F091F}"; + } + + function wifiGlyph() { + if (!wifiEnabled) return "\u{F05AA}"; + if (activeSsid === "") return "\u{F05A9}"; + return signalGlyph(activeSignal); + } + + // nmcli -t escapes ':' as '\:' and '\' as '\\'; split on unescaped colons. + function splitFields(line) { + const out = []; + let cur = ""; + for (let i = 0; i < line.length; i++) { + const c = line[i]; + if (c === "\\" && i + 1 < line.length) { + cur += line[i + 1]; + i++; + } else if (c === ":") { + out.push(cur); + cur = ""; + } else { + cur += c; + } + } + out.push(cur); + return out; + } + + function refresh() { + statusProc.running = true; + listProc.running = true; + knownProc.running = true; + } + + function setWifi(on) { + radioProc.command = ["nmcli", "radio", "wifi", on ? "on" : "off"]; + radioProc.running = true; + root.wifiEnabled = on; + Qt.callLater(root.refresh); + } + + function rescan() { + root.statusMsg = "Scanning…"; + rescanProc.running = true; + } + + function doConnect(ssid, password) { + root.statusMsg = "Connecting to " + ssid + "…"; + const cmd = ["nmcli", "device", "wifi", "connect", ssid]; + if (password && password.length > 0) + cmd.push("password", password); + connectProc.command = cmd; + connectProc.running = true; + root.pendingSsid = ""; + } + + function disconnect(ssid) { + root.statusMsg = "Disconnecting…"; + actionProc.command = ["nmcli", "connection", "down", "id", ssid]; + actionProc.running = true; + } + + function onNetworkClicked(net) { + if (net.active) { + root.disconnect(net.ssid); + return; + } + if (root.secured(net.security) && !root.knownSsids[net.ssid]) { + root.pendingSsid = (root.pendingSsid === net.ssid) ? "" : net.ssid; + return; + } + root.doConnect(net.ssid, ""); + } + + Process { + id: statusProc + command: ["nmcli", "-t", "-f", "WIFI", "radio"] + stdout: StdioCollector { + id: statusOut + onStreamFinished: root.wifiEnabled = statusOut.text.trim() === "enabled" + } + } + + Process { + id: knownProc + command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] + stdout: StdioCollector { + id: knownOut + onStreamFinished: { + const map = ({}); + const lines = knownOut.text.split("\n"); + for (const line of lines) { + if (!line) continue; + const f = root.splitFields(line); + if (f[1] && f[1].indexOf("wireless") >= 0) + map[f[0]] = true; + } + root.knownSsids = map; + } + } + } + + Process { + id: listProc + command: ["nmcli", "-t", "-f", "ACTIVE,SSID,SIGNAL,SECURITY", "device", "wifi", "list"] + stdout: StdioCollector { + id: listOut + onStreamFinished: { + const seen = ({}); + const list = []; + let actSsid = ""; + let actSig = 0; + const lines = listOut.text.split("\n"); + for (const line of lines) { + if (!line) continue; + const f = root.splitFields(line); + const active = f[0] === "yes"; + const ssid = f[1] || ""; + const sig = parseInt(f[2] || "0") || 0; + const sec = f[3] || ""; + if (ssid === "") continue; + if (active) { + actSsid = ssid; + actSig = sig; + } + if (seen[ssid] !== undefined) { + const ex = list[seen[ssid]]; + if (sig > ex.signal) ex.signal = sig; + if (active) ex.active = true; + continue; + } + seen[ssid] = list.length; + list.push({ ssid: ssid, signal: sig, security: sec, active: active }); + } + list.sort((a, b) => (b.active - a.active) || (b.signal - a.signal)); + root.networks = list; + root.activeSsid = actSsid; + root.activeSignal = actSig; + } + } + } + + Process { id: radioProc } + + Process { + id: rescanProc + command: ["nmcli", "device", "wifi", "rescan"] + onExited: { + root.statusMsg = ""; + root.refresh(); + } + } + + Process { + id: connectProc + stdout: StdioCollector { + id: connectOut + onStreamFinished: { + root.statusMsg = ""; + root.refresh(); + } + } + stderr: StdioCollector { + id: connectErr + onStreamFinished: { + const t = connectErr.text.trim(); + if (t !== "") + root.statusMsg = t.replace(/^Error:\s*/, ""); + } + } + } + + Process { + id: actionProc + onExited: root.refresh() + } + + onExpandedChanged: if (expanded) refresh() + + Timer { + interval: 8000 + running: root.expanded + repeat: true + onTriggered: root.refresh() + } + + Component.onCompleted: refresh() + + Text { + id: icon + anchors.centerIn: parent + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 17 + color: root.wifiEnabled ? (root.activeSsid !== "" ? "#89b4fa" : "#cdd6f4") : "#6c7086" + text: root.wifiGlyph() + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: if (root.popups) root.popups.toggle("network") + } + + LazyLoader { + active: root.expanded + + PanelWindow { + id: win + focusable: true + anchors.top: true + anchors.right: true + margins.top: 6 + margins.right: 8 + implicitWidth: 360 + implicitHeight: card.implicitHeight + color: "transparent" + exclusiveZone: 0 + + HyprlandFocusGrab { + active: true + windows: [win] + onCleared: if (root.popups) root.popups.current = "" + } + + Rectangle { + id: card + width: parent.width + implicitHeight: col.implicitHeight + 24 + radius: 16 + color: "#f21e1e2e" + border.color: "#45475a" + border.width: 1 + + ColumnLayout { + id: col + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 12 + spacing: 8 + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 16 + color: "#cdd6f4" + text: "\u{F05A9} Wi-Fi" + } + + Item { Layout.fillWidth: true } + + Pill { + visible: root.wifiEnabled + text: "Rescan" + onClicked: root.rescan() + } + + Pill { + active: root.wifiEnabled + text: root.wifiEnabled ? "On" : "Off" + onClicked: root.setWifi(!root.wifiEnabled) + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 1 + color: "#313244" + } + + Text { + visible: !root.wifiEnabled + color: "#6c7086" + font.pixelSize: 13 + text: "Wi-Fi is off" + } + + Text { + visible: root.statusMsg !== "" + Layout.fillWidth: true + wrapMode: Text.WordWrap + color: "#f9e2af" + font.pixelSize: 11 + text: root.statusMsg + } + + Repeater { + model: root.wifiEnabled ? root.networks : [] + + delegate: ColumnLayout { + id: nrow + + required property var modelData + + Layout.fillWidth: true + spacing: 4 + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 15 + color: nrow.modelData.active ? "#a6e3a1" : "#cdd6f4" + text: root.signalGlyph(nrow.modelData.signal) + } + + Text { + Layout.fillWidth: true + elide: Text.ElideRight + color: nrow.modelData.active ? "#a6e3a1" : "#cdd6f4" + font.pixelSize: 13 + text: nrow.modelData.ssid + } + + Text { + visible: root.secured(nrow.modelData.security) + font.family: "JetBrainsMono Nerd Font" + font.pixelSize: 12 + color: "#6c7086" + text: "\u{F033E}" + } + + Pill { + active: nrow.modelData.active + text: nrow.modelData.active ? "Disconnect" : "Connect" + onClicked: root.onNetworkClicked(nrow.modelData) + } + } + + RowLayout { + id: pwRow + visible: root.pendingSsid === nrow.modelData.ssid + Layout.fillWidth: true + spacing: 6 + + onVisibleChanged: if (visible) pwField.forceActiveFocus() + + Rectangle { + Layout.fillWidth: true + implicitHeight: 30 + radius: 8 + color: "#181825" + border.color: pwField.activeFocus ? "#89b4fa" : "#45475a" + border.width: 1 + + TextField { + id: pwField + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + verticalAlignment: TextInput.AlignVCenter + echoMode: TextInput.Password + placeholderText: "Password" + color: "#cdd6f4" + placeholderTextColor: "#6c7086" + font.pixelSize: 12 + background: null + onAccepted: root.doConnect(nrow.modelData.ssid, text) + } + } + + Pill { + active: true + text: "Join" + onClicked: root.doConnect(nrow.modelData.ssid, pwField.text) + } + } + } + } + } + } + } + } + + component Pill: Rectangle { + id: pill + + property string text: "" + property bool active: false + signal clicked + + implicitWidth: pillLabel.implicitWidth + 18 + implicitHeight: pillLabel.implicitHeight + 8 + radius: height / 2 + color: pill.active ? "#89b4fa" : "#313244" + + Text { + id: pillLabel + anchors.centerIn: parent + text: pill.text + color: pill.active ? "#11111b" : "#cdd6f4" + font.pixelSize: 12 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: pill.clicked() + } + } +} diff --git a/quickshell/modules/components/Sound.qml b/quickshell/modules/components/Sound.qml new file mode 100644 index 0000000..d703995 --- /dev/null +++ b/quickshell/modules/components/Sound.qml @@ -0,0 +1,23 @@ +import QtQuick +import Quickshell + +Text { + + required property var onClicked + + function click() { + if (mouseArea.containsMouse) { + onClicked(); + } + } + + color: "#fff" + text: "Sound" + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } +} diff --git a/quickshell/shell.qml b/quickshell/shell.qml new file mode 100644 index 0000000..6b10e8a --- /dev/null +++ b/quickshell/shell.qml @@ -0,0 +1,20 @@ +import QtQuick +import Quickshell + +import "./modules" + +ShellRoot { + id: root + + function toggleAudioMixer() { + audioMixer.visible = !audioMixer.visible; + } + + AudioMixer { + id: audioMixer + visible: false + } + OSD {} + BrightnessOSD {} + Bar {} +}