Add startup application picker
This commit is contained in:
@@ -12,6 +12,7 @@ SettingsPage {
|
||||
lede: "Choose what opens your files and links, and what starts with your session."
|
||||
|
||||
property string expandedRole: ""
|
||||
property bool addingAutostart: false
|
||||
readonly property var applications: DesktopEntries.applications.values
|
||||
readonly property var roles: [
|
||||
{ key: "browser", label: "Browser", detail: "Web links and HTML pages", categorySets: [["webbrowser"]], terms: ["web browser", "browser"] },
|
||||
@@ -178,7 +179,28 @@ SettingsPage {
|
||||
|
||||
SettingsCard {
|
||||
title: "User autostart"
|
||||
subtitle: "These desktop entries live in your user configuration. Select a row to toggle it."
|
||||
subtitle: "Choose what starts with your session. Entries live in your user configuration, not the compositor."
|
||||
|
||||
ActionRow {
|
||||
label: "Add an application"
|
||||
detail: root.addingAutostart
|
||||
? "Search the applications installed on this machine"
|
||||
: "Start another installed application when you sign in"
|
||||
action: root.addingAutostart ? "Close" : "Choose"
|
||||
divider: !root.addingAutostart || DefaultApps.autostartEntries.length > 0
|
||||
enabled: !DefaultApps.busy
|
||||
onTriggered: root.addingAutostart = !root.addingAutostart
|
||||
}
|
||||
|
||||
AutostartAppPicker {
|
||||
visible: root.addingAutostart
|
||||
width: parent.width
|
||||
existing: DefaultApps.autostartEntries.map(entry => entry.id)
|
||||
onPicked: id => {
|
||||
DefaultApps.addAutostart(id);
|
||||
root.addingAutostart = false;
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: !DefaultApps.busy && DefaultApps.autostartEntries.length === 0
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Adds an installed application to the user's freedesktop autostart directory.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.modules.clipboard
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property var existing
|
||||
signal picked(string id)
|
||||
|
||||
spacing: 0
|
||||
|
||||
function desktopId(entry: var): string {
|
||||
const id = String(entry?.id ?? "");
|
||||
return id.endsWith(".desktop") ? id : id + ".desktop";
|
||||
}
|
||||
|
||||
readonly property var matches: {
|
||||
const needle = search.text.trim().toLowerCase();
|
||||
if (needle === "")
|
||||
return [];
|
||||
const out = [];
|
||||
for (const entry of DesktopEntries.applications.values) {
|
||||
const desktopId = root.desktopId(entry);
|
||||
if (entry.noDisplay || root.existing.indexOf(desktopId) >= 0)
|
||||
continue;
|
||||
const haystack = `${entry.name ?? ""} ${entry.genericName ?? ""} ${desktopId}`.toLowerCase();
|
||||
if (haystack.indexOf(needle) >= 0)
|
||||
out.push(entry);
|
||||
if (out.length >= 8)
|
||||
break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
SearchField {
|
||||
id: search
|
||||
width: parent.width
|
||||
placeholder: "Search installed applications"
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.matches
|
||||
|
||||
SettingRow {
|
||||
id: candidate
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
label: String(candidate.modelData.name || root.desktopId(candidate.modelData))
|
||||
detail: String(candidate.modelData.genericName || root.desktopId(candidate.modelData))
|
||||
divider: candidate.index < root.matches.length - 1
|
||||
controlWidth: 86
|
||||
|
||||
SettingsButton {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Add"
|
||||
onClicked: {
|
||||
root.picked(root.desktopId(candidate.modelData));
|
||||
search.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
visible: search.text.trim() !== "" && root.matches.length === 0
|
||||
label: "No matching applications"
|
||||
detail: "Only installed desktop applications can start with the session"
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ DateTimePage 1.0 DateTimePage.qml
|
||||
AccessibilityPage 1.0 AccessibilityPage.qml
|
||||
WallpaperPicker 1.0 WallpaperPicker.qml
|
||||
ApplicationsPage 1.0 ApplicationsPage.qml
|
||||
AutostartAppPicker 1.0 AutostartAppPicker.qml
|
||||
DockPinsEditor 1.0 DockPinsEditor.qml
|
||||
DockAppPicker 1.0 DockAppPicker.qml
|
||||
ShortcutCapture 1.0 ShortcutCapture.qml
|
||||
|
||||
@@ -37,8 +37,8 @@ def xdg_data_roots() -> list[Path]:
|
||||
return [data_home, *(Path(item) for item in data_dirs.split(":") if item)]
|
||||
|
||||
|
||||
def discovered_desktop_ids() -> set[str]:
|
||||
desktop_ids: set[str] = set()
|
||||
def discovered_desktop_files() -> dict[str, Path]:
|
||||
desktop_files: dict[str, Path] = {}
|
||||
for root in xdg_data_roots():
|
||||
applications = root / "applications"
|
||||
if not applications.is_dir():
|
||||
@@ -47,8 +47,12 @@ def discovered_desktop_ids() -> set[str]:
|
||||
if not path.is_file():
|
||||
continue
|
||||
relative = path.relative_to(applications)
|
||||
desktop_ids.add("-".join(relative.parts))
|
||||
return desktop_ids
|
||||
desktop_files.setdefault("-".join(relative.parts), path)
|
||||
return desktop_files
|
||||
|
||||
|
||||
def discovered_desktop_ids() -> set[str]:
|
||||
return set(discovered_desktop_files())
|
||||
|
||||
|
||||
def require_desktop_id(desktop_id: str, *, discovered: set[str]) -> None:
|
||||
@@ -184,12 +188,7 @@ def set_default(role: str, desktop_id: str) -> None:
|
||||
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
|
||||
|
||||
def with_hidden(original: str, *, hidden: bool) -> str:
|
||||
lines = original.splitlines()
|
||||
output: list[str] = []
|
||||
section = ""
|
||||
@@ -216,24 +215,63 @@ def update_hidden(path: Path, *, hidden: bool) -> None:
|
||||
raise BoundaryError("That autostart entry is not a desktop file.")
|
||||
if not wrote_hidden:
|
||||
output.append(f"Hidden={'true' if hidden else 'false'}")
|
||||
return "\n".join(output) + "\n"
|
||||
|
||||
mode = path.stat().st_mode
|
||||
|
||||
def write_atomic(path: Path, text: str, *, mode: int) -> None:
|
||||
temporary_path: Path | None = None
|
||||
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.write(text)
|
||||
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():
|
||||
if temporary_path is not None:
|
||||
temporary_path.unlink(missing_ok=True)
|
||||
raise BoundaryError("That autostart entry could not be updated.") from error
|
||||
|
||||
|
||||
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
|
||||
|
||||
mode = path.stat().st_mode
|
||||
write_atomic(path, with_hidden(original, hidden=hidden), mode=mode)
|
||||
|
||||
|
||||
def add_autostart(desktop_id: str) -> None:
|
||||
desktop_files = discovered_desktop_files()
|
||||
require_desktop_id(desktop_id, discovered=set(desktop_files))
|
||||
source = desktop_files[desktop_id]
|
||||
directory = autostart_directory()
|
||||
try:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as error:
|
||||
raise BoundaryError("The user autostart directory could not be created.") from error
|
||||
|
||||
target = directory / desktop_id
|
||||
if target.is_symlink():
|
||||
raise BoundaryError("That autostart entry is not available.")
|
||||
if target.exists():
|
||||
if not target.is_file():
|
||||
raise BoundaryError("That autostart entry is not available.")
|
||||
update_hidden(target, hidden=False)
|
||||
return
|
||||
|
||||
try:
|
||||
original = source.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeError) as error:
|
||||
raise BoundaryError("That application could not be read.") from error
|
||||
write_atomic(target, with_hidden(original, hidden=False), mode=0o644)
|
||||
|
||||
|
||||
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.")
|
||||
@@ -260,10 +298,12 @@ def main(arguments: list[str]) -> int:
|
||||
set_default(arguments[1], arguments[2])
|
||||
elif len(arguments) == 3 and arguments[0] == "set-autostart":
|
||||
set_autostart(arguments[1], arguments[2])
|
||||
elif len(arguments) == 2 and arguments[0] == "add-autostart":
|
||||
add_autostart(arguments[1])
|
||||
else:
|
||||
raise BoundaryError(
|
||||
"Usage: panama-default-apps snapshot | set-default ROLE DESKTOP_ID | "
|
||||
"set-autostart DESKTOP_ID true|false"
|
||||
"set-autostart DESKTOP_ID true|false | add-autostart DESKTOP_ID"
|
||||
)
|
||||
except BoundaryError as error:
|
||||
print(str(error), file=sys.stderr)
|
||||
|
||||
@@ -98,5 +98,16 @@ Singleton {
|
||||
mutationProcess.exec([root.helper, "set-autostart", desktopId, String(enabled)]);
|
||||
}
|
||||
|
||||
function addAutostart(desktopId: string): void {
|
||||
if (root.busy)
|
||||
return;
|
||||
if (!root.knownDesktopId(desktopId)) {
|
||||
root.lastError = "Choose an installed application.";
|
||||
return;
|
||||
}
|
||||
root.lastError = "";
|
||||
mutationProcess.exec([root.helper, "add-autostart", desktopId]);
|
||||
}
|
||||
|
||||
Component.onCompleted: root.refresh()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user