Let a quicklink answer to an alias

Settings ▸ Quicklinks rows now carry an AliasField, keyed by the quicklink's
entry ID, so a quicklink can be reached by a short alias like any application.
AliasField takes the preference key and name directly, with an init(entry:)
for the panes built on LauncherItemsSection.

The field dims on a quicklink hidden from root search, whose entry the ranker
never sees. Ranking, the launcher's alias chip, backup coverage and alias
cleanup on delete were already kind-agnostic and needed no change.

Closes #383
This commit is contained in:
abue-ammar
2026-08-29 20:06:10 +06:00
committed by Abue Ammar
parent f9970ff790
commit fcadb5f982
4 changed files with 27 additions and 11 deletions
+16 -8
View File
@@ -105,7 +105,9 @@ struct SettingsFilterField: View {
/// Dressed like `ShortcutRecorder`; a persistent `TextField` — swapping views broke repeat focus.
struct AliasField: View {
let entry: AppEntry
/// The owner's `preferenceKey`, taken raw so a row without an `AppEntry` can carry one too.
let key: String
let name: String
@Environment(AliasStore.self) private var aliases
@State private var draft = ""
@FocusState private var focused: Bool
@@ -134,13 +136,13 @@ struct AliasField: View {
.foregroundStyle(.tertiary)
}
.buttonStyle(.plain)
.accessibilityLabel("Clear alias for \(entry.name)")
.accessibilityLabel("Clear alias for \(name)")
}
}
.onAppear { draft = aliases.alias(for: entry.preferenceKey) ?? "" }
.onAppear { draft = aliases.alias(for: key) ?? "" }
// A backup import replaces the table out from under an unfocused row.
.onChange(of: aliases.revision) { _, _ in
if !focused { draft = aliases.alias(for: entry.preferenceKey) ?? "" }
if !focused { draft = aliases.alias(for: key) ?? "" }
}
.padding(.horizontal, Theme.Spacing.sm)
.frame(width: Theme.Size.shortcutRecorder, height: 24)
@@ -150,17 +152,17 @@ struct AliasField: View {
focused ? Color.accentColor : Theme.Colors.cardStroke, lineWidth: 1)
)
.clipShape(shape)
.accessibilityLabel("Alias for \(entry.name)")
.accessibilityLabel("Alias for \(name)")
}
/// The one commit path — ↵ or focus landing elsewhere; a blank draft removes the alias.
private func commit() {
aliases.setAlias(draft, for: entry.preferenceKey)
draft = aliases.alias(for: entry.preferenceKey) ?? ""
aliases.setAlias(draft, for: key)
draft = aliases.alias(for: key) ?? ""
}
private func revert() {
draft = aliases.alias(for: entry.preferenceKey) ?? ""
draft = aliases.alias(for: key) ?? ""
focused = false
}
@@ -169,3 +171,9 @@ struct AliasField: View {
commit()
}
}
extension AliasField {
init(entry: AppEntry) {
self.init(key: entry.preferenceKey, name: entry.name)
}
}
@@ -83,7 +83,7 @@ struct QuicklinksSettingsView: View {
}
Button("Add Quicklink…") { core.quicklinkCoordinator.editQuicklink(nil) }
} footer: {
Text("Name it, paste a link, then give it a shortcut if you want one.")
Text("Name it, paste a link, then add an alias or a shortcut if you want one.")
.font(.caption)
.foregroundStyle(.secondary)
}
@@ -166,6 +166,10 @@ private struct QuicklinkSettingsRow: View {
.help("Hidden from root search")
}
// An alias only reaches the ranker through the root-search slice, so it dims with it.
AliasField(key: quicklink.entryID, name: quicklink.name)
.settingsEnabled(quicklink.showsInRootSearch)
ShortcutRecorder(action: .quicklink(id: quicklink.id))
Button(action: onEdit) {
+3 -1
View File
@@ -151,7 +151,9 @@ visibility checkbox, not a per-invocation action, so the ⌘K menu stays out of
on `LauncherItemsSection` puts an `AliasField` on each row, dressed like the `ShortcutRecorder`
beside it; edits store as typed and trim when the field loses focus, and a blank means none. That
list filters by **membership only**, keeping the index's name order — re-ranking it per keystroke
would move the row being edited out from under its own field editor.
would move the row being edited out from under its own field editor. A pane with a hand-written row
hands `AliasField` the key itself: Settings ▸ Quicklinks passes `Quicklink.entryID`, and dims the
field on a quicklink hidden from root search, whose entry the ranker never sees.
Aliases ride along in a settings backup (`launcherAliases`), and deleting what an alias points at —
uninstalling an app, deleting a quicklink or custom command, uninstalling an extension — removes it
+3 -1
View File
@@ -122,7 +122,9 @@ Tinycast's own dialog and leaves no partial state.
Quicklinks are their own `AppEntry.Kind`, their own `AppIndex` slice and their own launcher section,
between System Settings and Snippets. Only the **name** is indexed; the destination is not searchable
(a URL is a subsequence of almost any query). Per-quicklink "Show in root search" filters the slice;
(a URL is a subsequence of almost any query) — beside the name, a quicklink answers to whatever
[user alias](launcher.md#user-aliases) its Settings row carries, which is why a hidden one dims the
field. Per-quicklink "Show in root search" filters the slice;
the pane's "Show in launcher" takes the section and the four Quicklink commands out of the
launcher together, leaving shortcuts and the pane itself working.