Add application and autostart settings
This commit is contained in:
@@ -1,19 +1,207 @@
|
||||
// Applications — PLACEHOLDER.
|
||||
//
|
||||
// Owned by the codex agent, which is building default-application handling and
|
||||
// the autostart list. This stub exists only so the page id can be routed,
|
||||
// registered, and searchable before that work lands; it is expected to be
|
||||
// replaced wholesale rather than edited.
|
||||
// Applications and session startup.
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SettingsPage {
|
||||
id: root
|
||||
|
||||
objectName: "applications"
|
||||
title: "Applications"
|
||||
lede: "Default applications and what starts with your session."
|
||||
lede: "Choose what opens your files and links, and what starts with your session."
|
||||
|
||||
property string expandedRole: ""
|
||||
readonly property var applications: DesktopEntries.applications.values
|
||||
readonly property var roles: [
|
||||
{ key: "browser", label: "Browser", detail: "Web links and HTML pages", categories: ["webbrowser"], terms: ["browser", "web"] },
|
||||
{ key: "mail", label: "Mail", detail: "Email links", categories: ["email"], terms: ["mail", "email"] },
|
||||
{ key: "files", label: "Files", detail: "Folders and file locations", categories: ["filemanager"], terms: ["file manager", "files"] },
|
||||
{ key: "terminal", label: "Terminal", detail: "Terminal links and command-line handoffs", categories: ["terminalemulator"], terms: ["terminal", "console"] },
|
||||
{ key: "music", label: "Music", detail: "MP3 audio", categories: ["audio", "player"], terms: ["music", "audio player"] },
|
||||
{ key: "images", label: "Images", detail: "PNG images", categories: ["graphics", "viewer"], terms: ["image", "photo", "picture"] },
|
||||
{ key: "video", label: "Video", detail: "MP4 video", categories: ["video", "player"], terms: ["video", "media player"] }
|
||||
]
|
||||
|
||||
function desktopId(entry: var): string {
|
||||
const entryId = String(entry?.id ?? "");
|
||||
return entryId.endsWith(".desktop") ? entryId : entryId + ".desktop";
|
||||
}
|
||||
|
||||
function displayName(entry: var): string {
|
||||
return String(entry?.name || entry?.genericName || root.desktopId(entry));
|
||||
}
|
||||
|
||||
function currentHandler(role: string): string {
|
||||
return String(DefaultApps.handlers[role] ?? "");
|
||||
}
|
||||
|
||||
function currentEntry(role: string): var {
|
||||
const handler = root.currentHandler(role);
|
||||
return root.applications.find(entry => root.desktopId(entry) === handler) ?? null;
|
||||
}
|
||||
|
||||
function matchesRole(entry: var, role: var): bool {
|
||||
const categories = Array.isArray(entry.categories)
|
||||
? entry.categories.join(" ").toLowerCase()
|
||||
: String(entry.categories ?? "").toLowerCase();
|
||||
const metadata = [entry.name, entry.genericName, entry.comment]
|
||||
.map(value => String(value ?? "").toLowerCase())
|
||||
.join(" ");
|
||||
return role.categories.some(category => categories.includes(category))
|
||||
|| role.terms.some(term => metadata.includes(term));
|
||||
}
|
||||
|
||||
function choicesForRole(role: var): var {
|
||||
const choices = root.applications.filter(entry => root.matchesRole(entry, role));
|
||||
const currentEntry = root.currentEntry(role.key);
|
||||
if (currentEntry && !choices.some(entry => root.desktopId(entry) === root.desktopId(currentEntry)))
|
||||
choices.push(currentEntry);
|
||||
return choices.sort((left, right) => root.displayName(left).localeCompare(root.displayName(right)));
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: DefaultApps.lastError !== ""
|
||||
label: "Could not apply the change"
|
||||
detail: DefaultApps.lastError
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Being built"
|
||||
subtitle: "Default browser, mail, files, and terminal, plus the autostart list, are on their way."
|
||||
title: "Default applications"
|
||||
subtitle: "Open a row to choose from applications that advertise the matching role."
|
||||
|
||||
Repeater {
|
||||
model: root.roles
|
||||
|
||||
delegate: Column {
|
||||
id: roleBlock
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
readonly property var choices: root.choicesForRole(roleBlock.modelData)
|
||||
readonly property var selectedEntry: root.currentEntry(roleBlock.modelData.key)
|
||||
|
||||
width: parent.width
|
||||
|
||||
SettingRow {
|
||||
label: roleBlock.modelData.label
|
||||
detail: roleBlock.modelData.detail
|
||||
value: roleBlock.selectedEntry
|
||||
? root.displayName(roleBlock.selectedEntry)
|
||||
: (root.currentHandler(roleBlock.modelData.key) || "Not set")
|
||||
activatable: roleBlock.choices.length > 0 && !DefaultApps.busy
|
||||
divider: root.expandedRole !== roleBlock.modelData.key && roleBlock.index < root.roles.length - 1
|
||||
onActivated: {
|
||||
root.expandedRole = root.expandedRole === roleBlock.modelData.key
|
||||
? ""
|
||||
: roleBlock.modelData.key;
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
visible: root.expandedRole === roleBlock.modelData.key
|
||||
|
||||
Repeater {
|
||||
model: roleBlock.choices
|
||||
|
||||
delegate: SettingRow {
|
||||
id: candidateRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property string candidateId: root.desktopId(candidateRow.modelData)
|
||||
readonly property bool selected: candidateRow.candidateId === root.currentHandler(roleBlock.modelData.key)
|
||||
|
||||
label: root.displayName(candidateRow.modelData)
|
||||
detail: String(candidateRow.modelData.genericName || candidateRow.modelData.comment || candidateRow.candidateId)
|
||||
value: candidateRow.selected ? "Current" : ""
|
||||
activatable: !candidateRow.selected && !DefaultApps.busy
|
||||
divider: candidateRow.index < roleBlock.choices.length - 1 || roleBlock.index < root.roles.length - 1
|
||||
onActivated: {
|
||||
DefaultApps.setDefault(roleBlock.modelData.key, candidateRow.candidateId);
|
||||
root.expandedRole = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "User autostart"
|
||||
subtitle: "These desktop entries live in your user configuration. Select a row to toggle it."
|
||||
|
||||
TextRow {
|
||||
visible: DefaultApps.autostartEntries.length === 0
|
||||
label: "No user autostart entries"
|
||||
detail: "Applications can add entries to ~/.config/autostart."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: DefaultApps.autostartEntries
|
||||
|
||||
delegate: SettingRow {
|
||||
id: autostartRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
label: autostartRow.modelData.name
|
||||
detail: autostartRow.modelData.id
|
||||
value: autostartRow.modelData.enabled ? "Enabled" : "Disabled"
|
||||
activatable: !DefaultApps.busy
|
||||
divider: autostartRow.index < DefaultApps.autostartEntries.length - 1
|
||||
onActivated: DefaultApps.setAutostart(autostartRow.modelData.id, !autostartRow.modelData.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Compositor autostart"
|
||||
subtitle: "Panama starts these from Hyprland configuration. They are read-only here."
|
||||
|
||||
TextRow {
|
||||
visible: DefaultApps.luaAutostartEntries.length === 0
|
||||
label: "No compositor entries found"
|
||||
detail: "No hl.exec_cmd entries were found in config/dot/hypr/autostart.lua."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: DefaultApps.luaAutostartEntries
|
||||
|
||||
delegate: TextRow {
|
||||
id: luaRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
label: luaRow.modelData.name
|
||||
detail: luaRow.modelData.command
|
||||
value: "Hyprland"
|
||||
divider: luaRow.index < DefaultApps.luaAutostartEntries.length - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Refresh"
|
||||
|
||||
ActionRow {
|
||||
label: "Reload application settings"
|
||||
detail: "Re-read desktop entries, defaults, and user autostart files"
|
||||
action: DefaultApps.busy ? "Refreshing…" : "Refresh"
|
||||
enabled: !DefaultApps.busy
|
||||
divider: false
|
||||
onTriggered: DefaultApps.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Read and update freedesktop defaults for Panama's settings page."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
ROLE_TARGETS = {
|
||||
"browser": ("settings", "default-web-browser"),
|
||||
"mail": ("mime", "x-scheme-handler/mailto"),
|
||||
"files": ("mime", "inode/directory"),
|
||||
"terminal": ("mime", "x-scheme-handler/terminal"),
|
||||
"music": ("mime", "audio/mpeg"),
|
||||
"images": ("mime", "image/png"),
|
||||
"video": ("mime", "video/mp4"),
|
||||
}
|
||||
DESKTOP_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._+-]*\.desktop$")
|
||||
EXEC_CMD = re.compile(r"hl\.exec_cmd\(\s*(\"(?:\\.|[^\"\\])*\")\s*\)")
|
||||
|
||||
|
||||
class BoundaryError(RuntimeError):
|
||||
"""A user-visible validation or command failure."""
|
||||
|
||||
|
||||
def xdg_data_roots() -> list[Path]:
|
||||
data_home = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local/share"))
|
||||
data_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share")
|
||||
return [data_home, *(Path(item) for item in data_dirs.split(":") if item)]
|
||||
|
||||
|
||||
def discovered_desktop_ids() -> set[str]:
|
||||
desktop_ids: set[str] = set()
|
||||
for root in xdg_data_roots():
|
||||
applications = root / "applications"
|
||||
if not applications.is_dir():
|
||||
continue
|
||||
for path in applications.rglob("*.desktop"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
relative = path.relative_to(applications)
|
||||
desktop_ids.add("-".join(relative.parts))
|
||||
return desktop_ids
|
||||
|
||||
|
||||
def require_desktop_id(desktop_id: str, *, discovered: set[str]) -> None:
|
||||
if not DESKTOP_ID.fullmatch(desktop_id) or desktop_id not in discovered:
|
||||
raise BoundaryError("That application is not available.")
|
||||
|
||||
|
||||
def run(command: list[str]) -> str:
|
||||
completed = subprocess.run(command, check=False, capture_output=True, text=True)
|
||||
if completed.returncode != 0:
|
||||
detail = completed.stderr.strip()
|
||||
raise BoundaryError(detail or "The system default could not be updated.")
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
def query_handlers() -> dict[str, str]:
|
||||
handlers: dict[str, str] = {}
|
||||
for role, (kind, target) in ROLE_TARGETS.items():
|
||||
command = (
|
||||
["xdg-settings", "get", target]
|
||||
if kind == "settings"
|
||||
else ["xdg-mime", "query", "default", target]
|
||||
)
|
||||
output = run(command)
|
||||
handlers[role] = output.splitlines()[0] if output else ""
|
||||
return handlers
|
||||
|
||||
|
||||
def parse_desktop_entry(path: Path) -> dict[str, str]:
|
||||
values: dict[str, str] = {}
|
||||
section = ""
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
except (OSError, UnicodeError) as error:
|
||||
raise BoundaryError(f"Could not read {path.name}.") from error
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("[") and stripped.endswith("]"):
|
||||
section = stripped[1:-1]
|
||||
continue
|
||||
if section != "Desktop Entry" or "=" not in line or stripped.startswith("#"):
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
values.setdefault(key.strip(), value.strip())
|
||||
return values
|
||||
|
||||
|
||||
def autostart_directory() -> Path:
|
||||
config_home = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
||||
return config_home / "autostart"
|
||||
|
||||
|
||||
def user_autostart_entries() -> list[dict[str, object]]:
|
||||
directory = autostart_directory()
|
||||
if not directory.is_dir():
|
||||
return []
|
||||
entries: list[dict[str, object]] = []
|
||||
for path in directory.glob("*.desktop"):
|
||||
if path.is_symlink() or not path.is_file():
|
||||
continue
|
||||
values = parse_desktop_entry(path)
|
||||
entries.append(
|
||||
{
|
||||
"id": path.name,
|
||||
"name": values.get("Name", path.stem),
|
||||
"enabled": values.get("Hidden", "false").lower() != "true",
|
||||
}
|
||||
)
|
||||
return sorted(entries, key=lambda entry: (str(entry["name"]).casefold(), str(entry["id"])))
|
||||
|
||||
|
||||
def hypr_autostart_path() -> Path:
|
||||
override = os.environ.get("PANAMA_HYPR_AUTOSTART")
|
||||
if override:
|
||||
return Path(override)
|
||||
return Path(__file__).resolve().parents[2] / "hypr" / "autostart.lua"
|
||||
|
||||
|
||||
def lua_autostart_entries() -> list[dict[str, object]]:
|
||||
path = hypr_autostart_path()
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
except (OSError, UnicodeError):
|
||||
return []
|
||||
|
||||
commands: list[str] = []
|
||||
in_start_handler = False
|
||||
for line in lines:
|
||||
if not in_start_handler:
|
||||
in_start_handler = bool(re.search(r'hl\.on\(\s*"hyprland\.start"', line))
|
||||
continue
|
||||
if line.strip() == "end)":
|
||||
break
|
||||
match = EXEC_CMD.search(line)
|
||||
if match:
|
||||
try:
|
||||
commands.append(ast.literal_eval(match.group(1)))
|
||||
except (SyntaxError, ValueError):
|
||||
continue
|
||||
|
||||
return [
|
||||
{
|
||||
"id": f"hyprland:{index}",
|
||||
"name": command.split()[0].rsplit("/", 1)[-1],
|
||||
"command": command,
|
||||
"enabled": True,
|
||||
"readOnly": True,
|
||||
"source": "config/dot/hypr/autostart.lua",
|
||||
}
|
||||
for index, command in enumerate(commands, start=1)
|
||||
]
|
||||
|
||||
|
||||
def snapshot() -> dict[str, object]:
|
||||
return {
|
||||
"handlers": query_handlers(),
|
||||
"autostartEntries": user_autostart_entries(),
|
||||
"luaAutostartEntries": lua_autostart_entries(),
|
||||
}
|
||||
|
||||
|
||||
def set_default(role: str, desktop_id: str) -> None:
|
||||
target = ROLE_TARGETS.get(role)
|
||||
if target is None:
|
||||
raise BoundaryError("That default application role is not supported.")
|
||||
require_desktop_id(desktop_id, discovered=discovered_desktop_ids())
|
||||
kind, setting = target
|
||||
command = (
|
||||
["xdg-settings", "set", setting, desktop_id]
|
||||
if kind == "settings"
|
||||
else ["xdg-mime", "default", desktop_id, setting]
|
||||
)
|
||||
run(command)
|
||||
|
||||
|
||||
def update_hidden(path: Path, *, hidden: bool) -> None:
|
||||
try:
|
||||
original = path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeError) as error:
|
||||
raise BoundaryError("That autostart entry could not be read.") from error
|
||||
|
||||
lines = original.splitlines()
|
||||
output: list[str] = []
|
||||
section = ""
|
||||
found_section = False
|
||||
wrote_hidden = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("[") and stripped.endswith("]"):
|
||||
if section == "Desktop Entry" and not wrote_hidden:
|
||||
output.append(f"Hidden={'true' if hidden else 'false'}")
|
||||
wrote_hidden = True
|
||||
section = stripped[1:-1]
|
||||
found_section = found_section or section == "Desktop Entry"
|
||||
output.append(line)
|
||||
continue
|
||||
if section == "Desktop Entry" and line.split("=", 1)[0].strip() == "Hidden":
|
||||
if not wrote_hidden:
|
||||
output.append(f"Hidden={'true' if hidden else 'false'}")
|
||||
wrote_hidden = True
|
||||
continue
|
||||
output.append(line)
|
||||
|
||||
if not found_section:
|
||||
raise BoundaryError("That autostart entry is not a desktop file.")
|
||||
if not wrote_hidden:
|
||||
output.append(f"Hidden={'true' if hidden else 'false'}")
|
||||
|
||||
mode = path.stat().st_mode
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w", encoding="utf-8", dir=path.parent, prefix=f".{path.name}.", delete=False
|
||||
) as temporary:
|
||||
temporary.write("\n".join(output) + "\n")
|
||||
temporary.flush()
|
||||
os.fsync(temporary.fileno())
|
||||
temporary_path = Path(temporary.name)
|
||||
temporary_path.chmod(mode)
|
||||
os.replace(temporary_path, path)
|
||||
except OSError as error:
|
||||
if "temporary_path" in locals():
|
||||
temporary_path.unlink(missing_ok=True)
|
||||
raise BoundaryError("That autostart entry could not be updated.") from error
|
||||
|
||||
|
||||
def set_autostart(desktop_id: str, enabled_text: str) -> None:
|
||||
if enabled_text not in {"true", "false"}:
|
||||
raise BoundaryError("Autostart state must be true or false.")
|
||||
if not DESKTOP_ID.fullmatch(desktop_id):
|
||||
raise BoundaryError("That autostart entry is not available.")
|
||||
|
||||
directory = autostart_directory()
|
||||
path = directory / desktop_id
|
||||
try:
|
||||
resolved_directory = directory.resolve(strict=True)
|
||||
resolved_path = path.resolve(strict=True)
|
||||
except OSError as error:
|
||||
raise BoundaryError("That autostart entry is not available.") from error
|
||||
if path.is_symlink() or resolved_path.parent != resolved_directory or not resolved_path.is_file():
|
||||
raise BoundaryError("That autostart entry is not available.")
|
||||
update_hidden(resolved_path, hidden=enabled_text == "false")
|
||||
|
||||
|
||||
def main(arguments: list[str]) -> int:
|
||||
try:
|
||||
if arguments == ["snapshot"]:
|
||||
print(json.dumps(snapshot(), separators=(",", ":")))
|
||||
elif len(arguments) == 3 and arguments[0] == "set-default":
|
||||
set_default(arguments[1], arguments[2])
|
||||
elif len(arguments) == 3 and arguments[0] == "set-autostart":
|
||||
set_autostart(arguments[1], arguments[2])
|
||||
else:
|
||||
raise BoundaryError(
|
||||
"Usage: panama-default-apps snapshot | set-default ROLE DESKTOP_ID | "
|
||||
"set-autostart DESKTOP_ID true|false"
|
||||
)
|
||||
except BoundaryError as error:
|
||||
print(str(error), file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,102 @@
|
||||
pragma Singleton
|
||||
|
||||
// Freedesktop default handlers and session autostart entries.
|
||||
//
|
||||
// The helper owns parsing and atomic desktop-file writes. This singleton keeps
|
||||
// the QML side typed and reactive, and every external command crosses Process
|
||||
// as an argument array.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var handlers: ({})
|
||||
property var autostartEntries: []
|
||||
property var luaAutostartEntries: []
|
||||
property string lastError: ""
|
||||
|
||||
readonly property bool busy: snapshotProcess.running || mutationProcess.running
|
||||
readonly property string helper: Quickshell.shellDir + "/scripts/panama-default-apps"
|
||||
readonly property var supportedRoles: ["browser", "mail", "files", "terminal", "music", "images", "video"]
|
||||
|
||||
Process {
|
||||
id: snapshotProcess
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.applySnapshot(this.text)
|
||||
}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode !== 0)
|
||||
root.lastError = "Default applications could not be read. Try refreshing."
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: mutationProcess
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode !== 0) {
|
||||
root.lastError = "That application setting could not be changed."
|
||||
return;
|
||||
}
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function applySnapshot(text: string): void {
|
||||
try {
|
||||
const payload = JSON.parse(text);
|
||||
root.handlers = payload.handlers ?? ({});
|
||||
root.autostartEntries = payload.autostartEntries ?? [];
|
||||
root.luaAutostartEntries = payload.luaAutostartEntries ?? [];
|
||||
root.lastError = "";
|
||||
} catch (error) {
|
||||
root.lastError = "Default applications returned an unreadable response."
|
||||
}
|
||||
}
|
||||
|
||||
function refresh(): void {
|
||||
if (root.busy)
|
||||
return;
|
||||
root.lastError = "";
|
||||
snapshotProcess.exec([root.helper, "snapshot"]);
|
||||
}
|
||||
|
||||
function knownDesktopId(desktopId: string): bool {
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*\.desktop$/.test(desktopId))
|
||||
return false;
|
||||
const entries = DesktopEntries.applications.values;
|
||||
return entries.some(entry => {
|
||||
const entryId = String(entry.id ?? "");
|
||||
return entryId === desktopId || entryId + ".desktop" === desktopId;
|
||||
});
|
||||
}
|
||||
|
||||
function setDefault(role: string, desktopId: string): void {
|
||||
if (root.busy)
|
||||
return;
|
||||
if (!root.supportedRoles.includes(role) || !root.knownDesktopId(desktopId)) {
|
||||
root.lastError = "Choose an application from the available list."
|
||||
return;
|
||||
}
|
||||
root.lastError = "";
|
||||
mutationProcess.exec([root.helper, "set-default", role, desktopId]);
|
||||
}
|
||||
|
||||
function setAutostart(desktopId: string, enabled: bool): void {
|
||||
if (root.busy)
|
||||
return;
|
||||
const known = root.autostartEntries.some(entry => entry.id === desktopId);
|
||||
if (!known) {
|
||||
root.lastError = "That user autostart entry is no longer available."
|
||||
return;
|
||||
}
|
||||
root.lastError = "";
|
||||
mutationProcess.exec([root.helper, "set-autostart", desktopId, String(enabled)]);
|
||||
}
|
||||
|
||||
Component.onCompleted: root.refresh()
|
||||
}
|
||||
Reference in New Issue
Block a user