Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/com.github.tchx84.Flatseal.data.gresource.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<file>widgets/appInfoViewer.ui</file>
<file>widgets/applicationRow.ui</file>
<file>widgets/busNameRow.ui</file>
<file>widgets/conditionalStatusIcon.ui</file>
<file>widgets/docsViewer.ui</file>
<file>widgets/globalInfoViewer.ui</file>
<file>widgets/globalRow.ui</file>
Expand Down
1 change: 1 addition & 0 deletions src/com.github.tchx84.Flatseal.src.gresource.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<file>widgets/appInfoViewer.js</file>
<file>widgets/applicationRow.js</file>
<file>widgets/busNameRow.js</file>
<file>widgets/conditionalStatusIcon.js</file>
<file>widgets/detailsButton.js</file>
<file>widgets/docsViewer.js</file>
<file>widgets/globalInfoViewer.js</file>
Expand Down
35 changes: 29 additions & 6 deletions src/models/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const MODELS = {
unsupported: new FlatpakUnsupportedModel(),
};

/* Models that support conditional permissions */
const CONDITIONAL_MODELS = [MODELS.shared, MODELS.sockets, MODELS.devices, MODELS.features];
Comment thread
Malika7188 marked this conversation as resolved.
Outdated

function generate_index() {
const index = {};

Expand Down Expand Up @@ -102,6 +105,11 @@ function generate() {
const statusProperty = `${property}-status`;
properties[statusProperty] = GObject.ParamSpec.string(
statusProperty, statusProperty, statusProperty, FLAGS, FlatsealOverrideStatus.ORIGINAL);

/* conditional requests */
const conditionalProperty = `${property}-conditional`;
properties[conditionalProperty] = GObject.ParamSpec.string(
conditionalProperty, conditionalProperty, conditionalProperty, FLAGS, '');
Comment thread
Malika7188 marked this conversation as resolved.
Outdated
});
});

Expand Down Expand Up @@ -192,21 +200,34 @@ 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))
return;
let isConditional = false;
let bareOption = option;

if (option.startsWith(CONDITIONAL_PREFIX)) {
Comment thread
Malika7188 marked this conversation as resolved.
isConditional = true;
[bareOption] = option.slice(CONDITIONAL_PREFIX.length).split(':');
}
Comment thread
Malika7188 marked this conversation as resolved.

model = this.constructor._find(`${group}_${key}_${option.replace('!', '')}`);
model = this.constructor._find(`${group}_${key}_${bareOption.replace('!', '')}`);
Comment thread
Malika7188 marked this conversation as resolved.

if (model === null)
model = this.constructor._find(`${group}_${key}`);

if (model === null && overrides && !global)
model = MODELS.unsupported;

if (model !== null)
if (model === null)
Comment thread
Malika7188 marked this conversation as resolved.
Outdated
return;

/* Preserves the original conditional string for models
* that don't support conditionals, so the condition
* is not lost.*/
if (isConditional && model !== MODELS.unsupported) {
model.loadFromKeyFile(group, key, bareOption, overrides, global);
model.markConditional(bareOption, option);
} else {
model.loadFromKeyFile(group, key, option, overrides, global);
}
Comment thread
Malika7188 marked this conversation as resolved.
});
});
});
Expand Down Expand Up @@ -270,6 +291,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));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another data point regarding conditionals models list; we add a lot of properties that we don't ever use.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the implementation so that conditional properties are only created for the four models in CONDITIONAL_MODELS, so we no longer add properties that aren't used.


