diff --git a/src/com.github.tchx84.Flatseal.data.gresource.xml b/src/com.github.tchx84.Flatseal.data.gresource.xml
index 7afde932..ca5f3bae 100644
--- a/src/com.github.tchx84.Flatseal.data.gresource.xml
+++ b/src/com.github.tchx84.Flatseal.data.gresource.xml
@@ -6,6 +6,7 @@
widgets/appInfoViewer.ui
widgets/applicationRow.ui
widgets/busNameRow.ui
+ widgets/conditionalStatusIcon.ui
widgets/docsViewer.ui
widgets/globalInfoViewer.ui
widgets/globalRow.ui
diff --git a/src/com.github.tchx84.Flatseal.src.gresource.xml b/src/com.github.tchx84.Flatseal.src.gresource.xml
index 84232173..c22925bf 100644
--- a/src/com.github.tchx84.Flatseal.src.gresource.xml
+++ b/src/com.github.tchx84.Flatseal.src.gresource.xml
@@ -7,6 +7,7 @@
widgets/appInfoViewer.js
widgets/applicationRow.js
widgets/busNameRow.js
+ widgets/conditionalStatusIcon.js
widgets/detailsButton.js
widgets/docsViewer.js
widgets/globalInfoViewer.js
diff --git a/src/models/permissions.js b/src/models/permissions.js
index acd67d60..1cfa36fc 100644
--- a/src/models/permissions.js
+++ b/src/models/permissions.js
@@ -52,6 +52,14 @@ const MODELS = {
unsupported: new FlatpakUnsupportedModel(),
};
+/* Models that support conditional permissions */
+const CONDITIONAL_MODELS = [
+ MODELS.shared,
+ MODELS.sockets,
+ MODELS.devices,
+ MODELS.features,
+];
+
function generate_index() {
const index = {};
@@ -102,6 +110,13 @@ function generate() {
const statusProperty = `${property}-status`;
properties[statusProperty] = GObject.ParamSpec.string(
statusProperty, statusProperty, statusProperty, FLAGS, FlatsealOverrideStatus.ORIGINAL);
+
+ /* conditional requests */
+ if (CONDITIONAL_MODELS.includes(model)) {
+ const conditionalProperty = `${property}-conditional`;
+ properties[conditionalProperty] = GObject.ParamSpec.string(
+ conditionalProperty, conditionalProperty, conditionalProperty, FLAGS, '');
+ }
});
});
@@ -192,12 +207,30 @@ var FlatpakPermissionsModel = GObject.registerClass({
.split(';');
values.forEach(option => {
- /* Flatseal does not support conditionals, but skips them
- * to avoid corrupting the overrides file. */
- if (option.startsWith(CONDITIONAL_PREFIX))
+ let isConditional = false;
+ let bareOption = option;
+
+ if (option.startsWith(CONDITIONAL_PREFIX)) {
+ const parts = option.slice(CONDITIONAL_PREFIX.length).split(':');
+
+ /* A valid conditional has both an option and a
+ * condition after "if:". Ignore incomplete
+ * entries instead of treating them as
+ * conditionals. */
+ if (parts.length >= 2) {
+ isConditional = true;
+ [bareOption] = parts;
+ }
+ }
+
+ /* Conditionals from overrides (per-app or global)
+ * are dropped. They can't be written back without
+ * risking a change in their meaning, so they are
+ * neither loaded nor displayed. */
+ if (isConditional && overrides)
return;
- model = this.constructor._find(`${group}_${key}_${option.replace('!', '')}`);
+ model = this.constructor._find(`${group}_${key}_${bareOption.replace('!', '')}`);
if (model === null)
model = this.constructor._find(`${group}_${key}`);
@@ -205,8 +238,19 @@ var FlatpakPermissionsModel = GObject.registerClass({
if (model === null && overrides && !global)
model = MODELS.unsupported;
- if (model !== null)
- model.loadFromKeyFile(group, key, option, overrides, global);
+ /* Only the four models in CONDITIONAL_MODELS
+ * support conditionals. A conditional for any
+ * other permission is dropped instead of being
+ * loaded as a plain, unconditional permission. */
+ if (isConditional && !CONDITIONAL_MODELS.includes(model))
+ return;
+
+ if (model !== null) {
+ model.loadFromKeyFile(group, key, bareOption, overrides, global);
+
+ if (isConditional)
+ model.markConditional(bareOption, option);
+ }
});
});
});
@@ -270,6 +314,7 @@ var FlatpakPermissionsModel = GObject.registerClass({
GObject.signal_handler_block(this, this._notifyHandlerId);
Object.values(MODELS).forEach(model => model.updateStatusProperty(this));
+ CONDITIONAL_MODELS.forEach(model => model.updateConditionalProperty(this));
GObject.signal_handler_unblock(this, this._notifyHandlerId);
}
@@ -394,6 +439,8 @@ var FlatpakPermissionsModel = GObject.registerClass({
entry['groupStyle'] = model.constructor.getStyle();
entry['groupDescription'] = model.constructor.getDescription();
entry['statusProperty'] = `${property}-status`;
+ entry['conditionalProperty'] = CONDITIONAL_MODELS.includes(model)
+ ? `${property}-conditional` : null;
entry['serializeFunc'] = model.constructor.serialize;
entry['deserializeFunc'] = model.constructor.deserialize;
diff --git a/src/models/shared.js b/src/models/shared.js
index e5bea163..fc96fb55 100644
--- a/src/models/shared.js
+++ b/src/models/shared.js
@@ -150,6 +150,28 @@ var FlatpakSharedModel = GObject.registerClass({
});
}
+ /* Flatpak drops a conditional if a bare grant for the same
+ * option is applied afterward, so this being set doesn't
+ * guarantee the permission is actually granted at runtime. */
+ markConditional(option, rawValue) {
+ this._conditionals.set(option, rawValue);
+ }
+
+ updateConditionalProperty(proxy) {
+ Object.entries(this.getPermissions()).forEach(([property, permission]) => {
+ const {option} = permission;
+ const conditionalProperty = `${property}-conditional`;
+ let value = this._conditionals.get(option) || '';
+
+ /* The conditional marker should not show on the */
+ /* overriden conditional permisions. */
+ if (this._getStatusForPermission(option) !== FlatsealOverrideStatus.ORIGINAL)
+ value = '';
+
+ proxy.set_property(conditionalProperty, value);
+ });
+ }
+
updateProxyProperty(proxy) {
const originals = [...this._originals]
.filter(o => !this.constructor._isOverriden(this._globals, o))
@@ -190,6 +212,10 @@ var FlatpakSharedModel = GObject.registerClass({
const group = this.constructor.getGroup();
const key = this.constructor.getKey();
+ /* This only writes from _overrides (the on/off state), it does
+ * not write out anything from _conditionals. Saving can
+ * therefore drop an existing conditional entry from the
+ * override file. Write-back isn't implemented yet. */
this._overrides.forEach(value => {
let _value = value;
@@ -208,5 +234,6 @@ var FlatpakSharedModel = GObject.registerClass({
this._overrides = new Set();
this._globals = new Set();
this._originals = new Set();
+ this._conditionals = new Map();
}
});
diff --git a/src/style.css b/src/style.css
index a96bda31..907d8c5f 100644
--- a/src/style.css
+++ b/src/style.css
@@ -20,6 +20,14 @@ row .status.user {
row .status.global {
color: @insensitive_fg_color;
}
+row .conditional {
+ padding: 8px;
+ background-color: transparent;
+ background-image: -gtk-icontheme("dialog-question-symbolic");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 16px;
+}
row .content .bus .info,
row .content .variable .info,
diff --git a/src/widgets/conditionalStatusIcon.js b/src/widgets/conditionalStatusIcon.js
new file mode 100644
index 00000000..c853960e
--- /dev/null
+++ b/src/widgets/conditionalStatusIcon.js
@@ -0,0 +1,65 @@
+/* exported FlatsealConditionalStatusIcon */
+
+/* conditionalStatusIcon.js
+ *
+ * Copyright 2026 Malika Odeny Asman
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+const {GObject, Gtk} = imports.gi;
+
+
+var FlatsealConditionalStatusIcon = GObject.registerClass({
+ GTypeName: 'FlatsealConditionalStatusIcon',
+ Template: 'resource:///com/github/tchx84/Flatseal/widgets/conditionalStatusIcon.ui',
+ Properties: {
+ value: GObject.ParamSpec.string(
+ 'value',
+ 'value',
+ 'value',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
+ '',
+ ),
+ },
+}, class FlatsealConditionalStatusIcon extends Gtk.Image {
+ _init() {
+ super._init({});
+ this._value = '';
+ }
+
+ set value(value) {
+ if (this._value === value)
+ return;
+
+ this._value = value;
+
+ if (value === '') {
+ this.set_tooltip_text('');
+ this.visible = false;
+ return;
+ }
+
+ const condition = value
+ .split(':')
+ .slice(2)
+ .join(':');
+ this.set_tooltip_text(_('Only granted if: %s').format(condition));
+ this.visible = true;
+ }
+
+ get value() {
+ return this._value;
+ }
+});
diff --git a/src/widgets/conditionalStatusIcon.ui b/src/widgets/conditionalStatusIcon.ui
new file mode 100644
index 00000000..55a43bcc
--- /dev/null
+++ b/src/widgets/conditionalStatusIcon.ui
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/src/widgets/permissionSwitchRow.js b/src/widgets/permissionSwitchRow.js
index bfa97776..0c37ab49 100644
--- a/src/widgets/permissionSwitchRow.js
+++ b/src/widgets/permissionSwitchRow.js
@@ -3,6 +3,7 @@
/* permissionSwitchRow.js
*
* Copyright 2020 Martin Abente Lahaye
+ * Copyright 2026 Malika Odeny Asman
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,6 +21,7 @@
const {GObject, Adw} = imports.gi;
const {FlatsealOverrideStatusIcon} = imports.widgets.overrideStatusIcon;
+const {FlatsealConditionalStatusIcon} = imports.widgets.conditionalStatusIcon;
var FlatsealPermissionSwitchRow = GObject.registerClass({
@@ -36,6 +38,9 @@ var FlatsealPermissionSwitchRow = GObject.registerClass({
this._statusIcon = new FlatsealOverrideStatusIcon();
this._statusBox.append(this._statusIcon);
+
+ this._conditionalIcon = new FlatsealConditionalStatusIcon();
+ this._statusBox.append(this._conditionalIcon);
}
_update() {
@@ -53,6 +58,10 @@ var FlatsealPermissionSwitchRow = GObject.registerClass({
return this._statusIcon;
}
+ get conditional() {
+ return this._conditionalIcon;
+ }
+
get supported() {
return this.sensitive;
}
diff --git a/src/widgets/window.js b/src/widgets/window.js
index 21e67123..445f139a 100644
--- a/src/widgets/window.js
+++ b/src/widgets/window.js
@@ -284,6 +284,11 @@ var FlatsealWindow = GObject.registerClass({
return;
this._permissions.bind_property(p.statusProperty, row.status, 'status', _bindFlags);
+
+ if (!row.conditional || !p.conditionalProperty)
+ return;
+
+ this._permissions.bind_property(p.conditionalProperty, row.conditional, 'value', _bindFlags);
});
}
diff --git a/tests/content/globalConditional/flatpak/overrides/global b/tests/content/globalConditional/flatpak/overrides/global
new file mode 100644
index 00000000..7ed1f4f5
--- /dev/null
+++ b/tests/content/globalConditional/flatpak/overrides/global
@@ -0,0 +1,2 @@
+[Context]
+features=if:devel:true;
diff --git a/tests/content/system/flatpak/app/com.test.Conditional/current/active/metadata b/tests/content/system/flatpak/app/com.test.Conditional/current/active/metadata
index 274fccc4..d4f91b24 100644
--- a/tests/content/system/flatpak/app/com.test.Conditional/current/active/metadata
+++ b/tests/content/system/flatpak/app/com.test.Conditional/current/active/metadata
@@ -5,4 +5,4 @@ sdk=org.gnome.Sdk/x86_64/master
command=test
[Context]
-sockets=x11;if:x11:!has-wayland;
+sockets=x11;if:x11:!has-wayland;if:wayland:true;
diff --git a/tests/content/user/flatpak/overrides/com.test.Conditional b/tests/content/user/flatpak/overrides/com.test.Conditional
index f1c4aa7b..500dedbb 100644
--- a/tests/content/user/flatpak/overrides/com.test.Conditional
+++ b/tests/content/user/flatpak/overrides/com.test.Conditional
@@ -1,2 +1,2 @@
[Context]
-devices=all;if:all:!has-input-device;
+devices=all;if:all:!has-input-device;if:kvm:!has-input-device;
diff --git a/tests/content/user/flatpak/overrides/com.test.Unsupported b/tests/content/user/flatpak/overrides/com.test.Unsupported
index 973a8d09..e757196b 100644
--- a/tests/content/user/flatpak/overrides/com.test.Unsupported
+++ b/tests/content/user/flatpak/overrides/com.test.Unsupported
@@ -1,3 +1,3 @@
[Context]
shared=unsupported
-unsupported=always
+unsupported=always;if:unsupported-permission:!has-unsupported-permission
diff --git a/tests/src/testModels.js b/tests/src/testModels.js
index 7f3c0416..866f3a6d 100644
--- a/tests/src/testModels.js
+++ b/tests/src/testModels.js
@@ -67,6 +67,7 @@ const _user = GLib.build_filenamev(['..', 'tests', 'content', 'user', 'flatpak']
const _global = GLib.build_filenamev(['..', 'tests', 'content', 'global', 'flatpak']);
const _globalNegated = GLib.build_filenamev(['..', 'tests', 'content', 'globalNegated', 'flatpak']);
const _globalResetMode = GLib.build_filenamev(['..', 'tests', 'content', 'globalResetMode', 'flatpak']);
+const _globalConditional = GLib.build_filenamev(['..', 'tests', 'content', 'globalConditional', 'flatpak']);
const _statuses = GLib.build_filenamev(['..', 'tests', 'content', 'statuses', 'flatpak']);
const _tmp = GLib.build_filenamev([GLib.DIR_SEPARATOR_S, 'tmp']);
const _none = GLib.build_filenamev([GLib.DIR_SEPARATOR_S, 'dev', 'null']);
@@ -609,6 +610,28 @@ describe('Model', function() {
update();
});
+ it('ignores unsupported conditional permissions', function(done) {
+ GLib.setenv('FLATPAK_USER_DIR', _user, true);
+ permissionsDefault.appId = _unsupportedAppId;
+
+ GLib.setenv('FLATPAK_USER_DIR', _tmp, true);
+ permissionsDefault.set_property('filesystems-other', '');
+
+ GLib.timeout_add(GLib.PRIORITY_HIGH, delay + 1, () => {
+ expect(has(
+ _unsupportedOverride, 'Context', 'unsupported',
+ 'if:unsupported-permission:!has-unsupported-permission')).toBe(false);
+ expect(has(
+ _unsupportedOverride, 'Context', 'unsupported',
+ 'unsupported-permission')).toBe(false);
+
+ done();
+ return GLib.SOURCE_REMOVE;
+ });
+
+ update();
+ });
+
it('signals changes with overrides', function(done) {
spyOn(permissionsDefault, 'emit');
@@ -1461,10 +1484,28 @@ describe('Model', function() {
update();
});
- it('ignores conditional permissions', function() {
+ it('marks conditional permissions from original metadata', function() {
+ GLib.setenv('FLATPAK_USER_DIR', _user, true);
permissionsDefault.appId = _conditionalAppId;
- expect(permissionsDefault.sockets_x11).toBe(true);
+ expect(permissionsDefault.sockets_x11_conditional).toBe('if:x11:!has-wayland');
+
+ expect(permissionsDefault.sockets_wayland).toBe(true);
+ expect(permissionsDefault.sockets_wayland_conditional).toBe('if:wayland:true');
+ });
+
+ it('drops conditional permissions from overrides', function() {
+ GLib.setenv('FLATPAK_USER_DIR', _user, true);
+ permissionsDefault.appId = _conditionalAppId;
+
+ expect(permissionsDefault.devices_kvm).toBe(false);
+ expect(permissionsDefault.devices_kvm_conditional).toBe('');
+
+ GLib.setenv('FLATPAK_USER_DIR', _globalConditional, true);
+ permissionsDefault.appId = _conditionalAppId;
+
+ expect(permissionsDefault.features_devel).toBe(false);
+ expect(permissionsDefault.features_devel_conditional).toBe('');
});
it('does not write conditional permissions back', function(done) {
@@ -1491,6 +1532,24 @@ describe('Model', function() {
update();
});
+ it('does not mark overridden conditional permissions', function(done) {
+ GLib.setenv('FLATPAK_USER_DIR', _tmp, true);
+ permissionsDefault.appId = _conditionalAppId;
+
+ expect(permissionsDefault.sockets_x11_conditional).toBe('if:x11:!has-wayland');
+
+ permissionsDefault.set_property('sockets-x11', false);
+
+ GLib.timeout_add(GLib.PRIORITY_HIGH, delay + 1, () => {
+ expect(permissionsDefault.sockets_x11_status).toBe('user');
+ expect(permissionsDefault.sockets_x11_conditional).toBe('');
+ done();
+ return GLib.SOURCE_REMOVE;
+ });
+
+ update();
+ });
+
it('handles malformed overrides', function() {
spyOn(permissionsDefault, 'emit');