Skip to content
79 changes: 79 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Argos – Changes in this Fork

This fork is based on [p-e-w/argos](https://github.com/p-e-w/argos) and adds several features not available in upstream.
The goal is to stay compatible with existing scripts while making the extension more robust and convenient in daily use.

---

## New Features

### 1. Preserve Submenu State
- **Problem:** On every refresh (`updateInterval`), the entire menu was rebuilt, causing all open submenus to collapse.
- **Solution:**
- Open submenus are captured before `removeAll()` and restored after rebuilding.
- Script output now supports an `id` property for submenu headers:

```text
-- Submenu Header | id=connection
---- Item A
---- Item B
```

- Only if `id` is set will the state be stored and restored.
- No `id` → no restore (backward compatible).

- **Technical details:**
- A new helper class `submenu_state.js` manages capture/restore of submenu states.
- Internally, `GLib.idle_add` is used to ensure restoration happens after the rebuild, avoiding race conditions.

### 2. Reopen Menu After Action
- **Problem:** After running a command from the menu, the menu always closed. For certain workflows (e.g., toggles or quick repeat actions), this was disruptive.
- **Solution:**
- Scripts can now add the property `reopen=true` to a line.
- After executing the action, the menu automatically reopens with updated state.
- Works only for `bash` commands.

- **Technical details:**
- Integrated into `submenu_state.js` via `requestReopen()` and `finalizeUpdate()`.
- `GLib.idle_add` ensures the reopen happens after the rebuild, minimizing flicker.
- Backward compatible: lines without `reopen=true` behave as before.

### 3. Minimum Width for Submenus
- **Problem:** When menu entries of different length appear or disappear during refresh, the whole dropdown width jumps, leading to a distracting UI flicker.
- **Solution:**
- Scripts can now specify a minimum width in pixels for submenu headers using a `minwidth` property:

```text
-- Status | id=status minwidth=360
---- Connected
---- Last handshake: 2m ago
```

- The `minwidth` value is applied as a hard CSS `min-width` on the corresponding `PopupSubMenuMenuItem`.
- Other items remain dynamic, only the submenu header defines the lower bound for its content.

- **Technical details:**
- Applied directly when creating a `PopupSubMenuMenuItem` in `button.js`.
- Backward compatible: lines without `minwidth` behave as before.
- Prevents width “jumping” when submenu content changes.

---

## Internal Changes

- Code for tracking/restoring submenu state was moved out of `button.js` into a **dedicated utility class**.
- Refactored parts of the menu update logic for better maintainability.
- Centralized `reopen` handling in `submenu_state.js` instead of scattering flag checks across `button.js` and `menuitem.js`.
- Added support for per-submenu `minwidth` property.

---

## Known Limitations

- Only one submenu can be open at a time (GNOME Shell limitation).
- Restoring an open submenu causes a short re-animation (visible “flicker”) because the menu is rebuilt.
- Without an `id` property in the script line, no state is restored.
- Menu reopen still causes a very short flicker, as the menu must be rebuilt before reopening.
- Minimum width applies only to submenu headers (`PopupSubMenuMenuItem`), not to the entire dropdown or the panel button.

---
22 changes: 21 additions & 1 deletion argos@pew.worldwidemann.com/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import ArgosLineView from './lineview.js';
import ArgosMenuItem from './menuitem.js';
import * as Utilities from './utilities.js';
import SubmenuState from './submenu_state.js';

const cArgosButton = GObject.registerClass({
GTypeName: "ArgosButton",
Expand All @@ -30,6 +31,7 @@ class ArgosButton extends PanelMenu.Button {

this._file = file;
this._updateInterval = settings.updateInterval;
this._subs = new SubmenuState();

this._lineView = new ArgosLineView();
this._lineView.setMarkup("<small><i>" + GLib.markup_escape_text(file.get_basename(), -1) + " ...</i></small>");
Expand Down Expand Up @@ -131,7 +133,7 @@ class ArgosButton extends PanelMenu.Button {
buttonLines.push(line);
}
}

this._subs.prepareForUpdate(this);
this.menu.removeAll();

if (this._cycleTimeout !== null) {
Expand Down Expand Up @@ -190,6 +192,16 @@ class ArgosButton extends PanelMenu.Button {
// an error or warning, this should be considered a bug in GNOME Shell.
// Once it is fixed, this code will work as expected for nested submenus.
menuItem = new PopupMenu.PopupSubMenuMenuItem("", false);
//add optional support for restore after refresh
if (dropdownLines[i].id) {
menuItem._submenuKey = dropdownLines[i].id;
}
if (dropdownLines[i].minwidth) {
const px = parseInt(dropdownLines[i].minwidth, 10);
if (Number.isFinite(px) && px > 0) {
menuItem.actor.set_style(`min-width:${px}px;`);
}
}
let lineView = new ArgosLineView(dropdownLines[i]);
menuItem.actor.insert_child_below(lineView, menuItem.label);
menuItem.label.visible = false;
Expand All @@ -206,6 +218,12 @@ class ArgosButton extends PanelMenu.Button {
}

menu.addMenuItem(menuItem);
if (menuItem._submenuKey && this._subs.wasOpen(menuItem._submenuKey)) {
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
menuItem.menu.open(false);
return GLib.SOURCE_REMOVE;
});
}
}

if (dropdownLines.length > 0)
Expand All @@ -217,7 +235,9 @@ class ArgosButton extends PanelMenu.Button {
menuItem.connect("activate", () => {
Gio.AppInfo.launch_default_for_uri("file://" + this._file.get_path(), null);
});

this.menu.addMenuItem(menuItem);
this._subs.finalizeUpdate(this);
}
});