GObject.signal_handler_unblock(this, this._notifyHandlerId);
}
Expand Down Expand Up @@ -394,6 +416,7 @@ var FlatpakPermissionsModel = GObject.registerClass({
entry['groupStyle'] = model.constructor.getStyle();
entry['groupDescription'] = model.constructor.getDescription();
entry['statusProperty'] = `${property}-status`;
entry['conditionalProperty'] = `${property}-conditional`;
entry['serializeFunc'] = model.constructor.serialize;
entry['deserializeFunc'] = model.constructor.deserialize;

Expand Down
22 changes: 22 additions & 0 deletions src/models/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ 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);
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested the following scenario:

  1. Running latest version of this PR.
  2. Having installed your HelloConditional demo app.
  3. I launch Flatseal and select HelloConditional.
  4. I scroll down to the Socket section and negate PulseAudio.

What I see after that is:

Image

I am wondering about the semantics here; it definitely does make sense to display the "conditional" icon when the original permission is still valid but, after it's negated, does it still make sense to display it? Is it adding useful information ?

My first reaction is, probably not. Once we override, the "active" version of that permission is no longer the conditional but an explicit negation (not a conditional negation).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I've changed it so the conditional icon is hidden as soon as the permission is overridden, both by the user and globally. So in your PulseAudio example, only the override icon shows now. If the override is removed, the conditional icon comes back.


updateConditionalProperty(proxy) {
Object.entries(this.getPermissions()).forEach(([property, permission]) => {
const {option} = permission;
const conditionalProperty = `${property}-conditional`;
const value = this._conditionals.get(option) || '';

proxy.set_property(conditionalProperty, value);
});
}

updateProxyProperty(proxy) {
const originals = [...this._originals]
.filter(o => !this.constructor._isOverriden(this._globals, o))
Expand Down Expand Up @@ -190,6 +207,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;

Expand All @@ -208,5 +229,6 @@ var FlatpakSharedModel = GObject.registerClass({
this._overrides = new Set();
this._globals = new Set();
this._originals = new Set();
this._conditionals = new Map();
}
});
8 changes: 8 additions & 0 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explore other icons and colors options for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are some of the icon and color options I explored. Let me know what you think about them.

Option 1: The branch-arrow-symbolic choice is because it reads as “this depends on something,” which matches how a conditional permission works.

image

Option 2: The information icon (dialog-information-symbolic) in the default text color, instead of the current question-mark icon (which Flatseal also uses for help).
image


Option 3: The information icon (dialog-information-symbolic) in green
image

background-repeat: no-repeat;
background-position: center;
background-size: 16px;
}

row .content .bus .info,
row .content .variable .info,
Expand Down
65 changes: 65 additions & 0 deletions src/widgets/conditionalStatusIcon.js
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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;
}
});
8 changes: 8 additions & 0 deletions src/widgets/conditionalStatusIcon.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="FlatsealConditionalStatusIcon" parent="GtkImage">
<style>
<class name="conditional"/>
</style>
</template>
</interface>
9 changes: 9 additions & 0 deletions src/widgets/permissionSwitchRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +21,7 @@

const {GObject, Adw} = imports.gi;
const {FlatsealOverrideStatusIcon} = imports.widgets.overrideStatusIcon;
const {FlatsealConditionalStatusIcon} = imports.widgets.conditionalStatusIcon;


var FlatsealPermissionSwitchRow = GObject.registerClass({
Expand All @@ -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() {
Expand All @@ -53,6 +58,10 @@ var FlatsealPermissionSwitchRow = GObject.registerClass({
return this._statusIcon;
}

get conditional() {
return this._conditionalIcon;
}

get supported() {
return this.sensitive;
}
Expand Down
5 changes: 5 additions & 0 deletions src/widgets/window.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,11 @@ var FlatsealWindow = GObject.registerClass({
return;

this._permissions.bind_property(p.statusProperty, row.status, 'status', _bindFlags);

if (!row.conditional)
return;

this._permissions.bind_property(p.conditionalProperty, row.conditional, 'value', _bindFlags);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion tests/content/user/flatpak/overrides/com.test.Unsupported
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[Context]
shared=unsupported
unsupported=always
unsupported=always;if:teleport:true
Comment thread
Malika7188 marked this conversation as resolved.
Outdated
12 changes: 12 additions & 0 deletions tests/src/testModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ describe('Model', function() {
expect(has(_unsupportedOverride, 'Context', 'unsupported', 'always')).toBe(true);
expect(has(_unsupportedOverride, 'Context', 'unsupported', 'undefined')).toBe(false);
expect(has(_unsupportedOverride, 'Context', 'unsupported', 'null')).toBe(false);
expect(has(_unsupportedOverride, 'Context', 'unsupported', 'if:teleport:true')).toBe(true);
Comment thread
Malika7188 marked this conversation as resolved.
Outdated

expect(has(_unsupportedOverride, 'Context', 'shared', 'unsupported')).toBe(true);
expect(has(_unsupportedOverride, 'Context', 'shared', 'undefined')).toBe(false);
Expand Down Expand Up @@ -1467,6 +1468,17 @@ describe('Model', function() {
expect(permissionsDefault.sockets_x11).toBe(true);
});

it('marks conditional permissions', function() {
GLib.setenv('FLATPAK_USER_DIR', _user, true);
permissionsDefault.appId = _conditionalAppId;

expect(permissionsDefault.sockets_x11_conditional).toBe('if:x11:!has-wayland');
expect(permissionsDefault.devices_all_conditional).toBe('if:all:!has-input-device');

expect(permissionsDefault.sockets_wayland).toBe(true);
expect(permissionsDefault.sockets_wayland_conditional).toBe('if:wayland:true');
Comment thread
tchx84 marked this conversation as resolved.
});

it('does not write conditional permissions back', function(done) {
GLib.setenv('FLATPAK_USER_DIR', _user, true);
permissionsDefault.appId = _conditionalAppId;
Expand Down