Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package nwg-displays for openSUSE:Factory 
checked in at 2026-11-10 14:40:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/nwg-displays (Old)
 and      /work/SRC/openSUSE:Factory/.nwg-displays.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "nwg-displays"

Tue Nov 10 14:40:35 2026 rev:12 rq:1396415 version:0.5.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/nwg-displays/nwg-displays.changes        
2026-08-28 19:48:22.486057814 +0200
+++ /work/SRC/openSUSE:Factory/.nwg-displays.new.3759290/nwg-displays.changes   
2026-11-10 14:41:56.807402399 +0100
@@ -1,0 +2,7 @@
+Mon Nov  9 17:40:44 UTC 2026 - Nassim Lhadj <[email protected]>
+
+- Update to version 0.5.0:
+  * fix: respect existing niri includes with additional properties
+  * feat: add mangowm support
+
+-------------------------------------------------------------------

Old:
----
  nwg-displays-0.4.4.tar.gz

New:
----
  nwg-displays-0.5.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ nwg-displays.spec ++++++
--- /var/tmp/diff_new_pack.03H5Rh/_old  2026-11-10 14:41:57.748441820 +0100
+++ /var/tmp/diff_new_pack.03H5Rh/_new  2026-11-10 14:41:57.751441946 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           nwg-displays
-Version:        0.4.4
+Version:        0.5.0
 Release:        0
 Summary:        Output management utility for sway, Hyprland and Niri
 License:        MIT

++++++ nwg-displays-0.4.4.tar.gz -> nwg-displays-0.5.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-displays-0.4.4/README.md 
new/nwg-displays-0.5.0/README.md
--- old/nwg-displays-0.4.4/README.md    2026-08-24 21:48:09.000000000 +0200
+++ new/nwg-displays-0.5.0/README.md    2026-10-20 21:06:49.000000000 +0200
@@ -3,14 +3,14 @@
 
 This application is a part of the 