Expand Down
9 changes: 7 additions & 2 deletions argos@pew.worldwidemann.com/menuitem.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const cArgosMenuItem = GObject.registerClass(
});

let altSwitcher = null;

let lineView = new ArgosLineView(line);

if (typeof alternateLine === "undefined") {
Expand All @@ -56,6 +55,10 @@ const cArgosMenuItem = GObject.registerClass(
eval(activeLine.eval);

if (activeLine.hasOwnProperty("bash")) {
// support reopen
if (activeLine.reopen === "true") {
button._subs.requestReopen(button);
}
let argv = [];


Expand All @@ -78,7 +81,9 @@ const cArgosMenuItem = GObject.registerClass(
}
} else if (activeLine.refresh === "true") {
button.update();
}
}else if (activeLine.reopen === "true") { // trigger update even without bash
button.update();
}
});
}
}
Expand Down
45 changes: 45 additions & 0 deletions argos@pew.worldwidemann.com/submenu_state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import GLib from 'gi://GLib';
// submenu_state.js
export default class SubmenuState {
constructor() { this._openKeys = new Set(); }


wasOpen(key) { return key && this._openKeys.has(key); }

requestReopen(button) {
button._reopenAfterAction = true;
this._capture(button.menu);
}

prepareForUpdate(button) {
if(button._reopenAfterAction) return;
this._capture(button.menu);
}

finalizeUpdate(button) {
if (!button._reopenAfterAction) return;
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
button.menu.open(false);
return GLib.SOURCE_REMOVE;
});
button._reopenAfterAction = false;
this._openKeys.clear();
}

_capture(rootMenu) {
this._openKeys.clear();
this._walk(rootMenu, item => {
if (item.menu?.isOpen && item._submenuKey) this._openKeys.add(item._submenuKey);
});
}

// interne Hilfe: durch alle Items (rekursiv) laufen
_walk(menu, fn) {
for (const it of menu._getMenuItems()) {
if (it.constructor.name === 'PopupSubMenuMenuItem') {
fn(it);
this._walk(it.menu, fn);
}
}
}
}
30 changes: 30 additions & 0 deletions example.5s.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# demo-argos-features.10s.sh
# Shows: submenu state restore (id=...), reopen=true, and minwidth on header.

# Panel title
echo "🧪 Demo"

# Start dropdown
echo "---"

# 1) Submenu with a fixed minimum width and a stable id
echo "⚙️ Settings | id=demo-settings minwidth=360"
# Toggle a simple on/off flag in /tmp (purely demonstrative)
FLAG="/tmp/argos-demo-flag"
if [[ -f "$FLAG" ]]; then
echo "--✅ Enabled | bash='rm -f \"$FLAG\"' terminal=false refresh=true reopen=true"
else
echo "--❌ Disabled | bash='touch \"$FLAG\"' terminal=false refresh=true reopen=true"
fi

# 2) Another submenu to show that open state is preserved across refreshes
echo "---"
echo "📊 Status | id=demo-status minwidth=360"
NOW="$(date '+%H:%M:%S')"
echo "--Time: $NOW"
echo "--Flag file present: $( [[ -f \"$FLAG\" ]] && echo yes || echo no )"

# 3) A top-level action (closes unless reopen=true is used)
echo "---"
echo "♻️ Force Refresh | refresh=true"