SaraFem's file picker without a password where possible; clearer pkexec errors; portals.conf without a desktop name

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
sarahandClaude Opus 5.5 committed 2026-10-13 19:33:12 +02:00
1 parent 70c9730017
commit ce62249487
4 files changed
+76 -13

No files matched your search

+21
View File
@@ -13,6 +13,7 @@ picked for one Sara program looks the same in all of them.
"""
import os
import re
import shutil
import sys
from dataclasses import asdict, dataclass, field
@@ -268,6 +269,26 @@ def ChildEnvironment() -> dict[str, str]:
return Environment
def UserPortalsSupported() -> bool:
"""Whether xdg-desktop-portal reads portal files from
~/.local/share/xdg-desktop-portal/portals (1.17 and newer), so installing
one needs no password. True when it isn't installed either: then a
portal file can't be used anywhere, so it might as well not need root."""
import subprocess
for Candidate in ("/usr/libexec/xdg-desktop-portal", "/usr/lib/xdg-desktop-portal",
"/usr/lib/x86_64-linux-gnu/xdg-desktop-portal", "/usr/local/libexec/xdg-desktop-portal"):
if not Path(Candidate).is_file():
continue
try:
Output = subprocess.run([Candidate, "--version"], capture_output=True, text=True, timeout=10).stdout
except (OSError, subprocess.SubprocessError):
return False
Found = re.search(r"(\d+)\.(\d+)", Output)
return bool(Found) and (int(Found[1]), int(Found[2])) >= (1, 17)
return True
def CurrentDesktop() -> str:
"""First entry of $XDG_CURRENT_DESKTOP as-is (e.g. "COSMIC"), used for
portal descriptors' UseIn= and the <desktop>-portals.conf file name.
+36 -6
View File
@@ -21,8 +21,8 @@ from typing import Callable, Optional
from ConfigMerge import ApplySet, MergeConfig, MergeResult
import Services
from Core import (
AutostartDir, BackupsDir, ChildEnvironment, DownloadsDir, HasSystemd, IsWindows, LoadConfig, PristineDir,
StartMenuDir, StartupDir,
AutostartDir, BackupsDir, ChildEnvironment, DownloadsDir, HasSystemd, Home, IsWindows, LoadConfig,
PristineDir, StartMenuDir, StartupDir,
)
from Manifests import Leftover, Program, Render
from Receipts import IniRecord, LoadReceipt, PlacedFile, Receipt
@@ -81,18 +81,47 @@ def _BackupName(Target: str) -> str:
return Target.strip("/").replace("/", "__")
def _Pkexec(Command: list[str], Log: LogFn, Check: bool = True) -> int:
"""Run Command as root through pkexec, saying what went wrong in words."""
if shutil.which("pkexec") is None:
Code = 127
else:
Code = _Run(["pkexec", *Command], Log, Check=False)
if Code == 0:
return 0
if Code == 126:
Problem = "the password prompt was cancelled"
elif shutil.which("pkexec") is None:
Problem = "pkexec (polkit) isn't installed"
else:
Problem = ("no password prompt could be shown: polkit needs an authentication agent running "
"in your session (e.g. polkit-gnome or lxqt-policykit), which window managers don't start themselves")
Message = f"Couldn't do this as root ({Problem}). The command was: sudo {' '.join(Command)}"
if Check:
raise InstallError(Message)
Log(f"Warning: {Message}")
return Code
def _NeedsRoot(Template, Target: str) -> bool:
if Template.Root == "Auto":
return not (Target == str(Home) or Target.startswith(f"{Home}{os.sep}"))
return bool(Template.Root)
def _WriteRoot(Target: str, Text: str, Log: LogFn) -> None:
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".sarasuite") as Temporary:
Temporary.write(Text)
os.chmod(Temporary.name, 0o644)
try:
_Run(["pkexec", "install", "-D", "-m", "644", Temporary.name, Target], Log)
_Pkexec(["install", "-D", "-m", "644", Temporary.name, Target], Log)
finally:
os.unlink(Temporary.name)
def _RemoveRoot(Target: str, Log: LogFn) -> None:
_Run(["pkexec", "rm", "-f", Target], Log)
# A leftover outside $HOME isn't worth failing an install or removal over.
_Pkexec(["rm", "-f", Target], Log, Check=False)
def _ReadIni(FilePath: Path) -> configparser.ConfigParser:
@@ -381,7 +410,8 @@ def _ApplyTemplates(Program: Program, Context: dict, OldReceipt: Optional[Receip
Target = Render(Template.Target, Context)
Text = Render((Program.Folder / "Templates" / Template.Source).read_text(), Context)
Previous = OldReceipt.FindFile(Target) if OldReceipt is not None else None
Placed = PlacedFile(Target, "Template", Root=Template.Root, Displaced=Previous.Displaced if Previous else "")
Root = _NeedsRoot(Template, Target)
Placed = PlacedFile(Target, "Template", Root=Root, Displaced=Previous.Displaced if Previous else "")
# A file that's already there but wasn't placed by SaraSuite (e.g. a
# hand-made desktop entry from development) is moved aside, not lost.
@@ -393,7 +423,7 @@ def _ApplyTemplates(Program: Program, Context: dict, OldReceipt: Optional[Receip
Log(f"Moved your existing {Target} aside (it comes back if {Program.Name} is removed)")
NewReceipt.Files.append(Placed)
if Template.Root:
if Root:
if Path(Target).exists() and Path(Target).read_text() == Text:
continue
_WriteRoot(Target, Text, Log)
+12 -2
View File
@@ -17,7 +17,7 @@ import tomlkit
from Core import (
AutostartDir, CacheHome, ConfigHome, CurrentDesktop, DataHome, HasSystemd, Home, IsWindows, LoadConfig,
Platform, ProgramsDir, SharedConfigDir, StartMenuDir, StartupDir,
Platform, ProgramsDir, SharedConfigDir, StartMenuDir, StartupDir, UserPortalsSupported,
)
# Never worth copying out of a source checkout, whatever Include says.
@@ -55,7 +55,9 @@ class TemplateFile:
Source: str # relative to the program's Templates/ folder
Target: str
Option: str = ""
Root: bool = False # lives outside $HOME, written through pkexec
# True: lives outside $HOME, written through pkexec. "Auto": only when its
# Target turns out to be outside $HOME (e.g. {PortalsDir} on an old system).
Root: bool | str = False
Executable: bool = False
@@ -239,6 +241,14 @@ class Program:
AutostartDir=str(AutostartDir),
ApplicationsDir=str(DataHome / "applications"),
DBusServicesDir=str(DataHome / "dbus-1" / "services"),
# xdg-desktop-portal: where a portal file goes (your own folder
# where it's new enough, no password), the preferences file
# (portals.conf without a desktop name, e.g. a window manager),
# and the desktop's own entry in its list ("cosmic;" or "").
PortalsDir=str(DataHome / "xdg-desktop-portal" / "portals") if UserPortalsSupported()
else "/usr/share/xdg-desktop-portal/portals",
PortalsConf=f"{Desktop.lower()}-portals.conf" if Desktop else "portals.conf",
DesktopPortal=f"{Desktop.lower()};" if Desktop else "",
)
Context.update(Answers or {})
return Context
+7 -5
View File
@@ -18,7 +18,7 @@ RequirementsFile = "requirements.txt"
[[Options]]
Platform = "Linux"
Id = "FilePicker"
Label = "Use SaraFem as the system file picker (asks for your password: the portal file lives in /usr/share)"
Label = "Use SaraFem as the system file picker"
Default = true
[[Options]]
@@ -48,16 +48,18 @@ Option = "FilePicker"
[[Templates]]
Platform = "Linux"
Source = "SaraFem.portal"
Target = "/usr/share/xdg-desktop-portal/portals/SaraFem.portal"
# In your own folder where xdg-desktop-portal is new enough (1.17+); in
# /usr/share, through pkexec, on older systems.
Target = "{PortalsDir}/SaraFem.portal"
Option = "FilePicker"
Root = true
Root = "Auto"
[[IniEdits]]
Platform = "Linux"
File = "{ConfigHome}/xdg-desktop-portal/{Desktop}-portals.conf"
File = "{ConfigHome}/xdg-desktop-portal/{PortalsConf}"
Section = "preferred"
Key = "org.freedesktop.impl.portal.FileChooser"
Value = "SaraFem;{Desktop};gtk;"
Value = "SaraFem;{DesktopPortal}gtk;"
Option = "FilePicker"
[[IniEdits]]