[nwg-shell](https://nwg-piotr.github.io/nwg-shell) project.
 
-**Nwg-displays** is an output management utility for 
[sway](https://github.com/swaywm/sway), 
[Hyprland](https://github.com/hyprwm/Hyprland) and 
[Niri](https://github.com/niri-wm/niri)
+**Nwg-displays** is an output management utility for 
[sway](https://github.com/swaywm/sway), 
[Hyprland](https://github.com/hyprwm/Hyprland), 
[Niri](https://github.com/niri-wm/niri) and 
[mangowm](https://github.com/mangowm/mango)
 Wayland compositor, inspired by wdisplays and wlay. The program is expected to:
 
 - provide an intuitive GUI to manage multiple displays;
 - apply settings;
 - save outputs configuration to a text file;
 - save workspace -> output assignments to a text file;
-- support sway, Hyprland and Niri only.
+- support sway, Hyprland, Niri and mangowm only.
 
 <img 
src="https://user-images.githubusercontent.com/20579136/158013748-5b27f742-0e6a-4d82-a5ac-06368b4df008.png";
 width=640, alt="screenshot"><br>
 
@@ -130,6 +130,20 @@
 
 **Note:** Niri uses dynamic workspaces, so the workspace assignment feature is 
not available for Niri.
 
+### mangowm
+
+[Monitors](https://mangowm.github.io/docs/configuration/monitors):
+
+**Requires `wlr-randr` to query monitor state.**
+
+The configuration is saved to `~/.config/mango/monitors.conf`. Make sure your 
config sources this file, e.g. with:
+
+```text
+source_optional=./monitors.conf
+```
+
+**Note:** Mango uses dynamic tags, so the workspace assignment feature is not 
available for Mango.
+
 ## Settings
 
 The runtime configuration file is placed in your config directory, like 
`~/.config/nwg-displays/config`.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-displays-0.4.4/nwg_displays/langs/en_US.json 
new/nwg-displays-0.5.0/nwg_displays/langs/en_US.json
--- old/nwg-displays-0.4.4/nwg_displays/langs/en_US.json        2026-08-24 
21:48:09.000000000 +0200
+++ new/nwg-displays-0.5.0/nwg_displays/langs/en_US.json        2026-10-20 
21:06:49.000000000 +0200
@@ -51,5 +51,11 @@
   "no-profiles": "No profiles available",
   "load": "Load",
   "profile-bound-wallpapers": "Profile-bound wallpapers",
-  "profile-bound-wallpapers-tooltip": "Save and load wallpapers together with 
display profiles"
+  "profile-bound-wallpapers-tooltip": "Save and load wallpapers together with 
display profiles",
+  "mango-hdr-tooltip": "Enables HDR support. \n'Forced' also enables HDR even 
when the EDID \ndoes not advertise BT.2020/PQ.",
+  "mango-primary-tooltip": "Makes this monitor the X11 (RandR) primary 
\noutput for XWayland.",
+  "mango-hdr-min-lum-tooltip": "(HDR) Mastering display minimum luminance, 
cd/m² \n(0 = unset).",
+  "mango-hdr-max-lum-tooltip": "(HDR) Mastering display peak luminance 
(max_cll), \ncd/m² (0 = unset).",
+  "mango-hdr-max-avg-lum-tooltip": "(HDR) Max frame-average light level 
(max_fall), \ncd/m² (0 = unset).",
+  "mango-custom-mode-tooltip": "Enables a mode not listed in the available 
modes. \nNot supported on all displays — may cause \na black screen."
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-displays-0.4.4/nwg_displays/langs/pl_PL.json 
new/nwg-displays-0.5.0/nwg_displays/langs/pl_PL.json
--- old/nwg-displays-0.4.4/nwg_displays/langs/pl_PL.json        2026-08-24 
21:48:09.000000000 +0200
+++ new/nwg-displays-0.5.0/nwg_displays/langs/pl_PL.json        2026-10-20 
21:06:49.000000000 +0200
@@ -58,5 +58,13 @@
   "delete-error-message": "Nie można usunąć profilu: {e}",
   "load-error-missing-data": "Brak danych do załadowania profilu",
   "load-error-message": "Nie można załadować profilu",
-  "current-profile": "Profil"
+  "current-profile": "Profil",
+  "profile-bound-wallpapers": "Tapety profilu",
+  "profile-bound-wallpapers-tooltip": "Zapisuj i wczytuj tapetę razem z 
profilami wyświetlaczy",
+  "mango-hdr-tooltip": "Włącza wsparcie HDR. \n'Forced' włącza HDR nawet gdy 
EDID \ntego nie zaleca BT.2020/PQ.",
+  "mango-primary-tooltip": "Ustawia monitor jako główny dla wyjścia X11 
(RandR) \ndla XWayland.",
+"mango-hdr-min-lum-tooltip": "(HDR) Minimalna luminancja wyświetlacza 
wzorcowego, cd/m² \n(0 = nieustawione).",
+  "mango-hdr-max-lum-tooltip": "(HDR) Szczytowa luminancja wyświetlacza 
wzorcowego (max_cll), \ncd/m² (0 = nieustawione).",
+  "mango-hdr-max-avg-lum-tooltip": "(HDR) Maksymalny średni poziom jasności 
klatki (max_fall), \ncd/m² (0 = nieustawione).",
+  "mango-custom-mode-tooltip": "Włącza tryb nie występujący w dostępnych 
trybach. \nMoże spowodować czarny ekran."
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-displays-0.4.4/nwg_displays/main.py 
new/nwg-displays-0.5.0/nwg_displays/main.py
--- old/nwg-displays-0.4.4/nwg_displays/main.py 2026-08-24 21:48:09.000000000 
+0200
+++ new/nwg-displays-0.5.0/nwg_displays/main.py 2026-10-20 21:06:49.000000000 
+0200
@@ -43,9 +43,17 @@
 from nwg_displays.__about__ import __version__
 
 dir_name = os.path.dirname(__file__)
+
+# Clear orphaned SWAYSOCK variable if the socket file no longer exists on disk
+sway_sock = os.getenv("SWAYSOCK")
+if sway_sock and not os.path.exists(sway_sock):
+    eprint("Removing persistent SWAYSOCK env var")
+    os.environ.pop("SWAYSOCK", None)
+
 sway = os.getenv("SWAYSOCK") is not None
 hypr = os.getenv("HYPRLAND_INSTANCE_SIGNATURE") is not None
 niri = os.getenv("NIRI_SOCKET") is not None
+mango = os.getenv("MANGO_INSTANCE_SIGNATURE") is not None
 
 config_dir = os.path.join(get_config_home(), "nwg-displays")
 # This was done by mistake, and the config file need to be migrated to the 
proper path
@@ -77,6 +85,14 @@
     )
     sys.exit(1)
 
+mango_config_dir = os.path.join(get_config_home(), "mango")
+if mango and not os.path.isdir(mango_config_dir):
+    print(
+        "[Warning] Couldn't find Mango config directory 
'{}'".format(mango_config_dir),
+        file=sys.stderr,
+    )
+    sys.exit(1)
+
 # Create empty files if not found
 if sway:
     for name in ["outputs", "workspaces"]:
@@ -87,8 +103,11 @@
 elif niri:
     for name in ["monitor.kdl"]:
         create_empty_file(os.path.join(niri_config_dir, name))
+elif mango:
+    for name in ["monitors.conf"]:
+        create_empty_file(os.path.join(mango_config_dir, name))
 else:
-    eprint("[Error] Neither sway, Hyprland nor niri detected, terminating")
+    eprint("[Error] Neither sway, Hyprland, niri nor Mango detected, 
terminating")
     sys.exit(1)
 
 config = {}
@@ -135,6 +154,11 @@
 form_mirror = None
 form_ten_bit = None
 form_color_mode = None
+form_hdr = None
+form_primary = None
+form_hdr_min_lum = None
+form_hdr_max_lum = None
+form_hdr_max_avg_lum = None
 form_sdr_brightness = None
 form_sdr_saturation = None
 form_profile_wallpapers = None
@@ -356,6 +380,25 @@
     form_refresh.set_value(widget.refresh)
     if form_ten_bit:
         form_ten_bit.set_active(widget.ten_bit)
+    if form_hdr:
+        hdr_ids = {
+            "": "disabled",
+            "hdr": "enabled",
+            "force": "forced",
+            "hdredid": "enabled"
+        }
+        form_hdr.set_active_id(hdr_ids.get(widget.color_mode, "disabled"))
+        update_hdr_dependent_widgets()
+    if form_primary:
+        form_primary.set_active(getattr(widget, "primary", False))
+    if form_hdr_min_lum:
+        form_hdr_min_lum.set_value(getattr(widget, "hdr_min_lum", 0.0))
+    if form_hdr_max_lum:
+        form_hdr_max_lum.set_value(getattr(widget, "hdr_max_lum", 0.0))
+    if form_hdr_max_avg_lum:
+        form_hdr_max_avg_lum.set_value(
+            getattr(widget, "hdr_max_avg_lum", 0.0)
+        )
     if form_color_mode:
         form_color_mode.set_active_id(widget.color_mode if widget.color_mode 
else "")
         update_color_dependent_widgets(widget.color_mode)
@@ -427,6 +470,10 @@
         color_mode="",
         sdr_brightness=1.0,
         sdr_saturation=1.0,
+        primary=False,
+        hdr_min_lum=0.0,
+        hdr_max_lum=0.0,
+        hdr_max_avg_lum=0.0,
     ):
         super().__init__()
         # Output properties
@@ -458,6 +505,11 @@
         self.color_mode = color_mode
         self.sdr_brightness = sdr_brightness
         self.sdr_saturation = sdr_saturation
+        # HDR / primary (Mango)
+        self.primary = primary
+        self.hdr_min_lum = hdr_min_lum
+        self.hdr_max_lum = hdr_max_lum
+        self.hdr_max_avg_lum = hdr_max_avg_lum
 
         # Button properties
         self.selected = False
@@ -552,6 +604,41 @@
         selected_output_button.ten_bit = check_btn.get_active()
 
 
+def on_hdr_selected(combo):
+    if selected_output_button:
+        ids = {"disabled": "", "enabled": "hdr", "forced": "force"}
+        selected_output_button.color_mode = ids.get(combo.get_active_id(), "")
+    update_hdr_dependent_widgets()
+
+
+def update_hdr_dependent_widgets():
+    """Enable the mastering luminance fields only when HDR is enabled."""
+    enabled = bool(form_hdr and form_hdr.get_active_id() != "disabled")
+    for widget in (form_hdr_min_lum, form_hdr_max_lum, form_hdr_max_avg_lum):
+        if widget:
+            widget.set_sensitive(enabled)
+
+
+def on_primary_toggled(check_btn):
+    if selected_output_button:
+        selected_output_button.primary = check_btn.get_active()
+
+
+def on_hdr_min_lum_changed(spin):
+    if selected_output_button:
+        selected_output_button.hdr_min_lum = spin.get_value()
+
+
+def on_hdr_max_lum_changed(spin):
+    if selected_output_button:
+        selected_output_button.hdr_max_lum = spin.get_value()
+
+
+def on_hdr_max_avg_lum_changed(spin):
+    if selected_output_button:
+        selected_output_button.hdr_max_avg_lum = spin.get_value()
+
+
 def is_hdr_mode(color_mode):
     return color_mode in ["hdr", "hdredid"]
 
@@ -755,6 +842,10 @@
             color_mode=item.get("color_mode", ""),
             sdr_brightness=item.get("sdr_brightness", 1.0),
             sdr_saturation=item.get("sdr_saturation", 1.0),
+            primary=item.get("primary", False),
+            hdr_min_lum=item.get("hdr_min_lum", 0.0),
+            hdr_max_lum=item.get("hdr_max_lum", 0.0),
+            hdr_max_avg_lum=item.get("hdr_max_avg_lum", 0.0),
         )
 
         display_buttons.append(b)
@@ -1059,6 +1150,9 @@
     if os.getenv("NIRI_SOCKET"):
         niri_reload_config()
         GLib.timeout_add(2000, create_display_buttons)
+    if os.getenv("MANGO_INSTANCE_SIGNATURE"):
+        mango_reload_config()
+        GLib.timeout_add(2000, create_display_buttons)
 
     if config_dir and profile_name:
         if config.get("profile-bound-wallpapers", True):
@@ -1119,6 +1213,15 @@
         confirm_win.close()
         niri_reload_config()
         GLib.timeout_add(2000, create_display_buttons)
+    elif os.getenv("MANGO_INSTANCE_SIGNATURE"):
+        # For Mango: backup is a list of lines, restore and reload.
+        # DPMS power state is runtime-only (not stored in monitors.conf),
+        # so wake all monitors back on after restoring.
+        save_list_to_text_file(backup, path)
+        confirm_win.close()
+        mango_reload_config()
+        mango_wakeup_all_monitors()
+        GLib.timeout_add(2000, create_display_buttons)
 
 
 def main():
@@ -1193,6 +1296,25 @@
             help="number of Workspaces in use, default: 10",
         )
 
+    elif mango:
+        parser.add_argument(
+            "-m",
+            "--monitors_path",
+            type=str,
+            default="{}/monitors.conf".format(mango_config_dir),
+            help="path to save the monitors.conf file to, default: {}".format(
+                "{}/monitors.conf".format(mango_config_dir)
+            ),
+        )
+
+        parser.add_argument(
+            "-n",
+            "--num_ws",
+            type=int,
+            default=10,
+            help="number of Workspaces in use, default: 10",
+        )
+
     parser.add_argument(
         "-v",
         "--version",
@@ -1277,6 +1399,37 @@
         else:
             eprint("niri config directory not found!")
             outputs_path = ""
+    elif mango:
+        if os.path.isdir(mango_config_dir):
+            outputs_path = args.monitors_path
+            # Check if file is writable
+            if os.path.lexists(outputs_path):
+                is_writable = os.access(outputs_path, os.W_OK)
+                if os.path.islink(outputs_path) and not is_writable:
+                    eprint(f"INFO: '{outputs_path}' is a read-only symlink. 
Replacing with a writable file.")
+                    tmp_path = f"{outputs_path}.tmp"
+                    try:
+                        with open(outputs_path, "r") as src_file, 
open(tmp_path, "w") as tmp_file:
+                            tmp_file.write(src_file.read())
+                        backup_path = f"{outputs_path}.bkp"
+                        counter = 1
+                        while os.path.lexists(backup_path):
+                            backup_path = f"{outputs_path}.bkp{counter}"
+                            counter += 1
+                        eprint(f"INFO: Backing up '{outputs_path}' to 
'{backup_path}'")
+                        os.rename(outputs_path, backup_path)
+                        os.rename(tmp_path, outputs_path)
+                    except Exception as e:
+                        eprint(f"ERROR: Failed to replace read-only symlink: 
{e}")
+                elif not os.path.islink(outputs_path) and not is_writable:
+                    eprint(f"INFO: '{outputs_path}' is a read-only file. 
Making it writable.")
+                    try:
+                        os.chmod(outputs_path, os.stat(outputs_path).st_mode | 
stat.S_IWUSR)
+                    except Exception as e:
+                        eprint(f"ERROR: Failed to make file writable: {e}")
+        else:
+            eprint("Mango config directory not found!")
+            outputs_path = ""
 
     global num_ws
     num_ws = args.num_ws
@@ -1362,9 +1515,9 @@
 
     global form_adaptive_sync
     form_adaptive_sync = builder.get_object("adaptive-sync")
-    # sway, Hyprland (vrr) and niri (variable-refresh-rate) all support 
per-output
-    # adaptive sync, so the toggle is available on all three.
-    if sway or hypr or niri:
+    # sway, Hyprland (vrr), niri (variable-refresh-rate) and mangowm (vrr) all
+    # support per-output adaptive sync, so the toggle is available on all.
+    if sway or hypr or niri or mango:
         form_adaptive_sync.set_label(voc["adaptive-sync"])
         form_adaptive_sync.set_tooltip_text(voc["adaptive-sync-tooltip"])
         form_adaptive_sync.connect("toggled", on_adaptive_sync_toggled)
@@ -1373,7 +1526,7 @@
 
     global form_custom_mode
     form_custom_mode = builder.get_object("custom-mode")
-    if sway:
+    if sway or mango:
         form_custom_mode.set_label(voc["custom-mode"])
         form_custom_mode.set_tooltip_text(voc["custom-mode-tooltip"])
         form_custom_mode.connect("toggled", on_custom_mode_toggle)
@@ -1478,6 +1631,10 @@
         # Niri uses dynamic workspaces, no need for workspace assignment
         form_workspaces.set_sensitive(False)
         form_workspaces.set_tooltip_text("Niri uses dynamic workspaces")
+    elif mango:
+        # Mango uses dynamic tags, no need for workspace assignment
+        form_workspaces.set_sensitive(False)
+        form_workspaces.set_tooltip_text("Mango uses dynamic tags")
 
     global form_close
     form_close = builder.get_object("close")
@@ -1488,7 +1645,7 @@
     global form_apply
     form_apply = builder.get_object("apply")
     form_apply.set_label(voc["apply"])
-    if (sway and sway_config_dir) or (hypr and hypr_config_dir) or (niri and 
niri_config_dir):
+    if (sway and sway_config_dir) or (hypr and hypr_config_dir) or (niri and 
niri_config_dir) or (mango and mango_config_dir):
         form_apply.connect("clicked", on_apply_button, profile_manager)
     else:
         form_apply.set_sensitive(False)
@@ -1545,6 +1702,61 @@
         form_mirror.connect("changed", on_mirror_selected)
         grid.attach(form_mirror, 7, 4, 1, 1)
 
+    if mango:
+        grid = builder.get_object("grid")
+
+        global form_hdr
+        form_hdr = Gtk.ComboBoxText()
+        form_hdr.append("disabled", "Disabled")
+        form_hdr.append("enabled", "Enabled")
+        form_hdr.append("forced", "Forced")
+        form_hdr.set_active_id("disabled")
+        form_hdr.set_tooltip_text(voc["mango-hdr-tooltip"])
+        form_hdr.connect("changed", on_hdr_selected)
+        # Insert a row below the Size/Refresh/Modes row for the HDR controls.
+        grid.insert_row(4)
+
+        lbl = Gtk.Label.new("HDR:")
+        lbl.set_property("halign", Gtk.Align.END)
+        grid.attach(lbl, 6, 4, 1, 1)
+        grid.attach(form_hdr, 7, 4, 1, 1)
+
+        global form_hdr_min_lum, form_hdr_max_lum, form_hdr_max_avg_lum
+        lbl = Gtk.Label.new("Min lum:")
+        lbl.set_property("halign", Gtk.Align.END)
+        grid.attach(lbl, 0, 4, 1, 1)
+        form_hdr_min_lum = Gtk.SpinButton.new_with_range(0, 10000, 1)
+        form_hdr_min_lum.set_tooltip_text(voc["mango-hdr-min-lum-tooltip"])
+        form_hdr_min_lum.connect("value-changed", on_hdr_min_lum_changed)
+        grid.attach(form_hdr_min_lum, 1, 4, 1, 1)
+
+        lbl = Gtk.Label.new("Max lum:")
+        lbl.set_property("halign", Gtk.Align.END)
+        grid.attach(lbl, 2, 4, 1, 1)
+        form_hdr_max_lum = Gtk.SpinButton.new_with_range(0, 10000, 1)
+        form_hdr_max_lum.set_tooltip_text(voc["mango-hdr-max-lum-tooltip"])
+        form_hdr_max_lum.connect("value-changed", on_hdr_max_lum_changed)
+        grid.attach(form_hdr_max_lum, 3, 4, 1, 1)
+
+        lbl = Gtk.Label.new("Avg lum:")
+        lbl.set_property("halign", Gtk.Align.END)
+        grid.attach(lbl, 4, 4, 1, 1)
+        form_hdr_max_avg_lum = Gtk.SpinButton.new_with_range(0, 10000, 1)
+        
form_hdr_max_avg_lum.set_tooltip_text(voc["mango-hdr-max-avg-lum-tooltip"])
+        form_hdr_max_avg_lum.connect("value-changed", 
on_hdr_max_avg_lum_changed)
+        grid.attach(form_hdr_max_avg_lum, 5, 4, 1, 1)
+
+        update_hdr_dependent_widgets()
+
+        # Appended to the end of the checkbox row
+        global form_primary
+        form_primary = Gtk.CheckButton.new_with_label("Primary")
+        form_primary.set_tooltip_text(voc["mango-primary-tooltip"])
+        form_primary.connect("toggled", on_primary_toggled)
+        grid.attach(form_primary, 5, 5, 2, 1)
+
+        form_custom_mode.set_tooltip_text(voc["mango-custom-mode-tooltip"])
+
     if hypr:
         # Color management / HDR controls (Hyprland only)
         grid.insert_row(5)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-displays-0.4.4/nwg_displays/profiles.py 
new/nwg-displays-0.5.0/nwg_displays/profiles.py
--- old/nwg-displays-0.4.4/nwg_displays/profiles.py     2026-08-24 
21:48:09.000000000 +0200
+++ new/nwg-displays-0.5.0/nwg_displays/profiles.py     2026-10-20 
21:06:49.000000000 +0200
@@ -316,6 +316,10 @@
                 "color_mode": getattr(db, "color_mode", ""),
                 "sdr_brightness": getattr(db, "sdr_brightness", 1.0),
                 "sdr_saturation": getattr(db, "sdr_saturation", 1.0),
+                "primary": getattr(db, "primary", False),
+                "hdr_min_lum": getattr(db, "hdr_min_lum", 0.0),
+                "hdr_max_lum": getattr(db, "hdr_max_lum", 0.0),
+                "hdr_max_avg_lum": getattr(db, "hdr_max_avg_lum", 0.0),
                 "active": db.active,
             }
             profile_data["displays"].append(display)
@@ -407,6 +411,16 @@
                             if display.get("sdr_brightness") is not None
                             else 1.0
                         )
+                        db.primary = display.get("primary", False)
+                        db.hdr_min_lum = float(
+                            display.get("hdr_min_lum", 0.0)
+                        )
+                        db.hdr_max_lum = float(
+                            display.get("hdr_max_lum", 0.0)
+                        )
+                        db.hdr_max_avg_lum = float(
+                            display.get("hdr_max_avg_lum", 0.0)
+                        )
                         db.sdr_saturation = float(
                             display.get("sdr_saturation")
                             if display.get("sdr_saturation") is not None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/nwg-displays-0.4.4/nwg_displays/settings_applier/settings_applier.py 
new/nwg-displays-0.5.0/nwg_displays/settings_applier/settings_applier.py
--- old/nwg-displays-0.4.4/nwg_displays/settings_applier/settings_applier.py    
2026-08-24 21:48:09.000000000 +0200
+++ new/nwg-displays-0.5.0/nwg_displays/settings_applier/settings_applier.py    
2026-10-20 21:06:49.000000000 +0200
@@ -1,12 +1,15 @@
 import os
 import shutil
 import datetime
+import re
 import json
 import time
 from nwg_displays.tools import (
     hyprctl,
     niri_msg,
     niri_reload_config,
+    mmsg,
+    mango_reload_config,
     save_list_to_text_file,
     save_kdl_output,
     ensure_niri_config_include,
@@ -32,6 +35,10 @@
             SettingsApplier._apply_niri_json(
                 displays, use_desc, outputs_path, profile_data
             )
+        elif os.getenv("MANGO_INSTANCE_SIGNATURE"):
+            SettingsApplier._apply_mango_json(
+                displays, use_desc, outputs_path, profile_data
+            )
 
         elif os.getenv("HYPRLAND_INSTANCE_SIGNATURE"):
             SettingsApplier._apply_hyprland_json(
@@ -266,6 +273,16 @@
                 config_dir,
                 profile_name,
             )
+        elif os.getenv("MANGO_INSTANCE_SIGNATURE"):
+            SettingsApplier._apply_mango_gui(
+                display_buttons,
+                outputs_activity,
+                outputs_path,
+                use_desc,
+                create_confirm_win_callback,
+                config_dir,
+                profile_name,
+            )
 
         elif os.getenv("SWAYSOCK"):
             SettingsApplier._apply_sway_gui(
@@ -429,6 +446,138 @@
         # Pass backup file path and current file path to confirm window
         if create_confirm_win_callback:
             create_confirm_win_callback(backup_path, outputs_path, config_dir, 
profile_name)
+    @staticmethod
+    def _mango_rule_line(name, d):
+        """Build a monitor_rule line for the Mango monitors.conf file."""
+        transforms = {
+            "normal": 0,
+            "90": 1,
+            "180": 2,
+            "270": 3,
+            "flipped": 4,
+            "flipped-90": 5,
+            "flipped-180": 6,
+            "flipped-270": 7,
+        }
+
+        parts = [f"name:^{re.escape(name)}$"]
+
+        if not d["active"]:
+            parts.append("disable:1")
+        else:
+            parts.extend([
+                f"width:{d['physical_width']}",
+                f"height:{d['physical_height']}",
+                f"refresh:{d['refresh']:g}",
+                f"x:{d['x']}",
+                f"y:{d['y']}",
+                f"scale:{d['scale']:g}",
+            ])
+
+            rr = transforms.get(d["transform"], 0)
+            if rr:
+                parts.append(f"rr:{rr}")
+
+            parts.append("vrr:1" if d.get("adaptive_sync") else "vrr:0")
+            if d.get("custom_mode"):
+                parts.append("custom:1")
+
+            color = (d.get("color_mode") or "")
+            if color in ("hdr", "hdredid", "force"):
+                parts.append("hdr:1")
+            if color == "force":
+                parts.append("hdr_force:1")
+            for key, attr in (
+                ("min", "hdr_min_lum"),
+                ("max", "hdr_max_lum"),
+                ("max_avg", "hdr_max_avg_lum"),
+            ):
+                value = d.get(attr) or 0
+                if value:
+                    parts.append(f"hdr_{key}_lum:{value:g}")
+            if d.get("primary"):
+                parts.append("primary:1")
+
+        return "monitor_rule=" + ",".join(parts)
+
+    @staticmethod
+    def _apply_mango_json(displays, use_desc, outputs_path, profile_data):
+        """Applies configuration by writing the Mango monitors.conf file."""
+        print(f"[Profile] Applying {len(displays)} displays for Mango...")
+
+        header = SettingsApplier._get_header("Profile Loader")
+        lines_conf = []
+
+        for d in displays:
+            name = d["name"]
+            lines_conf.append(SettingsApplier._mango_rule_line(name, d))
+
+        save_list_to_text_file([header] + lines_conf, outputs_path)
+
+        # Reload first so the new monitor rules are applied before changing 
DPMS state.
+        mango_reload_config()
+
+        for d in displays:
+            cmd = "wakeup_monitor" if d["dpms"] else "sleep_monitor"
+            mmsg(f"dispatch {cmd},{d['name']}")
+
+        config, _ = get_config()
+
+        if "wallpapers" in profile_data and config.get(
+            "profile-bound-wallpapers", True
+        ):
+            print("[Profile] Applying wallpapers...")
+            time.sleep(1)
+            WallpaperManager.apply_wallpapers(profile_data["wallpapers"])
+
+    @staticmethod
+    def _apply_mango_gui(
+        display_buttons,
+        outputs_activity,
+        outputs_path,
+        use_desc,
+        create_confirm_win_callback,
+        config_dir=None,
+        profile_name=None,
+    ):
+        """Applies configuration from the GUI by writing the Mango 
monitors.conf file."""
+        print(f"[Mango] Applying {len(display_buttons)} displays...")
+
+        backup = []
+        if os.path.isfile(outputs_path):
+            backup = load_text_file(outputs_path).splitlines()
+
+        lines_conf = [SettingsApplier._get_header()]
+        for db in display_buttons:
+            d = {
+                "active": db.name in outputs_activity and 
outputs_activity[db.name],
+                "physical_width": db.physical_width,
+                "physical_height": db.physical_height,
+                "refresh": db.refresh,
+                "x": db.x,
+                "y": db.y,
+                "scale": db.scale,
+                "transform": db.transform,
+                "adaptive_sync": db.adaptive_sync,
+                "primary": getattr(db, "primary", False),
+                "hdr_min_lum": getattr(db, "hdr_min_lum", 0.0),
+                "hdr_max_lum": getattr(db, "hdr_max_lum", 0.0),
+                "hdr_max_avg_lum": getattr(db, "hdr_max_avg_lum", 0.0),
+                "color_mode": getattr(db, "color_mode", "") or ""
+            }
+            lines_conf.append(SettingsApplier._mango_rule_line(db.name, d))
+
+        save_list_to_text_file(lines_conf, outputs_path)
+
+        # Reload first so the new monitor rules are applied before changing 
DPMS state.
+        mango_reload_config()
+
+        for db in display_buttons:
+            cmd = "wakeup_monitor" if db.dpms else "sleep_monitor"
+            mmsg(f"dispatch {cmd},{db.name}")
+
+        if create_confirm_win_callback:
+            create_confirm_win_callback(backup, outputs_path, config_dir, 
profile_name)
 
     @staticmethod
     def _apply_hyprland_gui(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-displays-0.4.4/nwg_displays/tools.py 
new/nwg-displays-0.5.0/nwg_displays/tools.py
--- old/nwg-displays-0.4.4/nwg_displays/tools.py        2026-08-24 
21:48:09.000000000 +0200
+++ new/nwg-displays-0.5.0/nwg_displays/tools.py        2026-10-20 
21:06:49.000000000 +0200
@@ -2,6 +2,7 @@
 import datetime
 import json
 import os
+import re
 import shutil
 import socket
 import subprocess
@@ -73,6 +74,145 @@
         eprint(f"[niri] Failed to reload config: {e}")
 
 
+def mmsg(cmd):
+    """Execute mmsg command and return output"""
+    try:
+        result = subprocess.run(["mmsg", cmd], capture_output=True, text=True, 
timeout=5)
+        return result.stdout.strip()
+    except Exception as e:
+        eprint(f"Error communicating with Mango: {e}")
+        return ""
+
+
+def mango_reload_config():
+    """Reload Mango configuration via mmsg dispatch"""
+    try:
+        result = subprocess.run(
+            ["mmsg", "dispatch reload_config"],
+            capture_output=True,
+            text=True,
+            timeout=5,
+        )
+        if result.returncode == 0:
+            print("[Mango] Config reloaded")
+        else:
+            eprint(f"[Mango] Failed to reload config: {result.stderr.strip()}")
+    except Exception as e:
+        eprint(f"[Mango] Failed to reload config: {e}")
+
+
+def mango_wakeup_all_monitors():
+    """Wake up all connected monitors.
+
+    DPMS power state is runtime-only and not exposed by mmsg, so the only
+    way to guarantee a known state after restoring settings is to
+    explicitly wake every monitor."""
+    try:
+        data = json.loads(mmsg("get all-monitors"))
+    except json.JSONDecodeError as e:
+        eprint(f"[Mango] Failed to parse monitor list: {e}")
+        return
+    monitors = data.get("monitors", []) if isinstance(data, dict) else data
+    for mon in monitors:
+        name = mon.get("name")
+        if name:
+            mmsg(f"dispatch wakeup_monitor,{name}")
+
+
+def parse_wlr_randr():
+    """Parse wlr-randr output into a dict keyed by output name.
+    Provides enabled state, make/model/serial and available modes,
+    which mmsg does not expose."""
+    result = {}
+    try:
+        output = subprocess.run(
+            ["wlr-randr"], capture_output=True, text=True, timeout=5
+        ).stdout
+    except Exception as e:
+        eprint(f"[Mango] Failed to run wlr-randr: {e}, terminating")
+        sys.exit(1)
+
+    name = None
+    for line in output.splitlines():
+        stripped = line.strip()
+        if not stripped:
+            continue
+        if not line.startswith(" "):
+            # New output header, e.g.: DP-1 "Make Model Serial (DP-1)"
+            match = re.match(r"(\S+) \"([^\"]*)\"", line)
+            if match:
+                name = match.group(1)
+                result[name] = {
+                    "make": "",
+                    "model": "",
+                    "serial": "",
+                    "enabled": False,
+                    "modes": [],
+                    "current_refresh": None,
+                }
+            continue
+        if name is None:
+            continue
+        if stripped.startswith("Make:"):
+            result[name]["make"] = stripped.split(":", 1)[1].strip()
+        elif stripped.startswith("Model:"):
+            result[name]["model"] = stripped.split(":", 1)[1].strip()
+        elif stripped.startswith("Serial:"):
+            result[name]["serial"] = stripped.split(":", 1)[1].strip()
+        elif stripped.startswith("Enabled:"):
+            result[name]["enabled"] = line.strip().endswith("yes")
+        else:
+            mode_match = re.match(r"\s*(\d+)x(\d+) px, ([\d.]+) Hz(.*)", line)
+            if mode_match:
+                w, h, r, rest = mode_match.groups()
+                result[name]["modes"].append(
+                    {"width": int(w), "height": int(h), "refresh": float(r) * 
1000}
+                )
+                if "(current)" in rest:
+                    result[name]["current_refresh"] = float(r)
+    return result
+
+
+def parse_mango_monitor_rules(path):
+    """Parse monitor_rule lines from a mango monitors.conf, keyed by output 
name.
+    Used to restore state (transform, disabled) that mmsg does not expose."""
+    rules = {}
+    if not os.path.isfile(path):
+        return rules
+    for line in load_text_file(path).splitlines():
+        line = line.strip()
+        if not line or line.startswith("#") or "monitor_rule=" not in line:
+            continue
+        value = line.split("=", 1)[1]
+        # The value is a regex. Capture the value after "name:" up to the next
+        # comma-separated key or end of string.
+        name_match = re.search(r"(?:^|,)name:(.*?)(?=,\w+:|$)", value)
+        if not name_match:
+            continue
+        # Inverse of re.escape: drop the backslash from every escaped pair
+        name = re.sub(r"\\(.)", r"\1", name_match.group(1)).strip()
+        # Strip regex anchors
+        name = name.removeprefix("^")
+        name = name.removesuffix("$")
+        rr_match = re.search(r"(?<!v)rr:(\d+)", value)
+        disable_match = re.search(r"disable:1", value)
+        hdr_match = re.search(r"hdr:1", value)
+        lum = {}
+        for key in ("min", "max", "max_avg"):
+            lum_match = re.search(rf"hdr_{key}_lum:([\d.]+)", value)
+            lum[f"{key}_lum"] = float(lum_match.group(1)) if lum_match else 0.0
+        rules[name] = {
+            "transform": int(rr_match.group(1)) if rr_match else 0,
+            "disabled": bool(disable_match),
+            "hdr": bool(hdr_match),
+            "force": bool(re.search(r"hdr_force:1", value)),
+            "primary": bool(re.search(r"primary:1", value)),
+            "custom": bool(re.search(r"custom:1", value)),
+            **lum,
+        }
+    return rules
+
+
 def get_config():
     config_dir = get_config_dir()
     config_file = os.path.join(config_dir, "config")
@@ -338,6 +478,104 @@
 
             outputs_dict[m["name"]]["monitor"] = None
 
+    elif os.getenv("MANGO_INSTANCE_SIGNATURE"):
+        outputs_dict = {}
+        eprint("Running on Mango")
+
+        try:
+            mmsg_output = mmsg("get all-monitors")
+            randr = parse_wlr_randr()
+            monitors_file = os.path.join(
+                get_config_home(), "mango", "monitors.conf"
+            )
+            rules = parse_mango_monitor_rules(monitors_file)
+
+            if not mmsg_output:
+                eprint("[Mango] Could not communicate with the compositor")
+                return {}
+
+            data = json.loads(mmsg_output)
+            monitors = (
+                data.get("monitors", []) if isinstance(data, dict) else data
+            )
+
+            transforms = {
+                0: "normal", 1: "90", 2: "180", 3: "270",
+                4: "flipped", 5: "flipped-90", 6: "flipped-180", 7: 
"flipped-270",
+            }
+
+            for mon in monitors:
+                name = mon.get("name")
+                randr_mon = randr.get(name, {})
+                rule = rules.get(name, {})
+                scale = float(mon.get("scale", 1.0))
+
+                description = " ".join(
+                    part
+                    for part in [
+                        randr_mon.get("make"),
+                        randr_mon.get("model"),
+                        randr_mon.get("serial"),
+                    ]
+                    if part
+                )
+
+                # mmsg reports post-transform dimensions; normalize rotated
+                # outputs back to native orientation (odd rotations swap WxH)
+                transform = rule.get("transform", 0)
+                w, h = int(mon.get("width", 0)), int(mon.get("height", 0))
+                if transform % 4 in (1, 3):
+                    w, h = h, w
+
+                outputs_dict[name] = {
+                    "active": bool(
+                        randr_mon.get("enabled", not rule.get("disabled", 
False))
+                    ),
+                    # DPMS power state is not exposed by mmsg; assume the
+                    # monitor is powered on.
+                    "dpms": True,
+                    "description": description,
+                    "x": int(mon.get("x", 0)),
+                    "y": int(mon.get("y", 0)),
+                    "logical-width": w,
+                    "logical-height": h,
+                    "physical-width": round(w * scale),
+                    "physical-height": round(h * scale),
+                    "transform": transforms.get(transform, "normal"),
+                    "scale": scale,
+                    "scale_filter": "linear",
+                    "refresh": randr_mon.get("current_refresh") or 60.0,
+                    "modes": randr_mon.get("modes", []),
+                    # mmsg's "active" field is the focused/selected monitor
+                    "focused": mon.get("active", False),
+                    "adaptive_sync_status": "enabled" if mon.get("is_vrr", 
False) else "disabled",
+                    "mirror": "",
+                    "ten_bit": False,
+                    # HDR is a config-level setting; mmsg's is_hdr reflects
+                    # runtime state which may lag behind the applied rule
+                    "color_mode": (
+                        "force"
+                        if rule.get("force")
+                        else "hdr"
+                        if rule.get("hdr", False)
+                        else ""
+                    ),
+                    "primary": rule.get("primary", False),
+                    "custom_mode": rule.get("custom", False),
+                    "hdr_min_lum": rule.get("min_lum", 0.0),
+                    "hdr_max_lum": rule.get("max_lum", 0.0),
+                    "hdr_max_avg_lum": rule.get("max_avg_lum", 0.0),
+                    "sdr_brightness": 1.0,
+                    "sdr_saturation": 1.0,
+                    "monitor": None,
+                }
+        except json.JSONDecodeError as e:
+            eprint(f"[Mango] JSON decode error: {e}")
+            return {}
+        except Exception as e:
+            eprint(f"Error parsing Mango outputs: {e}")
+            return {}
+
     else:
         eprint("This program only supports sway, Hyprland and niri, and we 
seem to be elsewhere, terminating.")
         sys.exit(1)
@@ -352,8 +590,8 @@
         monitor = display.get_monitor(i)
         monitors.append(monitor)
 
-    if os.getenv("NIRI_SOCKET"):
-        # Niri: Use position-based matching with Gdk.Monitor geometry
+    if os.getenv("NIRI_SOCKET") or os.getenv("MANGO_INSTANCE_SIGNATURE"):
+        # Niri and Mango: Use position-based matching with Gdk.Monitor geometry
         # This ensures correct matching even when multiple monitors have 
identical properties
         for key in outputs_dict:
             if key in outputs_dict:
@@ -421,7 +659,11 @@
 
         for mon in monitors_all:
             name = mon["name"]
-            result[name] = True if name in active else False
+            result[name] = name in active
+    elif os.getenv("MANGO_INSTANCE_SIGNATURE"):
+        randr = parse_wlr_randr()
+        for name in randr:
+            result[name] = randr[name].get("enabled", True)
 
     return result
 
@@ -577,7 +819,16 @@
 def ensure_niri_config_include(config_dir, monitors_file):
     """Ensure that config.kdl includes monitor.kdl"""
     config_kdl = os.path.join(config_dir, "config.kdl")
-    monitors_rel_path = "monitor.kdl"
+    # Derive the file name from monitors_file so custom --monitors_path works,
+    # fall back to the default when not provided.
+    if monitors_file:
+        monitors_filename = os.path.basename(monitors_file)
+    else:
+        monitors_filename = "monitor.kdl"
+    try:
+        monitors_rel_path = os.path.relpath(monitors_file, config_dir) if 
monitors_file else monitors_filename
+    except Exception:
+        monitors_rel_path = monitors_filename
     
     # Check if config.kdl exists
     if not os.path.isfile(config_kdl):
@@ -595,12 +846,17 @@
     
     lines = content.splitlines()
     
-    # Check if include already exists
-    include_pattern = f'include "{monitors_rel_path}"'
-    include_pattern_single = f"include '{monitors_rel_path}'"
+    # Check if include already exists, regardless of extra properties
+    # e.g. `include "monitor.kdl"`, `include optional=true "monitor.kdl"`,
+    # `include "monitor.kdl" optional=true`, single quotes, absolute paths.
+    # Commented-out lines (`// include ...`) must NOT count.
+    include_re = re.compile(r'^\s*include\b.*' + re.escape(monitors_filename) 
+ r'\b')
     
     for line in lines:
-        if include_pattern in line or include_pattern_single in line:
+        stripped = line.strip()
+        if not stripped or stripped.startswith("//") or 
stripped.startswith("/-"):
+            continue
+        if include_re.search(line):
             # Already included
             return
     
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-displays-0.4.4/setup.py 
new/nwg-displays-0.5.0/setup.py
--- old/nwg-displays-0.4.4/setup.py     2026-08-24 21:48:09.000000000 +0200
+++ new/nwg-displays-0.5.0/setup.py     2026-10-20 21:06:49.000000000 +0200
@@ -9,7 +9,7 @@
 
 setup(
     name="nwg-displays",
-    version="0.4.4",
+    version="0.5.0",
     description="nwg-shell output configuration utility",
     packages=find_packages(),
     include_package_data=True,

Reply via email to