mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 04:09:15 -04:00
feat(mango): first-class MangoWM support across DMS, dankinstaller & UI tools
- Bring up Mango to parity with niri/hyprland via a native JSON-IPC w/Native MangoServic., replaces the legacy dwl/`mmsg` path and recent breaking changes - Dankinstall: mango supported installer, config/binds templates, and packaging (Arch AUR, Fedora Terra auto-enable, Gentoo GURU) - Window rules: Go provider + CLI + Settings GUI editor - Keybinds + config reload on edit (mmsg dispatch reload_config) - Misc new supported options in DMS settings
This commit is contained in:
@@ -42,7 +42,7 @@ configure passwordless sudo for your user.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri or hyprland (enables headless mode)")
|
||||
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri, hyprland, or mango (enables headless mode)")
|
||||
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
|
||||
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
|
||||
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
|
||||
@@ -95,7 +95,7 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
|
||||
func runHeadless() error {
|
||||
// Validate required flags
|
||||
if compositor == "" {
|
||||
return fmt.Errorf("--compositor is required for headless mode (niri or hyprland)")
|
||||
return fmt.Errorf("--compositor is required for headless mode (niri, hyprland, or mango)")
|
||||
}
|
||||
if term == "" {
|
||||
return fmt.Errorf("--term is required for headless mode (ghostty, kitty, or alacritty)")
|
||||
|
||||
@@ -102,32 +102,42 @@ var setupWindowrulesCmd = &cobra.Command{
|
||||
type dmsConfigSpec struct {
|
||||
niriFile string
|
||||
hyprFile string
|
||||
mangoFile string
|
||||
niriContent func(terminal string) string
|
||||
hyprContent func(terminal string) string
|
||||
mangoContent func(terminal string) string
|
||||
}
|
||||
|
||||
var dmsConfigSpecs = map[string]dmsConfigSpec{
|
||||
"binds": {
|
||||
niriFile: "binds.kdl",
|
||||
hyprFile: "binds.lua",
|
||||
mangoFile: "binds.conf",
|
||||
niriContent: func(t string) string {
|
||||
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||
},
|
||||
hyprContent: func(t string) string {
|
||||
return strings.ReplaceAll(config.DMSBindsLuaConfig, "{{TERMINAL_COMMAND}}", t)
|
||||
},
|
||||
mangoContent: func(t string) string {
|
||||
return strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||
},
|
||||
},
|
||||
"layout": {
|
||||
niriFile: "layout.kdl",
|
||||
hyprFile: "layout.lua",
|
||||
mangoFile: "layout.conf",
|
||||
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
||||
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
|
||||
mangoContent: func(_ string) string { return config.MangoLayoutConfig },
|
||||
},
|
||||
"colors": {
|
||||
niriFile: "colors.kdl",
|
||||
hyprFile: "colors.lua",
|
||||
mangoFile: "colors.conf",
|
||||
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
||||
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
|
||||
mangoContent: func(_ string) string { return config.MangoColorsConfig },
|
||||
},
|
||||
"alttab": {
|
||||
niriFile: "alttab.kdl",
|
||||
@@ -136,20 +146,26 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
|
||||
"outputs": {
|
||||
niriFile: "outputs.kdl",
|
||||
hyprFile: "outputs.lua",
|
||||
mangoFile: "outputs.conf",
|
||||
niriContent: func(_ string) string { return "" },
|
||||
hyprContent: func(_ string) string { return config.DMSOutputsLuaConfig },
|
||||
mangoContent: func(_ string) string { return "" },
|
||||
},
|
||||
"cursor": {
|
||||
niriFile: "cursor.kdl",
|
||||
hyprFile: "cursor.lua",
|
||||
mangoFile: "cursor.conf",
|
||||
niriContent: func(_ string) string { return "" },
|
||||
hyprContent: func(_ string) string { return config.DMSCursorLuaConfig },
|
||||
mangoContent: func(_ string) string { return "" },
|
||||
},
|
||||
"windowrules": {
|
||||
niriFile: "windowrules.kdl",
|
||||
hyprFile: "windowrules.lua",
|
||||
mangoFile: "windowrules.conf",
|
||||
niriContent: func(_ string) string { return "" },
|
||||
hyprContent: func(_ string) string { return config.DMSWindowRulesLuaConfig },
|
||||
mangoContent: func(_ string) string { return "" },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -192,7 +208,7 @@ func detectCompositorForSetup() (string, error) {
|
||||
|
||||
switch len(compositors) {
|
||||
case 0:
|
||||
return "", fmt.Errorf("no supported compositors found (niri or Hyprland required)")
|
||||
return "", fmt.Errorf("no supported compositors found (niri, Hyprland, or mango required)")
|
||||
case 1:
|
||||
return strings.ToLower(compositors[0]), nil
|
||||
}
|
||||
@@ -224,6 +240,9 @@ func runSetupDmsConfig(name string) error {
|
||||
case "hyprland":
|
||||
filename = spec.hyprFile
|
||||
contentFn = spec.hyprContent
|
||||
case "mango", "mangowc":
|
||||
filename = spec.mangoFile
|
||||
contentFn = spec.mangoContent
|
||||
default:
|
||||
return fmt.Errorf("unsupported compositor: %s", compositor)
|
||||
}
|
||||
@@ -238,6 +257,8 @@ func runSetupDmsConfig(name string) error {
|
||||
dmsDir = filepath.Join(utils.XDGConfigHome(), "niri", "dms")
|
||||
case "hyprland":
|
||||
dmsDir = filepath.Join(utils.XDGConfigHome(), "hypr", "dms")
|
||||
case "mango", "mangowc":
|
||||
dmsDir = filepath.Join(utils.XDGConfigHome(), "mango", "dms")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||
@@ -379,10 +400,11 @@ func promptCompositor() (deps.WindowManager, bool) {
|
||||
fmt.Println("Select compositor:")
|
||||
fmt.Println("1) Niri")
|
||||
fmt.Println("2) Hyprland")
|
||||
fmt.Println("3) None")
|
||||
fmt.Println("3) Mango")
|
||||
fmt.Println("4) None")
|
||||
|
||||
var response string
|
||||
fmt.Print("\nChoice (1-3): ")
|
||||
fmt.Print("\nChoice (1-4): ")
|
||||
fmt.Scanln(&response)
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
@@ -391,6 +413,8 @@ func promptCompositor() (deps.WindowManager, bool) {
|
||||
return deps.WindowManagerNiri, true
|
||||
case "2":
|
||||
return deps.WindowManagerHyprland, true
|
||||
case "3":
|
||||
return deps.WindowManagerMango, true
|
||||
default:
|
||||
return deps.WindowManagerNiri, false
|
||||
}
|
||||
@@ -447,6 +471,11 @@ func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.
|
||||
filepath.Join(homeDir, ".config", "hypr", "hyprland.lua"),
|
||||
filepath.Join(homeDir, ".config", "hypr", "hyprland.conf"),
|
||||
}
|
||||
case deps.WindowManagerMango:
|
||||
configPaths = []string{
|
||||
filepath.Join(homeDir, ".config", "mango", "config.conf"),
|
||||
filepath.Join(homeDir, ".config", "mango", "mango.conf"),
|
||||
}
|
||||
}
|
||||
|
||||
for _, configPath := range configPaths {
|
||||
|
||||
@@ -27,7 +27,7 @@ var windowrulesListCmd = &cobra.Command{
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
@@ -41,7 +41,7 @@ var windowrulesAddCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
@@ -55,7 +55,7 @@ var windowrulesUpdateCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(3),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
@@ -69,7 +69,7 @@ var windowrulesRemoveCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
@@ -83,7 +83,7 @@ var windowrulesReorderCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
@@ -121,6 +121,9 @@ func getCompositor(args []string) string {
|
||||
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||
return "hyprland"
|
||||
}
|
||||
if os.Getenv("MANGO_INSTANCE_SIGNATURE") != "" {
|
||||
return "mango"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -140,7 +143,7 @@ func writeRuleSuccess(id, path string) {
|
||||
func runWindowrulesList(cmd *cobra.Command, args []string) {
|
||||
compositor := getCompositor(args)
|
||||
if compositor == "" {
|
||||
log.Fatalf("Could not detect compositor. Please specify: hyprland or niri")
|
||||
log.Fatalf("Could not detect compositor. Please specify: hyprland, niri, or mango")
|
||||
}
|
||||
|
||||
var result WindowRulesListResult
|
||||
@@ -212,6 +215,38 @@ func runWindowrulesList(cmd *cobra.Command, args []string) {
|
||||
result.Rules = allRules
|
||||
result.DMSStatus = parseResult.DMSStatus
|
||||
|
||||
case "mango", "mangowc":
|
||||
configDir := filepath.Join(utils.XDGConfigHome(), "mango")
|
||||
|
||||
parseResult, err := providers.ParseMangoWindowRules(configDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse mango window rules: %v", err)
|
||||
}
|
||||
|
||||
allRules := providers.ConvertMangoRulesToWindowRules(parseResult.Rules)
|
||||
|
||||
provider := providers.NewMangoWritableProvider(configDir)
|
||||
dmsRules, _ := provider.LoadDMSRules()
|
||||
|
||||
dmsRuleMap := make(map[int]windowrules.WindowRule)
|
||||
for i, dr := range dmsRules {
|
||||
dmsRuleMap[i] = dr
|
||||
}
|
||||
|
||||
dmsIdx := 0
|
||||
for i, r := range allRules {
|
||||
if r.Source == "dms/windowrules.conf" {
|
||||
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
|
||||
allRules[i].ID = dmr.ID
|
||||
allRules[i].Name = dmr.Name
|
||||
}
|
||||
dmsIdx++
|
||||
}
|
||||
}
|
||||
|
||||
result.Rules = allRules
|
||||
result.DMSStatus = parseResult.DMSStatus
|
||||
|
||||
default:
|
||||
log.Fatalf("Unknown compositor: %s", compositor)
|
||||
}
|
||||
@@ -315,6 +350,9 @@ func getWindowRulesProvider(compositor string) windowrules.WritableProvider {
|
||||
case "hyprland":
|
||||
configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
|
||||
return providers.NewHyprlandWritableProvider(configDir)
|
||||
case "mango", "mangowc":
|
||||
configDir := filepath.Join(utils.XDGConfigHome(), "mango")
|
||||
return providers.NewMangoWritableProvider(configDir)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
||||
filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.lua"),
|
||||
filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
||||
},
|
||||
"Mango": {
|
||||
filepath.Join(os.Getenv("HOME"), ".config", "mango", "config.conf"),
|
||||
filepath.Join(os.Getenv("HOME"), ".config", "mango", "mango.conf"),
|
||||
},
|
||||
"Ghostty": {
|
||||
filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
||||
},
|
||||
@@ -126,6 +130,14 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
||||
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
|
||||
}
|
||||
}
|
||||
case deps.WindowManagerMango:
|
||||
if shouldReplaceConfig("Mango") {
|
||||
result, err := cd.deployMangoConfig(terminal, useSystemd)
|
||||
results = append(results, result)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Mango config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch terminal {
|
||||
@@ -269,6 +281,96 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployMangoConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
||||
result := DeploymentResult{
|
||||
ConfigType: "Mango",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "mango", "config.conf"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(result.Path)
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
dmsDir := filepath.Join(configDir, "dms")
|
||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
var terminalCommand string
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
terminalCommand = "ghostty"
|
||||
case deps.TerminalKitty:
|
||||
terminalCommand = "kitty"
|
||||
case deps.TerminalAlacritty:
|
||||
terminalCommand = "alacritty"
|
||||
default:
|
||||
terminalCommand = "ghostty"
|
||||
}
|
||||
|
||||
// DMS owns config.conf for mango (like niri/hyprland): back up and replace.
|
||||
if existingData, err := os.ReadFile(result.Path); err == nil {
|
||||
cd.log("Found existing Mango configuration")
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
result.BackupPath = result.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(result.BackupPath, existingData, 0o644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||
}
|
||||
|
||||
newConfig := strings.ReplaceAll(MangoConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
if err := cd.deployMangoDmsConfigs(dmsDir, terminalCommand); err != nil {
|
||||
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
result.Deployed = true
|
||||
cd.log("Successfully deployed Mango configuration")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string) error {
|
||||
configs := []struct {
|
||||
name string
|
||||
content string
|
||||
overwrite bool
|
||||
}{
|
||||
// binds.conf is DMS-owned (overwrite); the rest are runtime/user-managed.
|
||||
{"binds.conf", strings.ReplaceAll(MangoBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand), true},
|
||||
{"colors.conf", MangoColorsConfig, false},
|
||||
{"layout.conf", MangoLayoutConfig, false},
|
||||
{"outputs.conf", "", false},
|
||||
{"cursor.conf", "", false},
|
||||
{"windowrules.conf", "", false},
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
path := filepath.Join(dmsDir, cfg.name)
|
||||
if !cfg.overwrite {
|
||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||
}
|
||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
||||
var results []DeploymentResult
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
# DMS default keybinds (MangoWM) — managed by DMS, regenerated by `dms setup`.
|
||||
# Format: bind=MODS,key,action[,args]
|
||||
# Descriptions go on the line ABOVE each bind (mango does not strip inline
|
||||
# comments — a trailing `# ...` would be passed to spawn as extra arguments).
|
||||
|
||||
# === Application Launchers ===
|
||||
# Open Terminal
|
||||
bind=SUPER,t,spawn,{{TERMINAL_COMMAND}}
|
||||
# Open Terminal
|
||||
bind=SUPER,Return,spawn,{{TERMINAL_COMMAND}}
|
||||
# Application Launcher
|
||||
bind=SUPER,space,spawn,dms ipc call spotlight toggle
|
||||
# Spotlight Bar
|
||||
bind=ALT,space,spawn,dms ipc call spotlight-bar toggle
|
||||
# Clipboard Manager
|
||||
bind=SUPER,v,spawn,dms ipc call clipboard toggle
|
||||
# Task Manager
|
||||
bind=SUPER,m,spawn,dms ipc call processlist focusOrToggle
|
||||
# Settings
|
||||
bind=SUPER,comma,spawn,dms ipc call settings focusOrToggle
|
||||
# Notification Center
|
||||
bind=SUPER,n,spawn,dms ipc call notifications toggle
|
||||
# Notepad
|
||||
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
||||
# Browse Wallpapers
|
||||
bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
|
||||
# Power Menu
|
||||
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
||||
# Cycle Display Profile
|
||||
bind=SUPER,p,spawn,dms ipc outputs cycleProfile
|
||||
|
||||
# === Cheat sheet ===
|
||||
# Keyboard Shortcuts
|
||||
bind=SUPER+SHIFT,slash,spawn,dms ipc call keybinds toggle mangowc
|
||||
|
||||
# === Security ===
|
||||
# Lock Screen
|
||||
bind=SUPER+ALT,l,spawn,dms ipc call lock lock
|
||||
# Task Manager
|
||||
bind=CTRL+ALT,Delete,spawn,dms ipc call processlist focusOrToggle
|
||||
|
||||
# === Window Rules ===
|
||||
# Create Window Rule
|
||||
bind=SUPER+SHIFT,w,spawn,dms ipc call window-rules toggle
|
||||
|
||||
# === Screenshots ===
|
||||
# Screenshot: Interactive
|
||||
bind=none,Print,spawn,dms screenshot
|
||||
# Screenshot: Full Screen
|
||||
bind=CTRL,Print,spawn,dms screenshot full
|
||||
# Screenshot: Window
|
||||
bind=ALT,Print,spawn,dms screenshot window
|
||||
|
||||
# === Audio Controls ===
|
||||
# Volume Up
|
||||
bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3
|
||||
# Volume Down
|
||||
bind=none,XF86AudioLowerVolume,spawn,dms ipc call audio decrement 3
|
||||
# Mute Output
|
||||
bind=none,XF86AudioMute,spawn,dms ipc call audio mute
|
||||
# Mute Microphone
|
||||
bind=none,XF86AudioMicMute,spawn,dms ipc call audio micmute
|
||||
# Play/Pause
|
||||
bind=none,XF86AudioPlay,spawn,dms ipc call mpris playPause
|
||||
# Play/Pause
|
||||
bind=none,XF86AudioPause,spawn,dms ipc call mpris playPause
|
||||
# Previous Track
|
||||
bind=none,XF86AudioPrev,spawn,dms ipc call mpris previous
|
||||
# Next Track
|
||||
bind=none,XF86AudioNext,spawn,dms ipc call mpris next
|
||||
|
||||
# === Brightness Controls ===
|
||||
# Brightness Up
|
||||
bind=none,XF86MonBrightnessUp,spawn,dms ipc call brightness increment 5
|
||||
# Brightness Down
|
||||
bind=none,XF86MonBrightnessDown,spawn,dms ipc call brightness decrement 5
|
||||
|
||||
# === Window Management ===
|
||||
# Close Window
|
||||
bind=SUPER,q,killclient,
|
||||
# Toggle Fullscreen
|
||||
bind=SUPER,f,togglefullscreen,
|
||||
# Toggle Maximize
|
||||
bind=SUPER,a,togglemaximizescreen,
|
||||
# Toggle Floating
|
||||
bind=SUPER+SHIFT,space,togglefloating,
|
||||
# Exit Compositor
|
||||
bind=SUPER+SHIFT,e,quit,
|
||||
|
||||
# === Focus Navigation ===
|
||||
# Focus Next Window
|
||||
bind=SUPER,Tab,focusstack,next
|
||||
# Focus Previous Window
|
||||
bind=SUPER+SHIFT,Tab,focusstack,prev
|
||||
# Focus Left
|
||||
bind=SUPER,Left,focusdir,left
|
||||
# Focus Right
|
||||
bind=SUPER,Right,focusdir,right
|
||||
# Focus Up
|
||||
bind=SUPER,Up,focusdir,up
|
||||
# Focus Down
|
||||
bind=SUPER,Down,focusdir,down
|
||||
|
||||
# === Window Movement ===
|
||||
# Move Window Left
|
||||
bind=SUPER+SHIFT,Left,exchange_client,left
|
||||
# Move Window Right
|
||||
bind=SUPER+SHIFT,Right,exchange_client,right
|
||||
# Move Window Up
|
||||
bind=SUPER+SHIFT,Up,exchange_client,up
|
||||
# Move Window Down
|
||||
bind=SUPER+SHIFT,Down,exchange_client,down
|
||||
|
||||
# === Monitor Navigation ===
|
||||
# Focus Monitor Left
|
||||
bind=SUPER+ALT,Left,focusmon,left
|
||||
# Focus Monitor Right
|
||||
bind=SUPER+ALT,Right,focusmon,right
|
||||
# Move to Monitor Left
|
||||
bind=SUPER+ALT+SHIFT,Left,tagmon,left
|
||||
# Move to Monitor Right
|
||||
bind=SUPER+ALT+SHIFT,Right,tagmon,right
|
||||
|
||||
# === Layout ===
|
||||
# Cycle Layout
|
||||
bind=SUPER,j,switch_layout
|
||||
# Increase Gaps
|
||||
bind=SUPER+SHIFT,equal,incgaps,1
|
||||
# Decrease Gaps
|
||||
bind=SUPER+SHIFT,minus,incgaps,-1
|
||||
|
||||
# === Tags (1-9): view tag ===
|
||||
# View Tag 1
|
||||
bind=SUPER,1,view,1
|
||||
# View Tag 2
|
||||
bind=SUPER,2,view,2
|
||||
# View Tag 3
|
||||
bind=SUPER,3,view,3
|
||||
# View Tag 4
|
||||
bind=SUPER,4,view,4
|
||||
# View Tag 5
|
||||
bind=SUPER,5,view,5
|
||||
# View Tag 6
|
||||
bind=SUPER,6,view,6
|
||||
# View Tag 7
|
||||
bind=SUPER,7,view,7
|
||||
# View Tag 8
|
||||
bind=SUPER,8,view,8
|
||||
# View Tag 9
|
||||
bind=SUPER,9,view,9
|
||||
|
||||
# === Tags (1-9): move focused window to tag ===
|
||||
# Move to Tag 1
|
||||
bind=SUPER+SHIFT,1,tag,1
|
||||
# Move to Tag 2
|
||||
bind=SUPER+SHIFT,2,tag,2
|
||||
# Move to Tag 3
|
||||
bind=SUPER+SHIFT,3,tag,3
|
||||
# Move to Tag 4
|
||||
bind=SUPER+SHIFT,4,tag,4
|
||||
# Move to Tag 5
|
||||
bind=SUPER+SHIFT,5,tag,5
|
||||
# Move to Tag 6
|
||||
bind=SUPER+SHIFT,6,tag,6
|
||||
# Move to Tag 7
|
||||
bind=SUPER+SHIFT,7,tag,7
|
||||
# Move to Tag 8
|
||||
bind=SUPER+SHIFT,8,tag,8
|
||||
# Move to Tag 9
|
||||
bind=SUPER+SHIFT,9,tag,9
|
||||
|
||||
# === Touchpad Gestures ===
|
||||
# Syntax: gesturebind=MODIFIERS,DIRECTION,FINGERS,COMMAND,PARAMETERS
|
||||
# 3-finger horizontal swipe: switch between occupied workspaces
|
||||
gesturebind=none,left,3,viewtoleft_have_client
|
||||
gesturebind=none,right,3,viewtoright_have_client
|
||||
@@ -0,0 +1,6 @@
|
||||
# Auto-generated by DMS. Overwritten by matugen (dms/colors.conf).
|
||||
# Remove `source=./dms/colors.conf` from config.conf to override manually.
|
||||
|
||||
bordercolor = 0x595959ff
|
||||
focuscolor = 0x8ab4f8ff
|
||||
urgentcolor = 0xff5555ff
|
||||
@@ -0,0 +1,8 @@
|
||||
# Auto-generated by DMS. Regenerated from DMS settings (dms/layout.conf).
|
||||
|
||||
border_radius=12
|
||||
gappih=5
|
||||
gappiv=5
|
||||
gappoh=5
|
||||
gappov=5
|
||||
borderpx=2
|
||||
@@ -0,0 +1,18 @@
|
||||
# DankMaterialShell — MangoWM configuration (managed by `dms setup`)
|
||||
# Keybinds, colors, layout, outputs, cursor and window rules are pulled from the
|
||||
# ./dms fragments below. Add your own binds/rules here; they sit alongside DMS's.
|
||||
|
||||
env=XDG_CURRENT_DESKTOP,mango
|
||||
env=XDG_SESSION_TYPE,wayland
|
||||
|
||||
# exec_once runs only at startup. Do NOT use exec= for the shell: mango re-runs
|
||||
# every exec= on each config reload, and DMS reloads the config, which would
|
||||
# spawn a new shell on every reload.
|
||||
exec_once=dms run
|
||||
|
||||
source=./dms/colors.conf
|
||||
source=./dms/layout.conf
|
||||
source=./dms/cursor.conf
|
||||
source=./dms/outputs.conf
|
||||
source=./dms/windowrules.conf
|
||||
source=./dms/binds.conf
|
||||
@@ -0,0 +1,15 @@
|
||||
package config
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed embedded/mango.conf
|
||||
var MangoConfig string
|
||||
|
||||
//go:embed embedded/mango-colors.conf
|
||||
var MangoColorsConfig string
|
||||
|
||||
//go:embed embedded/mango-layout.conf
|
||||
var MangoLayoutConfig string
|
||||
|
||||
//go:embed embedded/mango-binds.conf
|
||||
var MangoBindsConfig string
|
||||
@@ -35,6 +35,7 @@ type WindowManager int
|
||||
const (
|
||||
WindowManagerHyprland WindowManager = iota
|
||||
WindowManagerNiri
|
||||
WindowManagerMango
|
||||
)
|
||||
|
||||
type Terminal int
|
||||
|
||||
@@ -112,6 +112,11 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
||||
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Mango-specific tools (dwl-based, uses xwayland-satellite like niri)
|
||||
if wm == deps.WindowManagerMango {
|
||||
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, a.detectMatugen())
|
||||
dependencies = append(dependencies, a.detectDgop())
|
||||
|
||||
@@ -172,6 +177,11 @@ func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
|
||||
return exec.Command("pacman", "-Si", pkg).Run() == nil
|
||||
}
|
||||
|
||||
// isSonameProvides reports whether dep is a shared-library soname
|
||||
func isSonameProvides(dep string) bool {
|
||||
return strings.HasSuffix(dep, ".so") || strings.Contains(dep, ".so.")
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
@@ -199,6 +209,9 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = a.getNiriMapping(variants["niri"])
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerMango:
|
||||
packages["mango"] = a.getMangoMapping(variants["mango"])
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
return packages
|
||||
@@ -222,6 +235,13 @@ func (a *ArchDistribution) getNiriMapping(variant deps.PackageVariant) PackageMa
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getMangoMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "mangowm-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
return PackageMapping{Name: "mangowm", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR}
|
||||
@@ -724,7 +744,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
continue
|
||||
}
|
||||
seen[dep] = true
|
||||
if a.isInSystemRepo(dep) {
|
||||
if isSonameProvides(dep) || a.isInSystemRepo(dep) {
|
||||
systemPkgs = append(systemPkgs, dep)
|
||||
} else {
|
||||
aurPkgs = append(aurPkgs, dep)
|
||||
|
||||
@@ -337,6 +337,36 @@ func (b *BaseDistribution) detectWindowManager(wm deps.WindowManager) deps.Depen
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
case deps.WindowManagerMango:
|
||||
status := deps.StatusMissing
|
||||
variant := deps.VariantStable
|
||||
version := ""
|
||||
|
||||
if b.commandExists("mango") {
|
||||
status = deps.StatusInstalled
|
||||
cmd := exec.Command("mango", "-v")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
outStr := string(output)
|
||||
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
|
||||
variant = deps.VariantGit
|
||||
}
|
||||
if versionRegex := regexp.MustCompile(`(\d+\.\d+\.\d+)`); versionRegex.MatchString(outStr) {
|
||||
matches := versionRegex.FindStringSubmatch(outStr)
|
||||
if len(matches) > 1 {
|
||||
version = matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "mango",
|
||||
Status: status,
|
||||
Version: version,
|
||||
Description: "dwl-based dynamic tiling Wayland compositor",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
default:
|
||||
return deps.Dependency{
|
||||
Name: "unknown-wm",
|
||||
|
||||
@@ -77,7 +77,11 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
|
||||
// Common detections using base methods
|
||||
dependencies = append(dependencies, f.detectGit())
|
||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||
wmDep := f.detectWindowManager(wm)
|
||||
if wm == deps.WindowManagerMango {
|
||||
wmDep.Description = "MangoWM (Wayland compositor) — the Terra repo will be enabled automatically to install it"
|
||||
}
|
||||
dependencies = append(dependencies, wmDep)
|
||||
dependencies = append(dependencies, f.detectQuickshell())
|
||||
dependencies = append(dependencies, f.detectDMSGreeter())
|
||||
dependencies = append(dependencies, f.detectXDGPortal())
|
||||
@@ -93,6 +97,11 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Mango-specific tools (dwl-based, uses xwayland-satellite like niri)
|
||||
if wm == deps.WindowManagerMango {
|
||||
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, f.detectMatugen())
|
||||
dependencies = append(dependencies, f.detectDgop())
|
||||
|
||||
@@ -139,6 +148,10 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = f.getNiriMapping(variants["niri"])
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerMango:
|
||||
// mangowm resolves via Terra, enabled automatically by enableTerraRepo.
|
||||
packages["mango"] = PackageMapping{Name: "mangowm", Repository: RepoTypeSystem}
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
return packages
|
||||
@@ -159,7 +172,7 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "sdegler/hyprland"}
|
||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "lionheartp/Hyprland"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||
@@ -297,6 +310,22 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2b: Enable Terra repo for MangoWM (not in Fedora's repos). Must run
|
||||
// before the DNF phase so `mangowm` resolves.
|
||||
if wm == deps.WindowManagerMango {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.25,
|
||||
Step: "Enabling Terra repository for MangoWM...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: "Setting up the Terra repo (fyralabs) to provide mango",
|
||||
}
|
||||
if err := f.enableTerraRepo(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to enable Terra repository: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: System Packages (DNF)
|
||||
if len(dnfPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
@@ -423,6 +452,30 @@ func (f *FedoraDistribution) extractPackageNames(packages []PackageMapping) []st
|
||||
return names
|
||||
}
|
||||
|
||||
// enableTerraRepo registers the persistent Terra repo (via terra-release) so
|
||||
// `mangowm` resolves in the DNF phase. $releasever is single-quoted so dnf, not
|
||||
// the shell, expands it.
|
||||
func (f *FedoraDistribution) enableTerraRepo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
// Skip if Terra is already configured
|
||||
if exec.CommandContext(ctx, "sh", "-c",
|
||||
"rpm -q terra-release >/dev/null 2>&1 || test -f /etc/yum.repos.d/terra.repo").Run() == nil {
|
||||
f.log("Terra repository already configured, skipping enable")
|
||||
return nil
|
||||
}
|
||||
|
||||
f.log("Enabling Terra repository (fyralabs) for mango...")
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
`dnf install -y --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release 2>&1`)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logError("failed to enable Terra repository", err)
|
||||
f.log(fmt.Sprintf("Terra enable output: %s", string(output)))
|
||||
return fmt.Errorf("failed to enable Terra repository: %w", err)
|
||||
}
|
||||
f.log(fmt.Sprintf("Terra repository enabled: %s", string(output)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
enabledRepos := make(map[string]bool)
|
||||
|
||||
|
||||
@@ -106,6 +106,11 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
||||
dependencies = append(dependencies, g.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Mango-specific tools (dwl-based, uses xwayland-satellite like niri)
|
||||
if wm == deps.WindowManagerMango {
|
||||
dependencies = append(dependencies, g.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, g.detectMatugen())
|
||||
dependencies = append(dependencies, g.detectDgop())
|
||||
|
||||
@@ -176,6 +181,10 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = g.getNiriMapping(variants["niri"])
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
||||
case deps.WindowManagerMango:
|
||||
packages["mango"] = g.getMangoMapping(variants["mango"])
|
||||
packages["scenefx"] = PackageMapping{Name: "gui-libs/scenefx", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
||||
}
|
||||
|
||||
return packages
|
||||
@@ -197,6 +206,10 @@ func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMappin
|
||||
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getMangoMapping(_ deps.PackageVariant) PackageMapping {
|
||||
return PackageMapping{Name: "gui-wm/mangowm", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getPrerequisites() []string {
|
||||
return []string{
|
||||
"app-eselect/eselect-repository",
|
||||
|
||||
@@ -680,6 +680,9 @@ func DetectCompositors() []string {
|
||||
if utils.CommandExists("Hyprland") {
|
||||
compositors = append(compositors, "Hyprland")
|
||||
}
|
||||
if utils.CommandExists("mango") {
|
||||
compositors = append(compositors, "mango")
|
||||
}
|
||||
|
||||
return compositors
|
||||
}
|
||||
|
||||
@@ -364,8 +364,10 @@ func (r *Runner) parseWindowManager() (deps.WindowManager, error) {
|
||||
return deps.WindowManagerNiri, nil
|
||||
case "hyprland":
|
||||
return deps.WindowManagerHyprland, nil
|
||||
case "mango", "mangowc":
|
||||
return deps.WindowManagerMango, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid --compositor value %q: must be 'niri' or 'hyprland'", r.cfg.Compositor)
|
||||
return 0, fmt.Errorf("invalid --compositor value %q: must be 'niri', 'hyprland', or 'mango'", r.cfg.Compositor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -397,6 +397,14 @@ func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverri
|
||||
mods, key := m.parseKeyString(bind.Key)
|
||||
command, params := m.parseAction(bind.Action)
|
||||
|
||||
// Description goes on the line ABOVE the bind: mango doesn't strip inline `#`
|
||||
// comments from a value, so a trailing comment would break spawn (extra argv).
|
||||
if bind.Description != "" {
|
||||
sb.WriteString("# ")
|
||||
sb.WriteString(bind.Description)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("bind=")
|
||||
if mods == "" {
|
||||
sb.WriteString("none")
|
||||
@@ -413,11 +421,6 @@ func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverri
|
||||
sb.WriteString(params)
|
||||
}
|
||||
|
||||
if bind.Description != "" {
|
||||
sb.WriteString(" # ")
|
||||
sb.WriteString(bind.Description)
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -216,101 +216,37 @@ func mangowcAutogenerateComment(command, params string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MangoWCParser) getKeybindAtLine(lineNumber int) *MangoWCKeyBinding {
|
||||
func (p *MangoWCParser) getKeybindAtLine(lineNumber int, precedingComment string) *MangoWCKeyBinding {
|
||||
if lineNumber >= len(p.contentLines) {
|
||||
return nil
|
||||
}
|
||||
|
||||
line := p.contentLines[lineNumber]
|
||||
|
||||
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
|
||||
matches := bindMatch.FindStringSubmatch(line)
|
||||
if len(matches) < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
bindType := matches[1]
|
||||
content := matches[2]
|
||||
|
||||
parts := strings.SplitN(content, "#", 2)
|
||||
keys := parts[0]
|
||||
|
||||
var comment string
|
||||
if len(parts) > 1 {
|
||||
comment = strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
if strings.HasPrefix(comment, MangoWCHideComment) {
|
||||
return nil
|
||||
}
|
||||
|
||||
keyFields := strings.SplitN(keys, ",", 4)
|
||||
if len(keyFields) < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
mods := strings.TrimSpace(keyFields[0])
|
||||
key := strings.TrimSpace(keyFields[1])
|
||||
command := strings.TrimSpace(keyFields[2])
|
||||
|
||||
var params string
|
||||
if len(keyFields) > 3 {
|
||||
params = strings.TrimSpace(keyFields[3])
|
||||
}
|
||||
|
||||
if comment == "" {
|
||||
comment = mangowcAutogenerateComment(command, params)
|
||||
}
|
||||
|
||||
var modList []string
|
||||
if mods != "" && !strings.EqualFold(mods, "none") {
|
||||
modstring := mods + string(MangoWCModSeparators[0])
|
||||
p := 0
|
||||
for index, char := range modstring {
|
||||
isModSep := false
|
||||
for _, sep := range MangoWCModSeparators {
|
||||
if char == sep {
|
||||
isModSep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isModSep {
|
||||
if index-p > 1 {
|
||||
modList = append(modList, modstring[p:index])
|
||||
}
|
||||
p = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = bindType
|
||||
|
||||
return &MangoWCKeyBinding{
|
||||
Mods: modList,
|
||||
Key: key,
|
||||
Command: command,
|
||||
Params: params,
|
||||
Comment: comment,
|
||||
}
|
||||
return p.getKeybindAtLineContent(p.contentLines[lineNumber], precedingComment)
|
||||
}
|
||||
|
||||
func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
|
||||
var keybinds []MangoWCKeyBinding
|
||||
var pendingComment string
|
||||
|
||||
for lineNumber := 0; lineNumber < len(p.contentLines); lineNumber++ {
|
||||
line := p.contentLines[lineNumber]
|
||||
if line == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
|
||||
trimmed := strings.TrimSpace(p.contentLines[lineNumber])
|
||||
if trimmed == "" {
|
||||
pendingComment = ""
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(trimmed, "bind") {
|
||||
pendingComment = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(strings.TrimSpace(line), "bind") {
|
||||
continue
|
||||
}
|
||||
|
||||
keybind := p.getKeybindAtLine(lineNumber)
|
||||
keybind := p.getKeybindAtLine(lineNumber, pendingComment)
|
||||
if keybind != nil {
|
||||
keybinds = append(keybinds, *keybind)
|
||||
}
|
||||
pendingComment = ""
|
||||
}
|
||||
|
||||
return keybinds
|
||||
@@ -459,21 +395,35 @@ func (p *MangoWCParser) parseFileWithSource(filePath string) ([]MangoWCKeyBindin
|
||||
p.currentSource = absPath
|
||||
|
||||
var keybinds []MangoWCKeyBinding
|
||||
var pendingComment string
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
for lineNum, line := range lines {
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if trimmed == "" {
|
||||
pendingComment = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(trimmed, "source") {
|
||||
p.handleSource(trimmed, filepath.Dir(absPath), &keybinds)
|
||||
pendingComment = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(trimmed, "bind") {
|
||||
pendingComment = ""
|
||||
continue
|
||||
}
|
||||
|
||||
kb := p.getKeybindAtLineContent(line, lineNum)
|
||||
kb := p.getKeybindAtLineContent(line, pendingComment)
|
||||
pendingComment = ""
|
||||
if kb == nil {
|
||||
continue
|
||||
}
|
||||
@@ -529,7 +479,10 @@ func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyB
|
||||
return keybinds
|
||||
}
|
||||
|
||||
func (p *MangoWCParser) getKeybindAtLineContent(line string, _ int) *MangoWCKeyBinding {
|
||||
// getKeybindAtLineContent parses one `bind=` line. precedingComment (a `# ...`
|
||||
// line directly above) is the description: mango feeds inline comments to spawn
|
||||
// as argv, so DMS keeps descriptions on the line above; inline `#` is a fallback.
|
||||
func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment string) *MangoWCKeyBinding {
|
||||
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
|
||||
matches := bindMatch.FindStringSubmatch(line)
|
||||
if len(matches) < 3 {
|
||||
@@ -544,6 +497,9 @@ func (p *MangoWCParser) getKeybindAtLineContent(line string, _ int) *MangoWCKeyB
|
||||
if len(parts) > 1 {
|
||||
comment = strings.TrimSpace(parts[1])
|
||||
}
|
||||
if comment == "" {
|
||||
comment = strings.TrimSpace(precedingComment)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(comment, MangoWCHideComment) {
|
||||
return nil
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewMangoWCParser("")
|
||||
parser.contentLines = []string{tt.line}
|
||||
result := parser.getKeybindAtLine(0)
|
||||
result := parser.getKeybindAtLine(0, "")
|
||||
|
||||
if tt.expected == nil {
|
||||
if result != nil {
|
||||
@@ -421,7 +421,7 @@ func TestMangoWCInvalidBindLines(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewMangoWCParser("")
|
||||
parser.contentLines = []string{tt.line}
|
||||
result := parser.getKeybindAtLine(0)
|
||||
result := parser.getKeybindAtLine(0, "")
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("expected nil for invalid line, got %+v", result)
|
||||
|
||||
@@ -103,15 +103,7 @@ func (m Model) updateDeployingConfigsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) deployConfigurations() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// Determine the selected window manager
|
||||
var wm deps.WindowManager
|
||||
switch m.selectedWM {
|
||||
case 0:
|
||||
wm = deps.WindowManagerNiri
|
||||
case 1:
|
||||
wm = deps.WindowManagerHyprland
|
||||
default:
|
||||
wm = deps.WindowManagerNiri
|
||||
}
|
||||
wm := m.selectedWindowManager()
|
||||
|
||||
// Determine the selected terminal
|
||||
var terminal deps.Terminal
|
||||
@@ -288,7 +280,8 @@ func (m Model) checkExistingConfigurations() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
var configs []ExistingConfigInfo
|
||||
|
||||
if m.selectedWM == 0 {
|
||||
switch m.selectedWindowManager() {
|
||||
case deps.WindowManagerNiri:
|
||||
niriPath := filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl")
|
||||
niriExists := false
|
||||
if _, err := os.Stat(niriPath); err == nil {
|
||||
@@ -299,7 +292,23 @@ func (m Model) checkExistingConfigurations() tea.Cmd {
|
||||
Path: niriPath,
|
||||
Exists: niriExists,
|
||||
})
|
||||
} else {
|
||||
case deps.WindowManagerMango:
|
||||
mangoConfPath := filepath.Join(os.Getenv("HOME"), ".config", "mango", "config.conf")
|
||||
mangoMainPath := filepath.Join(os.Getenv("HOME"), ".config", "mango", "mango.conf")
|
||||
mangoPath := mangoConfPath
|
||||
mangoExists := false
|
||||
if _, err := os.Stat(mangoConfPath); err == nil {
|
||||
mangoExists = true
|
||||
} else if _, err := os.Stat(mangoMainPath); err == nil {
|
||||
mangoPath = mangoMainPath
|
||||
mangoExists = true
|
||||
}
|
||||
configs = append(configs, ExistingConfigInfo{
|
||||
ConfigType: "Mango",
|
||||
Path: mangoPath,
|
||||
Exists: mangoExists,
|
||||
})
|
||||
default:
|
||||
hyprlandLuaPath := filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.lua")
|
||||
hyprlandConfPath := filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf")
|
||||
hyprlandPath := hyprlandLuaPath
|
||||
|
||||
@@ -209,12 +209,7 @@ func (m Model) installPackages() tea.Cmd {
|
||||
}
|
||||
|
||||
// Convert TUI selection to deps enum
|
||||
var wm deps.WindowManager
|
||||
if m.selectedWM == 0 {
|
||||
wm = deps.WindowManagerNiri
|
||||
} else {
|
||||
wm = deps.WindowManagerHyprland
|
||||
}
|
||||
wm := m.selectedWindowManager()
|
||||
|
||||
installerProgressChan := make(chan distros.InstallProgressMsg, 100)
|
||||
|
||||
@@ -245,8 +240,11 @@ func (m Model) installPackages() tea.Cmd {
|
||||
}
|
||||
if greeterSelected {
|
||||
compositorName := "niri"
|
||||
if m.selectedWM == 1 {
|
||||
switch m.selectedWindowManager() {
|
||||
case deps.WindowManagerHyprland:
|
||||
compositorName = "Hyprland"
|
||||
case deps.WindowManagerMango:
|
||||
compositorName = "mango"
|
||||
}
|
||||
m.packageProgressChan <- packageInstallProgressMsg{
|
||||
progress: 0.92,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
@@ -65,7 +66,7 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.skipGentooUseFlags = !m.skipGentooUseFlags
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.selectedWM == 1 {
|
||||
if m.selectedWindowManager() == deps.WindowManagerHyprland {
|
||||
return m, m.checkGCCVersion()
|
||||
}
|
||||
return m.enterAuthPhase()
|
||||
|
||||
@@ -199,8 +199,21 @@ func (m Model) viewInstallComplete() string {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
wm := m.selectedWindowManager()
|
||||
|
||||
// mango launches DMS via `exec_once=dms run` (not a systemd session target)
|
||||
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
|
||||
switch wm {
|
||||
case deps.WindowManagerNiri:
|
||||
loginHint = "If you do not have a greeter, login with \"niri-session\""
|
||||
case deps.WindowManagerHyprland:
|
||||
loginHint = "If you do not have a greeter, login with \"Hyprland\""
|
||||
case deps.WindowManagerMango:
|
||||
loginHint = "If you do not have a greeter, login with \"mango\""
|
||||
}
|
||||
|
||||
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.\n" + loginHint)
|
||||
b.WriteString(info)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
@@ -209,8 +222,13 @@ func (m Model) viewInstallComplete() string {
|
||||
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
|
||||
|
||||
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
|
||||
if wm == deps.WindowManagerMango {
|
||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec_once=dms run' from ~/.config/mango/config.conf") + "\n")
|
||||
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
|
||||
} else {
|
||||
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 != "" {
|
||||
|
||||
@@ -10,6 +10,26 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// windowManagerOptions returns the WM enums in selection-list order (debian omits
|
||||
// Hyprland). selectedWM indexes into this, so all index->WM conversions use it.
|
||||
func (m Model) windowManagerOptions() []deps.WindowManager {
|
||||
opts := []deps.WindowManager{deps.WindowManagerNiri}
|
||||
if m.osInfo == nil || m.osInfo.Distribution.ID != "debian" {
|
||||
opts = append(opts, deps.WindowManagerHyprland)
|
||||
}
|
||||
opts = append(opts, deps.WindowManagerMango)
|
||||
return opts
|
||||
}
|
||||
|
||||
// selectedWindowManager maps the current selectedWM index to its WM enum.
|
||||
func (m Model) selectedWindowManager() deps.WindowManager {
|
||||
opts := m.windowManagerOptions()
|
||||
if m.selectedWM >= 0 && m.selectedWM < len(opts) {
|
||||
return opts[m.selectedWM]
|
||||
}
|
||||
return deps.WindowManagerNiri
|
||||
}
|
||||
|
||||
func (m Model) viewSelectWindowManager() string {
|
||||
var b strings.Builder
|
||||
|
||||
@@ -34,6 +54,11 @@ func (m Model) viewSelectWindowManager() string {
|
||||
}{"Hyprland", "Dynamic tiling Wayland compositor."})
|
||||
}
|
||||
|
||||
options = append(options, struct {
|
||||
name string
|
||||
description string
|
||||
}{"mango", "dwl-based dynamic tiling Wayland compositor."})
|
||||
|
||||
for i, option := range options {
|
||||
if i == m.selectedWM {
|
||||
selected := m.styles.SelectedOption.Render("▶ " + option.name)
|
||||
@@ -152,10 +177,7 @@ func (m Model) updateSelectTerminalState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (m Model) updateSelectWindowManagerState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
maxWMIndex := 1
|
||||
if m.osInfo != nil && m.osInfo.Distribution.ID == "debian" {
|
||||
maxWMIndex = 0
|
||||
}
|
||||
maxWMIndex := len(m.windowManagerOptions()) - 1
|
||||
|
||||
switch keyMsg.String() {
|
||||
case "up":
|
||||
@@ -190,12 +212,7 @@ func (m Model) detectDependencies() tea.Cmd {
|
||||
}
|
||||
|
||||
// Convert TUI selection to deps enum
|
||||
var wm deps.WindowManager
|
||||
if m.selectedWM == 0 {
|
||||
wm = deps.WindowManagerNiri // First option is Niri
|
||||
} else {
|
||||
wm = deps.WindowManagerHyprland // Second option is Hyprland
|
||||
}
|
||||
wm := m.selectedWindowManager()
|
||||
|
||||
var terminal deps.Terminal
|
||||
if m.osInfo != nil && m.osInfo.Distribution.ID == "gentoo" {
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||
)
|
||||
|
||||
// Mango window rules are flat `windowrule=key:value,...` lines. DMS-managed rules
|
||||
// live in dms/windowrules.conf (sourced from config.conf), each preceded by an
|
||||
// `# @id=<id> @name=<name>` comment so they round-trip.
|
||||
|
||||
type MangoWindowRule struct {
|
||||
Source string
|
||||
Fields map[string]string
|
||||
}
|
||||
|
||||
var mangoWindowRuleRegex = regexp.MustCompile(`^windowrule\s*=\s*(.+)$`)
|
||||
var mangoMetaCommentRegex = regexp.MustCompile(`^#\s*@id=(\S*)\s*@name=(.*)$`)
|
||||
|
||||
func parseMangoWindowRuleLine(value string) map[string]string {
|
||||
fields := map[string]string{}
|
||||
for _, pair := range strings.Split(value, ",") {
|
||||
pair = strings.TrimSpace(pair)
|
||||
if pair == "" {
|
||||
continue
|
||||
}
|
||||
colon := strings.Index(pair, ":")
|
||||
if colon < 0 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(pair[:colon])
|
||||
val := strings.TrimSpace(pair[colon+1:])
|
||||
if key != "" {
|
||||
fields[key] = val
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// mangoConfigPath returns the main mango config (config.conf or mango.conf).
|
||||
func mangoConfigPath(configDir string) string {
|
||||
candidates := []string{
|
||||
filepath.Join(configDir, "config.conf"),
|
||||
filepath.Join(configDir, "mango.conf"),
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
func mangoOverridePath(configDir string) string {
|
||||
return filepath.Join(configDir, "dms", "windowrules.conf")
|
||||
}
|
||||
|
||||
// parseMangoRulesFile reads a config file and returns its windowrule= lines.
|
||||
func parseMangoRulesFile(path, source string) []MangoWindowRule {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var rules []MangoWindowRule
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if m := mangoWindowRuleRegex.FindStringSubmatch(trimmed); m != nil {
|
||||
rules = append(rules, MangoWindowRule{Source: source, Fields: parseMangoWindowRuleLine(m[1])})
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
type MangoRulesParseResult struct {
|
||||
Rules []MangoWindowRule
|
||||
DMSRulesIncluded bool
|
||||
DMSStatus *windowrules.DMSRulesStatus
|
||||
}
|
||||
|
||||
func ParseMangoWindowRules(configDir string) (*MangoRulesParseResult, error) {
|
||||
mainPath := mangoConfigPath(configDir)
|
||||
overridePath := mangoOverridePath(configDir)
|
||||
|
||||
var rules []MangoWindowRule
|
||||
rules = append(rules, parseMangoRulesFile(mainPath, "config.conf")...)
|
||||
rules = append(rules, parseMangoRulesFile(overridePath, "dms/windowrules.conf")...)
|
||||
|
||||
included := mangoDMSRulesIncluded(mainPath)
|
||||
return &MangoRulesParseResult{
|
||||
Rules: rules,
|
||||
DMSRulesIncluded: included,
|
||||
DMSStatus: &windowrules.DMSRulesStatus{
|
||||
Exists: fileExists(overridePath),
|
||||
Included: included,
|
||||
Effective: included,
|
||||
ConfigFormat: "conf",
|
||||
StatusMessage: mangoIncludeMessage(included),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func mangoDMSRulesIncluded(mainPath string) bool {
|
||||
data, err := os.ReadFile(mainPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "source") && strings.Contains(trimmed, "dms/windowrules.conf") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mangoIncludeMessage(included bool) string {
|
||||
if included {
|
||||
return "DMS window rules are sourced from config.conf"
|
||||
}
|
||||
return "Add `source=./dms/windowrules.conf` to config.conf to apply DMS window rules"
|
||||
}
|
||||
|
||||
func mangoBoolField(fields map[string]string, key string) *bool {
|
||||
v, ok := fields[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
b := v == "1" || strings.EqualFold(v, "true")
|
||||
return &b
|
||||
}
|
||||
|
||||
func mangoBoolStr(b *bool) string {
|
||||
if b != nil && *b {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func ConvertMangoRulesToWindowRules(mangoRules []MangoWindowRule) []windowrules.WindowRule {
|
||||
result := make([]windowrules.WindowRule, 0, len(mangoRules))
|
||||
for i, mr := range mangoRules {
|
||||
f := mr.Fields
|
||||
actions := windowrules.Actions{
|
||||
OpenFloating: mangoBoolField(f, "isfloating"),
|
||||
OpenFullscreen: mangoBoolField(f, "isfullscreen"),
|
||||
NoBlur: mangoBoolField(f, "noblur"),
|
||||
NoBorder: mangoBoolField(f, "isnoborder"),
|
||||
NoShadow: mangoBoolField(f, "isnoshadow"),
|
||||
NoRounding: mangoBoolField(f, "isnoradius"),
|
||||
NoAnim: mangoBoolField(f, "isnoanimation"),
|
||||
}
|
||||
if tags, ok := f["tags"]; ok {
|
||||
actions.Workspace = tags
|
||||
}
|
||||
if mon, ok := f["monitor"]; ok {
|
||||
actions.Monitor = mon
|
||||
}
|
||||
if w, ok := f["width"]; ok {
|
||||
if h, ok2 := f["height"]; ok2 {
|
||||
actions.Size = w + "x" + h
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, windowrules.WindowRule{
|
||||
ID: fmt.Sprintf("rule_%d", i),
|
||||
Enabled: true,
|
||||
Source: mr.Source,
|
||||
MatchCriteria: windowrules.MatchCriteria{
|
||||
AppID: f["appid"],
|
||||
Title: f["title"],
|
||||
},
|
||||
Actions: actions,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// formatMangoRule serializes a shared WindowRule into a mango windowrule= line.
|
||||
func formatMangoRule(rule windowrules.WindowRule) string {
|
||||
var parts []string
|
||||
add := func(k, v string) {
|
||||
if v != "" {
|
||||
parts = append(parts, k+":"+v)
|
||||
}
|
||||
}
|
||||
|
||||
add("appid", rule.MatchCriteria.AppID)
|
||||
add("title", rule.MatchCriteria.Title)
|
||||
add("tags", rule.Actions.Workspace)
|
||||
add("monitor", rule.Actions.Monitor)
|
||||
|
||||
if rule.Actions.Size != "" {
|
||||
if w, h, ok := splitSize(rule.Actions.Size); ok {
|
||||
add("width", w)
|
||||
add("height", h)
|
||||
}
|
||||
}
|
||||
|
||||
addBool := func(k string, b *bool) {
|
||||
if b != nil {
|
||||
parts = append(parts, k+":"+mangoBoolStr(b))
|
||||
}
|
||||
}
|
||||
addBool("isfloating", rule.Actions.OpenFloating)
|
||||
addBool("isfullscreen", rule.Actions.OpenFullscreen)
|
||||
addBool("noblur", rule.Actions.NoBlur)
|
||||
addBool("isnoborder", rule.Actions.NoBorder)
|
||||
addBool("isnoshadow", rule.Actions.NoShadow)
|
||||
addBool("isnoradius", rule.Actions.NoRounding)
|
||||
addBool("isnoanimation", rule.Actions.NoAnim)
|
||||
|
||||
return "windowrule=" + strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func splitSize(size string) (w, h string, ok bool) {
|
||||
for _, sep := range []string{"x", "X", " "} {
|
||||
if parts := strings.Split(size, sep); len(parts) == 2 {
|
||||
w = strings.TrimSpace(parts[0])
|
||||
h = strings.TrimSpace(parts[1])
|
||||
if _, err := strconv.ParseFloat(w, 64); err == nil {
|
||||
return w, h, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
type MangoWritableProvider struct {
|
||||
configDir string
|
||||
}
|
||||
|
||||
func NewMangoWritableProvider(configDir string) *MangoWritableProvider {
|
||||
return &MangoWritableProvider{configDir: configDir}
|
||||
}
|
||||
|
||||
func (p *MangoWritableProvider) Name() string { return "mango" }
|
||||
|
||||
func (p *MangoWritableProvider) GetOverridePath() string {
|
||||
return mangoOverridePath(p.configDir)
|
||||
}
|
||||
|
||||
func (p *MangoWritableProvider) GetRuleSet() (*windowrules.RuleSet, error) {
|
||||
result, err := ParseMangoWindowRules(p.configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &windowrules.RuleSet{
|
||||
Title: "Mango Window Rules",
|
||||
Provider: "mango",
|
||||
Rules: ConvertMangoRulesToWindowRules(result.Rules),
|
||||
DMSRulesIncluded: result.DMSRulesIncluded,
|
||||
DMSStatus: result.DMSStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *MangoWritableProvider) SetRule(rule windowrules.WindowRule) error {
|
||||
rules, err := p.LoadDMSRules()
|
||||
if err != nil {
|
||||
rules = []windowrules.WindowRule{}
|
||||
}
|
||||
found := false
|
||||
for i, r := range rules {
|
||||
if r.ID == rule.ID {
|
||||
rules[i] = rule
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
return p.writeDMSRules(rules)
|
||||
}
|
||||
|
||||
func (p *MangoWritableProvider) RemoveRule(id string) error {
|
||||
rules, err := p.LoadDMSRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newRules := make([]windowrules.WindowRule, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
if r.ID != id {
|
||||
newRules = append(newRules, r)
|
||||
}
|
||||
}
|
||||
return p.writeDMSRules(newRules)
|
||||
}
|
||||
|
||||
func (p *MangoWritableProvider) ReorderRules(ids []string) error {
|
||||
rules, err := p.LoadDMSRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ruleMap := make(map[string]windowrules.WindowRule, len(rules))
|
||||
for _, r := range rules {
|
||||
ruleMap[r.ID] = r
|
||||
}
|
||||
newRules := make([]windowrules.WindowRule, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if r, ok := ruleMap[id]; ok {
|
||||
newRules = append(newRules, r)
|
||||
delete(ruleMap, id)
|
||||
}
|
||||
}
|
||||
for _, r := range ruleMap {
|
||||
newRules = append(newRules, r)
|
||||
}
|
||||
return p.writeDMSRules(newRules)
|
||||
}
|
||||
|
||||
// LoadDMSRules parses only the DMS override file, preserving @id/@name metadata.
|
||||
func (p *MangoWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error) {
|
||||
data, err := os.ReadFile(p.GetOverridePath())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []windowrules.WindowRule{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rules []windowrules.WindowRule
|
||||
var curID, curName string
|
||||
idx := 0
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if m := mangoMetaCommentRegex.FindStringSubmatch(trimmed); m != nil {
|
||||
curID = m[1]
|
||||
curName = strings.TrimSpace(m[2])
|
||||
continue
|
||||
}
|
||||
if m := mangoWindowRuleRegex.FindStringSubmatch(trimmed); m != nil {
|
||||
converted := ConvertMangoRulesToWindowRules([]MangoWindowRule{{Source: "dms/windowrules.conf", Fields: parseMangoWindowRuleLine(m[1])}})
|
||||
wr := converted[0]
|
||||
if curID != "" {
|
||||
wr.ID = curID
|
||||
} else {
|
||||
wr.ID = fmt.Sprintf("rule_%d", idx)
|
||||
}
|
||||
wr.Name = curName
|
||||
rules = append(rules, wr)
|
||||
curID, curName = "", ""
|
||||
idx++
|
||||
}
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (p *MangoWritableProvider) writeDMSRules(rules []windowrules.WindowRule) error {
|
||||
overridePath := p.GetOverridePath()
|
||||
if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("# Auto-generated by DMS - DMS-managed mango window rules\n\n")
|
||||
for i, r := range rules {
|
||||
id := r.ID
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("rule_%d", i)
|
||||
}
|
||||
fmt.Fprintf(&sb, "# @id=%s @name=%s\n", id, r.Name)
|
||||
sb.WriteString(formatMangoRule(r))
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
|
||||
return os.WriteFile(overridePath, []byte(sb.String()), 0o644)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||
)
|
||||
|
||||
func TestParseMangoWindowRuleLine(t *testing.T) {
|
||||
fields := parseMangoWindowRuleLine("appid:firefox,title:Gmail,isfloating:1,tags:2,monitor:HDMI-A-1")
|
||||
if fields["appid"] != "firefox" {
|
||||
t.Errorf("appid = %q, want firefox", fields["appid"])
|
||||
}
|
||||
if fields["title"] != "Gmail" {
|
||||
t.Errorf("title = %q, want Gmail", fields["title"])
|
||||
}
|
||||
if fields["isfloating"] != "1" {
|
||||
t.Errorf("isfloating = %q, want 1", fields["isfloating"])
|
||||
}
|
||||
if fields["tags"] != "2" {
|
||||
t.Errorf("tags = %q, want 2", fields["tags"])
|
||||
}
|
||||
if fields["monitor"] != "HDMI-A-1" {
|
||||
t.Errorf("monitor = %q, want HDMI-A-1", fields["monitor"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertMangoRulesToWindowRules(t *testing.T) {
|
||||
mangoRules := []MangoWindowRule{
|
||||
{Source: "config.conf", Fields: parseMangoWindowRuleLine("appid:discord,tags:9,isfloating:1,noblur:1")},
|
||||
}
|
||||
rules := ConvertMangoRulesToWindowRules(mangoRules)
|
||||
if len(rules) != 1 {
|
||||
t.Fatalf("got %d rules, want 1", len(rules))
|
||||
}
|
||||
r := rules[0]
|
||||
if r.MatchCriteria.AppID != "discord" {
|
||||
t.Errorf("AppID = %q, want discord", r.MatchCriteria.AppID)
|
||||
}
|
||||
if r.Actions.Workspace != "9" {
|
||||
t.Errorf("Workspace = %q, want 9", r.Actions.Workspace)
|
||||
}
|
||||
if r.Actions.OpenFloating == nil || !*r.Actions.OpenFloating {
|
||||
t.Errorf("OpenFloating = %v, want true", r.Actions.OpenFloating)
|
||||
}
|
||||
if r.Actions.NoBlur == nil || !*r.Actions.NoBlur {
|
||||
t.Errorf("NoBlur = %v, want true", r.Actions.NoBlur)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoSetAndLoadRoundTrip(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
provider := NewMangoWritableProvider(tmpDir)
|
||||
|
||||
floating := true
|
||||
rule := windowrules.WindowRule{
|
||||
ID: "rule_test",
|
||||
Name: "Float Discord",
|
||||
Enabled: true,
|
||||
MatchCriteria: windowrules.MatchCriteria{
|
||||
AppID: "discord",
|
||||
},
|
||||
Actions: windowrules.Actions{
|
||||
OpenFloating: &floating,
|
||||
Workspace: "9",
|
||||
Size: "1000x900",
|
||||
},
|
||||
}
|
||||
|
||||
if err := provider.SetRule(rule); err != nil {
|
||||
t.Fatalf("SetRule: %v", err)
|
||||
}
|
||||
|
||||
expectedPath := filepath.Join(tmpDir, "dms", "windowrules.conf")
|
||||
if _, err := os.Stat(expectedPath); err != nil {
|
||||
t.Fatalf("override file not written: %v", err)
|
||||
}
|
||||
|
||||
loaded, err := provider.LoadDMSRules()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadDMSRules: %v", err)
|
||||
}
|
||||
if len(loaded) != 1 {
|
||||
t.Fatalf("got %d rules, want 1", len(loaded))
|
||||
}
|
||||
got := loaded[0]
|
||||
if got.ID != "rule_test" {
|
||||
t.Errorf("ID = %q, want rule_test", got.ID)
|
||||
}
|
||||
if got.Name != "Float Discord" {
|
||||
t.Errorf("Name = %q, want 'Float Discord'", got.Name)
|
||||
}
|
||||
if got.MatchCriteria.AppID != "discord" {
|
||||
t.Errorf("AppID = %q, want discord", got.MatchCriteria.AppID)
|
||||
}
|
||||
if got.Actions.Workspace != "9" {
|
||||
t.Errorf("Workspace = %q, want 9", got.Actions.Workspace)
|
||||
}
|
||||
if got.Actions.Size != "1000x900" {
|
||||
t.Errorf("Size = %q, want 1000x900", got.Actions.Size)
|
||||
}
|
||||
if got.Actions.OpenFloating == nil || !*got.Actions.OpenFloating {
|
||||
t.Errorf("OpenFloating = %v, want true", got.Actions.OpenFloating)
|
||||
}
|
||||
|
||||
// Remove and confirm empty.
|
||||
if err := provider.RemoveRule("rule_test"); err != nil {
|
||||
t.Fatalf("RemoveRule: %v", err)
|
||||
}
|
||||
loaded, _ = provider.LoadDMSRules()
|
||||
if len(loaded) != 0 {
|
||||
t.Errorf("after remove got %d rules, want 0", len(loaded))
|
||||
}
|
||||
}
|
||||
@@ -490,6 +490,9 @@ Singleton {
|
||||
},
|
||||
"dwl": {
|
||||
"cursorHideTimeout": 0
|
||||
},
|
||||
"mango": {
|
||||
"cursorHideTimeout": 0
|
||||
}
|
||||
})
|
||||
property var availableCursorThemes: ["System Default"]
|
||||
@@ -1222,6 +1225,8 @@ Singleton {
|
||||
HyprlandService.generateLayoutConfig();
|
||||
if (CompositorService.isDwl && typeof DwlService !== "undefined")
|
||||
DwlService.generateLayoutConfig();
|
||||
if (CompositorService.isMango && typeof MangoService !== "undefined")
|
||||
MangoService.generateLayoutConfig();
|
||||
}
|
||||
|
||||
function applyStoredIconTheme() {
|
||||
@@ -2235,7 +2240,7 @@ Singleton {
|
||||
|
||||
function getFilteredScreens(componentId) {
|
||||
var prefs = screenPreferences && screenPreferences[componentId] || ["all"];
|
||||
if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) {
|
||||
if (!prefs || prefs.length === 0 || prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) {
|
||||
return Quickshell.screens;
|
||||
}
|
||||
var filtered = Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
|
||||
@@ -2446,6 +2451,10 @@ Singleton {
|
||||
DwlService.generateCursorConfig();
|
||||
return;
|
||||
}
|
||||
if (CompositorService.isMango && typeof MangoService !== "undefined") {
|
||||
MangoService.generateCursorConfig();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function updateXResources() {
|
||||
|
||||
@@ -340,6 +340,9 @@ Item {
|
||||
if (CompositorService.isDwl && DwlService.activeOutput) {
|
||||
return DwlService.activeOutput;
|
||||
}
|
||||
if (CompositorService.isMango && MangoService.activeOutput) {
|
||||
return MangoService.activeOutput;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -322,6 +322,8 @@ Item {
|
||||
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-1";
|
||||
else if (CompositorService.isDwl)
|
||||
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
|
||||
else if (CompositorService.isMango)
|
||||
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
|
||||
Qt.openUrlExternally(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ Item {
|
||||
title: I18n.tr("Multi-Monitor", "greeter feature card title")
|
||||
description: I18n.tr("Per-screen config", "greeter feature card description")
|
||||
onClicked: {
|
||||
const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl;
|
||||
const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango;
|
||||
PopoutService.openSettingsWithTab(hasDisplayConfig ? "display_config" : "display_widgets");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ Rectangle {
|
||||
"text": I18n.tr("Window Rules"),
|
||||
"icon": "select_window",
|
||||
"tabIndex": 28,
|
||||
"hyprlandNiriOnly": true
|
||||
"windowRulesCapable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -370,6 +370,8 @@ Rectangle {
|
||||
return false;
|
||||
if (item.hyprlandNiriOnly && !CompositorService.isNiri && !CompositorService.isHyprland)
|
||||
return false;
|
||||
if (item.windowRulesCapable && !CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
|
||||
return false;
|
||||
if (item.niriOnly && !CompositorService.isNiri)
|
||||
return false;
|
||||
if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
|
||||
|
||||
@@ -12,6 +12,7 @@ FloatingWindow {
|
||||
property bool isEditMode: editingRule !== null
|
||||
property bool isNiri: CompositorService.isNiri
|
||||
property bool isHyprland: CompositorService.isHyprland
|
||||
property bool isMango: CompositorService.isMango
|
||||
property bool submitting: false
|
||||
property var targetWindow: null
|
||||
|
||||
@@ -95,6 +96,14 @@ FloatingWindow {
|
||||
moveInput.text = "";
|
||||
monitorInput.text = "";
|
||||
hyprWorkspaceInput.text = "";
|
||||
mangoTagsInput.text = "";
|
||||
mangoMonitorInput.text = "";
|
||||
mangoSizeInput.text = "";
|
||||
mangoNoBlurToggle.checked = false;
|
||||
mangoNoBorderToggle.checked = false;
|
||||
mangoNoShadowToggle.checked = false;
|
||||
mangoNoRoundingToggle.checked = false;
|
||||
mangoNoAnimToggle.checked = false;
|
||||
}
|
||||
|
||||
function show(window) {
|
||||
@@ -103,7 +112,10 @@ FloatingWindow {
|
||||
resetForm();
|
||||
if (targetWindow) {
|
||||
nameInput.text = targetWindow.appId || "";
|
||||
appIdInput.text = targetWindow.appId ? "^" + targetWindow.appId + "$" : "";
|
||||
if (targetWindow.appId)
|
||||
appIdInput.text = isMango ? targetWindow.appId : "^" + targetWindow.appId + "$";
|
||||
else
|
||||
appIdInput.text = "";
|
||||
}
|
||||
visible = true;
|
||||
Qt.callLater(() => nameInput.forceActiveFocus());
|
||||
@@ -209,6 +221,15 @@ FloatingWindow {
|
||||
moveInput.text = actions.move || "";
|
||||
monitorInput.text = actions.monitor || "";
|
||||
hyprWorkspaceInput.text = actions.workspace || "";
|
||||
|
||||
mangoTagsInput.text = actions.workspace || "";
|
||||
mangoMonitorInput.text = actions.monitor || "";
|
||||
mangoSizeInput.text = actions.size || "";
|
||||
mangoNoBlurToggle.checked = actions.noblur || false;
|
||||
mangoNoBorderToggle.checked = actions.noborder || false;
|
||||
mangoNoShadowToggle.checked = actions.noshadow || false;
|
||||
mangoNoRoundingToggle.checked = actions.norounding || false;
|
||||
mangoNoAnimToggle.checked = actions.noanim || false;
|
||||
}
|
||||
|
||||
function showEdit(rule) {
|
||||
@@ -387,6 +408,25 @@ FloatingWindow {
|
||||
actions.workspace = hyprWorkspaceInput.text.trim();
|
||||
}
|
||||
|
||||
if (isMango) {
|
||||
if (mangoTagsInput.text.trim())
|
||||
actions.workspace = mangoTagsInput.text.trim();
|
||||
if (mangoMonitorInput.text.trim())
|
||||
actions.monitor = mangoMonitorInput.text.trim();
|
||||
if (mangoSizeInput.text.trim())
|
||||
actions.size = mangoSizeInput.text.trim();
|
||||
if (mangoNoBlurToggle.checked)
|
||||
actions.noblur = true;
|
||||
if (mangoNoBorderToggle.checked)
|
||||
actions.noborder = true;
|
||||
if (mangoNoShadowToggle.checked)
|
||||
actions.noshadow = true;
|
||||
if (mangoNoRoundingToggle.checked)
|
||||
actions.norounding = true;
|
||||
if (mangoNoAnimToggle.checked)
|
||||
actions.noanim = true;
|
||||
}
|
||||
|
||||
const name = nameInput.text.trim() || matchCriteria.appId || I18n.tr("Rule");
|
||||
const compositor = CompositorService.compositor;
|
||||
|
||||
@@ -411,6 +451,8 @@ FloatingWindow {
|
||||
return;
|
||||
if (shouldValidate)
|
||||
NiriService.validate();
|
||||
if (CompositorService.isMango)
|
||||
MangoService.reloadConfig();
|
||||
root.ruleSubmitted();
|
||||
root.hide();
|
||||
});
|
||||
@@ -422,6 +464,8 @@ FloatingWindow {
|
||||
return;
|
||||
if (shouldValidate)
|
||||
NiriService.validate();
|
||||
if (CompositorService.isMango)
|
||||
MangoService.reloadConfig();
|
||||
root.ruleSubmitted();
|
||||
root.hide();
|
||||
});
|
||||
@@ -664,7 +708,7 @@ FloatingWindow {
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: isNiri ? I18n.tr("App ID regex (e.g. ^firefox$)") : I18n.tr("Class regex (e.g. ^firefox$)")
|
||||
placeholderText: isMango ? I18n.tr("App ID (e.g. firefox)") : isHyprland ? I18n.tr("Class regex (e.g. ^firefox$)") : I18n.tr("App ID regex (e.g. ^firefox$)")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
}
|
||||
@@ -682,7 +726,7 @@ FloatingWindow {
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: I18n.tr("Title regex (optional)")
|
||||
placeholderText: isMango ? I18n.tr("Title (optional)") : I18n.tr("Title regex (optional)")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
}
|
||||
@@ -702,7 +746,7 @@ FloatingWindow {
|
||||
onClicked: {
|
||||
if (!root.targetWindow?.title)
|
||||
return;
|
||||
titleInput.text = "^" + root.targetWindow.title + "$";
|
||||
titleInput.text = isMango ? root.targetWindow.title : "^" + root.targetWindow.title + "$";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -807,10 +851,12 @@ FloatingWindow {
|
||||
|
||||
SectionHeader {
|
||||
title: I18n.tr("Match Conditions")
|
||||
visible: isNiri || isHyprland
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
visible: isNiri || isHyprland
|
||||
text: I18n.tr("Optional state-based conditions applied to the first match.")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
@@ -820,6 +866,7 @@ FloatingWindow {
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: isNiri || isHyprland
|
||||
|
||||
MatchCond {
|
||||
id: condFloating
|
||||
@@ -892,6 +939,7 @@ FloatingWindow {
|
||||
CheckboxRow {
|
||||
id: maximizedToggle
|
||||
label: I18n.tr("Maximize")
|
||||
visible: !isMango
|
||||
}
|
||||
CheckboxRow {
|
||||
id: fullscreenToggle
|
||||
@@ -912,7 +960,7 @@ FloatingWindow {
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: true
|
||||
visible: isNiri || isHyprland
|
||||
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
@@ -1031,11 +1079,13 @@ FloatingWindow {
|
||||
|
||||
SectionHeader {
|
||||
title: I18n.tr("Dynamic Properties")
|
||||
visible: isNiri || isHyprland
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: isNiri || isHyprland
|
||||
|
||||
CheckboxRow {
|
||||
id: opacityEnabled
|
||||
@@ -1154,6 +1204,7 @@ FloatingWindow {
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: isNiri || isHyprland
|
||||
|
||||
CheckboxRow {
|
||||
id: cornerRadiusEnabled
|
||||
@@ -1352,11 +1403,13 @@ FloatingWindow {
|
||||
|
||||
SectionHeader {
|
||||
title: I18n.tr("Size Constraints")
|
||||
visible: isNiri || isHyprland
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: isNiri || isHyprland
|
||||
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingM * 3) / 4
|
||||
@@ -1639,6 +1692,131 @@ FloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
title: I18n.tr("Mango Options")
|
||||
visible: isMango
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
visible: isMango
|
||||
|
||||
CheckboxRow {
|
||||
id: mangoNoBlurToggle
|
||||
label: I18n.tr("No Blur")
|
||||
}
|
||||
CheckboxRow {
|
||||
id: mangoNoBorderToggle
|
||||
label: I18n.tr("No Border")
|
||||
}
|
||||
CheckboxRow {
|
||||
id: mangoNoShadowToggle
|
||||
label: I18n.tr("No Shadow")
|
||||
}
|
||||
CheckboxRow {
|
||||
id: mangoNoRoundingToggle
|
||||
label: I18n.tr("No Rounding")
|
||||
}
|
||||
CheckboxRow {
|
||||
id: mangoNoAnimToggle
|
||||
label: I18n.tr("No Anim")
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: isMango
|
||||
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Tags")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
InputField {
|
||||
width: parent.width
|
||||
hasFocus: mangoTagsInput.activeFocus
|
||||
DankTextField {
|
||||
id: mangoTagsInput
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: "1"
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Monitor")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
InputField {
|
||||
width: parent.width
|
||||
hasFocus: mangoMonitorInput.activeFocus
|
||||
DankTextField {
|
||||
id: mangoMonitorInput
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: "HDMI-A-1"
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: isMango
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Size")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
InputField {
|
||||
width: parent.width
|
||||
hasFocus: mangoSizeInput.activeFocus
|
||||
DankTextField {
|
||||
id: mangoSizeInput
|
||||
anchors.fill: parent
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: "800x600"
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingM
|
||||
|
||||
@@ -110,6 +110,8 @@ Item {
|
||||
focusedScreenName = focusedWs?.monitor?.name || "";
|
||||
} else if (CompositorService.isDwl && DwlService.activeOutput) {
|
||||
focusedScreenName = DwlService.activeOutput;
|
||||
} else if (CompositorService.isMango && MangoService.activeOutput) {
|
||||
focusedScreenName = MangoService.activeOutput;
|
||||
}
|
||||
|
||||
if (!focusedScreenName && barVariants.instances.length > 0) {
|
||||
@@ -139,6 +141,8 @@ Item {
|
||||
focusedScreenName = focusedWs?.monitor?.name || "";
|
||||
} else if (CompositorService.isDwl && DwlService.activeOutput) {
|
||||
focusedScreenName = DwlService.activeOutput;
|
||||
} else if (CompositorService.isMango && MangoService.activeOutput) {
|
||||
focusedScreenName = MangoService.activeOutput;
|
||||
}
|
||||
|
||||
if (!focusedScreenName && barVariants.instances.length > 0) {
|
||||
|
||||
@@ -29,6 +29,7 @@ Item {
|
||||
readonly property real _frameEdgeFloorInset: (SettingsData.frameEnabled && _usesFrameBarChrome) ? Math.max(0, SettingsData.frameThickness - _edgeBaseMargin) : 0
|
||||
readonly property bool _barIsVertical: _hasBarWindow ? barWindow.isVertical : false
|
||||
readonly property string _barScreenName: _hasBarWindow ? (barWindow.screenName || "") : ""
|
||||
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
|
||||
readonly property bool hasAdjacentTopBarLive: _hasBarWindow && barWindow.hasAdjacentTopBar
|
||||
readonly property bool hasAdjacentBottomBarLive: _hasBarWindow && barWindow.hasAdjacentBottomBar
|
||||
readonly property bool hasAdjacentLeftBarLive: _hasBarWindow && barWindow.hasAdjacentLeftBar
|
||||
@@ -189,16 +190,16 @@ Item {
|
||||
}
|
||||
|
||||
return monitorWorkspaces.sort((a, b) => a.id - b.id);
|
||||
} else if (CompositorService.isDwl) {
|
||||
if (!DwlService.dwlAvailable) {
|
||||
} else if (CompositorService.isDwl || CompositorService.isMango) {
|
||||
if (!dwlSvc.available) {
|
||||
return [0];
|
||||
}
|
||||
if (SettingsData.dwlShowAllTags) {
|
||||
return Array.from({
|
||||
length: DwlService.tagCount
|
||||
length: dwlSvc.tagCount
|
||||
}, (_, i) => i);
|
||||
}
|
||||
return DwlService.getVisibleTags(screenName);
|
||||
return dwlSvc.getVisibleTags(screenName);
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
const workspaces = I3.workspaces?.values || [];
|
||||
if (workspaces.length === 0)
|
||||
@@ -234,13 +235,13 @@ Item {
|
||||
const monitors = Hyprland.monitors?.values || [];
|
||||
const currentMonitor = monitors.find(monitor => monitor.name === screenName);
|
||||
return currentMonitor?.activeWorkspace?.id ?? 1;
|
||||
} else if (CompositorService.isDwl) {
|
||||
if (!DwlService.dwlAvailable)
|
||||
} else if (CompositorService.isDwl || CompositorService.isMango) {
|
||||
if (!dwlSvc.available)
|
||||
return 0;
|
||||
const outputState = DwlService.getOutputState(screenName);
|
||||
const outputState = dwlSvc.getOutputState(screenName);
|
||||
if (!outputState || !outputState.tags)
|
||||
return 0;
|
||||
const activeTags = DwlService.getActiveTags(screenName);
|
||||
const activeTags = dwlSvc.getActiveTags(screenName);
|
||||
return activeTags.length > 0 ? activeTags[0] : 0;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
if (!screenName || SettingsData.workspaceFollowFocus) {
|
||||
@@ -282,14 +283,14 @@ Item {
|
||||
if (nextIndex !== validIndex) {
|
||||
HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id);
|
||||
}
|
||||
} else if (CompositorService.isDwl) {
|
||||
} else if (CompositorService.isDwl || CompositorService.isMango) {
|
||||
const currentTag = getCurrentWorkspace();
|
||||
const currentIndex = realWorkspaces.findIndex(tag => tag === currentTag);
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex;
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
|
||||
|
||||
if (nextIndex !== validIndex) {
|
||||
DwlService.switchToTag(_barScreenName, realWorkspaces[nextIndex]);
|
||||
dwlSvc.switchToTag(_barScreenName, realWorkspaces[nextIndex]);
|
||||
}
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
const currentWs = getCurrentWorkspace();
|
||||
|
||||
@@ -327,6 +327,24 @@ PanelWindow {
|
||||
hasMaximizedToplevel = false;
|
||||
return;
|
||||
}
|
||||
if (CompositorService.isMango) {
|
||||
const out = MangoService.outputs[screenName];
|
||||
const active = new Set((out?.activeTags) || []);
|
||||
const wins = MangoService.windows || [];
|
||||
for (let i = 0; i < wins.length; i++) {
|
||||
const w = wins[i];
|
||||
if (!w || w.monitor !== screenName || w.is_minimized)
|
||||
continue;
|
||||
if (active.size > 0 && !(w.tags || []).some(t => active.has(t)))
|
||||
continue;
|
||||
if (w.is_maximized || w.is_fullscreen) {
|
||||
hasMaximizedToplevel = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
hasMaximizedToplevel = false;
|
||||
return;
|
||||
}
|
||||
if (!CompositorService.isHyprland && !CompositorService.isNiri) {
|
||||
hasMaximizedToplevel = false;
|
||||
return;
|
||||
@@ -351,7 +369,7 @@ PanelWindow {
|
||||
shouldHideForWindows = false;
|
||||
return;
|
||||
}
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland) {
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) {
|
||||
shouldHideForWindows = false;
|
||||
return;
|
||||
}
|
||||
@@ -825,7 +843,7 @@ PanelWindow {
|
||||
return true;
|
||||
|
||||
const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false;
|
||||
if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland)) {
|
||||
if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)) {
|
||||
if (barWindow.shouldHideForWindows)
|
||||
return topBarMouseArea.containsMouse || popoutPinsReveal || revealSticky || ipcReveal;
|
||||
return true;
|
||||
|
||||
@@ -10,6 +10,10 @@ DankPopout {
|
||||
|
||||
property var triggerScreen: null
|
||||
|
||||
// mango shares dwl's layout model; route to the right service.
|
||||
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
|
||||
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
|
||||
triggerX = x;
|
||||
triggerY = y;
|
||||
@@ -33,8 +37,8 @@ DankPopout {
|
||||
onScreenChanged: updateOutputState()
|
||||
|
||||
function updateOutputState() {
|
||||
if (screen && DwlService.dwlAvailable) {
|
||||
outputState = DwlService.getOutputState(screen.name);
|
||||
if (screen && root.dwlSvc.available) {
|
||||
outputState = root.dwlSvc.getOutputState(screen.name);
|
||||
} else {
|
||||
outputState = null;
|
||||
}
|
||||
@@ -215,7 +219,7 @@ DankPopout {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: DwlService.layouts
|
||||
model: root.dwlSvc.layouts
|
||||
|
||||
delegate: Rectangle {
|
||||
required property string modelData
|
||||
@@ -269,11 +273,11 @@ DankPopout {
|
||||
if (!root.triggerScreen) {
|
||||
return;
|
||||
}
|
||||
if (!DwlService.dwlAvailable) {
|
||||
if (!root.dwlSvc.available) {
|
||||
return;
|
||||
}
|
||||
|
||||
DwlService.setLayout(root.triggerScreen.name, index);
|
||||
root.dwlSvc.setLayout(root.triggerScreen.name, index);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ Loader {
|
||||
"cpuTemp": dgopAvailable,
|
||||
"gpuTemp": dgopAvailable,
|
||||
"network_speed_monitor": dgopAvailable,
|
||||
"layout": CompositorService.isDwl && DwlService.dwlAvailable
|
||||
"layout": (CompositorService.isDwl && DwlService.dwlAvailable) || (CompositorService.isMango && MangoService.available)
|
||||
};
|
||||
|
||||
return widgetVisibility[widgetId] ?? true;
|
||||
|
||||
@@ -12,9 +12,13 @@ BasePill {
|
||||
|
||||
signal toggleLayoutPopup
|
||||
|
||||
visible: CompositorService.isDwl && DwlService.dwlAvailable
|
||||
// mango shares dwl's tag/layout model; route to the right service.
|
||||
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
|
||||
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
|
||||
|
||||
property var outputState: parentScreen ? DwlService.getOutputState(parentScreen.name) : null
|
||||
visible: layout.isDwlLike && layout.dwlSvc.available
|
||||
|
||||
property var outputState: parentScreen ? layout.dwlSvc.getOutputState(parentScreen.name) : null
|
||||
property string currentLayoutSymbol: outputState?.layoutSymbol || ""
|
||||
property int currentLayoutIndex: outputState?.layout || 0
|
||||
|
||||
@@ -37,9 +41,9 @@ BasePill {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DwlService
|
||||
target: layout.dwlSvc
|
||||
function onStateChanged() {
|
||||
outputState = parentScreen ? DwlService.getOutputState(parentScreen.name) : null;
|
||||
outputState = parentScreen ? layout.dwlSvc.getOutputState(parentScreen.name) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +101,13 @@ BasePill {
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
if (!parentScreen || !DwlService.dwlAvailable || DwlService.layouts.length === 0) {
|
||||
if (!parentScreen || !layout.dwlSvc.available || layout.dwlSvc.layouts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = layout.currentLayoutIndex;
|
||||
const nextIndex = (currentIndex + 1) % DwlService.layouts.length;
|
||||
const nextIndex = (currentIndex + 1) % layout.dwlSvc.layouts.length;
|
||||
|
||||
DwlService.setLayout(parentScreen.name, nextIndex);
|
||||
layout.dwlSvc.setLayout(parentScreen.name, nextIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,8 @@ BasePill {
|
||||
return NiriService.getCurrentKeyboardLayoutName();
|
||||
} else if (CompositorService.isDwl) {
|
||||
return DwlService.currentKeyboardLayout;
|
||||
} else if (CompositorService.isMango) {
|
||||
return MangoService.currentKeyboardLayout;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -208,7 +210,9 @@ BasePill {
|
||||
} else if (CompositorService.isHyprland) {
|
||||
Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]);
|
||||
} else if (CompositorService.isDwl) {
|
||||
Quickshell.execDetached(["mmsg", "-d", "switch_keyboard_layout"]);
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "switch_keyboard_layout"]);
|
||||
} else if (CompositorService.isMango) {
|
||||
MangoService.cycleKeyboardLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ BasePill {
|
||||
}
|
||||
|
||||
IconImage {
|
||||
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
|
||||
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
@@ -68,6 +68,8 @@ BasePill {
|
||||
return "file://" + Theme.shellDir + "/assets/hyprland.svg";
|
||||
} else if (CompositorService.isDwl) {
|
||||
return "file://" + Theme.shellDir + "/assets/mango.png";
|
||||
} else if (CompositorService.isMango) {
|
||||
return "file://" + Theme.shellDir + "/assets/mango.png";
|
||||
} else if (CompositorService.isSway) {
|
||||
return "file://" + Theme.shellDir + "/assets/sway.svg";
|
||||
} else if (CompositorService.isScroll) {
|
||||
|
||||
@@ -22,6 +22,11 @@ Item {
|
||||
property var hyprlandOverviewLoader: null
|
||||
property var parentScreen: null
|
||||
|
||||
// mango shares dwl's tag model; route to the right service so one set of
|
||||
// branches serves both.
|
||||
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
|
||||
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
|
||||
|
||||
readonly property real _leftMargin: {
|
||||
if (isVertical)
|
||||
return 0;
|
||||
@@ -76,7 +81,8 @@ Item {
|
||||
case "hyprland":
|
||||
return Hyprland.focusedWorkspace?.monitor?.name || root.screenName;
|
||||
case "dwl":
|
||||
return DwlService.activeOutput || root.screenName;
|
||||
case "mango":
|
||||
return root.dwlSvc.activeOutput || root.screenName;
|
||||
case "sway":
|
||||
case "scroll":
|
||||
case "miracle":
|
||||
@@ -95,6 +101,7 @@ Item {
|
||||
case "niri":
|
||||
case "hyprland":
|
||||
case "dwl":
|
||||
case "mango":
|
||||
case "sway":
|
||||
case "scroll":
|
||||
case "miracle":
|
||||
@@ -121,6 +128,7 @@ Item {
|
||||
case "hyprland":
|
||||
return getHyprlandActiveWorkspace();
|
||||
case "dwl":
|
||||
case "mango":
|
||||
const activeTags = getDwlActiveTags();
|
||||
return activeTags.length > 0 ? activeTags[0] : -1;
|
||||
case "sway":
|
||||
@@ -132,7 +140,7 @@ Item {
|
||||
}
|
||||
}
|
||||
property var dwlActiveTags: {
|
||||
if (CompositorService.isDwl) {
|
||||
if (root.isDwlLike) {
|
||||
return getDwlActiveTags();
|
||||
}
|
||||
return [];
|
||||
@@ -152,6 +160,7 @@ Item {
|
||||
baseList = getHyprlandWorkspaces();
|
||||
break;
|
||||
case "dwl":
|
||||
case "mango":
|
||||
baseList = getDwlTags();
|
||||
break;
|
||||
case "sway":
|
||||
@@ -288,7 +297,7 @@ Item {
|
||||
}
|
||||
} else if (CompositorService.isHyprland) {
|
||||
targetWorkspaceId = ws.id !== undefined ? ws.id : ws;
|
||||
} else if (CompositorService.isDwl) {
|
||||
} else if (root.isDwlLike) {
|
||||
if (typeof ws !== "object" || ws.tag === undefined) {
|
||||
return [];
|
||||
}
|
||||
@@ -308,8 +317,8 @@ Item {
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
||||
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
|
||||
} else if (CompositorService.isDwl) {
|
||||
const output = DwlService.getOutputState(root.effectiveScreenName);
|
||||
} else if (root.isDwlLike) {
|
||||
const output = root.dwlSvc.getOutputState(root.effectiveScreenName);
|
||||
if (output && output.tags) {
|
||||
const tag = output.tags.find(t => t.tag === targetWorkspaceId);
|
||||
isActiveWs = tag ? (tag.state === 1) : false;
|
||||
@@ -323,6 +332,11 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CompositorService.isMango) {
|
||||
// mangoTags are 1-based; targetWorkspaceId is 0-based.
|
||||
if (!(w.mangoTags || []).includes(targetWorkspaceId + 1))
|
||||
return;
|
||||
} else {
|
||||
let winWs = null;
|
||||
if (CompositorService.isNiri) {
|
||||
winWs = w.workspace_id;
|
||||
@@ -337,6 +351,7 @@ Item {
|
||||
if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown");
|
||||
const moddedId = Paths.moddedAppId(keyBase);
|
||||
@@ -391,7 +406,7 @@ Item {
|
||||
"id": -1,
|
||||
"name": ""
|
||||
};
|
||||
} else if (CompositorService.isDwl) {
|
||||
} else if (root.isDwlLike) {
|
||||
placeholder = {
|
||||
"tag": -1
|
||||
};
|
||||
@@ -473,11 +488,11 @@ Item {
|
||||
}
|
||||
|
||||
function getDwlTags() {
|
||||
if (!DwlService.dwlAvailable)
|
||||
if (!root.dwlSvc.available)
|
||||
return [];
|
||||
|
||||
const targetScreen = root.effectiveScreenName;
|
||||
const output = DwlService.getOutputState(targetScreen);
|
||||
const output = root.dwlSvc.getOutputState(targetScreen);
|
||||
if (!output || !output.tags || output.tags.length === 0)
|
||||
return [];
|
||||
|
||||
@@ -490,7 +505,7 @@ Item {
|
||||
}));
|
||||
}
|
||||
|
||||
const visibleTagIndices = DwlService.getVisibleTags(targetScreen);
|
||||
const visibleTagIndices = root.dwlSvc.getVisibleTags(targetScreen);
|
||||
return visibleTagIndices.map(tagIndex => {
|
||||
const tagData = output.tags.find(t => t.tag === tagIndex);
|
||||
return {
|
||||
@@ -503,10 +518,10 @@ Item {
|
||||
}
|
||||
|
||||
function getDwlActiveTags() {
|
||||
if (!DwlService.dwlAvailable)
|
||||
if (!root.dwlSvc.available)
|
||||
return [];
|
||||
|
||||
return DwlService.getActiveTags(root.effectiveScreenName);
|
||||
return root.dwlSvc.getActiveTags(root.effectiveScreenName);
|
||||
}
|
||||
|
||||
function getExtWorkspaceWorkspaces() {
|
||||
@@ -557,7 +572,7 @@ Item {
|
||||
return ws && ws.idx !== -1;
|
||||
if (CompositorService.isHyprland)
|
||||
return ws && ws.id !== -1;
|
||||
if (CompositorService.isDwl)
|
||||
if (root.isDwlLike)
|
||||
return ws && ws.tag !== -1;
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return ws && ws.num !== -1;
|
||||
@@ -586,8 +601,9 @@ Item {
|
||||
}
|
||||
break;
|
||||
case "dwl":
|
||||
case "mango":
|
||||
if (data.tag !== undefined)
|
||||
DwlService.switchToTag(root.screenName, data.tag);
|
||||
root.dwlSvc.switchToTag(root.screenName, data.tag);
|
||||
break;
|
||||
case "sway":
|
||||
case "scroll":
|
||||
@@ -673,7 +689,7 @@ Item {
|
||||
}
|
||||
|
||||
HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id);
|
||||
} else if (CompositorService.isDwl) {
|
||||
} else if (root.isDwlLike) {
|
||||
const realWorkspaces = getRealWorkspaces();
|
||||
if (realWorkspaces.length < 2) {
|
||||
return;
|
||||
@@ -687,7 +703,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
|
||||
root.dwlSvc.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
const realWorkspaces = getRealWorkspaces();
|
||||
if (realWorkspaces.length < 2) {
|
||||
@@ -715,7 +731,7 @@ Item {
|
||||
return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : "";
|
||||
if (CompositorService.isHyprland)
|
||||
return modelData?.id || "";
|
||||
if (CompositorService.isDwl)
|
||||
if (root.isDwlLike)
|
||||
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return modelData?.num || "";
|
||||
@@ -730,7 +746,7 @@ Item {
|
||||
isPlaceholder = modelData?.idx === -1;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
isPlaceholder = modelData?.id === -1;
|
||||
} else if (CompositorService.isDwl) {
|
||||
} else if (root.isDwlLike) {
|
||||
isPlaceholder = modelData?.tag === -1;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
isPlaceholder = modelData?.num === -1;
|
||||
@@ -765,7 +781,7 @@ Item {
|
||||
return getWorkspaceIndexFallback(modelData, index);
|
||||
}
|
||||
|
||||
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || root.isDwlLike || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
|
||||
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
|
||||
|
||||
@@ -983,7 +999,7 @@ Item {
|
||||
return !!(modelData && modelData.idx === root.currentWorkspace);
|
||||
if (CompositorService.isHyprland)
|
||||
return !!(modelData && modelData.id === root.currentWorkspace);
|
||||
if (CompositorService.isDwl)
|
||||
if (root.isDwlLike)
|
||||
return !!(modelData && root.dwlActiveTags.includes(modelData.tag));
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return !!(modelData && modelData.num === root.currentWorkspace);
|
||||
@@ -992,7 +1008,7 @@ Item {
|
||||
property bool isOccupied: {
|
||||
if (CompositorService.isHyprland)
|
||||
return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id);
|
||||
if (CompositorService.isDwl)
|
||||
if (root.isDwlLike)
|
||||
return modelData.clients > 0;
|
||||
if (CompositorService.isNiri) {
|
||||
const workspace = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName);
|
||||
@@ -1007,7 +1023,7 @@ Item {
|
||||
return !!(modelData && modelData.idx === -1);
|
||||
if (CompositorService.isHyprland)
|
||||
return !!(modelData && modelData.id === -1);
|
||||
if (CompositorService.isDwl)
|
||||
if (root.isDwlLike)
|
||||
return !!(modelData && modelData.tag === -1);
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return !!(modelData && modelData.num === -1);
|
||||
@@ -1024,7 +1040,7 @@ Item {
|
||||
return modelData?.urgent ?? false;
|
||||
if (CompositorService.isNiri)
|
||||
return loadedIsUrgent;
|
||||
if (CompositorService.isDwl)
|
||||
if (root.isDwlLike)
|
||||
return modelData?.state === 2;
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return loadedIsUrgent;
|
||||
@@ -1081,8 +1097,12 @@ Item {
|
||||
winWs = hyprToplevel?.workspace?.id;
|
||||
}
|
||||
|
||||
if (winWs !== targetWorkspaceId)
|
||||
if (CompositorService.isMango) {
|
||||
if (!(w.mangoTags || []).includes(targetWorkspaceId + 1))
|
||||
continue;
|
||||
} else if (winWs !== targetWorkspaceId) {
|
||||
continue;
|
||||
}
|
||||
totalCount++;
|
||||
|
||||
const appKey = w.app_id || w.appId || w.class || w.windowClass || "unknown";
|
||||
@@ -1311,8 +1331,8 @@ Item {
|
||||
}
|
||||
} else if (CompositorService.isHyprland && modelData?.id) {
|
||||
HyprlandService.focusWorkspace(modelData.id);
|
||||
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
|
||||
DwlService.switchToTag(root.screenName, modelData.tag);
|
||||
} else if (root.isDwlLike && modelData?.tag !== undefined) {
|
||||
root.dwlSvc.switchToTag(root.screenName, modelData.tag);
|
||||
} else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) {
|
||||
try {
|
||||
I3.dispatch(`workspace number ${modelData.num}`);
|
||||
@@ -1323,8 +1343,8 @@ Item {
|
||||
NiriService.toggleOverview();
|
||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
|
||||
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
|
||||
DwlService.toggleTag(root.screenName, modelData.tag);
|
||||
} else if (root.isDwlLike && modelData?.tag !== undefined) {
|
||||
root.dwlSvc.toggleTag(root.screenName, modelData.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1348,7 +1368,7 @@ Item {
|
||||
wsData = modelData || null;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isDwl) {
|
||||
} else if (root.isDwlLike) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
wsData = modelData;
|
||||
@@ -1362,7 +1382,7 @@ Item {
|
||||
}
|
||||
|
||||
if (SettingsData.showWorkspaceApps) {
|
||||
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
if (root.isDwlLike || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
|
||||
} else if (CompositorService.isNiri) {
|
||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
|
||||
@@ -1922,8 +1942,8 @@ Item {
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: DwlService
|
||||
enabled: CompositorService.isDwl
|
||||
target: root.dwlSvc
|
||||
enabled: root.isDwlLike
|
||||
function onStateChanged() {
|
||||
delegateRoot.updateAllData();
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ Card {
|
||||
// technically they might not be on mangowc, but its what we support in the docs
|
||||
if (CompositorService.isDwl)
|
||||
return I18n.tr("on MangoWC");
|
||||
if (CompositorService.isMango)
|
||||
return I18n.tr("on MangoWC");
|
||||
if (CompositorService.isSway)
|
||||
return I18n.tr("on Sway");
|
||||
if (CompositorService.isScroll)
|
||||
|
||||
@@ -227,7 +227,7 @@ Variants {
|
||||
readonly property bool shouldHideForWindows: {
|
||||
if (!SettingsData.dockSmartAutoHide)
|
||||
return false;
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland)
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
|
||||
return false;
|
||||
|
||||
const screenName = dock.modelData?.name ?? "";
|
||||
@@ -291,6 +291,12 @@ Variants {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CompositorService.isMango) {
|
||||
MangoService.windows;
|
||||
MangoService.outputs;
|
||||
return CompositorService.mangoDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
|
||||
}
|
||||
|
||||
// Hyprland implementation (current workspace + visible special workspaces)
|
||||
Hyprland.focusedWorkspace;
|
||||
Hyprland.toplevels;
|
||||
|
||||
@@ -236,7 +236,7 @@ Item {
|
||||
}
|
||||
|
||||
IconImage {
|
||||
visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
|
||||
visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
|
||||
anchors.centerIn: parent
|
||||
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||
@@ -249,6 +249,8 @@ Item {
|
||||
return "file://" + Theme.shellDir + "/assets/hyprland.svg";
|
||||
} else if (CompositorService.isDwl) {
|
||||
return "file://" + Theme.shellDir + "/assets/mango.png";
|
||||
} else if (CompositorService.isMango) {
|
||||
return "file://" + Theme.shellDir + "/assets/mango.png";
|
||||
} else if (CompositorService.isSway) {
|
||||
return "file://" + Theme.shellDir + "/assets/sway.svg";
|
||||
} else if (CompositorService.isScroll) {
|
||||
|
||||
@@ -429,9 +429,9 @@ MIRACLE_EOF
|
||||
mango|mangowc)
|
||||
require_command "mango"
|
||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
||||
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
|
||||
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg dispatch quit"
|
||||
else
|
||||
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
|
||||
exec_compositor "mango" mango -s "$QS_CMD && mmsg dispatch quit"
|
||||
fi
|
||||
;;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Item {
|
||||
property bool isSway: CompositorService.isSway
|
||||
property bool isScroll: CompositorService.isScroll
|
||||
property bool isMiracle: CompositorService.isMiracle
|
||||
property bool isDwl: CompositorService.isDwl
|
||||
property bool isDwl: CompositorService.isDwl || CompositorService.isMango
|
||||
property bool isLabwc: CompositorService.isLabwc
|
||||
|
||||
property string compositorName: {
|
||||
|
||||
@@ -659,7 +659,7 @@ Item {
|
||||
|
||||
SettingsToggleRow {
|
||||
width: parent.width - parent.leftPadding
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
|
||||
text: I18n.tr("Hide When Windows Open")
|
||||
description: I18n.tr("Show the bar only when no windows are open")
|
||||
checked: selectedBarConfig?.showOnWindowsOpen ?? false
|
||||
@@ -1144,7 +1144,7 @@ Item {
|
||||
iconName: "fit_screen"
|
||||
title: I18n.tr("Maximize Detection")
|
||||
description: I18n.tr("Remove gaps and border when windows are maximized")
|
||||
visible: selectedBarConfig?.enabled && (CompositorService.isNiri || CompositorService.isHyprland)
|
||||
visible: selectedBarConfig?.enabled && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)
|
||||
checked: selectedBarConfig?.maximizeDetection ?? true
|
||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||
maximizeDetection: checked
|
||||
|
||||
@@ -158,12 +158,14 @@ Singleton {
|
||||
const compositorDirs = {
|
||||
"niri": configDir + "/niri/dms/profiles",
|
||||
"hyprland": configDir + "/hypr/dms/profiles",
|
||||
"dwl": configDir + "/mango/dms/profiles"
|
||||
"dwl": configDir + "/mango/dms/profiles",
|
||||
"mango": configDir + "/mango/dms/profiles"
|
||||
};
|
||||
const compositorExts = {
|
||||
"niri": ".kdl",
|
||||
"hyprland": ".conf",
|
||||
"dwl": ".conf"
|
||||
"dwl": ".conf",
|
||||
"mango": ".conf"
|
||||
};
|
||||
|
||||
const tasks = [];
|
||||
@@ -542,6 +544,14 @@ Singleton {
|
||||
onWriteFailed();
|
||||
});
|
||||
break;
|
||||
case "mango":
|
||||
MangoService.generateOutputsConfig(outputsData, success => {
|
||||
if (success)
|
||||
onWriteSuccess();
|
||||
else
|
||||
onWriteFailed();
|
||||
});
|
||||
break;
|
||||
case "dwl":
|
||||
DwlService.generateOutputsConfig(outputsData, success => {
|
||||
if (success)
|
||||
@@ -1032,6 +1042,7 @@ Singleton {
|
||||
case "hyprland":
|
||||
return parseHyprlandOutputs(content);
|
||||
case "dwl":
|
||||
case "mango":
|
||||
return parseMangoOutputs(content);
|
||||
default:
|
||||
return {};
|
||||
@@ -1302,7 +1313,7 @@ Singleton {
|
||||
params[pair.substring(0, colonIdx).trim()] = pair.substring(colonIdx + 1).trim();
|
||||
}
|
||||
|
||||
const name = params.name;
|
||||
const name = (params.name || "").replace(/^\^/, "").replace(/\$$/, "");
|
||||
if (!name)
|
||||
continue;
|
||||
|
||||
@@ -1370,6 +1381,7 @@ Singleton {
|
||||
"includeLine": "require(\"dms.outputs\")"
|
||||
};
|
||||
case "dwl":
|
||||
case "mango":
|
||||
return {
|
||||
"configFile": configDir + "/mango/config.conf",
|
||||
"outputsFile": configDir + "/mango/dms/outputs.conf",
|
||||
@@ -1383,7 +1395,7 @@ Singleton {
|
||||
|
||||
function checkIncludeStatus() {
|
||||
const compositor = CompositorService.compositor;
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl" && compositor !== "mango") {
|
||||
includeStatus = {
|
||||
"exists": false,
|
||||
"included": false,
|
||||
@@ -1394,7 +1406,8 @@ Singleton {
|
||||
}
|
||||
|
||||
const filename = (compositor === "niri") ? "outputs.kdl" : ((compositor === "hyprland") ? "outputs.lua" : "outputs.conf");
|
||||
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
|
||||
// mango and dwl both use outputs.conf under ~/.config/mango
|
||||
const compositorArg = (compositor === "dwl" || compositor === "mango") ? "mangowc" : compositor;
|
||||
|
||||
checkingInclude = true;
|
||||
Proc.runCommand("check-outputs-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
|
||||
@@ -1569,6 +1582,9 @@ Singleton {
|
||||
}
|
||||
HyprlandService.generateOutputsConfig(outputsData, buildMergedHyprlandSettings());
|
||||
break;
|
||||
case "mango":
|
||||
MangoService.generateOutputsConfig(outputsData);
|
||||
break;
|
||||
case "dwl":
|
||||
DwlService.generateOutputsConfig(outputsData);
|
||||
break;
|
||||
|
||||
@@ -317,7 +317,7 @@ StyledRect {
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Variable Refresh Rate")
|
||||
visible: root.isConnected && !root.isDisabled && !CompositorService.isDwl && !CompositorService.isHyprland && !CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false)
|
||||
visible: root.isConnected && !root.isDisabled && !CompositorService.isDwl && !CompositorService.isMango && !CompositorService.isHyprland && !CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false)
|
||||
checked: {
|
||||
const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr");
|
||||
if (pendingVrr !== undefined)
|
||||
|
||||
@@ -500,7 +500,7 @@ Item {
|
||||
|
||||
Column {
|
||||
id: displayFormatColumn
|
||||
visible: !CompositorService.isDwl
|
||||
visible: !CompositorService.isDwl && !CompositorService.isMango
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ Item {
|
||||
text: I18n.tr("Intelligent Auto-hide")
|
||||
description: I18n.tr("Show dock when floating windows don't overlap its area")
|
||||
checked: SettingsData.dockSmartAutoHide
|
||||
visible: SettingsData.showDock && (CompositorService.isNiri || CompositorService.isHyprland)
|
||||
visible: SettingsData.showDock && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)
|
||||
onToggled: checked => {
|
||||
if (checked && SettingsData.dockAutoHide) {
|
||||
SettingsData.set("dockAutoHide", false);
|
||||
@@ -284,6 +284,8 @@ Item {
|
||||
modes.push("Hyprland");
|
||||
} else if (CompositorService.isDwl) {
|
||||
modes.push("mango");
|
||||
} else if (CompositorService.isMango) {
|
||||
modes.push("mango");
|
||||
} else if (CompositorService.isSway) {
|
||||
modes.push("Sway");
|
||||
} else if (CompositorService.isScroll) {
|
||||
|
||||
@@ -306,6 +306,8 @@ Item {
|
||||
modes.push("Hyprland");
|
||||
} else if (CompositorService.isDwl) {
|
||||
modes.push("mango");
|
||||
} else if (CompositorService.isMango) {
|
||||
modes.push("mango");
|
||||
} else if (CompositorService.isSway) {
|
||||
modes.push("Sway");
|
||||
} else if (CompositorService.isScroll) {
|
||||
|
||||
@@ -49,6 +49,7 @@ Item {
|
||||
"includeLine": "require(\"dms.cursor\")"
|
||||
};
|
||||
case "dwl":
|
||||
case "mango":
|
||||
return {
|
||||
"configFile": configDir + "/mango/config.conf",
|
||||
"cursorFile": configDir + "/mango/dms/cursor.conf",
|
||||
@@ -62,7 +63,7 @@ Item {
|
||||
|
||||
function checkCursorIncludeStatus() {
|
||||
const compositor = CompositorService.compositor;
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl" && compositor !== "mango") {
|
||||
cursorIncludeStatus = {
|
||||
"exists": false,
|
||||
"included": false,
|
||||
@@ -73,7 +74,7 @@ Item {
|
||||
}
|
||||
|
||||
const filename = (compositor === "niri") ? "cursor.kdl" : ((compositor === "hyprland") ? "cursor.lua" : "cursor.conf");
|
||||
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
|
||||
const compositorArg = (compositor === "dwl" || compositor === "mango") ? "mangowc" : compositor;
|
||||
|
||||
checkingCursorInclude = true;
|
||||
Proc.runCommand("check-cursor-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
|
||||
@@ -193,7 +194,7 @@ Item {
|
||||
themeColorsTab.templateDetection = JSON.parse(output.trim());
|
||||
} catch (e) {}
|
||||
});
|
||||
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl)
|
||||
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango)
|
||||
checkCursorIncludeStatus();
|
||||
}
|
||||
|
||||
@@ -2177,7 +2178,7 @@ Item {
|
||||
title: I18n.tr("MangoWC Layout Overrides")
|
||||
settingKey: "mangoLayout"
|
||||
iconName: "crop_square"
|
||||
visible: CompositorService.isDwl
|
||||
visible: CompositorService.isDwl || CompositorService.isMango
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "theme"
|
||||
@@ -2334,7 +2335,7 @@ Item {
|
||||
title: I18n.tr("Cursor Theme")
|
||||
settingKey: "cursorTheme"
|
||||
iconName: "mouse"
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
@@ -2490,6 +2491,8 @@ Item {
|
||||
return SettingsData.cursorSettings.hyprland?.inactiveTimeout || 0;
|
||||
if (CompositorService.isDwl)
|
||||
return SettingsData.cursorSettings.dwl?.cursorHideTimeout || 0;
|
||||
if (CompositorService.isMango)
|
||||
return SettingsData.cursorSettings.mango?.cursorHideTimeout || 0;
|
||||
return 0;
|
||||
}
|
||||
minimum: 0
|
||||
@@ -2510,6 +2513,10 @@ Item {
|
||||
if (!updated.dwl)
|
||||
updated.dwl = {};
|
||||
updated.dwl.cursorHideTimeout = newValue;
|
||||
} else if (CompositorService.isMango) {
|
||||
if (!updated.mango)
|
||||
updated.mango = {};
|
||||
updated.mango.cursorHideTimeout = newValue;
|
||||
}
|
||||
SettingsData.set("cursorSettings", updated);
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ Item {
|
||||
"text": I18n.tr("Layout"),
|
||||
"description": I18n.tr("Display and switch DWL layouts"),
|
||||
"icon": "view_quilt",
|
||||
"enabled": CompositorService.isDwl && DwlService.dwlAvailable,
|
||||
"warning": !CompositorService.isDwl ? I18n.tr("Requires DWL compositor") : (!DwlService.dwlAvailable ? I18n.tr("DWL service not available") : undefined)
|
||||
"enabled": (CompositorService.isDwl && DwlService.dwlAvailable) || (CompositorService.isMango && MangoService.available),
|
||||
"warning": CompositorService.isMango ? (!MangoService.available ? I18n.tr("DWL service not available") : undefined) : (!CompositorService.isDwl ? I18n.tr("Requires DWL compositor") : (!DwlService.dwlAvailable ? I18n.tr("DWL service not available") : undefined))
|
||||
},
|
||||
{
|
||||
"id": "launcherButton",
|
||||
|
||||
@@ -30,6 +30,7 @@ Item {
|
||||
property var externalRules: []
|
||||
property var activeWindows: getActiveWindows()
|
||||
property string expandedExternalId: ""
|
||||
readonly property string dmsRulesFileName: CompositorService.isNiri ? "dms/windowrules.kdl" : CompositorService.isMango ? "dms/windowrules.conf" : "dms/windowrules.lua"
|
||||
|
||||
readonly property var matchLabels: ({
|
||||
"appId": I18n.tr("App ID"),
|
||||
@@ -166,6 +167,13 @@ Item {
|
||||
"grepPattern": "dms.windowrules",
|
||||
"includeLine": "require(\"dms.windowrules\")"
|
||||
};
|
||||
case "mango":
|
||||
return {
|
||||
"configFile": configDir + "/mango/config.conf",
|
||||
"rulesFile": configDir + "/mango/dms/windowrules.conf",
|
||||
"grepPattern": "dms/windowrules.conf",
|
||||
"includeLine": "source=./dms/windowrules.conf"
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -173,7 +181,7 @@ Item {
|
||||
|
||||
function loadWindowRules() {
|
||||
const compositor = CompositorService.compositor;
|
||||
if (compositor !== "niri" && compositor !== "hyprland") {
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango") {
|
||||
windowRules = [];
|
||||
externalRules = [];
|
||||
return;
|
||||
@@ -211,11 +219,13 @@ Item {
|
||||
return;
|
||||
}
|
||||
const compositor = CompositorService.compositor;
|
||||
if (compositor !== "niri" && compositor !== "hyprland")
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango")
|
||||
return;
|
||||
|
||||
Proc.runCommand("remove-windowrule", ["dms", "config", "windowrules", "remove", compositor, ruleId], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
if (CompositorService.isMango)
|
||||
MangoService.reloadConfig();
|
||||
loadWindowRules();
|
||||
rulesChanged();
|
||||
}
|
||||
@@ -231,7 +241,7 @@ Item {
|
||||
return;
|
||||
|
||||
const compositor = CompositorService.compositor;
|
||||
if (compositor !== "niri" && compositor !== "hyprland")
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango")
|
||||
return;
|
||||
|
||||
let ids = windowRules.map(r => r.id);
|
||||
@@ -240,6 +250,8 @@ Item {
|
||||
|
||||
Proc.runCommand("reorder-windowrules", ["dms", "config", "windowrules", "reorder", compositor, JSON.stringify(ids)], (output, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
if (CompositorService.isMango)
|
||||
MangoService.reloadConfig();
|
||||
loadWindowRules();
|
||||
rulesChanged();
|
||||
}
|
||||
@@ -248,7 +260,7 @@ Item {
|
||||
|
||||
function checkWindowRulesIncludeStatus() {
|
||||
const compositor = CompositorService.compositor;
|
||||
if (compositor !== "niri" && compositor !== "hyprland") {
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango") {
|
||||
windowRulesIncludeStatus = {
|
||||
"exists": false,
|
||||
"included": false,
|
||||
@@ -258,7 +270,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = (compositor === "niri") ? "windowrules.kdl" : "windowrules.lua";
|
||||
const filename = (compositor === "niri") ? "windowrules.kdl" : (compositor === "mango") ? "windowrules.conf" : "windowrules.lua";
|
||||
checkingInclude = true;
|
||||
Proc.runCommand("check-windowrules-include", ["dms", "config", "resolve-include", compositor, filename], (output, exitCode) => {
|
||||
checkingInclude = false;
|
||||
@@ -306,6 +318,8 @@ Item {
|
||||
fixingInclude = false;
|
||||
if (exitCode !== 0)
|
||||
return;
|
||||
if (CompositorService.isMango)
|
||||
MangoService.reloadConfig();
|
||||
checkWindowRulesIncludeStatus();
|
||||
loadWindowRules();
|
||||
});
|
||||
@@ -358,7 +372,7 @@ Item {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (CompositorService.isNiri || CompositorService.isHyprland) {
|
||||
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango) {
|
||||
checkWindowRulesIncludeStatus();
|
||||
loadWindowRules();
|
||||
}
|
||||
@@ -415,7 +429,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Define rules for window behavior. Saves to %1").arg(CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.lua")
|
||||
text: I18n.tr("Define rules for window behavior. Saves to %1").arg(root.dmsRulesFileName)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
@@ -489,7 +503,7 @@ Item {
|
||||
color: (showLegacy || showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.15) : "transparent"
|
||||
border.color: (showLegacy || showError || showSetup) ? Theme.withAlpha(Theme.warning, 0.3) : "transparent"
|
||||
border.width: 1
|
||||
visible: (showLegacy || showError || showSetup) && !root.checkingInclude && (CompositorService.isNiri || CompositorService.isHyprland)
|
||||
visible: (showLegacy || showError || showSetup) && !root.checkingInclude && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)
|
||||
|
||||
Row {
|
||||
id: warningSection
|
||||
@@ -519,7 +533,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
readonly property string rulesFile: CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.lua"
|
||||
readonly property string rulesFile: root.dmsRulesFileName
|
||||
text: warningBox.showLegacy ? I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing window rules in Settings.") : (warningBox.showSetup ? I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg(rulesFile) : I18n.tr("%1 exists but is not included. Window rules won't apply.").arg(rulesFile))
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
|
||||
@@ -59,7 +59,7 @@ Item {
|
||||
text: I18n.tr("Show Workspace Apps")
|
||||
description: I18n.tr("Display application icons in workspace indicators")
|
||||
checked: SettingsData.showWorkspaceApps
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
|
||||
onToggled: checked => SettingsData.set("showWorkspaceApps", checked)
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ Item {
|
||||
text: I18n.tr("Follow Monitor Focus")
|
||||
description: I18n.tr("Show workspaces of the currently focused monitor")
|
||||
checked: SettingsData.workspaceFollowFocus
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
onToggled: checked => SettingsData.set("workspaceFollowFocus", checked)
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ Item {
|
||||
text: I18n.tr("Show Occupied Workspaces Only")
|
||||
description: I18n.tr("Display only workspaces that contain windows")
|
||||
checked: SettingsData.showOccupiedWorkspacesOnly
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
|
||||
onToggled: checked => SettingsData.set("showOccupiedWorkspacesOnly", checked)
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ Item {
|
||||
text: I18n.tr("Reverse Scrolling Direction")
|
||||
description: I18n.tr("Reverse workspace switch direction when scrolling over the bar")
|
||||
checked: SettingsData.reverseScrolling
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
|
||||
onToggled: checked => SettingsData.set("reverseScrolling", checked)
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ Item {
|
||||
text: I18n.tr("Show All Tags")
|
||||
description: I18n.tr("Show all 9 tags instead of only occupied tags (DWL only)")
|
||||
checked: SettingsData.dwlShowAllTags
|
||||
visible: CompositorService.isDwl
|
||||
visible: CompositorService.isDwl || CompositorService.isMango
|
||||
onToggled: checked => SettingsData.set("dwlShowAllTags", checked)
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ Item {
|
||||
SettingsButtonGroupRow {
|
||||
text: I18n.tr("Occupied Color")
|
||||
model: ["none", "sec", "s", "sc", "sch", "schh"]
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango
|
||||
buttonHeight: 22
|
||||
minButtonWidth: 36
|
||||
buttonPadding: Theme.spacingS
|
||||
@@ -279,7 +279,7 @@ Item {
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
@@ -316,12 +316,12 @@ Item {
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
text: I18n.tr("Urgent Color")
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
model: ["err", "pri", "sec", "s", "sc"]
|
||||
buttonHeight: 22
|
||||
minButtonWidth: 36
|
||||
|
||||
@@ -16,6 +16,7 @@ Singleton {
|
||||
property bool isHyprland: false
|
||||
property bool isNiri: false
|
||||
property bool isDwl: false
|
||||
property bool isMango: false
|
||||
property bool isSway: false
|
||||
property bool isScroll: false
|
||||
property bool isMiracle: false
|
||||
@@ -29,7 +30,9 @@ Singleton {
|
||||
readonly property string scrollSocket: Quickshell.env("SWAYSOCK")
|
||||
readonly property string miracleSocket: Quickshell.env("MIRACLESOCK")
|
||||
readonly property string labwcPid: Quickshell.env("LABWC_PID")
|
||||
readonly property string mangoSignature: Quickshell.env("MANGO_INSTANCE_SIGNATURE")
|
||||
property bool useNiriSorting: isNiri && NiriService
|
||||
property bool useMangoSorting: isMango && MangoService
|
||||
|
||||
property var randrScales: ({})
|
||||
property bool randrReady: false
|
||||
@@ -100,6 +103,12 @@ Singleton {
|
||||
return dwlScale;
|
||||
}
|
||||
|
||||
if (isMango && screen) {
|
||||
const mangoScale = MangoService.getOutputScale(screen.name);
|
||||
if (mangoScale !== undefined && mangoScale > 0)
|
||||
return mangoScale;
|
||||
}
|
||||
|
||||
return screen?.devicePixelRatio || 1;
|
||||
}
|
||||
|
||||
@@ -114,6 +123,8 @@ Singleton {
|
||||
screenName = focusedWs?.monitor?.name || "";
|
||||
} else if (isDwl && DwlService.activeOutput)
|
||||
screenName = DwlService.activeOutput;
|
||||
else if (isMango && MangoService.activeOutput)
|
||||
screenName = MangoService.activeOutput;
|
||||
|
||||
if (!screenName)
|
||||
return Quickshell.screens.length > 0 ? Quickshell.screens[0] : null;
|
||||
@@ -194,6 +205,18 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: MangoService
|
||||
function onStateChanged() {
|
||||
if (isMango)
|
||||
scheduleSort();
|
||||
}
|
||||
function onWindowsChanged() {
|
||||
if (isMango)
|
||||
scheduleSort();
|
||||
}
|
||||
}
|
||||
|
||||
function computeSortedToplevels() {
|
||||
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values)
|
||||
return [];
|
||||
@@ -201,6 +224,9 @@ Singleton {
|
||||
if (useNiriSorting)
|
||||
return NiriService.sortToplevels(ToplevelManager.toplevels.values);
|
||||
|
||||
if (useMangoSorting)
|
||||
return MangoService.sortToplevels(ToplevelManager.toplevels.values);
|
||||
|
||||
if (isHyprland)
|
||||
return sortHyprlandToplevelsSafe();
|
||||
|
||||
@@ -697,6 +723,51 @@ Singleton {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mango clients carry absolute geometry + tags; count those on the screen's
|
||||
// active tags (not minimized), made screen-relative via the monitor offset.
|
||||
function mangoDockOverlapForSmartAutoHide(screenName, dockPosition, dockThickness, screenWidth, screenHeight) {
|
||||
if (!isMango || !screenName || !MangoService.windows)
|
||||
return false;
|
||||
|
||||
const out = MangoService.outputs[screenName];
|
||||
const active = new Set((out?.activeTags) || []);
|
||||
const monX = out?.x ?? 0;
|
||||
const monY = out?.y ?? 0;
|
||||
|
||||
for (let i = 0; i < MangoService.windows.length; i++) {
|
||||
const win = MangoService.windows[i];
|
||||
if (!win || win.monitor !== screenName || win.is_minimized)
|
||||
continue;
|
||||
if (active.size > 0 && !(win.tags || []).some(t => active.has(t)))
|
||||
continue;
|
||||
|
||||
const winX = (win.x ?? 0) - monX;
|
||||
const winY = (win.y ?? 0) - monY;
|
||||
const winW = win.width ?? 0;
|
||||
const winH = win.height ?? 0;
|
||||
|
||||
switch (dockPosition) {
|
||||
case SettingsData.Position.Top:
|
||||
if (winY < dockThickness)
|
||||
return true;
|
||||
break;
|
||||
case SettingsData.Position.Bottom:
|
||||
if (winY + winH > screenHeight - dockThickness)
|
||||
return true;
|
||||
break;
|
||||
case SettingsData.Position.Left:
|
||||
if (winX < dockThickness)
|
||||
return true;
|
||||
break;
|
||||
case SettingsData.Position.Right:
|
||||
if (winX + winW > screenWidth - dockThickness)
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function filterHyprlandCurrentDisplaySafe(toplevels, screenName) {
|
||||
if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels)
|
||||
return toplevels;
|
||||
@@ -790,15 +861,31 @@ Singleton {
|
||||
NiriService.generateNiriLayoutConfig();
|
||||
HyprlandService.generateLayoutConfig();
|
||||
DwlService.generateLayoutConfig();
|
||||
MangoService.generateLayoutConfig();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function detectCompositor() {
|
||||
if (mangoSignature && mangoSignature.length > 0) {
|
||||
isHyprland = false;
|
||||
isNiri = false;
|
||||
isDwl = false;
|
||||
isMango = true;
|
||||
isSway = false;
|
||||
isScroll = false;
|
||||
isMiracle = false;
|
||||
isLabwc = false;
|
||||
compositor = "mango";
|
||||
log.info("Detected MangoWM via MANGO_INSTANCE_SIGNATURE");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !scrollSocket && !miracleSocket && !labwcPid) {
|
||||
isHyprland = true;
|
||||
isNiri = false;
|
||||
isDwl = false;
|
||||
isMango = false;
|
||||
isSway = false;
|
||||
isScroll = false;
|
||||
isMiracle = false;
|
||||
@@ -814,6 +901,7 @@ Singleton {
|
||||
isNiri = true;
|
||||
isHyprland = false;
|
||||
isDwl = false;
|
||||
isMango = false;
|
||||
isSway = false;
|
||||
isScroll = false;
|
||||
isMiracle = false;
|
||||
@@ -849,6 +937,7 @@ Singleton {
|
||||
isNiri = false;
|
||||
isHyprland = false;
|
||||
isDwl = false;
|
||||
isMango = false;
|
||||
isSway = false;
|
||||
isScroll = false;
|
||||
isMiracle = true;
|
||||
@@ -866,6 +955,7 @@ Singleton {
|
||||
isNiri = false;
|
||||
isHyprland = false;
|
||||
isDwl = false;
|
||||
isMango = false;
|
||||
isSway = false;
|
||||
isScroll = true;
|
||||
isMiracle = false;
|
||||
@@ -881,6 +971,7 @@ Singleton {
|
||||
isHyprland = false;
|
||||
isNiri = false;
|
||||
isDwl = false;
|
||||
isMango = false;
|
||||
isSway = false;
|
||||
isScroll = false;
|
||||
isMiracle = false;
|
||||
@@ -896,6 +987,7 @@ Singleton {
|
||||
isHyprland = false;
|
||||
isNiri = false;
|
||||
isDwl = false;
|
||||
isMango = false;
|
||||
isSway = false;
|
||||
isScroll = false;
|
||||
isMiracle = false;
|
||||
@@ -908,13 +1000,15 @@ Singleton {
|
||||
Connections {
|
||||
target: DMSService
|
||||
function onCapabilitiesReceived() {
|
||||
if (!isHyprland && !isNiri && !isDwl && !isLabwc) {
|
||||
if (!isHyprland && !isNiri && !isDwl && !isMango && !isLabwc) {
|
||||
checkForDwl();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkForDwl() {
|
||||
if (isMango)
|
||||
return;
|
||||
if (DMSService.apiVersion >= 12 && DMSService.capabilities.includes("dwl")) {
|
||||
isHyprland = false;
|
||||
isNiri = false;
|
||||
@@ -935,6 +1029,8 @@ Singleton {
|
||||
return HyprlandService.dpmsOff();
|
||||
if (isDwl)
|
||||
return _dwlPowerOffMonitors();
|
||||
if (isMango)
|
||||
return MangoService.powerOffMonitors();
|
||||
if (isSway || isScroll || isMiracle) {
|
||||
try {
|
||||
I3.dispatch("output * dpms off");
|
||||
@@ -954,6 +1050,8 @@ Singleton {
|
||||
return HyprlandService.dpmsOn();
|
||||
if (isDwl)
|
||||
return _dwlPowerOnMonitors();
|
||||
if (isMango)
|
||||
return MangoService.powerOnMonitors();
|
||||
if (isSway || isScroll || isMiracle) {
|
||||
try {
|
||||
I3.dispatch("output * dpms on");
|
||||
@@ -975,7 +1073,7 @@ Singleton {
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
const screen = Quickshell.screens[i];
|
||||
if (screen && screen.name) {
|
||||
Quickshell.execDetached(["mmsg", "-d", "disable_monitor," + screen.name]);
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "disable_monitor," + screen.name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -989,7 +1087,7 @@ Singleton {
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
const screen = Quickshell.screens[i];
|
||||
if (screen && screen.name) {
|
||||
Quickshell.execDetached(["mmsg", "-d", "enable_monitor," + screen.name]);
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "enable_monitor," + screen.name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ Singleton {
|
||||
property int _lastGapValue: -1
|
||||
|
||||
property bool dwlAvailable: false
|
||||
// Alias so consumers can treat DwlService/MangoService uniformly via `.available`.
|
||||
readonly property bool available: dwlAvailable
|
||||
property var outputs: ({})
|
||||
property var tagCount: 9
|
||||
property var layouts: []
|
||||
@@ -233,27 +235,23 @@ Singleton {
|
||||
}
|
||||
|
||||
function quit() {
|
||||
Quickshell.execDetached(["mmsg", "-d", "quit"]);
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "quit"]);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: scaleQueryProcess
|
||||
command: ["mmsg", "-A"]
|
||||
command: ["mmsg", "get", "all-monitors"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const newScales = {};
|
||||
const lines = text.trim().split('\n');
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 3 && parts[1] === "scale_factor") {
|
||||
const outputName = parts[0];
|
||||
const scale = parseFloat(parts[2]);
|
||||
if (!isNaN(scale)) {
|
||||
newScales[outputName] = scale;
|
||||
}
|
||||
const data = JSON.parse(text.trim());
|
||||
const monitors = data.monitors || [];
|
||||
for (const mon of monitors) {
|
||||
if (mon.name && typeof mon.scale === "number" && mon.scale > 0) {
|
||||
newScales[mon.name] = mon.scale;
|
||||
}
|
||||
}
|
||||
outputScales = newScales;
|
||||
@@ -327,7 +325,7 @@ Singleton {
|
||||
const transform = transformToMango(output.logical?.transform ?? "Normal");
|
||||
const vrr = output.vrr_enabled ? 1 : 0;
|
||||
|
||||
const rule = ["name:" + outputName, "width:" + width, "height:" + height, "refresh:" + refreshRate, "x:" + x, "y:" + y, "scale:" + scale, "rr:" + transform, "vrr:" + vrr].join(",");
|
||||
const rule = ["name:^" + outputName + "$", "width:" + width, "height:" + height, "refresh:" + refreshRate, "x:" + x, "y:" + y, "scale:" + scale, "rr:" + transform, "vrr:" + vrr].join(",");
|
||||
|
||||
lines.push("monitorrule=" + rule);
|
||||
}
|
||||
@@ -352,7 +350,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function reloadConfig() {
|
||||
Proc.runCommand("mango-reload", ["mmsg", "-d", "reload_config"], (output, exitCode) => {
|
||||
Proc.runCommand("mango-reload", ["mmsg", "dispatch", "reload_config"], (output, exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
log.warn("mmsg reload_config failed:", output);
|
||||
});
|
||||
|
||||
@@ -14,13 +14,13 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("KeybindsService")
|
||||
|
||||
property bool available: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl
|
||||
property bool available: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango
|
||||
property string currentProvider: {
|
||||
if (CompositorService.isNiri)
|
||||
return "niri";
|
||||
if (CompositorService.isHyprland)
|
||||
return "hyprland";
|
||||
if (CompositorService.isDwl)
|
||||
if (CompositorService.isDwl || CompositorService.isMango)
|
||||
return "mangowc";
|
||||
return "";
|
||||
}
|
||||
@@ -30,7 +30,7 @@ Singleton {
|
||||
return "niri";
|
||||
if (CompositorService.isHyprland)
|
||||
return "hyprland";
|
||||
if (CompositorService.isDwl)
|
||||
if (CompositorService.isDwl || CompositorService.isMango)
|
||||
return "mangowc";
|
||||
return "";
|
||||
}
|
||||
@@ -118,7 +118,7 @@ Singleton {
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onCompositorChanged() {
|
||||
if (!CompositorService.isNiri)
|
||||
if (!CompositorService.isNiri && !CompositorService.isMango)
|
||||
return;
|
||||
Qt.callLater(root.loadBinds);
|
||||
}
|
||||
@@ -203,6 +203,8 @@ Singleton {
|
||||
}
|
||||
root.lastError = "";
|
||||
root.bindSaveCompleted(true);
|
||||
if (CompositorService.isMango)
|
||||
MangoService.reloadConfig();
|
||||
root.loadBinds(false);
|
||||
}
|
||||
}
|
||||
@@ -226,6 +228,8 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
root.lastError = "";
|
||||
if (CompositorService.isMango)
|
||||
MangoService.reloadConfig();
|
||||
root.loadBinds(false);
|
||||
}
|
||||
}
|
||||
@@ -254,6 +258,8 @@ Singleton {
|
||||
root.dmsBindsFixed();
|
||||
const bindsRel = root.currentProvider === "niri" ? "dms/binds.kdl" : root.currentProvider === "hyprland" ? "dms/binds.lua" : "dms/binds.conf";
|
||||
ToastService.showInfo(I18n.tr("Binds include added"), I18n.tr("%1 is now included in config").arg(bindsRel), "", "keybinds");
|
||||
if (CompositorService.isMango)
|
||||
MangoService.reloadConfig();
|
||||
Qt.callLater(root.forceReload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,561 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
// Native MangoWM IPC client. mango advertises a JSON-over-Unix-socket protocol
|
||||
// via MANGO_INSTANCE_SIGNATURE; each connection issues one `watch <target>` verb
|
||||
// and gets a full JSON snapshot followed by newline-delimited updates. Replaces
|
||||
// the legacy dwl-ipc-v2 path (DwlService) for mango, exposing a
|
||||
// DwlService-compatible tag API plus a per-client window list.
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("MangoService")
|
||||
|
||||
readonly property string socketPath: Quickshell.env("MANGO_INSTANCE_SIGNATURE")
|
||||
readonly property bool available: socketPath.length > 0
|
||||
|
||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||
readonly property string mangoDmsDir: configDir + "/mango/dms"
|
||||
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
|
||||
readonly property string layoutPath: mangoDmsDir + "/layout.conf"
|
||||
readonly property string cursorPath: mangoDmsDir + "/cursor.conf"
|
||||
|
||||
property int _lastGapValue: -1
|
||||
|
||||
// name -> { name, active, x, y, width, height, scale, layoutIndex,
|
||||
// layoutSymbol, lastOpenSurface, kbLayout, keymode,
|
||||
// tags: [{ tag, state, clients, focused, urgent, layout }] }
|
||||
property var outputs: ({})
|
||||
property string activeOutput: ""
|
||||
property int tagCount: 9
|
||||
property var displayScales: ({})
|
||||
property string currentKeyboardLayout: ""
|
||||
// Rich client list from `watch all-clients` (mango "clients").
|
||||
property var windows: []
|
||||
|
||||
// windowsChanged is auto-generated by the `windows` property's change signal.
|
||||
signal stateChanged
|
||||
|
||||
// ── State sockets ──────────────────────────────────────────────────────
|
||||
// One connection per watch target; mango streams a fresh full snapshot on
|
||||
// every change, so each line is treated as the complete state.
|
||||
|
||||
DankSocket {
|
||||
id: monitorsSocket
|
||||
path: root.socketPath
|
||||
connected: root.available
|
||||
|
||||
onConnectionStateChanged: {
|
||||
if (connected)
|
||||
send("watch all-monitors");
|
||||
}
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: line => root._handleMonitors(line)
|
||||
}
|
||||
}
|
||||
|
||||
DankSocket {
|
||||
id: clientsSocket
|
||||
path: root.socketPath
|
||||
connected: root.available
|
||||
|
||||
onConnectionStateChanged: {
|
||||
if (connected)
|
||||
send("watch all-clients");
|
||||
}
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: line => root._handleClients(line)
|
||||
}
|
||||
}
|
||||
|
||||
function _handleMonitors(line) {
|
||||
if (!line || !line.trim())
|
||||
return;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(line);
|
||||
} catch (e) {
|
||||
log.warn("Failed to parse all-monitors:", e);
|
||||
return;
|
||||
}
|
||||
const monitors = data.monitors;
|
||||
if (!Array.isArray(monitors))
|
||||
return;
|
||||
|
||||
const newOutputs = {};
|
||||
const newScales = {};
|
||||
let newActive = "";
|
||||
let newTagCount = root.tagCount;
|
||||
let newKbLayout = root.currentKeyboardLayout;
|
||||
|
||||
for (const m of monitors) {
|
||||
if (!m.name)
|
||||
continue;
|
||||
const tags = (m.tags || []).map(t => ({
|
||||
// 0-based to match the legacy dwl tag model used by consumers
|
||||
"tag": (t.index ?? 1) - 1,
|
||||
"state": t.is_urgent ? 2 : (t.is_active ? 1 : 0),
|
||||
"clients": t.client_count ?? 0,
|
||||
"focused": !!t.is_active,
|
||||
"urgent": !!t.is_urgent,
|
||||
"layout": t.layout ?? ""
|
||||
}));
|
||||
newOutputs[m.name] = {
|
||||
"name": m.name,
|
||||
"active": !!m.active,
|
||||
"x": m.x ?? 0,
|
||||
"y": m.y ?? 0,
|
||||
"width": m.width ?? 0,
|
||||
"height": m.height ?? 0,
|
||||
"scale": m.scale ?? 1.0,
|
||||
"layoutIndex": m.layout_index ?? 0,
|
||||
"layout": m.layout_index ?? 0,
|
||||
"activeTags": m.active_tags || [],
|
||||
"layoutSymbol": m.layout_symbol ?? "",
|
||||
"lastOpenSurface": m.last_open_surface ?? "",
|
||||
"keymode": m.keymode ?? "",
|
||||
"kbLayout": m.keyboardlayout ?? "",
|
||||
"tags": tags
|
||||
};
|
||||
if (typeof m.scale === "number" && m.scale > 0)
|
||||
newScales[m.name] = m.scale;
|
||||
if (m.active) {
|
||||
newActive = m.name;
|
||||
if (m.keyboardlayout)
|
||||
newKbLayout = m.keyboardlayout;
|
||||
}
|
||||
if (tags.length > 0)
|
||||
newTagCount = tags.length;
|
||||
}
|
||||
|
||||
root.outputs = newOutputs;
|
||||
root.displayScales = newScales;
|
||||
root.tagCount = newTagCount;
|
||||
if (newActive)
|
||||
root.activeOutput = newActive;
|
||||
root.currentKeyboardLayout = newKbLayout;
|
||||
root.stateChanged();
|
||||
}
|
||||
|
||||
function _handleClients(line) {
|
||||
if (!line || !line.trim())
|
||||
return;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(line);
|
||||
} catch (e) {
|
||||
log.warn("Failed to parse all-clients:", e);
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(data.clients))
|
||||
return;
|
||||
root.windows = data.clients;
|
||||
}
|
||||
|
||||
// ── DwlService-compatible tag API ──────────────────────────────────────
|
||||
|
||||
function getOutputState(outputName) {
|
||||
return (outputs && outputs[outputName]) ? outputs[outputName] : null;
|
||||
}
|
||||
|
||||
function getActiveTags(outputName) {
|
||||
const output = getOutputState(outputName);
|
||||
if (!output || !output.tags)
|
||||
return [];
|
||||
return output.tags.filter(tag => tag.state === 1).map(tag => tag.tag);
|
||||
}
|
||||
|
||||
function getTagsWithClients(outputName) {
|
||||
const output = getOutputState(outputName);
|
||||
if (!output || !output.tags)
|
||||
return [];
|
||||
return output.tags.filter(tag => tag.clients > 0).map(tag => tag.tag);
|
||||
}
|
||||
|
||||
function getUrgentTags(outputName) {
|
||||
const output = getOutputState(outputName);
|
||||
if (!output || !output.tags)
|
||||
return [];
|
||||
return output.tags.filter(tag => tag.state === 2).map(tag => tag.tag);
|
||||
}
|
||||
|
||||
function getVisibleTags(outputName) {
|
||||
const output = getOutputState(outputName);
|
||||
if (!output || !output.tags)
|
||||
return [];
|
||||
const visibleTags = new Set();
|
||||
output.tags.forEach(tag => {
|
||||
if (tag.state === 1 || tag.clients > 0)
|
||||
visibleTags.add(tag.tag);
|
||||
});
|
||||
return Array.from(visibleTags).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function getOutputScale(outputName) {
|
||||
return displayScales[outputName];
|
||||
}
|
||||
|
||||
// ── Window list ↔ wlr toplevel matching (per-tag sort/filter) ──────────
|
||||
// Match mango clients to wlr foreign-toplevels by appId+title to enrich them
|
||||
// with owning tags/monitor for per-tag filtering and stable ordering.
|
||||
|
||||
function _screenName(screenOrName) {
|
||||
return (typeof screenOrName === "string") ? screenOrName : (screenOrName?.name ?? "");
|
||||
}
|
||||
|
||||
function _orderedClients() {
|
||||
const list = (windows || []).slice();
|
||||
list.sort((a, b) => {
|
||||
const ma = outputs[a.monitor], mb = outputs[b.monitor];
|
||||
const ax = ma?.x ?? 1e9, ay = ma?.y ?? 1e9;
|
||||
const bx = mb?.x ?? 1e9, by = mb?.y ?? 1e9;
|
||||
if (ax !== bx)
|
||||
return ax - bx;
|
||||
if (ay !== by)
|
||||
return ay - by;
|
||||
if ((a.y ?? 0) !== (b.y ?? 0))
|
||||
return (a.y ?? 0) - (b.y ?? 0);
|
||||
if ((a.x ?? 0) !== (b.x ?? 0))
|
||||
return (a.x ?? 0) - (b.x ?? 0);
|
||||
return (a.id ?? 0) - (b.id ?? 0);
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
function _matchAndEnrich(toplevels, clients) {
|
||||
const used = new Set();
|
||||
const result = [];
|
||||
for (const client of clients) {
|
||||
let bestMatch = null;
|
||||
let bestScore = -1;
|
||||
for (const toplevel of toplevels) {
|
||||
if (used.has(toplevel))
|
||||
continue;
|
||||
if (toplevel.appId !== client.appid)
|
||||
continue;
|
||||
let score = 1;
|
||||
if (client.title && toplevel.title) {
|
||||
if (toplevel.title === client.title)
|
||||
score = 3;
|
||||
else if (toplevel.title.includes(client.title) || client.title.includes(toplevel.title))
|
||||
score = 2;
|
||||
}
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestMatch = toplevel;
|
||||
if (score === 3)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bestMatch)
|
||||
continue;
|
||||
used.add(bestMatch);
|
||||
|
||||
const enriched = {
|
||||
"appId": bestMatch.appId,
|
||||
"title": bestMatch.title,
|
||||
"activated": !!client.is_focused,
|
||||
"mangoWindowId": client.id,
|
||||
"mangoTags": client.tags || [],
|
||||
"mangoMonitor": client.monitor
|
||||
};
|
||||
for (let prop in bestMatch) {
|
||||
if (!(prop in enriched))
|
||||
enriched[prop] = bestMatch[prop];
|
||||
}
|
||||
result.push(enriched);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function sortToplevels(toplevels) {
|
||||
if (!toplevels || toplevels.length === 0 || windows.length === 0)
|
||||
return [...toplevels];
|
||||
const enriched = _matchAndEnrich(toplevels, _orderedClients());
|
||||
const used = new Set(enriched.map(e => e.mangoWindowId));
|
||||
// Append wlr toplevels that had no mango client match (rare).
|
||||
const matchedTitles = new Set(enriched.map(e => e.title + "\u0000" + e.appId));
|
||||
for (const t of toplevels) {
|
||||
if (!matchedTitles.has((t.title || "") + "\u0000" + (t.appId || "")))
|
||||
enriched.push(t);
|
||||
}
|
||||
return enriched;
|
||||
}
|
||||
|
||||
function _activeTagSet(screenName) {
|
||||
const out = outputs[screenName];
|
||||
return new Set((out?.activeTags) || []);
|
||||
}
|
||||
|
||||
function filterCurrentWorkspace(toplevels, screenOrName) {
|
||||
const screenName = _screenName(screenOrName);
|
||||
if (!screenName)
|
||||
return toplevels;
|
||||
const active = _activeTagSet(screenName);
|
||||
if (active.size === 0)
|
||||
return toplevels;
|
||||
|
||||
const onActive = tags => (tags || []).some(t => active.has(t));
|
||||
|
||||
if (toplevels.length > 0 && toplevels[0].mangoTags !== undefined)
|
||||
return toplevels.filter(t => t.mangoMonitor === screenName && onActive(t.mangoTags));
|
||||
|
||||
const clients = (windows || []).filter(c => c.monitor === screenName && onActive(c.tags));
|
||||
return _matchAndEnrich(toplevels, clients);
|
||||
}
|
||||
|
||||
function filterCurrentDisplay(toplevels, screenOrName) {
|
||||
const screenName = _screenName(screenOrName);
|
||||
if (!toplevels || toplevels.length === 0 || !screenName)
|
||||
return toplevels;
|
||||
|
||||
if (toplevels.length > 0 && toplevels[0].mangoMonitor !== undefined)
|
||||
return toplevels.filter(t => t.mangoMonitor === screenName);
|
||||
|
||||
const clients = (windows || []).filter(c => c.monitor === screenName);
|
||||
return _matchAndEnrich(toplevels, clients);
|
||||
}
|
||||
|
||||
// ── Commands (mango verb IPC: mmsg dispatch <func>,<args>) ─────────────
|
||||
|
||||
function reloadConfig() {
|
||||
Proc.runCommand("mango-reload", ["mmsg", "dispatch", "reload_config"], (output, exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
log.warn("mmsg reload_config failed:", output);
|
||||
});
|
||||
}
|
||||
|
||||
function quit() {
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "quit"]);
|
||||
}
|
||||
|
||||
// mango tag dispatches act on the focused monitor; tagIndex is 0-based
|
||||
// (dwl model), mango `view`/`toggleview` take a 1-based tag number.
|
||||
function switchToTag(outputName, tagIndex) {
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "view," + (tagIndex + 1)]);
|
||||
}
|
||||
|
||||
function toggleTag(outputName, tagIndex) {
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "toggleview," + (tagIndex + 1)]);
|
||||
}
|
||||
|
||||
// mango's tiling layouts are a fixed compiled-in set the IPC doesn't expose,
|
||||
// so mirror it here in mango's layouts[] order (layout_index aligns). The
|
||||
// parallel name list exists because `setlayout` dispatches by name, not index.
|
||||
readonly property var layouts: ["T", "S", "G", "M", "K", "CT", "RT", "VS", "VT", "VG", "VK", "DW", "F", "VF"]
|
||||
readonly property var _layoutNames: ["tile", "scroller", "grid", "monocle", "deck", "center_tile", "right_tile", "vertical_scroller", "vertical_tile", "vertical_grid", "vertical_deck", "dwindle", "fair", "vertical_fair"]
|
||||
|
||||
function setLayout(outputName, index) {
|
||||
const name = _layoutNames[index];
|
||||
if (name)
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "setlayout," + name]);
|
||||
}
|
||||
|
||||
function cycleKeyboardLayout() {
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "switch_keyboard_layout"]);
|
||||
}
|
||||
|
||||
function powerOffMonitors() {
|
||||
const screens = Quickshell.screens || [];
|
||||
for (let i = 0; i < screens.length; i++) {
|
||||
if (screens[i] && screens[i].name)
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "disable_monitor," + screens[i].name]);
|
||||
}
|
||||
}
|
||||
|
||||
function powerOnMonitors() {
|
||||
const screens = Quickshell.screens || [];
|
||||
for (let i = 0; i < screens.length; i++) {
|
||||
if (screens[i] && screens[i].name)
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "enable_monitor," + screens[i].name]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Config generation (mango config fragments under ~/.config/mango/dms) ─
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onBarConfigsChanged() {
|
||||
if (!CompositorService.isMango)
|
||||
return;
|
||||
const newGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
|
||||
if (newGaps === root._lastGapValue)
|
||||
return;
|
||||
root._lastGapValue = newGaps;
|
||||
generateLayoutConfig();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onIsMangoChanged() {
|
||||
if (CompositorService.isMango)
|
||||
generateLayoutConfig();
|
||||
}
|
||||
}
|
||||
|
||||
function transformToMango(transform) {
|
||||
switch (transform) {
|
||||
case "Normal":
|
||||
return 0;
|
||||
case "90":
|
||||
return 1;
|
||||
case "180":
|
||||
return 2;
|
||||
case "270":
|
||||
return 3;
|
||||
case "Flipped":
|
||||
return 4;
|
||||
case "Flipped90":
|
||||
return 5;
|
||||
case "Flipped180":
|
||||
return 6;
|
||||
case "Flipped270":
|
||||
return 7;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function generateOutputsConfig(outputsData, callback) {
|
||||
if (!outputsData || Object.keys(outputsData).length === 0) {
|
||||
if (callback)
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
|
||||
|
||||
for (const outputName in outputsData) {
|
||||
const output = outputsData[outputName];
|
||||
if (!output)
|
||||
continue;
|
||||
let width = 1920;
|
||||
let height = 1080;
|
||||
let refreshRate = 60;
|
||||
if (output.modes && output.current_mode !== undefined) {
|
||||
const mode = output.modes[output.current_mode];
|
||||
if (mode) {
|
||||
width = mode.width || 1920;
|
||||
height = mode.height || 1080;
|
||||
refreshRate = Math.round((mode.refresh_rate || 60000) / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
const x = output.logical?.x ?? 0;
|
||||
const y = output.logical?.y ?? 0;
|
||||
const scale = output.logical?.scale ?? 1.0;
|
||||
const transform = transformToMango(output.logical?.transform ?? "Normal");
|
||||
const vrr = output.vrr_enabled ? 1 : 0;
|
||||
|
||||
// Anchor the name regex: mango matches `name:` unanchored (first-match
|
||||
// wins), so a bare "DP-1" would also match "eDP-1" and collapse outputs.
|
||||
const rule = ["name:^" + outputName + "$", "width:" + width, "height:" + height, "refresh:" + refreshRate, "x:" + x, "y:" + y, "scale:" + scale, "rr:" + transform, "vrr:" + vrr].join(",");
|
||||
|
||||
lines.push("monitorrule=" + rule);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
|
||||
const content = lines.join("\n");
|
||||
|
||||
Proc.runCommand("mango-write-outputs", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
log.warn("Failed to write outputs config:", output);
|
||||
if (callback)
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
log.info("Generated outputs config at", outputsPath);
|
||||
if (CompositorService.isMango)
|
||||
reloadConfig();
|
||||
if (callback)
|
||||
callback(true);
|
||||
});
|
||||
}
|
||||
|
||||
function generateLayoutConfig() {
|
||||
if (!CompositorService.isMango)
|
||||
return;
|
||||
|
||||
const defaultRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
|
||||
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
|
||||
const defaultBorderSize = 2;
|
||||
|
||||
const cornerRadius = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutRadiusOverride >= 0) ? SettingsData.mangoLayoutRadiusOverride : defaultRadius;
|
||||
const gaps = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutGapsOverride >= 0) ? SettingsData.mangoLayoutGapsOverride : defaultGaps;
|
||||
const borderSize = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutBorderSize >= 0) ? SettingsData.mangoLayoutBorderSize : defaultBorderSize;
|
||||
|
||||
let content = `# Auto-generated by DMS - do not edit manually
|
||||
border_radius=${cornerRadius}
|
||||
gappih=${gaps}
|
||||
gappiv=${gaps}
|
||||
gappoh=${gaps}
|
||||
gappov=${gaps}
|
||||
borderpx=${borderSize}
|
||||
`;
|
||||
|
||||
Proc.runCommand("mango-write-layout", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${layoutPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
log.warn("Failed to write layout config:", output);
|
||||
return;
|
||||
}
|
||||
log.info("Generated layout config at", layoutPath);
|
||||
reloadConfig();
|
||||
});
|
||||
}
|
||||
|
||||
function generateCursorConfig() {
|
||||
if (!CompositorService.isMango)
|
||||
return;
|
||||
|
||||
const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null;
|
||||
if (!settings) {
|
||||
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
log.warn("Failed to write cursor config:", output);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const themeName = settings.theme === "System Default" ? (SettingsData.systemDefaultCursorTheme || "") : settings.theme;
|
||||
const size = settings.size || 24;
|
||||
const hideTimeout = settings.mango?.cursorHideTimeout || 0;
|
||||
|
||||
const isDefaultConfig = !themeName && size === 24 && hideTimeout === 0;
|
||||
if (isDefaultConfig) {
|
||||
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
log.warn("Failed to write cursor config:", output);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let content = `# Auto-generated by DMS - do not edit manually
|
||||
cursor_size=${size}`;
|
||||
|
||||
if (themeName)
|
||||
content += `\ncursor_theme=${themeName}`;
|
||||
|
||||
if (hideTimeout > 0)
|
||||
content += `\ncursor_hide_timeout=${hideTimeout}`;
|
||||
|
||||
content += `\n`;
|
||||
|
||||
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${cursorPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
log.warn("Failed to write cursor config:", output);
|
||||
return;
|
||||
}
|
||||
log.info("Generated cursor config at", cursorPath);
|
||||
reloadConfig();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -319,6 +319,11 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CompositorService.isMango) {
|
||||
MangoService.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (CompositorService.isLabwc) {
|
||||
LabwcService.quit();
|
||||
return;
|
||||
|
||||
@@ -36,6 +36,7 @@ Singleton {
|
||||
"isNiri": () => CompositorService.isNiri,
|
||||
"isHyprland": () => CompositorService.isHyprland,
|
||||
"isDwl": () => CompositorService.isDwl,
|
||||
"isMango": () => CompositorService.isMango,
|
||||
"keybindsAvailable": () => KeybindsService.available,
|
||||
"soundsAvailable": () => AudioService.soundsAvailable,
|
||||
"cupsAvailable": () => CupsService.cupsAvailable,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[templates.dmsmango]
|
||||
input_path = 'SHELL_DIR/matugen/templates/mango-colors.conf'
|
||||
output_path = 'CONFIG_DIR/mango/dms/colors.conf'
|
||||
post_hook = 'sh -c "mmsg -d reload_config 2>&1 || true"'
|
||||
post_hook = 'sh -c "mmsg dispatch reload_config 2>&1 || true"'
|
||||
|
||||
Reference in New Issue
Block a user