1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 07:42:46 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Marcus Ramberg 8fad2826b1 ci: add flake check 2025-12-08 16:09:16 +01:00
69 changed files with 830 additions and 1605 deletions
+8 -1
View File
@@ -10,14 +10,21 @@ jobs:
check-flake: check-flake:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix - name: Install Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@v31
- name: Check the flake - name: Update vendorHash in flake.nix
run: nix flake check run: nix flake check
+1 -1
View File
@@ -127,7 +127,7 @@ dms plugins search # Browse plugin registry
## Documentation ## Documentation
- **Website:** [danklinux.com](https://danklinux.com) - **Website:** [danklinux.com](https://danklinux.com)
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs/) - **Docs:** [danklinux.com/docs](https://danklinux.com/docs)
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes) - **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview) - **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc) - **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
-5
View File
@@ -12,11 +12,6 @@ import (
var Version = "dev" var Version = "dev"
func main() { func main() {
if os.Getuid() == 0 {
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
os.Exit(1)
}
fileLogger, err := log.NewFileLogger() fileLogger, err := log.NewFileLogger()
if err != nil { if err != nil {
fmt.Printf("Warning: Failed to create log file: %v\n", err) fmt.Printf("Warning: Failed to create log file: %v\n", err)
+13 -51
View File
@@ -295,14 +295,7 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
data := buf.Data() data := buf.Data()
rgb := make([]byte, dstW*dstH*3) rgb := make([]byte, dstW*dstH*3)
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
var swapRB bool
switch pixelFormat {
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
for y := 0; y < dstH; y++ { for y := 0; y < dstH; y++ {
srcY := int(float64(y) / scale) srcY := int(float64(y) / scale)
@@ -316,17 +309,16 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
} }
si := srcY*buf.Stride + srcX*4 si := srcY*buf.Stride + srcX*4
di := (y*dstW + x) * 3 di := (y*dstW + x) * 3
if si+3 >= len(data) { if si+2 < len(data) {
continue if swapRB {
} rgb[di+0] = data[si+2]
if swapRB { rgb[di+1] = data[si+1]
rgb[di+0] = data[si+2] rgb[di+2] = data[si+0]
rgb[di+1] = data[si+1] } else {
rgb[di+2] = data[si+0] rgb[di+0] = data[si+0]
} else { rgb[di+1] = data[si+1]
rgb[di+0] = data[si+0] rgb[di+2] = data[si+2]
rgb[di+1] = data[si+1] }
rgb[di+2] = data[si+2]
} }
} }
} }
@@ -378,37 +370,7 @@ func runScreenshotList(cmd *cobra.Command, args []string) {
} }
for _, o := range outputs { for _, o := range outputs {
scaleStr := fmt.Sprintf("%.2f", o.FractionalScale) fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n",
if o.FractionalScale == float64(int(o.FractionalScale)) { o.Name, o.Width, o.Height, o.X, o.Y, o.Scale)
scaleStr = fmt.Sprintf("%d", int(o.FractionalScale))
}
transformStr := transformName(o.Transform)
fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n",
o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr)
}
}
func transformName(t int32) string {
switch t {
case 0:
return "normal"
case 1:
return "90"
case 2:
return "180"
case 3:
return "270"
case 4:
return "flipped"
case 5:
return "flipped-90"
case 6:
return "flipped-180"
case 7:
return "flipped-270"
default:
return fmt.Sprintf("%d", t)
} }
} }
+3 -17
View File
@@ -29,7 +29,6 @@ func runSetup() error {
wm, wmSelected := promptCompositor() wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal() terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd()
if !wmSelected && !terminalSelected { if !wmSelected && !terminalSelected {
fmt.Println("No configurations selected. Exiting.") fmt.Println("No configurations selected. Exiting.")
@@ -68,14 +67,14 @@ func runSetup() error {
var err error var err error
if wmSelected && terminalSelected { if wmSelected && terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd) results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, terminal)
} else if wmSelected { } else if wmSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd) results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
if len(results) > 1 { if len(results) > 1 {
results = results[:1] results = results[:1]
} }
} else if terminalSelected { } else if terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd) results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal)
if len(results) > 0 && results[0].ConfigType == "Niri" { if len(results) > 0 && results[0].ConfigType == "Niri" {
results = results[1:] results = results[1:]
} }
@@ -145,19 +144,6 @@ func promptTerminal() (deps.Terminal, bool) {
} }
} }
func promptSystemd() bool {
fmt.Println("\nUse systemd for session management?")
fmt.Println("1) Yes (recommended for most distros)")
fmt.Println("2) No (standalone, no systemd integration)")
var response string
fmt.Print("\nChoice (1-2): ")
fmt.Scanln(&response)
response = strings.TrimSpace(response)
return response != "2"
}
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool { func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
homeDir := os.Getenv("HOME") homeDir := os.Getenv("HOME")
willBackup := false willBackup := false
+1 -14
View File
@@ -30,7 +30,6 @@ type Output struct {
height int32 height int32
scale int32 scale int32
fractionalScale float64 fractionalScale float64
transform int32
} }
type LayerSurface struct { type LayerSurface struct {
@@ -277,7 +276,6 @@ func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
if o, ok := p.outputs[name]; ok { if o, ok := p.outputs[name]; ok {
o.x = e.X o.x = e.X
o.y = e.Y o.y = e.Y
o.transform = int32(e.Transform)
} }
p.outputsMu.Unlock() p.outputsMu.Unlock()
}) })
@@ -487,19 +485,8 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) { frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
ls.state.OnScreencopyReady() ls.state.OnScreencopyReady()
screenBuf := ls.state.ScreenBuffer()
if screenBuf != nil && ls.output.transform != TransformNormal {
invTransform := InverseTransform(ls.output.transform)
transformed, err := screenBuf.ApplyTransform(invTransform)
if err != nil {
log.Error("apply transform failed", "err", err)
} else if transformed != screenBuf {
ls.state.ReplaceScreenBuffer(transformed)
}
}
logicalW, _ := ls.state.LogicalSize() logicalW, _ := ls.state.LogicalSize()
screenBuf = ls.state.ScreenBuffer() screenBuf := ls.state.ScreenBuffer()
if logicalW > 0 && screenBuf != nil { if logicalW > 0 && screenBuf != nil {
ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW) ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW)
} }
-15
View File
@@ -4,25 +4,10 @@ import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
type ShmBuffer = shm.Buffer type ShmBuffer = shm.Buffer
const (
TransformNormal = shm.TransformNormal
Transform90 = shm.Transform90
Transform180 = shm.Transform180
Transform270 = shm.Transform270
TransformFlipped = shm.TransformFlipped
TransformFlipped90 = shm.TransformFlipped90
TransformFlipped180 = shm.TransformFlipped180
TransformFlipped270 = shm.TransformFlipped270
)
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) { func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride) return shm.CreateBuffer(width, height, stride)
} }
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}
func GetPixelColor(buf *ShmBuffer, x, y int) Color { func GetPixelColor(buf *ShmBuffer, x, y int) Color {
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888) return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
} }
+21 -78
View File
@@ -1,7 +1,6 @@
package colorpicker package colorpicker
import ( import (
"fmt"
"math" "math"
"strings" "strings"
"sync" "sync"
@@ -16,8 +15,6 @@ const (
FormatXRGB8888 = shm.FormatXRGB8888 FormatXRGB8888 = shm.FormatXRGB8888
FormatABGR8888 = shm.FormatABGR8888 FormatABGR8888 = shm.FormatABGR8888
FormatXBGR8888 = shm.FormatXBGR8888 FormatXBGR8888 = shm.FormatXBGR8888
FormatRGB888 = shm.FormatRGB888
FormatBGR888 = shm.FormatBGR888
) )
type SurfaceState struct { type SurfaceState struct {
@@ -82,11 +79,6 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
bpp := format.BytesPerPixel()
if stride < width*bpp {
return fmt.Errorf("invalid stride %d for width %d (bpp=%d)", stride, width, bpp)
}
if s.screenBuf != nil { if s.screenBuf != nil {
s.screenBuf.Close() s.screenBuf.Close()
s.screenBuf = nil s.screenBuf = nil
@@ -98,7 +90,6 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
} }
s.screenBuf = buf s.screenBuf = buf
s.screenBuf.Format = format
s.screenFormat = format s.screenFormat = format
return nil return nil
} }
@@ -115,20 +106,6 @@ func (s *SurfaceState) ScreenFormat() PixelFormat {
return s.screenFormat return s.screenFormat
} }
func (s *SurfaceState) ReplaceScreenBuffer(newBuf *ShmBuffer) {
s.mu.Lock()
defer s.mu.Unlock()
if s.screenBuf != nil {
s.screenBuf.Close()
}
s.screenBuf = newBuf
s.screenFormat = newBuf.Format
s.recomputeScale()
s.ensureRenderBuffers()
}
func (s *SurfaceState) OnScreencopyFlags(flags uint32) { func (s *SurfaceState) OnScreencopyFlags(flags uint32) {
s.mu.Lock() s.mu.Lock()
s.yInverted = (flags & 1) != 0 s.yInverted = (flags & 1) != 0
@@ -143,15 +120,6 @@ func (s *SurfaceState) OnScreencopyReady() {
return return
} }
if s.screenFormat.Is24Bit() {
converted, newFormat, err := s.screenBuf.ConvertTo32Bit(s.screenFormat)
if err == nil && converted != s.screenBuf {
s.screenBuf.Close()
s.screenBuf = converted
s.screenFormat = newFormat
}
}
s.recomputeScale() s.recomputeScale()
s.ensureRenderBuffers() s.ensureRenderBuffers()
s.readyForDisplay = true s.readyForDisplay = true
@@ -311,10 +279,10 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
drawMagnifierWithInversion( drawMagnifierWithInversion(
dst.Data(), dst.Stride, dst.Width, dst.Height, dst.Data(), dst.Stride, dst.Width, dst.Height,
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height, s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
px, py, picked, s.yInverted, s.screenFormat, px, py, picked, s.yInverted,
) )
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat) drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase)
return dst return dst
} }
@@ -422,7 +390,6 @@ func drawMagnifierWithInversion(
cx, cy int, cx, cy int,
borderColor Color, borderColor Color,
yInverted bool, yInverted bool,
format PixelFormat,
) { ) {
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 { if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
return return
@@ -440,14 +407,6 @@ func drawMagnifierWithInversion(
innerRadius := float64(outerRadius - borderThickness) innerRadius := float64(outerRadius - borderThickness)
outerRadiusF := float64(outerRadius) outerRadiusF := float64(outerRadius)
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ { for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ {
y := cy + dy y := cy + dy
if y < 0 || y >= dstH { if y < 0 || y >= dstH {
@@ -472,9 +431,9 @@ func drawMagnifierWithInversion(
} }
bgColor := Color{ bgColor := Color{
R: dst[dstOff+rOff], B: dst[dstOff+0],
G: dst[dstOff+1], G: dst[dstOff+1],
B: dst[dstOff+bOff], R: dst[dstOff+2],
A: dst[dstOff+3], A: dst[dstOff+3],
} }
@@ -503,7 +462,7 @@ func drawMagnifierWithInversion(
} }
srcOff := sy*srcStride + sx*4 srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) { if srcOff+4 <= len(src) {
magColor := Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255} magColor := Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
finalColor = blendColors(magColor, borderColor, alpha) finalColor = blendColors(magColor, borderColor, alpha)
} else { } else {
finalColor = borderColor finalColor = borderColor
@@ -524,25 +483,24 @@ func drawMagnifierWithInversion(
} }
srcOff := sy*srcStride + sx*4 srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) { if srcOff+4 <= len(src) {
finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255} finalColor = Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
} else { } else {
continue continue
} }
} }
dst[dstOff+rOff] = finalColor.R dst[dstOff+0] = finalColor.B
dst[dstOff+1] = finalColor.G dst[dstOff+1] = finalColor.G
dst[dstOff+bOff] = finalColor.B dst[dstOff+2] = finalColor.R
dst[dstOff+3] = 255 dst[dstOff+3] = 255
} }
} }
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius, format) drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius)
} }
func drawMagnifierCrosshair( func drawMagnifierCrosshair(
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int, data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
format PixelFormat,
) { ) {
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
return return
@@ -1040,7 +998,7 @@ var fontGlyphs = map[rune][fontH]uint8{
}, },
} }
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) { func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool) {
text := formatColorForPreview(c, format, lowercase) text := formatColorForPreview(c, format, lowercase)
if len(text) == 0 { if len(text) == 0 {
return return
@@ -1075,8 +1033,9 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
y = height - boxH y = height - boxH
} }
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c, pixelFormat) drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c)
// Use contrasting text color based on luminance
lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B) lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
var fg Color var fg Color
if lum > 128 { if lum > 128 {
@@ -1084,7 +1043,7 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
} else { } else {
fg = Color{R: 255, G: 255, B: 255, A: 255} fg = Color{R: 255, G: 255, B: 255, A: 255}
} }
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg, pixelFormat) drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg)
} }
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string { func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
@@ -1105,7 +1064,7 @@ func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string
} }
} }
func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color, format PixelFormat) { func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color) {
if w <= 0 || h <= 0 { if w <= 0 || h <= 0 {
return return
} }
@@ -1114,14 +1073,6 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
x = clamp(x, 0, width) x = clamp(x, 0, width)
y = clamp(y, 0, height) y = clamp(y, 0, height)
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for yy := y; yy < yEnd; yy++ { for yy := y; yy < yEnd; yy++ {
rowOff := yy * stride rowOff := yy * stride
for xx := x; xx < xEnd; xx++ { for xx := x; xx < xEnd; xx++ {
@@ -1129,34 +1080,26 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
if off+4 > len(data) { if off+4 > len(data) {
continue continue
} }
data[off+rOff] = col.R data[off+0] = col.B
data[off+1] = col.G data[off+1] = col.G
data[off+bOff] = col.B data[off+2] = col.R
data[off+3] = 255 data[off+3] = 255
} }
} }
} }
func drawText(data []byte, stride, width, height, x, y int, text string, col Color, format PixelFormat) { func drawText(data []byte, stride, width, height, x, y int, text string, col Color) {
for i, r := range text { for i, r := range text {
drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col, format) drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col)
} }
} }
func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color, format PixelFormat) { func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color) {
g, ok := fontGlyphs[r] g, ok := fontGlyphs[r]
if !ok { if !ok {
return return
} }
var rOff, bOff int
switch format {
case FormatABGR8888, FormatXBGR8888:
rOff, bOff = 0, 2
default:
rOff, bOff = 2, 0
}
for row := 0; row < fontH; row++ { for row := 0; row < fontH; row++ {
yy := y + row yy := y + row
if yy < 0 || yy >= height { if yy < 0 || yy >= height {
@@ -1180,9 +1123,9 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color,
continue continue
} }
data[off+rOff] = col.R data[off+0] = col.B
data[off+1] = col.G data[off+1] = col.G
data[off+bOff] = col.B data[off+2] = col.R
data[off+3] = 255 data[off+3] = 255
} }
} }
+69 -91
View File
@@ -46,20 +46,11 @@ func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context,
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil) return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
} }
// DeployConfigurationsWithSystemd deploys configurations with systemd option
func (cd *ConfigDeployer) DeployConfigurationsWithSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, nil, nil, nil, useSystemd)
}
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil) return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
} }
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
}
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult var results []DeploymentResult
shouldReplaceConfig := func(configType string) bool { shouldReplaceConfig := func(configType string) bool {
@@ -73,7 +64,7 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
switch wm { switch wm {
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
if shouldReplaceConfig("Niri") { if shouldReplaceConfig("Niri") {
result, err := cd.deployNiriConfig(terminal, useSystemd) result, err := cd.deployNiriConfig(terminal)
results = append(results, result) results = append(results, result)
if err != nil { if err != nil {
return results, fmt.Errorf("failed to deploy Niri config: %w", err) return results, fmt.Errorf("failed to deploy Niri config: %w", err)
@@ -81,7 +72,7 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
} }
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
if shouldReplaceConfig("Hyprland") { if shouldReplaceConfig("Hyprland") {
result, err := cd.deployHyprlandConfig(terminal, useSystemd) result, err := cd.deployHyprlandConfig(terminal)
results = append(results, result) results = append(results, result)
if err != nil { if err != nil {
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err) return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
@@ -119,7 +110,7 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
return results, nil return results, nil
} }
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) { func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
result := DeploymentResult{ result := DeploymentResult{
ConfigType: "Niri", ConfigType: "Niri",
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"), Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
@@ -157,6 +148,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
} }
polkitPath, err := cd.detectPolkitAgent()
if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1"
}
var terminalCommand string var terminalCommand string
switch terminal { switch terminal {
case deps.TerminalGhostty: case deps.TerminalGhostty:
@@ -169,11 +166,8 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
terminalCommand = "ghostty" terminalCommand = "ghostty"
} }
newConfig := strings.ReplaceAll(NiriConfig, "{{TERMINAL_COMMAND}}", terminalCommand) newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformNiriConfigForNonSystemd(newConfig, terminalCommand)
}
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig) mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
@@ -410,6 +404,41 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
return results, nil return results, nil
} }
// detectPolkitAgent tries to find the polkit authentication agent on the system
// Prioritizes mate-polkit paths since that's what we install
func (cd *ConfigDeployer) detectPolkitAgent() (string, error) {
// Prioritize mate-polkit paths first
matePaths := []string{
"/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path
"/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
"/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1",
"/usr/lib/polkit-mate/polkit-mate-authentication-agent-1",
"/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1",
}
for _, path := range matePaths {
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path))
return path, nil
}
}
// Fallback to other polkit agents if mate-polkit is not found
fallbackPaths := []string{
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
"/usr/libexec/polkit-gnome-authentication-agent-1",
}
for _, path := range fallbackPaths {
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path))
return path, nil
}
}
return "", fmt.Errorf("no polkit agent found in common locations")
}
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config // mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) { func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match output sections (including commented ones) // Regular expression to match output sections (including commented ones)
@@ -453,7 +482,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
} }
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging // deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) { func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) {
result := DeploymentResult{ result := DeploymentResult{
ConfigType: "Hyprland", ConfigType: "Hyprland",
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"), Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
@@ -485,6 +514,14 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
} }
// Detect polkit agent path
polkitPath, err := cd.detectPolkitAgent()
if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
}
// Determine terminal command based on choice
var terminalCommand string var terminalCommand string
switch terminal { switch terminal {
case deps.TerminalGhostty: case deps.TerminalGhostty:
@@ -494,15 +531,13 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
case deps.TerminalAlacritty: case deps.TerminalAlacritty:
terminalCommand = "alacritty" terminalCommand = "alacritty"
default: default:
terminalCommand = "ghostty" terminalCommand = "ghostty" // fallback to ghostty
} }
newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand) newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
}
// If there was an existing config, merge the monitor sections
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig) mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
if err != nil { if err != nil {
@@ -525,16 +560,24 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config // mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) { func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match monitor lines (including commented ones)
// Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc.
// Also matches commented versions: # monitor = ...
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`) monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
// Find all monitor lines in the existing config
existingMonitors := monitorRegex.FindAllString(existingConfig, -1) existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
if len(existingMonitors) == 0 { if len(existingMonitors) == 0 {
// No monitor sections to merge
return newConfig, nil return newConfig, nil
} }
// Remove the example monitor line from the new config
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`) exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "") mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`) monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig) headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
@@ -542,7 +585,8 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return "", fmt.Errorf("could not find MONITOR CONFIG section") return "", fmt.Errorf("could not find MONITOR CONFIG section")
} }
insertPos := headerMatch[1] + 1 // Insert after the header
insertPos := headerMatch[1] + 1 // +1 for the newline
var builder strings.Builder var builder strings.Builder
builder.WriteString(mergedConfig[:insertPos]) builder.WriteString(mergedConfig[:insertPos])
@@ -557,69 +601,3 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return builder.String(), nil return builder.String(), nil
} }
func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalCommand string) string {
lines := strings.Split(config, "\n")
var result []string
startupSectionFound := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
continue
}
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
startupSectionFound = true
result = append(result, "exec-once = dms run")
result = append(result, "env = QT_QPA_PLATFORM,wayland")
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
result = append(result, fmt.Sprintf("env = TERMINAL,%s", terminalCommand))
continue
}
result = append(result, line)
}
if !startupSectionFound {
for i, line := range result {
if strings.Contains(line, "STARTUP APPS") {
insertLines := []string{
"exec-once = dms run",
"env = QT_QPA_PLATFORM,wayland",
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
"env = QT_QPA_PLATFORMTHEME,gtk3",
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
fmt.Sprintf("env = TERMINAL,%s", terminalCommand),
}
result = append(result[:i+2], append(insertLines, result[i+2:]...)...)
break
}
}
}
return strings.Join(result, "\n")
}
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
envVars := fmt.Sprintf(`environment {
XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
TERMINAL "%s"
}`, terminalCommand)
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
spawnDms := `spawn-at-startup "dms" "run"`
if !strings.Contains(config, spawnDms) {
config = strings.Replace(config,
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`,
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`+"\n"+spawnDms,
1)
}
return config
}
+38 -6
View File
@@ -3,6 +3,7 @@ package config
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -10,6 +11,23 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDetectPolkitAgent(t *testing.T) {
cd := &ConfigDeployer{}
// This test depends on the system having a polkit agent installed
// We'll just test that the function doesn't crash and returns some path or error
path, err := cd.detectPolkitAgent()
if err != nil {
// If no polkit agent is found, that's okay for testing
assert.Contains(t, err.Error(), "no polkit agent found")
} else {
// If found, it should be a valid path
assert.NotEmpty(t, path)
assert.True(t, strings.Contains(path, "polkit"))
}
}
func TestMergeNiriOutputSections(t *testing.T) { func TestMergeNiriOutputSections(t *testing.T) {
cd := &ConfigDeployer{} cd := &ConfigDeployer{}
@@ -254,6 +272,17 @@ func getGhosttyPath() string {
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config") return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
} }
func TestPolkitPathInjection(t *testing.T) {
testConfig := `spawn-at-startup "{{POLKIT_AGENT_PATH}}"
other content`
result := strings.Replace(testConfig, "{{POLKIT_AGENT_PATH}}", "/test/polkit/path", 1)
assert.Contains(t, result, `spawn-at-startup "/test/polkit/path"`)
assert.NotContains(t, result, "{{POLKIT_AGENT_PATH}}")
}
func TestMergeHyprlandMonitorSections(t *testing.T) { func TestMergeHyprlandMonitorSections(t *testing.T) {
cd := &ConfigDeployer{} cd := &ConfigDeployer{}
@@ -395,7 +424,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
cd := NewConfigDeployer(logChan) cd := NewConfigDeployer(logChan)
t.Run("deploy hyprland config to empty directory", func(t *testing.T) { t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true) result, err := cd.deployHyprlandConfig(deps.TerminalGhostty)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType) assert.Equal(t, "Hyprland", result.ConfigType)
@@ -406,7 +435,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
content, err := os.ReadFile(result.Path) content, err := os.ReadFile(result.Path)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, string(content), "# MONITOR CONFIG") assert.Contains(t, string(content), "# MONITOR CONFIG")
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty") assert.Contains(t, string(content), "bind = $mod, T, exec, $TERMINAL")
assert.Contains(t, string(content), "exec-once = ") assert.Contains(t, string(content), "exec-once = ")
}) })
@@ -425,7 +454,7 @@ general {
err = os.WriteFile(hyprPath, []byte(existingContent), 0644) err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
require.NoError(t, err) require.NoError(t, err)
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true) result, err := cd.deployHyprlandConfig(deps.TerminalKitty)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType) assert.Equal(t, "Hyprland", result.ConfigType)
@@ -442,7 +471,7 @@ general {
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144") assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60") assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty") assert.Contains(t, string(newContent), "bind = $mod, T, exec, $TERMINAL")
assert.NotContains(t, string(newContent), "monitor = eDP-2") assert.NotContains(t, string(newContent), "monitor = eDP-2")
}) })
} }
@@ -450,6 +479,7 @@ general {
func TestNiriConfigStructure(t *testing.T) { func TestNiriConfigStructure(t *testing.T) {
assert.Contains(t, NiriConfig, "input {") assert.Contains(t, NiriConfig, "input {")
assert.Contains(t, NiriConfig, "layout {") assert.Contains(t, NiriConfig, "layout {")
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, NiriBindsConfig, "binds {") assert.Contains(t, NiriBindsConfig, "binds {")
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`) assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
@@ -460,9 +490,11 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandConfig, "# STARTUP APPS") assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG") assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS") assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}") assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, $TERMINAL")
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle") assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$") assert.Contains(t, HyprlandConfig, "windowrule {")
assert.Contains(t, HyprlandConfig, "match:class = ^(com\\.mitchellh\\.ghostty)$")
} }
func TestGhosttyConfigStructure(t *testing.T) { func TestGhosttyConfigStructure(t *testing.T) {
+121 -26
View File
@@ -10,9 +10,8 @@ monitor = , preferred,auto,auto
# ================== # ==================
# STARTUP APPS # STARTUP APPS
# ================== # ==================
exec-once = dbus-update-activation-environment --systemd --all
exec-once = systemctl --user start hyprland-session.target
exec-once = bash -c "wl-paste --watch cliphist store &" exec-once = bash -c "wl-paste --watch cliphist store &"
exec-once = {{POLKIT_AGENT_PATH}}
# ================== # ==================
# INPUT CONFIG # INPUT CONFIG
@@ -91,36 +90,132 @@ misc {
# ================== # ==================
# WINDOW RULES # WINDOW RULES
# ================== # ==================
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$ windowrule {
name = windowrule-1
tile = on
match:class = ^(org\.wezfurlong\.wezterm)$
border_size = 0
}
windowrulev2 = rounding 12, class:^(org\.gnome\.)
windowrulev2 = noborder, class:^(org\.gnome\.)
windowrulev2 = tile, class:^(gnome-control-center)$ windowrule {
windowrulev2 = tile, class:^(pavucontrol)$ name = windowrule-2
windowrulev2 = tile, class:^(nm-connection-editor)$ rounding = 12
match:class = ^(org\.gnome\.)
border_size = 0
}
windowrulev2 = float, class:^(gnome-calculator)$
windowrulev2 = float, class:^(galculator)$
windowrulev2 = float, class:^(blueman-manager)$
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
windowrulev2 = float, class:^(steam)$
windowrulev2 = float, class:^(xdg-desktop-portal)$
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
windowrulev2 = noborder, class:^(Alacritty)$
windowrulev2 = noborder, class:^(zen)$
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
windowrulev2 = noborder, class:^(kitty)$
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$ windowrule {
windowrulev2 = float, class:^(zoom)$ name = windowrule-3
tile = on
match:class = ^(gnome-control-center)$
}
# DMS windows floating by default windowrule {
windowrulev2 = float, class:^(org.quickshell)$ name = windowrule-4
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0 tile = on
match:class = ^(pavucontrol)$
}
layerrule = noanim, ^(quickshell)$ windowrule {
name = windowrule-5
tile = on
match:class = ^(nm-connection-editor)$
}
windowrule {
name = windowrule-6
float = on
match:class = ^(gnome-calculator)$
}
windowrule {
name = windowrule-7
float = on
match:class = ^(galculator)$
}
windowrule {
name = windowrule-8
float = on
match:class = ^(blueman-manager)$
}
windowrule {
name = windowrule-9
float = on
match:class = ^(org\.gnome\.Nautilus)$
}
windowrule {
name = windowrule-10
float = on
match:class = ^(steam)$
}
windowrule {
name = windowrule-11
float = on
match:class = ^(xdg-desktop-portal)$
}
windowrule {
name = windowrule-12
border_size = 0
match:class = ^(Alacritty)$
}
windowrule {
name = windowrule-13
border_size = 0
match:class = ^(zen)$
}
windowrule {
name = windowrule-14
border_size = 0
match:class = ^(com\.mitchellh\.ghostty)$
}
windowrule {
name = windowrule-15
border_size = 0
match:class = ^(kitty)$
}
windowrule {
name = windowrule-16
float = on
match:class = ^(firefox)$
match:title = ^(Picture-in-Picture)$
}
windowrule {
name = windowrule-17
float = on
match:class = ^(zoom)$
}
windowrule {
name = windowrule-18
opacity = 0.9 0.9
match:float = 0
match:focus = 0
}
layerrule {
name = layerrule-1
no_anim = on
match:namespace = ^(quickshell)$
}
# ================== # ==================
# KEYBINDINGS # KEYBINDINGS
@@ -128,7 +223,7 @@ layerrule = noanim, ^(quickshell)$
$mod = SUPER $mod = SUPER
# === Application Launchers === # === Application Launchers ===
bind = $mod, T, exec, {{TERMINAL_COMMAND}} bind = $mod, T, exec, $TERMINAL
bind = $mod, space, exec, dms ipc call spotlight toggle bind = $mod, space, exec, dms ipc call spotlight toggle
bind = $mod, V, exec, dms ipc call clipboard toggle bind = $mod, V, exec, dms ipc call clipboard toggle
bind = $mod, M, exec, dms ipc call processlist focusOrToggle bind = $mod, M, exec, dms ipc call processlist focusOrToggle
+7
View File
@@ -44,6 +44,7 @@ input {
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout // https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout { layout {
// Set gaps around windows in logical pixels. // Set gaps around windows in logical pixels.
gaps 5
background-color "transparent" background-color "transparent"
// When to center a column when changing focus, options are: // When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left // - "never", default behavior, focusing an off-screen column will keep at the left
@@ -86,6 +87,11 @@ layout {
inactive-color "#d0d0d0" // Light gray inactive-color "#d0d0d0" // Light gray
urgent-color "#cc4444" // Softer red urgent-color "#cc4444" // Softer red
} }
focus-ring {
width 2
active-color "#808080" // Medium gray
inactive-color "#505050" // Dark gray
}
shadow { shadow {
softness 30 softness 30
spread 5 spread 5
@@ -110,6 +116,7 @@ overview {
// See the binds section below for more spawn examples. // See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors. // This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &" spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
environment { environment {
XDG_CURRENT_DESKTOP "niri" XDG_CURRENT_DESKTOP "niri"
} }
+5 -7
View File
@@ -182,11 +182,13 @@ func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) Pac
if forceQuickshellGit || variant == deps.VariantGit { if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR} return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
} }
// ! TODO - for now we're only forcing quickshell-git on ARCH, as other distros use DL repos which pin a newer quickshell return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
} }
func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { func (a *ArchDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
} }
@@ -360,10 +362,6 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := a.WriteWindowManagerConfig(wm); err != nil {
a.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := a.EnableDMSService(ctx); err != nil { if err := a.EnableDMSService(ctx); err != nil {
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
-35
View File
@@ -611,41 +611,6 @@ func (b *BaseDistribution) EnableDMSService(ctx context.Context) error {
return nil return nil
} }
func (b *BaseDistribution) WriteWindowManagerConfig(wm deps.WindowManager) error {
if wm == deps.WindowManagerHyprland {
if err := b.WriteHyprlandSessionTarget(); err != nil {
return fmt.Errorf("failed to write hyprland session target: %w", err)
}
}
return nil
}
func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("failed to create systemd user directory: %w", err)
}
targetPath := filepath.Join(targetDir, "hyprland-session.target")
content := `[Unit]
Description=Hyprland Session Target
Requires=graphical-session.target
After=graphical-session.target
`
if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
}
b.log(fmt.Sprintf("Wrote hyprland-session.target to %s", targetPath))
return nil
}
// installDMSBinary installs the DMS binary from GitHub releases // installDMSBinary installs the DMS binary from GitHub releases
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
b.log("Installing/updating DMS binary...") b.log("Installing/updating DMS binary...")
+7 -11
View File
@@ -208,7 +208,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential") checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil { if err := checkCmd.Run(); err != nil {
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential") cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil { if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
return fmt.Errorf("failed to install build-essential: %w", err) return fmt.Errorf("failed to install build-essential: %w", err)
} }
@@ -225,7 +225,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") "apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
} }
@@ -338,10 +338,6 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := d.WriteWindowManagerConfig(wm); err != nil {
d.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := d.EnableDMSService(ctx); err != nil { if err := d.EnableDMSService(ctx); err != nil {
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
@@ -453,7 +449,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL), CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
} }
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath) keyCmd := fmt.Sprintf("curl -fsSL %s/Release.key | gpg --dearmor -o %s", baseURL, keyringPath)
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd) cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil { if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err) return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
@@ -471,7 +467,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
} }
addRepoCmd := ExecSudoCommand(ctx, sudoPassword, addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile)) fmt.Sprintf("echo '%s' | tee %s", repoLine, listFile))
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err) return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
} }
@@ -506,7 +502,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"} args := []string{"apt-get", "install", "-y"}
args = append(args, packages...) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -616,7 +612,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
CommandInfo: "sudo apt-get install rustup", CommandInfo: "sudo apt-get install rustup",
} }
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup") rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err) return fmt.Errorf("failed to install rustup: %w", err)
} }
@@ -655,7 +651,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo apt-get install golang-go", CommandInfo: "sudo apt-get install golang-go",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go") installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90) return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
} }
+5 -6
View File
@@ -166,7 +166,10 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"} return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
} }
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
}
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"} return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
} }
@@ -174,7 +177,7 @@ func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) Package
if variant == deps.VariantGit { if variant == deps.VariantGit {
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"} return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
} }
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"} return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
} }
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency { func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -359,10 +362,6 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := f.WriteWindowManagerConfig(wm); err != nil {
f.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := f.EnableDMSService(ctx); err != nil { if err := f.EnableDMSService(ctx); err != nil {
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+22 -6
View File
@@ -95,6 +95,7 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectWindowManager(wm)) dependencies = append(dependencies, g.detectWindowManager(wm))
dependencies = append(dependencies, g.detectQuickshell()) dependencies = append(dependencies, g.detectQuickshell())
dependencies = append(dependencies, g.detectXDGPortal()) dependencies = append(dependencies, g.detectXDGPortal())
dependencies = append(dependencies, g.detectPolkitAgent())
dependencies = append(dependencies, g.detectAccountsService()) dependencies = append(dependencies, g.detectAccountsService())
if wm == deps.WindowManagerHyprland { if wm == deps.WindowManagerHyprland {
@@ -126,6 +127,20 @@ func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
} }
} }
func (g *GentooDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if g.packageInstalled("mate-extra/mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
}
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency { func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing status := deps.StatusMissing
if g.packageInstalled("gui-apps/xwayland-satellite") { if g.packageInstalled("gui-apps/xwayland-satellite") {
@@ -172,6 +187,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"}, "alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"}, "xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"}, "qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
@@ -207,8 +223,12 @@ func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"} return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
} }
func (g *GentooDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: g.getArchKeyword()} archKeyword := g.getArchKeyword()
if variant == deps.VariantGit {
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword}
}
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
} }
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping { func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
@@ -436,10 +456,6 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := g.WriteWindowManagerConfig(wm); err != nil {
g.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := g.EnableDMSService(ctx); err != nil { if err := g.EnableDMSService(ctx); err != nil {
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+1 -5
View File
@@ -377,10 +377,6 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := o.WriteWindowManagerConfig(wm); err != nil {
o.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := o.EnableDMSService(ctx); err != nil { if err := o.EnableDMSService(ctx); err != nil {
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
@@ -476,7 +472,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
cmd := ExecSudoCommand(ctx, sudoPassword, cmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("zypper addrepo -f %s", repoURL)) fmt.Sprintf("zypper addrepo -f %s", repoURL))
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err)) return fmt.Errorf("failed to enable OBS repo %s: %w", pkg.RepoURL, err)
} }
enabledRepos[pkg.RepoURL] = true enabledRepos[pkg.RepoURL] = true
-4
View File
@@ -357,10 +357,6 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
} }
if err := u.WriteWindowManagerConfig(wm); err != nil {
u.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := u.EnableDMSService(ctx); err != nil { if err := u.EnableDMSService(ctx); err != nil {
u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
} }
+8 -107
View File
@@ -7,7 +7,6 @@ import (
"os/exec" "os/exec"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client" wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
@@ -90,15 +89,12 @@ func SetCompositorDWL() {
} }
type WindowGeometry struct { type WindowGeometry struct {
X int32 X int32
Y int32 Y int32
Width int32 Width int32
Height int32 Height int32
Output string Output string
Scale float64 Scale float64
OutputX int32
OutputY int32
OutputTransform int32
} }
func GetActiveWindow() (*WindowGeometry, error) { func GetActiveWindow() (*WindowGeometry, error) {
@@ -386,92 +382,6 @@ func GetFocusedMonitor() string {
return "" return ""
} }
type outputInfo struct {
x, y int32
transform int32
}
func getOutputInfo(outputName string) (*outputInfo, bool) {
display, err := client.Connect("")
if err != nil {
return nil, false
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, false
}
var outputManager *wlr_output_management.ZwlrOutputManagerV1
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
mgr := wlr_output_management.NewZwlrOutputManagerV1(ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
outputManager = mgr
}
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, false
}
if outputManager == nil {
return nil, false
}
type headState struct {
name string
x, y int32
transform int32
}
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
done := false
outputManager.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
state := &headState{}
heads[e.Head] = state
e.Head.SetNameHandler(func(ne wlr_output_management.ZwlrOutputHeadV1NameEvent) {
state.name = ne.Name
})
e.Head.SetPositionHandler(func(pe wlr_output_management.ZwlrOutputHeadV1PositionEvent) {
state.x = pe.X
state.y = pe.Y
})
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
state.transform = te.Transform
})
})
outputManager.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
done = true
})
for !done {
if err := ctx.Dispatch(); err != nil {
return nil, false
}
}
for _, state := range heads {
if state.name == outputName {
return &outputInfo{
x: state.x,
y: state.y,
transform: state.transform,
}, true
}
}
return nil, false
}
func getDWLActiveWindow() (*WindowGeometry, error) { func getDWLActiveWindow() (*WindowGeometry, error) {
display, err := client.Connect("") display, err := client.Connect("")
if err != nil { if err != nil {
@@ -599,23 +509,14 @@ func getDWLActiveWindow() (*WindowGeometry, error) {
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
} }
return &WindowGeometry{
geom := &WindowGeometry{
X: state.x, X: state.x,
Y: state.y, Y: state.y,
Width: state.w, Width: state.w,
Height: state.h, Height: state.h,
Output: state.name, Output: state.name,
Scale: scale, Scale: scale,
} }, nil
if info, ok := getOutputInfo(state.name); ok {
geom.OutputX = info.x
geom.OutputY = info.y
geom.OutputTransform = info.transform
}
return geom, nil
} }
return nil, fmt.Errorf("no active output found") return nil, fmt.Errorf("no active output found")
+1 -7
View File
@@ -20,13 +20,7 @@ func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height)) img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
data := buf.Data() data := buf.Data()
var swapRB bool swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0
switch format {
case uint32(FormatABGR8888), uint32(FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
for y := 0; y < buf.Height; y++ { for y := 0; y < buf.Height; y++ {
srcOff := y * buf.Stride srcOff := y * buf.Stride
+6 -52
View File
@@ -380,24 +380,19 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
return return
} }
var capturedBuf *ShmBuffer
var capturedFormat PixelFormat
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
capturedFormat = PixelFormat(e.Format)
bpp := capturedFormat.BytesPerPixel()
if int(e.Stride) < int(e.Width)*bpp {
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp)
return
}
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil { if err != nil {
log.Error("create screen buffer failed", "err", err) log.Error("create screen buffer failed", "err", err)
return return
} }
capturedBuf = buf if withCursor {
buf.Format = capturedFormat pc.screenBuf = buf
pc.format = e.Format
} else {
pc.screenBufNoCursor = buf
}
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size())) pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil { if err != nil {
@@ -426,47 +421,6 @@ func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture,
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) { frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
frame.Destroy() frame.Destroy()
if capturedBuf == nil {
onReady()
return
}
if capturedFormat.Is24Bit() {
converted, newFormat, err := capturedBuf.ConvertTo32Bit(capturedFormat)
if err != nil {
log.Error("convert 24-bit to 32-bit failed", "err", err)
} else if converted != capturedBuf {
capturedBuf.Close()
capturedBuf = converted
capturedFormat = newFormat
}
}
pc.format = uint32(capturedFormat)
if pc.yInverted {
capturedBuf.FlipVertical()
pc.yInverted = false
}
if output.transform != TransformNormal {
invTransform := InverseTransform(output.transform)
transformed, err := capturedBuf.ApplyTransform(invTransform)
if err != nil {
log.Error("apply transform failed", "err", err)
} else if transformed != capturedBuf {
capturedBuf.Close()
capturedBuf = transformed
}
}
if withCursor {
pc.screenBuf = capturedBuf
} else {
pc.screenBufNoCursor = capturedBuf
}
onReady() onReady()
}) })
+38 -162
View File
@@ -150,33 +150,51 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
case CompositorHyprland: case CompositorHyprland:
return s.captureAndCrop(output, region) return s.captureAndCrop(output, region)
case CompositorDWL: case CompositorDWL:
return s.captureDWLWindow(output, region, geom) return s.captureDWLWindow(output, region, geom.Scale)
default: default:
return s.captureRegionOnOutput(output, region) return s.captureRegionOnOutput(output, region)
} }
} }
func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) { func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, dwlScale float64) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output) result, err := s.captureWholeOutput(output)
if err != nil { if err != nil {
return nil, err return nil, err
} }
scale := geom.Scale scale := dwlScale
if scale <= 0 || scale == 1.0 { if scale <= 0 {
if output.fractionalScale > 1.0 { scale = float64(result.Buffer.Width) / float64(output.width)
scale = output.fractionalScale
}
} }
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
} }
localX := int(float64(region.X-geom.OutputX) * scale) localX := int(float64(region.X) * scale)
localY := int(float64(region.Y-geom.OutputY) * scale) localY := int(float64(region.Y) * scale)
if localX >= result.Buffer.Width {
localX = localX % result.Buffer.Width
}
if localY >= result.Buffer.Height {
localY = localY % result.Buffer.Height
}
w := int(float64(region.Width) * scale) w := int(float64(region.Width) * scale)
h := int(float64(region.Height) * scale) h := int(float64(region.Height) * scale)
if localY+h > result.Buffer.Height && h <= result.Buffer.Height {
localY = result.Buffer.Height - h
if localY < 0 {
localY = 0
}
}
if localX+w > result.Buffer.Width && w <= result.Buffer.Width {
localX = result.Buffer.Width - w
if localX < 0 {
localX = 0
}
}
if localX < 0 { if localX < 0 {
w += localX w += localX
localX = 0 localX = 0
@@ -324,18 +342,13 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
outX, outY := output.x, output.y outX, outY := output.x, output.y
scale := float64(output.scale) scale := float64(output.scale)
switch DetectCompositor() { if DetectCompositor() == CompositorHyprland {
case CompositorHyprland:
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok { if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
outX, outY = hx, hy outX, outY = hx, hy
} }
if s := GetHyprlandMonitorScale(output.name); s > 0 { if s := GetHyprlandMonitorScale(output.name); s > 0 {
scale = s scale = s
} }
case CompositorDWL:
if info, ok := getOutputInfo(output.name); ok {
outX, outY = info.x, info.y
}
} }
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
@@ -463,42 +476,13 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult
return nil, fmt.Errorf("capture output: %w", err) return nil, fmt.Errorf("capture output: %w", err)
} }
result, err := s.processFrame(frame, Region{ return s.processFrame(frame, Region{
X: output.x, X: output.x,
Y: output.y, Y: output.y,
Width: output.width, Width: output.width,
Height: output.height, Height: output.height,
Output: output.name, Output: output.name,
}) })
if err != nil {
return nil, err
}
if result.YInverted {
result.Buffer.FlipVertical()
result.YInverted = false
}
if output.transform == TransformNormal {
return result, nil
}
invTransform := InverseTransform(output.transform)
transformed, err := result.Buffer.ApplyTransform(invTransform)
if err != nil {
result.Buffer.Close()
return nil, fmt.Errorf("apply transform: %w", err)
}
if transformed != result.Buffer {
result.Buffer.Close()
result.Buffer = transformed
}
result.Region.Width = int32(transformed.Width)
result.Region.Height = int32(transformed.Height)
return result, nil
} }
func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) { func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) {
@@ -579,10 +563,6 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca
} }
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) { func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
if output.transform != TransformNormal {
return s.captureRegionOnTransformedOutput(output, region)
}
scale := output.fractionalScale scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland { if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name) scale = GetHyprlandMonitorScale(output.name)
@@ -637,76 +617,6 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
return s.processFrame(frame, region) return s.processFrame(frame, region)
} }
func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output)
if err != nil {
return nil, err
}
scale := output.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name)
}
if scale <= 0 {
scale = float64(output.scale)
}
if scale <= 0 {
scale = 1.0
}
localX := int(float64(region.X-output.x) * scale)
localY := int(float64(region.Y-output.y) * scale)
w := int(float64(region.Width) * scale)
h := int(float64(region.Height) * scale)
if localX < 0 {
w += localX
localX = 0
}
if localY < 0 {
h += localY
localY = 0
}
if localX+w > result.Buffer.Width {
w = result.Buffer.Width - localX
}
if localY+h > result.Buffer.Height {
h = result.Buffer.Height - localY
}
if w <= 0 || h <= 0 {
result.Buffer.Close()
return nil, fmt.Errorf("region not visible on output")
}
cropped, err := CreateShmBuffer(w, h, w*4)
if err != nil {
result.Buffer.Close()
return nil, fmt.Errorf("create crop buffer: %w", err)
}
srcData := result.Buffer.Data()
dstData := cropped.Data()
for y := 0; y < h; y++ {
srcOff := (localY+y)*result.Buffer.Stride + localX*4
dstOff := y * cropped.Stride
if srcOff+w*4 <= len(srcData) && dstOff+w*4 <= len(dstData) {
copy(dstData[dstOff:dstOff+w*4], srcData[srcOff:srcOff+w*4])
}
}
result.Buffer.Close()
cropped.Format = PixelFormat(result.Format)
return &CaptureResult{
Buffer: cropped,
Region: region,
YInverted: false,
Format: result.Format,
}, nil
}
func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) { func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
var buf *ShmBuffer var buf *ShmBuffer
var pool *client.ShmPool var pool *client.ShmPool
@@ -717,18 +627,13 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
failed := false failed := false
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) { frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
format = PixelFormat(e.Format)
bpp := format.BytesPerPixel()
if int(e.Stride) < int(e.Width)*bpp {
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp)
return
}
var err error var err error
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride)) buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil { if err != nil {
log.Error("failed to create buffer", "err", err) log.Error("failed to create buffer", "err", err)
return return
} }
format = PixelFormat(e.Format)
buf.Format = format buf.Format = format
}) })
@@ -791,19 +696,6 @@ func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1,
return nil, fmt.Errorf("frame capture failed") return nil, fmt.Errorf("frame capture failed")
} }
if format.Is24Bit() {
converted, newFormat, err := buf.ConvertTo32Bit(format)
if err != nil {
buf.Close()
return nil, fmt.Errorf("convert 24-bit to 32-bit: %w", err)
}
if converted != buf {
buf.Close()
buf = converted
}
format = newFormat
}
return &CaptureResult{ return &CaptureResult{
Buffer: buf, Buffer: buf,
Region: region, Region: region,
@@ -1032,32 +924,16 @@ func ListOutputs() ([]Output, error) {
sc.outputsMu.Lock() sc.outputsMu.Lock()
defer sc.outputsMu.Unlock() defer sc.outputsMu.Unlock()
compositor := DetectCompositor()
result := make([]Output, 0, len(sc.outputs)) result := make([]Output, 0, len(sc.outputs))
for _, o := range sc.outputs { for _, o := range sc.outputs {
out := Output{ result = append(result, Output{
Name: o.name, Name: o.name,
X: o.x, X: o.x,
Y: o.y, Y: o.y,
Width: o.width, Width: o.width,
Height: o.height, Height: o.height,
Scale: o.scale, Scale: o.scale,
FractionalScale: o.fractionalScale, })
Transform: o.transform,
}
switch compositor {
case CompositorHyprland:
if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok {
out.X, out.Y = hx, hy
out.Width, out.Height = hw, hh
}
if s := GetHyprlandMonitorScale(o.name); s > 0 {
out.FractionalScale = s
}
}
result = append(result, out)
} }
return result, nil return result, nil
} }
-17
View File
@@ -9,19 +9,6 @@ const (
FormatXRGB8888 = shm.FormatXRGB8888 FormatXRGB8888 = shm.FormatXRGB8888
FormatABGR8888 = shm.FormatABGR8888 FormatABGR8888 = shm.FormatABGR8888
FormatXBGR8888 = shm.FormatXBGR8888 FormatXBGR8888 = shm.FormatXBGR8888
FormatRGB888 = shm.FormatRGB888
FormatBGR888 = shm.FormatBGR888
)
const (
TransformNormal = shm.TransformNormal
Transform90 = shm.Transform90
Transform180 = shm.Transform180
Transform270 = shm.Transform270
TransformFlipped = shm.TransformFlipped
TransformFlipped90 = shm.TransformFlipped90
TransformFlipped180 = shm.TransformFlipped180
TransformFlipped270 = shm.TransformFlipped270
) )
type ShmBuffer = shm.Buffer type ShmBuffer = shm.Buffer
@@ -29,7 +16,3 @@ type ShmBuffer = shm.Buffer
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) { func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride) return shm.CreateBuffer(width, height, stride)
} }
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}
+5 -7
View File
@@ -32,13 +32,11 @@ func (r Region) IsEmpty() bool {
} }
type Output struct { type Output struct {
Name string Name string
X, Y int32 X, Y int32
Width int32 Width int32
Height int32 Height int32
Scale int32 Scale int32
FractionalScale float64
Transform int32
} }
type Config struct { type Config struct {
-9
View File
@@ -306,15 +306,6 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
return return
} }
if len(ledStates) == 0 {
log.Debug("No LED state available (empty map)")
// This means the device either:
// - doesn't support LED reporting at all, or
// - the kernel returned an empty state
return
}
capsLockState := ledStates[ledCapslockKey] capsLockState := ledStates[ledCapslockKey]
m.updateCapsLockStateDirect(capsLockState) m.updateCapsLockStateDirect(capsLockState)
} }
+38 -55
View File
@@ -562,62 +562,53 @@ func (m *Manager) transitionWorker() {
case <-m.stopChan: case <-m.stopChan:
return return
case targetTemp := <-m.transitionChan: case targetTemp := <-m.transitionChan:
m.runTransition(targetTemp, steps, stepDur) m.transitionMutex.Lock()
} currentTemp := m.currentTemp
} m.targetTemp = targetTemp
} m.transitionMutex.Unlock()
func (m *Manager) runTransition(targetTemp int, steps int, stepDur time.Duration) { if currentTemp == targetTemp {
for { continue
m.transitionMutex.Lock()
currentTemp := m.currentTemp
m.targetTemp = targetTemp
m.transitionMutex.Unlock()
if currentTemp == targetTemp {
return
}
log.Debugf("Starting smooth transition: %dK -> %dK over %v", currentTemp, targetTemp, stepDur*time.Duration(steps))
redirected := false
for i := 0; i <= steps; i++ {
select {
case newTarget := <-m.transitionChan:
m.transitionMutex.Lock()
m.targetTemp = newTarget
m.transitionMutex.Unlock()
if newTarget != targetTemp {
log.Debugf("Transition %dK -> %dK redirected to %dK", currentTemp, targetTemp, newTarget)
targetTemp = newTarget
redirected = true
}
default:
} }
if redirected { log.Debugf("Starting smooth transition: %dK -> %dK over %v", currentTemp, targetTemp, dur)
break
stepLoop:
for i := 0; i <= steps; i++ {
select {
case newTarget := <-m.transitionChan:
m.transitionMutex.Lock()
m.targetTemp = newTarget
m.transitionMutex.Unlock()
log.Debugf("Transition %dK -> %dK aborted (newer transition started)", currentTemp, targetTemp)
break stepLoop
default:
}
m.transitionMutex.RLock()
if m.targetTemp != targetTemp {
m.transitionMutex.RUnlock()
break
}
m.transitionMutex.RUnlock()
progress := float64(i) / float64(steps)
temp := currentTemp + int(float64(targetTemp-currentTemp)*progress)
m.post(func() { m.applyNowOnActor(temp) })
if i < steps {
time.Sleep(stepDur)
}
} }
m.transitionMutex.RLock() m.transitionMutex.RLock()
currentTarget := m.targetTemp finalTarget := m.targetTemp
m.transitionMutex.RUnlock() m.transitionMutex.RUnlock()
if currentTarget != targetTemp { if finalTarget == targetTemp {
targetTemp = currentTarget
break
}
progress := float64(i) / float64(steps)
temp := currentTemp + int(float64(targetTemp-currentTemp)*progress)
m.post(func() { m.applyNowOnActor(temp) })
if i == steps {
log.Debugf("Transition complete: now at %dK", targetTemp) log.Debugf("Transition complete: now at %dK", targetTemp)
return
} }
time.Sleep(stepDur)
} }
} }
} }
@@ -1215,18 +1206,11 @@ func (m *Manager) SetGamma(gamma float64) error {
func (m *Manager) SetEnabled(enabled bool) { func (m *Manager) SetEnabled(enabled bool) {
m.configMutex.Lock() m.configMutex.Lock()
wasEnabled := m.config.Enabled
m.config.Enabled = enabled m.config.Enabled = enabled
highTemp := m.config.HighTemp
m.configMutex.Unlock() m.configMutex.Unlock()
if enabled { if enabled {
if !m.controlsInitialized { if !m.controlsInitialized {
m.transitionMutex.Lock()
m.currentTemp = highTemp
m.targetTemp = highTemp
m.transitionMutex.Unlock()
m.post(func() { m.post(func() {
log.Info("Creating gamma controls") log.Info("Creating gamma controls")
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
@@ -1237,10 +1221,9 @@ func (m *Manager) SetEnabled(enabled bool) {
log.Errorf("Failed to create gamma controls: %v", err) log.Errorf("Failed to create gamma controls: %v", err)
} else { } else {
m.controlsInitialized = true m.controlsInitialized = true
m.triggerUpdate()
} }
}) })
} else if !wasEnabled { } else {
m.triggerUpdate() m.triggerUpdate()
} }
} else { } else {
+2 -74
View File
@@ -4,8 +4,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@@ -120,59 +118,6 @@ func (m Model) viewInstallingPackages() string {
return b.String() return b.String()
} }
func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
config, ok := distros.Registry[distroID]
if !ok {
return "dms"
}
var isGit bool
for _, dep := range dependencies {
if dep.Name == "dms (DankMaterialShell)" {
isGit = dep.Variant == deps.VariantGit
break
}
}
switch config.Family {
case distros.FamilyArch:
if isGit {
return "dms-shell-git"
}
return "dms-shell-bin"
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
if isGit {
return "dms-git"
}
return "dms"
default:
return "dms"
}
}
func uninstallCommand(distroID string, dependencies []deps.Dependency) string {
config, ok := distros.Registry[distroID]
if !ok {
return ""
}
if config.Family == distros.FamilyGentoo {
return "rm -rf ~/.config/quickshell/dms && sudo rm /usr/local/bin/dms"
}
pkg := dmsPackageName(distroID, dependencies)
switch config.Family {
case distros.FamilyArch:
return "sudo pacman -Rs " + pkg
case distros.FamilyFedora:
return "sudo dnf remove " + pkg
case distros.FamilyUbuntu, distros.FamilyDebian:
return "sudo apt remove " + pkg
case distros.FamilySUSE:
return "sudo zypper remove " + pkg
default:
return ""
}
}
func (m Model) viewInstallComplete() string { func (m Model) viewInstallComplete() string {
var b strings.Builder var b strings.Builder
@@ -187,6 +132,7 @@ func (m Model) viewInstallComplete() string {
b.WriteString(success) b.WriteString(success)
b.WriteString("\n\n") b.WriteString("\n\n")
// Show what was accomplished
accomplishments := []string{ accomplishments := []string{
"• Window manager and dependencies installed", "• Window manager and dependencies installed",
"• Terminal and development tools configured", "• Terminal and development tools configured",
@@ -200,26 +146,8 @@ func (m Model) viewInstallComplete() string {
} }
b.WriteString("\n") b.WriteString("\n")
info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\nIf you do not have a greeter, login with \"niri-session\" or \"Hyprland\"") info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\nIf you do not have a greeter, login with \"niri-session\" or \"Hyprland\" \n\nPress Enter to exit.")
b.WriteString(info) b.WriteString(info)
b.WriteString("\n\n")
theme := TerminalTheme()
cmdStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Accent))
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("systemctl --user disable dms") + "\n")
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("journalctl --user -u dms") + "\n")
if m.osInfo != nil {
if cmd := uninstallCommand(m.osInfo.Distribution.ID, m.dependencies); cmd != "" {
b.WriteString(labelStyle.Render(" Uninstall: ") + cmdStyle.Render(cmd) + "\n")
}
}
b.WriteString("\n")
b.WriteString(m.styles.Normal.Render("Press Enter to exit."))
if m.logFilePath != "" { if m.logFilePath != "" {
b.WriteString("\n\n") b.WriteString("\n\n")
+1 -1
View File
@@ -40,7 +40,7 @@ func (m Model) viewWelcome() string {
subtitle := lipgloss.NewStyle(). subtitle := lipgloss.NewStyle().
Foreground(lipgloss.Color(theme.Subtle)). Foreground(lipgloss.Color(theme.Subtle)).
Italic(true). Italic(true).
Render("Quickstart for a Dank Desktop") Render("Quickstart for a Dank Desktop")
b.WriteString(decorator) b.WriteString(decorator)
b.WriteString("\n") b.WriteString("\n")
+1 -160
View File
@@ -13,23 +13,8 @@ const (
FormatXRGB8888 PixelFormat = 1 FormatXRGB8888 PixelFormat = 1
FormatABGR8888 PixelFormat = 0x34324241 FormatABGR8888 PixelFormat = 0x34324241
FormatXBGR8888 PixelFormat = 0x34324258 FormatXBGR8888 PixelFormat = 0x34324258
FormatRGB888 PixelFormat = 0x34324752
FormatBGR888 PixelFormat = 0x34324742
) )
func (f PixelFormat) BytesPerPixel() int {
switch f {
case FormatRGB888, FormatBGR888:
return 3
default:
return 4
}
}
func (f PixelFormat) Is24Bit() bool {
return f == FormatRGB888 || f == FormatBGR888
}
type Buffer struct { type Buffer struct {
fd int fd int
data []byte data []byte
@@ -93,52 +78,6 @@ func (b *Buffer) Close() error {
return firstErr return firstErr
} }
func (b *Buffer) ConvertTo32Bit(srcFormat PixelFormat) (*Buffer, PixelFormat, error) {
if !srcFormat.Is24Bit() {
return b, srcFormat, nil
}
dstFormat := FormatXRGB8888
dstStride := b.Width * 4
dst, err := CreateBuffer(b.Width, b.Height, dstStride)
if err != nil {
return nil, srcFormat, err
}
dst.Format = dstFormat
srcData := b.data
dstData := dst.data
// DRM format names are counterintuitive on little-endian:
// RGB888 memory layout: B, G, R (name is logical order, not memory)
// BGR888 memory layout: R, G, B
isBGRMemory := srcFormat == FormatRGB888
for y := 0; y < b.Height; y++ {
srcRow := y * b.Stride
dstRow := y * dstStride
for x := 0; x < b.Width; x++ {
si := srcRow + x*3
di := dstRow + x*4
if isBGRMemory {
// RGB888: src memory is B,G,R -> dst XRGB8888 memory B,G,R,X
dstData[di+0] = srcData[si+0]
dstData[di+1] = srcData[si+1]
dstData[di+2] = srcData[si+2]
} else {
// BGR888: src memory is R,G,B -> dst XRGB8888 memory B,G,R,X
dstData[di+0] = srcData[si+2]
dstData[di+1] = srcData[si+1]
dstData[di+2] = srcData[si+0]
}
dstData[di+3] = 0xFF
}
}
return dst, dstFormat, nil
}
func (b *Buffer) GetPixelRGBA(x, y int) (r, g, b2, a uint8) { func (b *Buffer) GetPixelRGBA(x, y int) (r, g, b2, a uint8) {
if x < 0 || x >= b.Width || y < 0 || y >= b.Height { if x < 0 || x >= b.Width || y < 0 || y >= b.Height {
return return
@@ -149,12 +88,7 @@ func (b *Buffer) GetPixelRGBA(x, y int) (r, g, b2, a uint8) {
return return
} }
switch b.Format { return b.data[off+2], b.data[off+1], b.data[off], b.data[off+3]
case FormatXBGR8888, FormatABGR8888:
return b.data[off], b.data[off+1], b.data[off+2], 0xFF
default:
return b.data[off+2], b.data[off+1], b.data[off], 0xFF
}
} }
func (b *Buffer) GetPixelBGRA(x, y int) (b2, g, r, a uint8) { func (b *Buffer) GetPixelBGRA(x, y int) (b2, g, r, a uint8) {
@@ -203,96 +137,3 @@ func (b *Buffer) Clear() {
func (b *Buffer) CopyFrom(src *Buffer) { func (b *Buffer) CopyFrom(src *Buffer) {
copy(b.data, src.data) copy(b.data, src.data)
} }
const (
TransformNormal = 0
Transform90 = 1
Transform180 = 2
Transform270 = 3
TransformFlipped = 4
TransformFlipped90 = 5
TransformFlipped180 = 6
TransformFlipped270 = 7
)
func (b *Buffer) ApplyTransform(transform int32) (*Buffer, error) {
if transform == TransformNormal {
return b, nil
}
var newW, newH int
switch transform {
case Transform90, Transform270, TransformFlipped90, TransformFlipped270:
newW, newH = b.Height, b.Width
default:
newW, newH = b.Width, b.Height
}
newBuf, err := CreateBuffer(newW, newH, newW*4)
if err != nil {
return nil, err
}
newBuf.Format = b.Format
srcData := b.data
dstData := newBuf.data
for sy := 0; sy < b.Height; sy++ {
for sx := 0; sx < b.Width; sx++ {
var dx, dy int
switch transform {
case Transform90: // 90° CCW
dx = sy
dy = b.Width - 1 - sx
case Transform180:
dx = b.Width - 1 - sx
dy = b.Height - 1 - sy
case Transform270: // 270° CCW = 90° CW
dx = b.Height - 1 - sy
dy = sx
case TransformFlipped:
dx = b.Width - 1 - sx
dy = sy
case TransformFlipped90:
dx = sy
dy = sx
case TransformFlipped180:
dx = sx
dy = b.Height - 1 - sy
case TransformFlipped270:
dx = b.Height - 1 - sy
dy = b.Width - 1 - sx
default:
dx, dy = sx, sy
}
si := sy*b.Stride + sx*4
di := dy*newBuf.Stride + dx*4
if si+3 < len(srcData) && di+3 < len(dstData) {
dstData[di+0] = srcData[si+0]
dstData[di+1] = srcData[si+1]
dstData[di+2] = srcData[si+2]
dstData[di+3] = srcData[si+3]
}
}
}
return newBuf, nil
}
func InverseTransform(transform int32) int32 {
switch transform {
case Transform90:
return Transform270
case Transform270:
return Transform90
case TransformFlipped90:
return TransformFlipped270
case TransformFlipped270:
return TransformFlipped90
default:
return transform
}
}
+6 -16
View File
@@ -10,14 +10,13 @@
user = config.services.greetd.settings.default_session.user; user = config.services.greetd.settings.default_session.user;
cacheDir = "/var/lib/dms-greeter";
greeterScript = pkgs.writeShellScriptBin "dms-greeter" '' greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
export PATH=$PATH:${lib.makeBinPath [cfg.quickshell.package config.programs.${cfg.compositor.name}.package]} export PATH=$PATH:${lib.makeBinPath [cfg.quickshell.package config.programs.${cfg.compositor.name}.package]}
${lib.escapeShellArgs ([ ${lib.escapeShellArgs ([
"sh" "sh"
"${../../quickshell/Modules/Greetd/assets/dms-greeter}" "${../../quickshell/Modules/Greetd/assets/dms-greeter}"
"--cache-dir" "--cache-dir"
cacheDir "/var/lib/dmsgreeter"
"--command" "--command"
cfg.compositor.name cfg.compositor.name
"-p" "-p"
@@ -28,8 +27,6 @@
"${pkgs.writeText "dmsgreeter-compositor-config" cfg.compositor.customConfig}" "${pkgs.writeText "dmsgreeter-compositor-config" cfg.compositor.customConfig}"
])} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"} ])} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"}
''; '';
jq = lib.getExe pkgs.jq;
in { in {
imports = let imports = let
msg = "The option 'programs.dankMaterialShell.greeter.compositor.extraConfig' is deprecated. Please use 'programs.dankMaterialShell.greeter.compositor.customConfig' instead."; msg = "The option 'programs.dankMaterialShell.greeter.compositor.extraConfig' is deprecated. Please use 'programs.dankMaterialShell.greeter.compositor.customConfig' instead.";
@@ -96,17 +93,17 @@ in {
material-symbols material-symbols
]; ];
systemd.tmpfiles.settings."10-dmsgreeter" = { systemd.tmpfiles.settings."10-dmsgreeter" = {
${cacheDir}.d = { "/var/lib/dmsgreeter".d = {
user = user; user = user;
group = group =
if config.users.users.${user}.group != "" if config.users.users.${user}.group != ""
then config.users.users.${user}.group then config.users.users.${user}.group
else "greeter"; else "greeter";
mode = "0750"; mode = "0755";
}; };
}; };
systemd.services.greetd.preStart = '' systemd.services.greetd.preStart = ''
cd ${cacheDir} cd /var/lib/dmsgreeter
${lib.concatStringsSep "\n" (lib.map (f: '' ${lib.concatStringsSep "\n" (lib.map (f: ''
if [ -f "${f}" ]; then if [ -f "${f}" ]; then
cp "${f}" . cp "${f}" .
@@ -115,16 +112,9 @@ in {
cfg.configFiles)} cfg.configFiles)}
if [ -f session.json ]; then if [ -f session.json ]; then
if cp "$(${jq} -r '.wallpaperPath' session.json)" wallpaper.jpg; then if cp "$(${lib.getExe pkgs.jq} -r '.wallpaperPath' session.json)" wallpaper.jpg; then
mv session.json session.orig.json mv session.json session.orig.json
${jq} '.wallpaperPath = "${cacheDir}/wallpaper.jpg"' session.orig.json > session.json ${lib.getExe pkgs.jq} '.wallpaperPath = "/var/lib/dmsgreeter/wallpaper.jpg"' session.orig.json > session.json
fi
fi
if [ -f settings.json ]; then
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
mv settings.json settings.orig.json
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
fi fi
fi fi
-3
View File
@@ -99,9 +99,6 @@
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \ substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash --replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
installShellCompletion --cmd dms \ installShellCompletion --cmd dms \
--bash <($out/bin/dms completion bash) \ --bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish) \ --fish <($out/bin/dms completion fish) \
-1
View File
@@ -1 +0,0 @@
The Dark Knight
+1 -1
View File
@@ -53,7 +53,7 @@ Singleton {
if (appId === "home assistant desktop") if (appId === "home assistant desktop")
return "homeassistant-desktop"; return "homeassistant-desktop";
if (appId.includes("com.transmissionbt.transmission")) if (appId.includes("com.transmissionbt.transmission"))
return "transmission"; return "transmission-gtk";
return appId; return appId;
} }
+15 -7
View File
@@ -128,13 +128,21 @@ Singleton {
setDesiredTheme("image", rawWallpaperPath, isLight, iconTheme, selectedMatugenType); setDesiredTheme("image", rawWallpaperPath, isLight, iconTheme, selectedMatugenType);
} }
} }
} else if (currentTheme !== "custom") { } else {
const darkTheme = StockThemes.getThemeByName(currentTheme, false); let primaryColor;
const lightTheme = StockThemes.getThemeByName(currentTheme, true); let matugenType;
if (darkTheme && darkTheme.primary) { if (currentTheme === "custom") {
const stockColors = buildMatugenColorsFromTheme(darkTheme, lightTheme); if (customThemeData && customThemeData.primary) {
const themeData = isLight ? lightTheme : darkTheme; primaryColor = customThemeData.primary;
setDesiredTheme("hex", themeData.primary, isLight, iconTheme, themeData.matugen_type, stockColors); matugenType = customThemeData.matugen_type;
}
} else {
primaryColor = currentThemeData.primary;
matugenType = currentThemeData.matugen_type;
}
if (primaryColor) {
setDesiredTheme("hex", primaryColor, isLight, iconTheme, matugenType);
} }
} }
}, 0); }, 0);
+34 -69
View File
@@ -108,14 +108,13 @@ Item {
id: barRepeaterModel id: barRepeaterModel
values: { values: {
const configs = SettingsData.barConfigs; const configs = SettingsData.barConfigs;
return configs.map(c => ({ return configs
id: c.id, .map(c => ({ id: c.id, position: c.position }))
position: c.position .sort((a, b) => {
})).sort((a, b) => { const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right; const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right; return aVertical - bVertical;
return aVertical - bVertical; });
});
} }
} }
@@ -143,34 +142,9 @@ Item {
} }
} }
property bool dockEnabled: false
Timer {
id: dockRecreateDebounce
interval: 500
repeat: false
onTriggered: {
root.dockEnabled = false;
Qt.callLater(() => {
root.dockEnabled = true;
});
}
}
Component.onCompleted: {
dockRecreateDebounce.start();
}
Connections {
target: SettingsData
function onBarConfigsChanged() {
dockRecreateDebounce.restart();
}
}
Loader { Loader {
id: dockLoader id: dockLoader
active: root.dockEnabled active: true
asynchronous: false asynchronous: false
property var currentPosition: SettingsData.dockPosition property var currentPosition: SettingsData.dockPosition
@@ -466,71 +440,62 @@ Item {
title: I18n.tr("Open with...") title: I18n.tr("Open with...")
function shellEscape(str) { function shellEscape(str) {
return "'" + str.replace(/'/g, "'\\''") + "'"; return "'" + str.replace(/'/g, "'\\''") + "'"
} }
onApplicationSelected: (app, filePath) => { onApplicationSelected: (app, filePath) => {
if (!app) if (!app) return
return;
let cmd = app.exec || "";
const escapedPath = shellEscape(filePath);
const escapedUri = shellEscape("file://" + filePath);
let hasField = false; let cmd = app.exec || ""
if (cmd.includes("%f")) { const escapedPath = shellEscape(filePath)
cmd = cmd.replace("%f", escapedPath); const escapedUri = shellEscape("file://" + filePath)
hasField = true;
} else if (cmd.includes("%F")) {
cmd = cmd.replace("%F", escapedPath);
hasField = true;
} else if (cmd.includes("%u")) {
cmd = cmd.replace("%u", escapedUri);
hasField = true;
} else if (cmd.includes("%U")) {
cmd = cmd.replace("%U", escapedUri);
hasField = true;
}
cmd = cmd.replace(/%[ikc]/g, ""); let hasField = false
if (cmd.includes("%f")) { cmd = cmd.replace("%f", escapedPath); hasField = true }
else if (cmd.includes("%F")) { cmd = cmd.replace("%F", escapedPath); hasField = true }
else if (cmd.includes("%u")) { cmd = cmd.replace("%u", escapedUri); hasField = true }
else if (cmd.includes("%U")) { cmd = cmd.replace("%U", escapedUri); hasField = true }
cmd = cmd.replace(/%[ikc]/g, "")
if (!hasField) { if (!hasField) {
cmd += " " + escapedPath; cmd += " " + escapedPath
} }
console.log("FilePicker: Launching", cmd); console.log("FilePicker: Launching", cmd)
Quickshell.execDetached({ Quickshell.execDetached({
command: ["sh", "-c", cmd] command: ["sh", "-c", cmd]
}); })
} }
} }
Connections { Connections {
target: DMSService target: DMSService
function onOpenUrlRequested(url) { function onOpenUrlRequested(url) {
browserPickerModal.url = url; browserPickerModal.url = url
browserPickerModal.open(); browserPickerModal.open()
} }
function onAppPickerRequested(data) { function onAppPickerRequested(data) {
console.log("DMSShell: App picker requested with data:", JSON.stringify(data)); console.log("DMSShell: App picker requested with data:", JSON.stringify(data))
if (!data || !data.target) { if (!data || !data.target) {
console.warn("DMSShell: Invalid app picker request data"); console.warn("DMSShell: Invalid app picker request data")
return; return
} }
filePickerModal.targetData = data.target; filePickerModal.targetData = data.target
filePickerModal.targetDataLabel = data.requestType || "file"; filePickerModal.targetDataLabel = data.requestType || "file"
if (data.categories && data.categories.length > 0) { if (data.categories && data.categories.length > 0) {
filePickerModal.categoryFilter = data.categories; filePickerModal.categoryFilter = data.categories
} else { } else {
filePickerModal.categoryFilter = []; filePickerModal.categoryFilter = []
} }
filePickerModal.usageHistoryKey = "filePickerUsageHistory"; filePickerModal.usageHistoryKey = "filePickerUsageHistory"
filePickerModal.open(); filePickerModal.open()
} }
} }
+14 -8
View File
@@ -332,16 +332,18 @@ Item {
if (!provider) if (!provider)
return "ERROR: No provider specified"; return "ERROR: No provider specified";
KeybindsService.loadCheatsheet(provider); KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true; root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item) if (!root.hyprKeybindsModalLoader.item)
return `KEYBINDS_TOGGLE_FAILED: ${provider}`; return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close(); root.hyprKeybindsModalLoader.item.close();
else } else {
root.hyprKeybindsModalLoader.item.open(); root.hyprKeybindsModalLoader.item.open();
}
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`; return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`;
} }
@@ -349,16 +351,18 @@ Item {
if (!provider) if (!provider)
return "ERROR: No provider specified"; return "ERROR: No provider specified";
KeybindsService.loadCheatsheet(provider); KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true; root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item) if (!root.hyprKeybindsModalLoader.item)
return `KEYBINDS_TOGGLE_FAILED: ${provider}`; return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close(); root.hyprKeybindsModalLoader.item.close();
else } else {
root.hyprKeybindsModalLoader.item.open(); root.hyprKeybindsModalLoader.item.open();
}
return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})`; return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})`;
} }
@@ -366,7 +370,8 @@ Item {
if (!provider) if (!provider)
return "ERROR: No provider specified"; return "ERROR: No provider specified";
KeybindsService.loadCheatsheet(provider); KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true; root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item) if (!root.hyprKeybindsModalLoader.item)
@@ -380,7 +385,8 @@ Item {
if (!provider) if (!provider)
return "ERROR: No provider specified"; return "ERROR: No provider specified";
KeybindsService.loadCheatsheet(provider); KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true; root.hyprKeybindsModalLoader.active = true;
if (!root.hyprKeybindsModalLoader.item) if (!root.hyprKeybindsModalLoader.item)
@@ -46,7 +46,6 @@ FocusScope {
property bool pathInputHasFocus: false property bool pathInputHasFocus: false
property int actualGridColumns: 5 property int actualGridColumns: 5
property bool _initialized: false property bool _initialized: false
property bool closeOnEscape: true
signal fileSelected(string path) signal fileSelected(string path)
signal closeRequested signal closeRequested
@@ -299,7 +298,7 @@ FocusScope {
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns) property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
function handleKey(event) { function handleKey(event) {
if (event.key === Qt.Key_Escape && root.closeOnEscape) { if (event.key === Qt.Key_Escape) {
closeRequested(); closeRequested();
event.accepted = true; event.accepted = true;
return; return;
@@ -35,7 +35,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400) minimumSize: Qt.size(500, 400)
implicitWidth: 800 implicitWidth: 800
implicitHeight: 600 implicitHeight: 600
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -59,7 +59,6 @@ FloatingWindow {
id: content id: content
anchors.fill: parent anchors.fill: parent
focus: true focus: true
closeOnEscape: false
browserTitle: fileBrowserModal.browserTitle browserTitle: fileBrowserModal.browserTitle
browserIcon: fileBrowserModal.browserIcon browserIcon: fileBrowserModal.browserIcon
+18 -46
View File
@@ -17,11 +17,7 @@ DankModal {
modalWidth: _maxW modalWidth: _maxW
modalHeight: _maxH modalHeight: _maxH
onBackgroundClicked: close() onBackgroundClicked: close()
onOpened: { onOpened: () => Qt.callLater(() => modalFocusScope.forceActiveFocus())
Qt.callLater(() => modalFocusScope.forceActiveFocus());
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
KeybindsService.loadCheatsheet();
}
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [root.contentWindow] windows: [root.contentWindow]
@@ -70,7 +66,7 @@ DankModal {
spacing: Theme.spacingL spacing: Theme.spacingL
StyledText { StyledText {
text: KeybindsService.cheatsheet.title || "Keybinds" text: KeybindsService.keybinds.title || "Keybinds"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold font.weight: Font.Bold
color: Theme.primary color: Theme.primary
@@ -86,7 +82,7 @@ DankModal {
Component.onCompleted: root.activeFlickable = mainFlickable Component.onCompleted: root.activeFlickable = mainFlickable
property var rawBinds: KeybindsService.cheatsheet.binds || {} property var rawBinds: KeybindsService.keybinds.binds || {}
property var categories: { property var categories: {
const processed = {}; const processed = {};
for (const cat in rawBinds) { for (const cat in rawBinds) {
@@ -118,36 +114,12 @@ DankModal {
} }
property var categoryKeys: Object.keys(categories) property var categoryKeys: Object.keys(categories)
function estimateCategoryHeight(catName) {
const catData = categories[catName];
if (!catData)
return 0;
let bindCount = 0;
for (const key of catData.subcatKeys) {
bindCount += catData.subcats[key]?.length || 0;
if (key !== "_root")
bindCount += 1;
}
return 40 + bindCount * 28;
}
function distributeCategories(cols) { function distributeCategories(cols) {
const columns = []; const columns = [];
const heights = []; for (let i = 0; i < cols; i++)
for (let i = 0; i < cols; i++) {
columns.push([]); columns.push([]);
heights.push(0); for (let i = 0; i < categoryKeys.length; i++)
} columns[i % cols].push(categoryKeys[i]);
const sorted = [...categoryKeys].sort((a, b) => estimateCategoryHeight(b) - estimateCategoryHeight(a));
for (const cat of sorted) {
let minIdx = 0;
for (let i = 1; i < cols; i++) {
if (heights[i] < heights[minIdx])
minIdx = i;
}
columns[minIdx].push(cat);
heights[minIdx] += estimateCategoryHeight(cat);
}
return columns; return columns;
} }
@@ -165,7 +137,7 @@ DankModal {
Column { Column {
id: masonryColumn id: masonryColumn
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
spacing: Theme.spacingXL spacing: Theme.spacingM
Repeater { Repeater {
model: rowLayout.columnCategories[index] || [] model: rowLayout.columnCategories[index] || []
@@ -227,37 +199,37 @@ DankModal {
Repeater { Repeater {
model: parent.parent.subcatBinds model: parent.parent.subcatBinds
Item { Row {
width: parent.width width: parent.width
height: 24 spacing: Theme.spacingS
StyledRect { StyledRect {
id: keyBadge width: Math.min(140, parent.width * 0.42)
width: Math.min(keyText.implicitWidth + 12, 160)
height: 22 height: 22
radius: 4 radius: 4
anchors.verticalCenter: parent.verticalCenter opacity: 0.9
StyledText { StyledText {
id: keyText
anchors.centerIn: parent anchors.centerIn: parent
anchors.margins: 2
width: parent.width - 4
color: Theme.secondary color: Theme.secondary
text: modelData.key || "" text: modelData.key || ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
isMonospace: true isMonospace: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
} }
} }
StyledText { StyledText {
anchors.left: parent.left width: parent.width - 150
anchors.leftMargin: 170 text: modelData.desc || ""
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: modelData.desc || modelData.action || ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
opacity: 0.9 opacity: 0.9
elide: Text.ElideRight elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
} }
} }
} }
-1
View File
@@ -59,7 +59,6 @@ DankModal {
modalWidth: 500 modalWidth: 500
modalHeight: 700 modalHeight: 700
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onBackgroundClicked: hide() onBackgroundClicked: hide()
onOpened: () => { onOpened: () => {
+1 -1
View File
@@ -45,7 +45,7 @@ FloatingWindow {
title: I18n.tr("Authentication") title: I18n.tr("Authentication")
minimumSize: Qt.size(420, calculatedHeight) minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight) maximumSize: Qt.size(420, calculatedHeight)
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
+7 -1
View File
@@ -66,7 +66,7 @@ FloatingWindow {
minimumSize: Qt.size(650, 400) minimumSize: Qt.size(650, 400)
implicitWidth: 900 implicitWidth: 900
implicitHeight: 680 implicitHeight: 680
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -112,6 +112,12 @@ FloatingWindow {
focus: true focus: true
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
}
switch (event.key) { switch (event.key) {
case Qt.Key_1: case Qt.Key_1:
currentTab = 0; currentTab = 0;
+6 -1
View File
@@ -60,7 +60,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400) minimumSize: Qt.size(500, 400)
implicitWidth: 800 implicitWidth: 800
implicitHeight: 940 implicitHeight: 940
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onIsCompactModeChanged: { onIsCompactModeChanged: {
@@ -139,6 +139,11 @@ FloatingWindow {
focus: true focus: true
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Down || (event.key === Qt.Key_Tab && !event.modifiers)) { if (event.key === Qt.Key_Down || (event.key === Qt.Key_Tab && !event.modifiers)) {
sidebar.navigateNext(); sidebar.navigateNext();
event.accepted = true; event.accepted = true;
+1 -1
View File
@@ -191,7 +191,7 @@ FloatingWindow {
title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password") title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password")
minimumSize: Qt.size(420, calculatedHeight) minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight) maximumSize: Qt.size(420, calculatedHeight)
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -97,7 +97,7 @@ DankPopout {
property alias searchField: searchField property alias searchField: searchField
color: "transparent" color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
antialiasing: true antialiasing: true
smooth: true smooth: true
@@ -113,7 +113,11 @@ DankPopout {
implicitHeight: mainColumn.implicitHeight + Theme.spacingM implicitHeight: mainColumn.implicitHeight + Theme.spacingM
property alias bluetoothCodecSelector: bluetoothCodecSelector property alias bluetoothCodecSelector: bluetoothCodecSelector
color: "transparent" color: {
const transparency = Theme.popupTransparency;
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1);
return Qt.rgba(surface.r, surface.g, surface.b, transparency);
}
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0 border.width: 0
@@ -1,6 +1,9 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Services.UPower import Quickshell.Services.UPower
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -23,12 +26,13 @@ DankPopout {
function setProfile(profile) { function setProfile(profile) {
if (typeof PowerProfiles === "undefined") { if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available"); ToastService.showError("power-profiles-daemon not available");
return; return ;
} }
PowerProfiles.profile = profile; PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile) { if (PowerProfiles.profile !== profile) {
ToastService.showError("Failed to set power profile"); ToastService.showError("Failed to set power profile");
} }
} }
popupWidth: 400 popupWidth: 400
@@ -44,7 +48,7 @@ DankPopout {
id: batteryContent id: batteryContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: "transparent" color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Theme.outlineMedium border.color: Theme.outlineMedium
border.width: 0 border.width: 0
@@ -55,8 +59,9 @@ DankPopout {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
forceActiveFocus(); forceActiveFocus();
} }
} }
Keys.onPressed: function (event) { Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.close(); root.close();
event.accepted = true; event.accepted = true;
@@ -66,10 +71,11 @@ DankPopout {
Connections { Connections {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(function () { Qt.callLater(function() {
batteryContent.forceActiveFocus(); batteryContent.forceActiveFocus();
}); });
} }
} }
target: root target: root
@@ -240,8 +246,7 @@ DankPopout {
StyledText { StyledText {
text: { text: {
if (!BatteryService.batteryAvailable) if (!BatteryService.batteryAvailable) return "Power profile management available"
return "Power profile management available";
const time = BatteryService.formatTimeRemaining(); const time = BatteryService.formatTimeRemaining();
if (time !== "Unknown") { if (time !== "Unknown") {
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`; return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
@@ -465,14 +470,14 @@ DankPopout {
StyledText { StyledText {
text: { text: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0) if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return "N/A"; return "N/A"
return `${Math.round(modelData.healthPercentage)}%`; return `${Math.round(modelData.healthPercentage)}%`
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: { color: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0) if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return Theme.surfaceText; return Theme.surfaceText
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText; return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText
} }
font.weight: Font.Bold font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -521,7 +526,10 @@ DankPopout {
spacing: 2 spacing: 2
StyledText { StyledText {
text: modelData.state === UPowerDeviceState.Charging ? I18n.tr("To Full") : modelData.state === UPowerDeviceState.Discharging ? I18n.tr("Left") : "" text: modelData.state === UPowerDeviceState.Charging
? I18n.tr("To Full")
: modelData.state === UPowerDeviceState.Discharging
? I18n.tr("Left") : ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
font.weight: Font.Medium font.weight: Font.Medium
@@ -530,14 +538,17 @@ DankPopout {
StyledText { StyledText {
text: { text: {
const time = modelData.state === UPowerDeviceState.Charging ? modelData.timeToFull : modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0 ? (3600 * modelData.energy) / BatteryService.changeRate : 0; const time = modelData.state === UPowerDeviceState.Charging
? modelData.timeToFull
: modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0
? (3600 * modelData.energy) / BatteryService.changeRate : 0
if (!time || time <= 0 || time > 86400) if (!time || time <= 0 || time > 86400)
return "N/A"; return "N/A"
const hours = Math.floor(time / 3600); const hours = Math.floor(time / 3600)
const minutes = Math.floor((time % 3600) / 60); const minutes = Math.floor((time % 3600) / 60)
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
@@ -555,9 +566,8 @@ DankPopout {
DankButtonGroup { DankButtonGroup {
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: { property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined") if (typeof PowerProfiles === "undefined") return 1
return 1; return profileModel.findIndex(profile => root.isActiveProfile(profile))
return profileModel.findIndex(profile => root.isActiveProfile(profile));
} }
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile)) model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
@@ -565,9 +575,8 @@ DankPopout {
selectionMode: "single" selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => { onSelectionChanged: (index, selected) => {
if (!selected) if (!selected) return
return; root.setProfile(profileModel[index])
root.setProfile(profileModel[index]);
} }
} }
@@ -625,4 +634,5 @@ DankPopout {
} }
} }
} }
} }
@@ -1,4 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -11,31 +15,31 @@ DankPopout {
property var triggerScreen: null property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) { function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
triggerX = x; triggerX = x
triggerY = y; triggerY = y
triggerWidth = width; triggerWidth = width
triggerSection = section; triggerSection = section
root.screen = screen; root.screen = screen
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4); storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4)
storedBarSpacing = barSpacing !== undefined ? barSpacing : 4; storedBarSpacing = barSpacing !== undefined ? barSpacing : 4
storedBarConfig = barConfig; storedBarConfig = barConfig
const pos = barPosition !== undefined ? barPosition : 0; const pos = barPosition !== undefined ? barPosition : 0
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0; const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0
setBarContext(pos, bottomGap); setBarContext(pos, bottomGap)
updateOutputState(); updateOutputState()
} }
onScreenChanged: updateOutputState() onScreenChanged: updateOutputState()
function updateOutputState() { function updateOutputState() {
if (screen && DwlService.dwlAvailable) { if (screen && DwlService.dwlAvailable) {
outputState = DwlService.getOutputState(screen.name); outputState = DwlService.getOutputState(screen.name)
} else { } else {
outputState = null; outputState = null
} }
} }
@@ -43,56 +47,56 @@ DankPopout {
property string currentLayoutSymbol: outputState?.layoutSymbol || "" property string currentLayoutSymbol: outputState?.layoutSymbol || ""
readonly property var layoutNames: ({ readonly property var layoutNames: ({
"CT": I18n.tr("Center Tiling"), "CT": I18n.tr("Center Tiling"),
"G": I18n.tr("Grid"), "G": I18n.tr("Grid"),
"K": I18n.tr("Deck"), "K": I18n.tr("Deck"),
"M": I18n.tr("Monocle"), "M": I18n.tr("Monocle"),
"RT": I18n.tr("Right Tiling"), "RT": I18n.tr("Right Tiling"),
"S": I18n.tr("Scrolling"), "S": I18n.tr("Scrolling"),
"T": I18n.tr("Tiling"), "T": I18n.tr("Tiling"),
"VG": I18n.tr("Vertical Grid"), "VG": I18n.tr("Vertical Grid"),
"VK": I18n.tr("Vertical Deck"), "VK": I18n.tr("Vertical Deck"),
"VS": I18n.tr("Vertical Scrolling"), "VS": I18n.tr("Vertical Scrolling"),
"VT": I18n.tr("Vertical Tiling") "VT": I18n.tr("Vertical Tiling")
}) })
readonly property var layoutIcons: ({ readonly property var layoutIcons: ({
"CT": "view_compact", "CT": "view_compact",
"G": "grid_view", "G": "grid_view",
"K": "layers", "K": "layers",
"M": "fullscreen", "M": "fullscreen",
"RT": "view_sidebar", "RT": "view_sidebar",
"S": "view_carousel", "S": "view_carousel",
"T": "view_quilt", "T": "view_quilt",
"VG": "grid_on", "VG": "grid_on",
"VK": "view_day", "VK": "view_day",
"VS": "scrollable_header", "VS": "scrollable_header",
"VT": "clarify" "VT": "clarify"
}) })
function getLayoutName(symbol) { function getLayoutName(symbol) {
return layoutNames[symbol] || symbol; return layoutNames[symbol] || symbol
} }
function getLayoutIcon(symbol) { function getLayoutIcon(symbol) {
return layoutIcons[symbol] || "view_quilt"; return layoutIcons[symbol] || "view_quilt"
} }
Connections { Connections {
target: DwlService target: DwlService
function onStateChanged() { function onStateChanged() {
updateOutputState(); updateOutputState()
} }
} }
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (shouldBeVisible) { if (shouldBeVisible) {
updateOutputState(); updateOutputState()
} }
} }
Component.onCompleted: { Component.onCompleted: {
updateOutputState(); updateOutputState()
} }
popupWidth: 300 popupWidth: 300
@@ -107,7 +111,7 @@ DankPopout {
id: layoutContent id: layoutContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: "transparent" color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Theme.outlineMedium border.color: Theme.outlineMedium
border.width: 0 border.width: 0
@@ -117,14 +121,14 @@ DankPopout {
Component.onCompleted: { Component.onCompleted: {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
forceActiveFocus(); forceActiveFocus()
} }
} }
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.close(); root.close()
event.accepted = true; event.accepted = true
} }
} }
@@ -133,8 +137,8 @@ DankPopout {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(() => { Qt.callLater(() => {
layoutContent.forceActiveFocus(); layoutContent.forceActiveFocus()
}); })
} }
} }
} }
@@ -208,7 +212,7 @@ DankPopout {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: { onPressed: {
root.close(); root.close()
} }
} }
} }
@@ -278,14 +282,14 @@ DankPopout {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: { onPressed: {
if (!root.triggerScreen) { if (!root.triggerScreen) {
return; return
} }
if (!DwlService.dwlAvailable) { if (!DwlService.dwlAvailable) {
return; return
} }
DwlService.setLayout(root.triggerScreen.name, index); DwlService.setLayout(root.triggerScreen.name, index)
root.close(); root.close()
} }
} }
@@ -36,7 +36,7 @@ DankPopout {
id: content id: content
implicitHeight: contentColumn.height + Theme.spacingL * 2 implicitHeight: contentColumn.height + Theme.spacingL * 2
color: "transparent" color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Theme.outlineMedium border.color: Theme.outlineMedium
border.width: 0 border.width: 0
@@ -167,7 +167,7 @@ DankPopout {
id: mainContainer id: mainContainer
implicitHeight: contentColumn.height + Theme.spacingM * 2 implicitHeight: contentColumn.height + Theme.spacingM * 2
color: "transparent" color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
focus: true focus: true
@@ -358,8 +358,6 @@ DankPopout {
popoutWidth: root.alignedWidth popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight popoutHeight: root.alignedHeight
contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS
section: root.triggerSection
barPosition: root.effectiveBarPosition
Component.onCompleted: root.__mediaTabRef = this Component.onCompleted: root.__mediaTabRef = this
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => { onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
root.__showVolumeDropdown(pos, rightEdge, player, players); root.__showVolumeDropdown(pos, rightEdge, player, players);
@@ -384,7 +382,7 @@ DankPopout {
active: true active: true
tabBarItem: tabBar tabBarItem: tabBar
keyForwardTarget: mainContainer keyForwardTarget: mainContainer
targetScreen: root.screen targetScreen: root.triggerScreen
parentPopout: root parentPopout: root
} }
} }
@@ -3,6 +3,7 @@ import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Io import Quickshell.Io
import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -18,8 +19,6 @@ Item {
property real popoutWidth: 0 property real popoutWidth: 0
property real popoutHeight: 0 property real popoutHeight: 0
property real contentOffsetY: 0 property real contentOffsetY: 0
property string section: ""
property int barPosition: SettingsData.Position.Top
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players) signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge) signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
@@ -41,13 +40,7 @@ Item {
id: sharedTooltip id: sharedTooltip
} }
readonly property bool isRightEdge: { readonly property bool isRightEdge: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Right
if (barPosition === SettingsData.Position.Right)
return true;
if (barPosition === SettingsData.Position.Left)
return false;
return section === "right";
}
readonly property bool __isChromeBrowser: { readonly property bool __isChromeBrowser: {
if (!activePlayer?.identity) if (!activePlayer?.identity)
return false; return false;
+18 -39
View File
@@ -4,7 +4,6 @@ import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Greetd import Quickshell.Services.Greetd
import Quickshell.Services.Pam import Quickshell.Services.Pam
@@ -36,40 +35,19 @@ Item {
randomFact = Facts.getRandomFact() randomFact = Facts.getRandomFact()
} }
property bool weatherInitialized: false
function initWeatherService() {
if (weatherInitialized)
return
if (!GreetdSettings.settingsLoaded)
return
if (!GreetdSettings.weatherEnabled)
return
weatherInitialized = true
WeatherService.addRef()
WeatherService.forceRefresh()
}
Connections {
target: GreetdSettings
function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded)
initWeatherService()
}
}
Component.onCompleted: { Component.onCompleted: {
pickRandomFact() pickRandomFact()
initWeatherService() WeatherService.addRef()
if (isPrimaryScreen) { if (isPrimaryScreen) {
sessionListProc.running = true sessionListProc.running = true
applyLastSuccessfulUser() applyLastSuccessfulUser()
} }
if (CompositorService.isHyprland) if (CompositorService.isHyprland) {
updateHyprlandLayout() updateHyprlandLayout()
hyprlandLayoutUpdateTimer.start()
}
} }
function applyLastSuccessfulUser() { function applyLastSuccessfulUser() {
@@ -83,8 +61,10 @@ Item {
} }
Component.onDestruction: { Component.onDestruction: {
if (weatherInitialized) WeatherService.removeRef()
WeatherService.removeRef() if (CompositorService.isHyprland) {
hyprlandLayoutUpdateTimer.stop()
}
} }
function updateHyprlandLayout() { function updateHyprlandLayout() {
@@ -126,16 +106,15 @@ Item {
} }
} }
Connections { Timer {
target: CompositorService.isHyprland ? Hyprland : null id: hyprlandLayoutUpdateTimer
enabled: CompositorService.isHyprland interval: 1000
running: false
function onRawEvent(event) { repeat: true
if (event.name === "activelayout") onTriggered: updateHyprlandLayout()
updateHyprlandLayout()
}
} }
Connections { Connections {
target: GreetdMemory target: GreetdMemory
enabled: isPrimaryScreen enabled: isPrimaryScreen
@@ -771,13 +750,13 @@ Item {
visible: { visible: {
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) || const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) ||
(CompositorService.isHyprland && hyprlandLayoutCount > 1) (CompositorService.isHyprland && hyprlandLayoutCount > 1)
return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available return keyboardVisible && WeatherService.weather.available
} }
} }
Row { Row {
spacing: 6 spacing: 6
visible: GreetdSettings.weatherEnabled && WeatherService.weather.available visible: WeatherService.weather.available
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
DankIcon { DankIcon {
@@ -801,7 +780,7 @@ Item {
height: 24 height: 24
color: Qt.rgba(255, 255, 255, 0.2) color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: GreetdSettings.weatherEnabled && WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable) visible: WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable)
} }
Row { Row {
@@ -1110,7 +1110,7 @@ Item {
DankIcon { DankIcon {
name: "vpn_lock" name: "vpn_lock"
size: Theme.iconSize - 2 size: Theme.iconSize - 2
color: "white" color: NetworkService.vpnConnected ? Theme.primary : Qt.rgba(255, 255, 255, 0.5)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.vpnAvailable && NetworkService.vpnConnected visible: NetworkService.vpnAvailable && NetworkService.vpnConnected
} }
@@ -124,7 +124,7 @@ DankPopout {
return Math.max(300, Math.min(baseHeight, maxHeight)); return Math.max(300, Math.min(baseHeight, maxHeight));
} }
color: "transparent" color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0 border.width: 0
@@ -138,7 +138,7 @@ DankPopout {
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
notificationHistoryVisible = false; root.close();
event.accepted = true; event.accepted = true;
} else if (externalKeyboardController) { } else if (externalKeyboardController) {
externalKeyboardController.handleKey(event); externalKeyboardController.handleKey(event);
+11 -10
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -24,7 +25,7 @@ DankPopout {
id: popoutContainer id: popoutContainer
implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2
color: "transparent" color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 0 border.width: 0
antialiasing: true antialiasing: true
@@ -33,14 +34,14 @@ DankPopout {
Component.onCompleted: { Component.onCompleted: {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
forceActiveFocus(); forceActiveFocus()
} }
} }
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.close(); root.close()
event.accepted = true; event.accepted = true
} }
} }
@@ -49,8 +50,8 @@ DankPopout {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(() => { Qt.callLater(() => {
popoutContainer.forceActiveFocus(); popoutContainer.forceActiveFocus()
}); })
} }
} }
} }
@@ -69,12 +70,12 @@ DankPopout {
onLoaded: { onLoaded: {
if (item && "closePopout" in item) { if (item && "closePopout" in item) {
item.closePopout = function () { item.closePopout = function() {
root.close(); root.close()
}; }
} }
if (item) { if (item) {
root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2); root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2)
} }
} }
} }
@@ -1,5 +1,11 @@
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modules.ProcessList import qs.Modules.ProcessList
import qs.Services import qs.Services
@@ -51,7 +57,7 @@ DankPopout {
id: processListContent id: processListContent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: "transparent" color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0 border.width: 0
clip: true clip: true
@@ -64,7 +70,7 @@ DankPopout {
} }
processContextMenu.parent = processListContent; processContextMenu.parent = processListContent;
} }
Keys.onPressed: event => { Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
processListPopout.close(); processListPopout.close();
event.accepted = true; event.accepted = true;
@@ -102,6 +108,7 @@ DankPopout {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2 width: parent.width - Theme.spacingM * 2
} }
} }
Rectangle { Rectangle {
@@ -117,8 +124,13 @@ DankPopout {
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
contextMenu: processContextMenu contextMenu: processContextMenu
} }
} }
} }
} }
} }
} }
+1 -10
View File
@@ -3,6 +3,7 @@ import QtQuick.Effects
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import qs.Modules.Settings.Widgets
Item { Item {
id: aboutTab id: aboutTab
@@ -199,16 +200,6 @@ Item {
width: parent.width width: parent.width
} }
StyledText {
visible: SystemUpdateService.shellCodename.length > 0
text: `"${SystemUpdateService.shellCodename}"`
font.pixelSize: Theme.fontSizeMedium
font.italic: true
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
Row { Row {
id: resourceButtonsRow id: resourceButtonsRow
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -106,7 +106,7 @@ FloatingWindow {
minimumSize: Qt.size(450, 400) minimumSize: Qt.size(450, 400)
implicitWidth: 600 implicitWidth: 600
implicitHeight: 650 implicitHeight: 650
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -537,7 +537,7 @@ FloatingWindow {
title: I18n.tr("Third-Party Plugin Warning") title: I18n.tr("Third-Party Plugin Warning")
implicitWidth: 500 implicitWidth: 500
implicitHeight: 350 implicitHeight: 350
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
FocusScope { FocusScope {
@@ -93,7 +93,7 @@ FloatingWindow {
minimumSize: Qt.size(400, 350) minimumSize: Qt.size(400, 350)
implicitWidth: 500 implicitWidth: 500
implicitHeight: 550 implicitHeight: 550
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
+1 -1
View File
@@ -36,7 +36,7 @@ DankPopout {
Rectangle { Rectangle {
id: updaterPanel id: updaterPanel
color: "transparent" color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
antialiasing: true antialiasing: true
smooth: true smooth: true
+17 -20
View File
@@ -82,7 +82,6 @@ Variants {
readonly property bool transitioning: transitionAnimation.running readonly property bool transitioning: transitionAnimation.running
property bool effectActive: false property bool effectActive: false
property bool useNextForEffect: false property bool useNextForEffect: false
property string pendingWallpaper: ""
function getFillMode(modeName) { function getFillMode(modeName) {
switch (modeName) { switch (modeName) {
@@ -163,10 +162,12 @@ Variants {
return; return;
if (!newPath || newPath.startsWith("#")) if (!newPath || newPath.startsWith("#"))
return; return;
if (root.transitioning) {
if (root.transitioning || root.effectActive) { transitionAnimation.stop();
root.pendingWallpaper = newPath; root.transitionProgress = 0;
return; root.effectActive = false;
currentWallpaper.source = nextWallpaper.source;
nextWallpaper.source = "";
} }
if (!currentWallpaper.source) { if (!currentWallpaper.source) {
@@ -487,28 +488,24 @@ Variants {
currentWallpaper.source = nextWallpaper.source; currentWallpaper.source = nextWallpaper.source;
} }
root.useNextForEffect = false; root.useNextForEffect = false;
nextWallpaper.source = ""; Qt.callLater(() => {
root.transitionProgress = 0.0; nextWallpaper.source = "";
currentWallpaper.layer.enabled = false;
nextWallpaper.layer.enabled = false;
currentWallpaper.cache = true;
nextWallpaper.cache = false;
root.effectActive = false;
if (root.pendingWallpaper) {
var pending = root.pendingWallpaper;
root.pendingWallpaper = "";
Qt.callLater(() => { Qt.callLater(() => {
root.changeWallpaper(pending, true); root.effectActive = false;
currentWallpaper.layer.enabled = false;
nextWallpaper.layer.enabled = false;
currentWallpaper.cache = true;
nextWallpaper.cache = false;
root.transitionProgress = 0.0;
}); });
} });
} }
} }
MultiEffect { MultiEffect {
anchors.fill: parent anchors.fill: parent
source: effectLoader.active ? effectLoader.item : currentWallpaper source: effectLoader.active ? effectLoader.item : (root.actualTransitionType === "none" ? currentWallpaper : null)
visible: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview && currentWallpaper.source !== "" visible: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview && source !== null
blurEnabled: true blurEnabled: true
blur: 0.8 blur: 0.8
blurMax: 75 blurMax: 75
+7 -52
View File
@@ -22,18 +22,6 @@ Singleton {
property bool available: CompositorService.isNiri && shortcutInhibitorAvailable property bool available: CompositorService.isNiri && shortcutInhibitorAvailable
property string currentProvider: "niri" property string currentProvider: "niri"
readonly property string cheatsheetProvider: {
if (CompositorService.isNiri)
return "niri";
if (CompositorService.isHyprland)
return "hyprland";
return "";
}
property bool cheatsheetAvailable: cheatsheetProvider !== ""
property bool cheatsheetLoading: false
property var cheatsheet: ({})
property bool loading: false property bool loading: false
property bool saving: false property bool saving: false
property bool fixing: false property bool fixing: false
@@ -70,14 +58,17 @@ Singleton {
signal bindSaveCompleted(bool success) signal bindSaveCompleted(bool success)
signal bindRemoved(string key) signal bindRemoved(string key)
signal dmsBindsFixed signal dmsBindsFixed
signal cheatsheetLoaded
Component.onCompleted: {
if (available)
Qt.callLater(loadBinds);
}
Connections { Connections {
target: CompositorService target: CompositorService
function onCompositorChanged() { function onCompositorChanged() {
if (!CompositorService.isNiri) if (CompositorService.isNiri)
return; Qt.callLater(root.loadBinds);
Qt.callLater(root.loadBinds);
} }
} }
@@ -89,31 +80,6 @@ Singleton {
} }
} }
Process {
id: cheatsheetProcess
running: false
stdout: StdioCollector {
onStreamFinished: {
try {
root.cheatsheet = JSON.parse(text);
} catch (e) {
console.error("[KeybindsService] Failed to parse cheatsheet:", e);
root.cheatsheet = {};
}
root.cheatsheetLoading = false;
root.cheatsheetLoaded();
}
}
onExited: exitCode => {
if (exitCode === 0)
return;
console.warn("[KeybindsService] Cheatsheet load failed with code:", exitCode);
root.cheatsheetLoading = false;
}
}
Process { Process {
id: loadProcess id: loadProcess
running: false running: false
@@ -233,17 +199,6 @@ Singleton {
loadBinds(true); loadBinds(true);
} }
function loadCheatsheet(provider) {
if (cheatsheetProcess.running)
return;
const target = provider || cheatsheetProvider;
if (!target)
return;
cheatsheetLoading = true;
cheatsheetProcess.command = ["dms", "keybinds", "show", target];
cheatsheetProcess.running = true;
}
function loadBinds(showLoading) { function loadBinds(showLoading) {
if (loadProcess.running || !available) if (loadProcess.running || !available)
return; return;
+75 -82
View File
@@ -1,4 +1,5 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
@@ -19,7 +20,6 @@ Singleton {
property string distribution: "" property string distribution: ""
property bool distributionSupported: false property bool distributionSupported: false
property string shellVersion: "" property string shellVersion: ""
property string shellCodename: ""
readonly property var archBasedUCSettings: { readonly property var archBasedUCSettings: {
"listUpdatesSettings": { "listUpdatesSettings": {
@@ -34,7 +34,7 @@ Singleton {
"currentVersion": match[2], "currentVersion": match[2],
"newVersion": match[3], "newVersion": match[3],
"description": `${match[1]} ${match[2]} ${match[3]}` "description": `${match[1]} ${match[2]} ${match[3]}`
}; }
} }
} }
} }
@@ -56,7 +56,7 @@ Singleton {
"currentVersion": match[2], "currentVersion": match[2],
"newVersion": match[3], "newVersion": match[3],
"description": `${match[1]} ${match[2]} ${match[3]}` "description": `${match[1]} ${match[2]} ${match[3]}`
}; }
} }
} }
} }
@@ -78,7 +78,7 @@ Singleton {
"currentVersion": "", "currentVersion": "",
"newVersion": match[2], "newVersion": match[2],
"description": `${match[1]} ${match[2]}` "description": `${match[1]} ${match[2]}`
}; }
} }
} }
} }
@@ -100,49 +100,40 @@ Singleton {
command: ["sh", "-c", "cat /etc/os-release | grep '^ID=' | cut -d'=' -f2 | tr -d '\"'"] command: ["sh", "-c", "cat /etc/os-release | grep '^ID=' | cut -d'=' -f2 | tr -d '\"'"]
running: true running: true
onExited: exitCode => { onExited: (exitCode) => {
if (exitCode === 0) { if (exitCode === 0) {
distribution = stdout.text.trim().toLowerCase(); distribution = stdout.text.trim().toLowerCase()
distributionSupported = supportedDistributions.includes(distribution); distributionSupported = supportedDistributions.includes(distribution)
if (distributionSupported) { if (distributionSupported) {
updateFinderDetection.running = true; updateFinderDetection.running = true
pkgManagerDetection.running = true; pkgManagerDetection.running = true
checkForUpdates(); checkForUpdates()
} else { } else {
console.warn("SystemUpdate: Unsupported distribution:", distribution); console.warn("SystemUpdate: Unsupported distribution:", distribution)
} }
} else { } else {
console.warn("SystemUpdate: Failed to detect distribution"); console.warn("SystemUpdate: Failed to detect distribution")
} }
} }
stdout: StdioCollector {} stdout: StdioCollector {}
Component.onCompleted: { Component.onCompleted: {
versionDetection.running = true; versionDetection.running = true
} }
} }
Process { Process {
id: versionDetection id: versionDetection
command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -d .git ]; then echo "(git) $(git rev-parse --short HEAD)"; elif [ -f VERSION ]; then cat VERSION; fi`] command: [
"sh", "-c",
`cd "${Quickshell.shellDir}" && if [ -d .git ]; then echo "(git) $(git rev-parse --short HEAD)"; elif [ -f VERSION ]; then cat VERSION; fi`
]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
shellVersion = text.trim(); shellVersion = text.trim()
}
}
}
Process {
id: codenameDetection
command: ["sh", "-c", `cd "${Quickshell.shellDir}" && if [ -f CODENAME ]; then cat CODENAME; fi`]
running: true
stdout: StdioCollector {
onStreamFinished: {
shellCodename = text.trim();
} }
} }
} }
@@ -151,12 +142,12 @@ Singleton {
id: updateFinderDetection id: updateFinderDetection
command: ["sh", "-c", "which checkupdates"] command: ["sh", "-c", "which checkupdates"]
onExited: exitCode => { onExited: (exitCode) => {
if (exitCode === 0) { if (exitCode === 0) {
const exeFound = stdout.text.trim(); const exeFound = stdout.text.trim()
updChecker = exeFound.split('/').pop(); updChecker = exeFound.split('/').pop()
} else { } else {
console.warn("SystemUpdate: No update checker found. Will use package manager."); console.warn("SystemUpdate: No update checker found. Will use package manager.")
} }
} }
@@ -167,12 +158,12 @@ Singleton {
id: pkgManagerDetection id: pkgManagerDetection
command: ["sh", "-c", "which paru || which yay || which dnf"] command: ["sh", "-c", "which paru || which yay || which dnf"]
onExited: exitCode => { onExited: (exitCode) => {
if (exitCode === 0) { if (exitCode === 0) {
const exeFound = stdout.text.trim(); const exeFound = stdout.text.trim()
pkgManager = exeFound.split('/').pop(); pkgManager = exeFound.split('/').pop()
} else { } else {
console.warn("SystemUpdate: No package manager found"); console.warn("SystemUpdate: No package manager found")
} }
} }
@@ -182,17 +173,19 @@ Singleton {
Process { Process {
id: updateChecker id: updateChecker
onExited: exitCode => { onExited: (exitCode) => {
isChecking = false; isChecking = false
const correctExitCodes = updChecker.length > 0 ? [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.correctExitCodes) : [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.correctExitCodes); const correctExitCodes = updChecker.length > 0 ?
[updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.correctExitCodes) :
[pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.correctExitCodes)
if (correctExitCodes.includes(exitCode)) { if (correctExitCodes.includes(exitCode)) {
parseUpdates(stdout.text); parseUpdates(stdout.text)
hasError = false; hasError = false
errorMessage = ""; errorMessage = ""
} else { } else {
hasError = true; hasError = true
errorMessage = "Failed to check for updates"; errorMessage = "Failed to check for updates"
console.warn("SystemUpdate: Update check failed with code:", exitCode); console.warn("SystemUpdate: Update check failed with code:", exitCode)
} }
} }
@@ -201,67 +194,67 @@ Singleton {
Process { Process {
id: updater id: updater
onExited: exitCode => { onExited: (exitCode) => {
checkForUpdates(); checkForUpdates()
} }
} }
function checkForUpdates() { function checkForUpdates() {
if (!distributionSupported || (!pkgManager && !updChecker) || isChecking) if (!distributionSupported || (!pkgManager && !updChecker) || isChecking) return
return;
isChecking = true; isChecking = true
hasError = false; hasError = false
if (updChecker.length > 0) { if (updChecker.length > 0) {
updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params); updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params)
} else { } else {
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params); updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params)
} }
updateChecker.running = true; updateChecker.running = true
} }
function parseUpdates(output) { function parseUpdates(output) {
const lines = output.trim().split('\n').filter(line => line.trim()); const lines = output.trim().split('\n').filter(line => line.trim())
const updates = []; const updates = []
const regex = packageManagerParams[pkgManager].parserSettings.lineRegex; const regex = packageManagerParams[pkgManager].parserSettings.lineRegex
const entryProducer = packageManagerParams[pkgManager].parserSettings.entryProducer; const entryProducer = packageManagerParams[pkgManager].parserSettings.entryProducer
for (const line of lines) { for (const line of lines) {
const match = line.match(regex); const match = line.match(regex)
if (match) { if (match) {
updates.push(entryProducer(match)); updates.push(entryProducer(match))
} }
} }
availableUpdates = updates; availableUpdates = updates
} }
function runUpdates() { function runUpdates() {
if (!distributionSupported || !pkgManager || updateCount === 0) if (!distributionSupported || !pkgManager || updateCount === 0) return
return;
const terminal = Quickshell.env("TERMINAL") || "xterm"; const terminal = Quickshell.env("TERMINAL") || "xterm"
if (SettingsData.updaterUseCustomCommand && SettingsData.updaterCustomCommand.length > 0) { if (SettingsData.updaterUseCustomCommand && SettingsData.updaterCustomCommand.length > 0) {
const updateCommand = `${SettingsData.updaterCustomCommand} && echo "Updates complete! Press Enter to close..." && read`; const updateCommand = `${SettingsData.updaterCustomCommand} && echo "Updates complete! Press Enter to close..." && read`
const termClass = SettingsData.updaterTerminalAdditionalParams; const termClass = SettingsData.updaterTerminalAdditionalParams
var finalCommand = [terminal]; var finalCommand = [terminal]
if (termClass.length > 0) { if (termClass.length > 0) {
finalCommand = finalCommand.concat(termClass.split(" ")); finalCommand = finalCommand.concat(termClass.split(" "))
} }
finalCommand.push("-e"); finalCommand.push("-e")
finalCommand.push("sh"); finalCommand.push("sh")
finalCommand.push("-c"); finalCommand.push("-c")
finalCommand.push(updateCommand); finalCommand.push(updateCommand)
updater.command = finalCommand; updater.command = finalCommand
} else { } else {
const params = packageManagerParams[pkgManager].upgradeSettings.params.join(" "); const params = packageManagerParams[pkgManager].upgradeSettings.params.join(" ")
const sudo = packageManagerParams[pkgManager].upgradeSettings.requiresSudo ? "sudo" : ""; const sudo = packageManagerParams[pkgManager].upgradeSettings.requiresSudo ? "sudo" : ""
const updateCommand = `${sudo} ${pkgManager} ${params} && echo "Updates complete! Press Enter to close..." && read`; const updateCommand = `${sudo} ${pkgManager} ${params} && echo "Updates complete! Press Enter to close..." && read`
updater.command = [terminal, "-e", "sh", "-c", updateCommand]; updater.command = [terminal, "-e", "sh", "-c", updateCommand]
} }
updater.running = true; updater.running = true
} }
Timer { Timer {
@@ -276,16 +269,16 @@ Singleton {
function updatestatus(): string { function updatestatus(): string {
if (root.isChecking) { if (root.isChecking) {
return "ERROR: already checking"; return "ERROR: already checking"
} }
if (!distributionSupported) { if (!distributionSupported) {
return "ERROR: distribution not supported"; return "ERROR: distribution not supported"
} }
if (!pkgManager && !updChecker) { if (!pkgManager && !updChecker) {
return "ERROR: update checker not available"; return "ERROR: update checker not available"
} }
root.checkForUpdates(); root.checkForUpdates()
return "SUCCESS: Now checking..."; return "SUCCESS: Now checking..."
} }
} }
} }
+18 -21
View File
@@ -5,7 +5,6 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modules.Greetd
import "../Common/suncalc.js" as SunCalc import "../Common/suncalc.js" as SunCalc
Singleton { Singleton {
@@ -510,29 +509,27 @@ Singleton {
} }
function updateLocation() { function updateLocation() {
const useAuto = SessionData.isGreeterMode ? GreetdSettings.useAutoLocation : SettingsData.useAutoLocation; if (SettingsData.useAutoLocation) {
const coords = SessionData.isGreeterMode ? GreetdSettings.weatherCoordinates : SettingsData.weatherCoordinates;
const cityName = SessionData.isGreeterMode ? GreetdSettings.weatherLocation : SettingsData.weatherLocation;
if (useAuto) {
getLocationFromIP(); getLocationFromIP();
return; } else {
} const coords = SettingsData.weatherCoordinates;
if (coords) {
if (coords) { const parts = coords.split(",");
const parts = coords.split(","); if (parts.length === 2) {
if (parts.length === 2) { const lat = parseFloat(parts[0]);
const lat = parseFloat(parts[0]); const lon = parseFloat(parts[1]);
const lon = parseFloat(parts[1]); if (!isNaN(lat) && !isNaN(lon)) {
if (!isNaN(lat) && !isNaN(lon)) { getLocationFromCoords(lat, lon);
getLocationFromCoords(lat, lon); return;
return; }
} }
} }
}
if (cityName) const cityName = SettingsData.weatherLocation;
getLocationFromCity(cityName); if (cityName) {
getLocationFromCity(cityName);
}
}
} }
function getLocationFromCoords(lat, lon) { function getLocationFromCoords(lat, lon) {
@@ -870,7 +867,7 @@ Singleton {
Timer { Timer {
id: updateTimer id: updateTimer
interval: nextInterval() interval: nextInterval()
running: root.refCount > 0 && SettingsData.weatherEnabled && !SessionData.isGreeterMode running: root.refCount > 0 && SettingsData.weatherEnabled
repeat: true repeat: true
triggeredOnStart: true triggeredOnStart: true
onTriggered: { onTriggered: {
+12 -25
View File
@@ -46,12 +46,11 @@ Flickable {
lastWheelTime = currentTime; lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0; const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
const deltaY = event.angleDelta.y; const deltaY = event.angleDelta.y;
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0; const isMouseWheel = !hasPixel && hasAngle;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isTraditionalMouse) { if (isMouseWheel) {
sessionUsedMouseWheel = true; sessionUsedMouseWheel = true;
momentumTimer.stop(); momentumTimer.stop();
flickable.isMomentumActive = false; flickable.isMomentumActive = false;
@@ -59,7 +58,7 @@ Flickable {
momentum = 0; momentum = 0;
flickable.momentumVelocity = 0; flickable.momentumVelocity = 0;
const lines = Math.round(Math.abs(deltaY) / 120); const lines = Math.floor(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed; const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed;
let newY = flickable.contentY + scrollAmount; let newY = flickable.contentY + scrollAmount;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY)); newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
@@ -69,29 +68,17 @@ Flickable {
} }
flickable.contentY = newY; flickable.contentY = newY;
} else if (isHighDpiMouse) { } else {
sessionUsedMouseWheel = true;
momentumTimer.stop();
flickable.isMomentumActive = false;
velocitySamples = [];
momentum = 0;
flickable.momentumVelocity = 0;
let delta = deltaY / 8 * touchpadSpeed;
let newY = flickable.contentY - delta;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking) {
flickable.cancelFlick();
}
flickable.contentY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false; sessionUsedMouseWheel = false;
momentumTimer.stop(); momentumTimer.stop();
flickable.isMomentumActive = false; flickable.isMomentumActive = false;
let delta = event.pixelDelta.y * touchpadSpeed; let delta = 0;
if (event.pixelDelta.y !== 0) {
delta = event.pixelDelta.y * touchpadSpeed;
} else {
delta = event.angleDelta.y / 8 * touchpadSpeed;
}
velocitySamples.push({ velocitySamples.push({
"delta": delta, "delta": delta,
@@ -107,7 +94,7 @@ Flickable {
} }
} }
if (timeDelta < 50) { if (event.pixelDelta.y !== 0 && timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15; momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum; delta += momentum;
} else { } else {
+7 -25
View File
@@ -50,12 +50,11 @@ GridView {
lastWheelTime = currentTime; lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0; const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
const deltaY = event.angleDelta.y; const deltaY = event.angleDelta.y;
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0; const isMouseWheel = !hasPixel && hasAngle;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isTraditionalMouse) { if (isMouseWheel) {
sessionUsedMouseWheel = true; sessionUsedMouseWheel = true;
momentumTimer.stop(); momentumTimer.stop();
isMomentumActive = false; isMomentumActive = false;
@@ -63,7 +62,7 @@ GridView {
momentum = 0; momentum = 0;
momentumVelocity = 0; momentumVelocity = 0;
const lines = Math.round(Math.abs(deltaY) / 120); const lines = Math.floor(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35; const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35;
let newY = contentY + scrollAmount; let newY = contentY + scrollAmount;
newY = Math.max(0, Math.min(contentHeight - height, newY)); newY = Math.max(0, Math.min(contentHeight - height, newY));
@@ -73,29 +72,12 @@ GridView {
} }
contentY = newY; contentY = newY;
} else if (isHighDpiMouse) { } else {
sessionUsedMouseWheel = true;
momentumTimer.stop();
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
let delta = deltaY / 120 * cellHeight * 1.2;
let newY = contentY - delta;
newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking) {
cancelFlick();
}
contentY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false; sessionUsedMouseWheel = false;
momentumTimer.stop(); momentumTimer.stop();
isMomentumActive = false; isMomentumActive = false;
let delta = event.pixelDelta.y * touchpadSpeed; let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * touchpadSpeed : event.angleDelta.y / 120 * cellHeight * 1.2;
velocitySamples.push({ velocitySamples.push({
"delta": delta, "delta": delta,
@@ -111,7 +93,7 @@ GridView {
} }
} }
if (timeDelta < 50) { if (event.pixelDelta.y !== 0 && timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15; momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum; delta += momentum;
} else { } else {
+12 -27
View File
@@ -69,12 +69,11 @@ ListView {
lastWheelTime = currentTime; lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0; const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const hasAngle = event.angleDelta && event.angleDelta.y !== 0;
const deltaY = event.angleDelta.y; const deltaY = event.angleDelta.y;
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0; const isMouseWheel = !hasPixel && hasAngle;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isTraditionalMouse) { if (isMouseWheel) {
sessionUsedMouseWheel = true; sessionUsedMouseWheel = true;
momentumTimer.stop(); momentumTimer.stop();
isMomentumActive = false; isMomentumActive = false;
@@ -82,7 +81,7 @@ ListView {
momentum = 0; momentum = 0;
momentumVelocity = 0; momentumVelocity = 0;
const lines = Math.round(Math.abs(deltaY) / 120); const lines = Math.floor(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed; const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
let newY = listView.contentY + scrollAmount; let newY = listView.contentY + scrollAmount;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY); const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
@@ -94,31 +93,17 @@ ListView {
listView.contentY = newY; listView.contentY = newY;
savedY = newY; savedY = newY;
} else if (isHighDpiMouse) { } else {
sessionUsedMouseWheel = true;
momentumTimer.stop();
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
let delta = deltaY / 8 * touchpadSpeed;
let newY = listView.contentY - delta;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
newY = Math.max(listView.originY, Math.min(maxY, newY));
if (listView.flicking) {
listView.cancelFlick();
}
listView.contentY = newY;
savedY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false; sessionUsedMouseWheel = false;
momentumTimer.stop(); momentumTimer.stop();
isMomentumActive = false; isMomentumActive = false;
let delta = event.pixelDelta.y * touchpadSpeed; let delta = 0;
if (event.pixelDelta.y !== 0) {
delta = event.pixelDelta.y * touchpadSpeed;
} else {
delta = event.angleDelta.y / 8 * touchpadSpeed;
}
velocitySamples.push({ velocitySamples.push({
"delta": delta, "delta": delta,
@@ -134,7 +119,7 @@ ListView {
} }
} }
if (timeDelta < 50) { if (event.pixelDelta.y !== 0 && timeDelta < 50) {
momentum = momentum * 0.92 + delta * 0.15; momentum = momentum * 0.92 + delta * 0.15;
delta += momentum; delta += momentum;
} else { } else {
+1 -1
View File
@@ -396,6 +396,7 @@ Item {
Item { Item {
id: bgShadowLayer id: bgShadowLayer
anchors.fill: parent anchors.fill: parent
visible: contentWrapper.popupSurfaceAlpha >= 0.95
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
layer.smooth: false layer.smooth: false
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr)) layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
@@ -420,7 +421,6 @@ Item {
DankRectangle { DankRectangle {
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
} }
} }