aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-16 16:45:36 +0200
committerDanilo M. <danix@danix.xyz>2026-10-16 16:45:36 +0200
commit193cd8c2e1cc7e16b1389b95036bf7f4f99f97e0 (patch)
treeee6e88c3069baf51462f8f036ae8e69da3bd2902 /desktop/assistant
parent607934e74ffabc1dcce72b29fc28e92a669e54ce (diff)
downloadquickshell-193cd8c2e1cc7e16b1389b95036bf7f4f99f97e0.tar.gz
quickshell-193cd8c2e1cc7e16b1389b95036bf7f4f99f97e0.zip
feat(assistant): Ask and Do as captioned tile grids with icons
The actions sat as buttons bunched at the left of the card. Each group is now a grid of equal tiles across the card's width, icon over label, about 150 px a column, with a small Ask or Do caption. Icons are Font Awesome and Octicons codepoints checked in Inconsolata Nerd Font's cmap; a saved preset without one gets a fallback. The lists referenced from QML are var, since a library's top-level const is not reliably exposed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/assistant')
-rw-r--r--desktop/assistant/Card.qml136
-rw-r--r--desktop/assistant/Ops.js41
-rw-r--r--desktop/assistant/Presets.js24
3 files changed, 135 insertions, 66 deletions
diff --git a/desktop/assistant/Card.qml b/desktop/assistant/Card.qml
index 02ee476..069d3a0 100644
--- a/desktop/assistant/Card.qml
+++ b/desktop/assistant/Card.qml
@@ -205,7 +205,87 @@ Rectangle {
// Ask: canned questions matching the staged kinds (Presets.js). A click
// sends one; Shift+click puts it in the input to edit first.
- Flow {
+ // One action as a tile: its icon over its label, the tiles of a group
+ // sharing the card's width equally.
+ component Tile: Rectangle {
+ id: tile
+ property string icon
+ property string label
+ property bool on: true
+ signal picked(var mouse)
+ implicitHeight: 62
+ radius: 8
+ opacity: on ? 1 : 0.4
+ color: area.pressed && on ? Qt.alpha(Theme.accent, 0.35)
+ : area.containsMouse && on ? Qt.alpha(Theme.accent, 0.18)
+ : Qt.alpha(Theme.surface, 0.8)
+ border.width: 1
+ border.color: Qt.alpha(Theme.text, 0.12)
+ Glow { anchors.fill: parent; z: -1; radius: parent.radius; on: area.containsMouse && tile.on }
+ Column {
+ anchors.centerIn: parent
+ width: parent.width - 12
+ spacing: 4
+ Text {
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ text: tile.icon
+ color: Theme.accent
+ font { family: Theme.iconFamily; pixelSize: Theme.fontSize + 2 }
+ }
+ Text {
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ text: tile.label
+ elide: Text.ElideRight
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+ }
+ MouseArea {
+ id: area
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: tile.on ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onClicked: mouse => { if (tile.on) tile.picked(mouse); }
+ }
+ }
+
+ // A captioned grid of tiles; columns follow the width, about 150 px each.
+ component Group: Column {
+ id: group
+ property string caption
+ property var items: []
+ property bool on: true
+ signal picked(var item, var mouse)
+ spacing: 6
+ Text {
+ text: group.caption
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5; capitalization: Font.AllUppercase }
+ }
+ Grid {
+ id: grid
+ width: group.width
+ spacing: 8
+ columns: Math.max(1, Math.floor((width + spacing) / (150 + spacing)))
+ Repeater {
+ model: group.items
+ Tile {
+ required property var modelData
+ width: (grid.width - (grid.columns - 1) * grid.spacing) / grid.columns
+ icon: modelData.icon ?? Presets.FALLBACK
+ label: modelData.label
+ on: group.on && (modelData.op !== "md" || card.canvas.connected)
+ onPicked: mouse => group.picked(modelData, mouse)
+ }
+ }
+ }
+ }
+
+ // Ask: canned questions matching the staged kinds (Presets.js). A click
+ // sends one; Shift+click puts it in the input to edit first.
+ Group {
id: asks
readonly property var offered: Presets.offered(Presets.list(Prefs.presets), card.canvas.staged)
visible: card.mode === "file" && offered.length > 0 && !card.canvas.opRunning
@@ -213,57 +293,41 @@ Rectangle {
bottom: dos.visible ? dos.top : status.visible ? status.top : actions.top; bottomMargin: 10
left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14
}
- spacing: 8
-
- Repeater {
- model: asks.offered
- Button {
- required property var modelData
- text: modelData.label
- enabled: card.canvas.connected && !card.canvas.blocked
- onClicked: mouse => {
- if (mouse && (mouse.modifiers & Qt.ShiftModifier)) {
- input.text = Presets.prompt(modelData);
- input.cursorPosition = input.text.length;
- input.forceActiveFocus();
- } else {
- card.canvas.ask(Presets.prompt(modelData));
- }
- }
+ caption: "Ask"
+ items: offered
+ on: card.canvas.connected && !card.canvas.blocked
+ onPicked: (p, mouse) => {
+ if (mouse.modifiers & Qt.ShiftModifier) {
+ input.text = Presets.prompt(p);
+ input.cursorPosition = input.text.length;
+ input.forceActiveFocus();
+ } else {
+ card.canvas.ask(Presets.prompt(p));
}
}
}
// Do: local operations (Ops.js, run by fileops or the daemon's write).
- // Compress… swaps the row for its formats. Hidden while one runs.
+ // Compress… swaps the group for its formats. Hidden while one runs.
property bool compressing: false
Connections { target: card.canvas; function onStagedChanged() { card.compressing = false; } }
- Flow {
+ Group {
id: dos
readonly property var offered: card.compressing
- ? Ops.COMPRESS.concat([{ label: "Back", op: "back" }])
+ ? Ops.COMPRESS.concat([{ label: "Back", op: "back", icon: "" }])
: Ops.offered(card.canvas.staged)
visible: card.mode === "file" && offered.length > 0 && !card.canvas.opRunning
anchors {
bottom: status.visible ? status.top : actions.top; bottomMargin: 10
left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14
}
- spacing: 8
-
- Repeater {
- model: dos.offered
- Button {
- required property var modelData
- text: modelData.label
- // Local operations need no daemon; To .md does.
- enabled: modelData.op !== "md" || card.canvas.connected
- onClicked: {
- if (modelData.op === "compress") card.compressing = true;
- else if (modelData.op === "back") card.compressing = false;
- else { card.compressing = false; card.canvas.runOp(modelData); }
- }
- }
+ caption: card.compressing ? "Compress as" : "Do"
+ items: offered
+ onPicked: o => {
+ if (o.op === "compress") card.compressing = true;
+ else if (o.op === "back") card.compressing = false;
+ else { card.compressing = false; card.canvas.runOp(o); }
}
}
diff --git a/desktop/assistant/Ops.js b/desktop/assistant/Ops.js
index e59560d..2761a2d 100644
--- a/desktop/assistant/Ops.js
+++ b/desktop/assistant/Ops.js
@@ -15,29 +15,30 @@
// daemon's write, or "compress", which opens the COMPRESS formats. kinds
// lists the staged kinds it fits ("any" fits every mix); min is the number
// of files it needs; format is the suffix a file already has when the
-// operation would not change it.
+// operation would not change it. icon is a Nerd Font codepoint, checked in
+// Inconsolata Nerd Font's cmap.
const OPS = [
- { label: "To .md", op: "md", kinds: ["pdf"] },
- { label: "To images", op: "pdf-images", kinds: ["pdf"] },
- { label: "Merge", op: "pdf-merge", kinds: ["pdf"], min: 2 },
- { label: "Extract audio", op: "audio", kinds: ["video"] },
- { label: "To MP4", op: "mp4", kinds: ["video"], format: [".mp4"] },
- { label: "To WebM", op: "webm", kinds: ["video"], format: [".webm"] },
- { label: "To GIF", op: "gif", kinds: ["video"] },
- { label: "Trackcrop", op: "trackcrop", kinds: ["video"] },
- { label: "To PNG", op: "png", kinds: ["image"], format: [".png"] },
- { label: "To JPEG", op: "jpeg", kinds: ["image"], format: [".jpg", ".jpeg"] },
- { label: "To WebP", op: "webp", kinds: ["image"], format: [".webp"] },
- { label: "Resize 50%", op: "half", kinds: ["image"] },
- { label: "Extract", op: "extract", kinds: ["archive"] },
- { label: "Compress…", op: "compress", kinds: ["any"] },
+ { label: "To .md", icon: "\uf48a", op: "md", kinds: ["pdf"] },
+ { label: "To images", icon: "\uf1c5", op: "pdf-images", kinds: ["pdf"] },
+ { label: "Merge", icon: "\uf419", op: "pdf-merge", kinds: ["pdf"], min: 2 },
+ { label: "Extract audio", icon: "\uf001", op: "audio", kinds: ["video"] },
+ { label: "To MP4", icon: "\uf008", op: "mp4", kinds: ["video"], format: [".mp4"] },
+ { label: "To WebM", icon: "\uf008", op: "webm", kinds: ["video"], format: [".webm"] },
+ { label: "To GIF", icon: "\uf1c8", op: "gif", kinds: ["video"] },
+ { label: "Trackcrop", icon: "\uf05b", op: "trackcrop", kinds: ["video"] },
+ { label: "To PNG", icon: "\uf03e", op: "png", kinds: ["image"], format: [".png"] },
+ { label: "To JPEG", icon: "\uf03e", op: "jpeg", kinds: ["image"], format: [".jpg", ".jpeg"] },
+ { label: "To WebP", icon: "\uf03e", op: "webp", kinds: ["image"], format: [".webp"] },
+ { label: "Resize 50%", icon: "\uf065", op: "half", kinds: ["image"] },
+ { label: "Extract", icon: "\uf07c", op: "extract", kinds: ["archive"] },
+ { label: "Compress…", icon: "\uf1c6", op: "compress", kinds: ["any"] },
];
-const COMPRESS = [
- { label: ".zip", op: "zip" },
- { label: ".tar.gz", op: "tgz" },
- { label: ".tar.xz", op: "txz" },
- { label: ".tar.bz2", op: "tbz2" },
+var COMPRESS = [
+ { label: ".zip", icon: "\uf1c6", op: "zip" },
+ { label: ".tar.gz", icon: "\uf1c6", op: "tgz" },
+ { label: ".tar.xz", icon: "\uf1c6", op: "txz" },
+ { label: ".tar.bz2", icon: "\uf1c6", op: "tbz2" },
];
function ext(name) {
diff --git a/desktop/assistant/Presets.js b/desktop/assistant/Presets.js
index 116d7d0..057f993 100644
--- a/desktop/assistant/Presets.js
+++ b/desktop/assistant/Presets.js
@@ -11,22 +11,26 @@
.pragma library
-// Canned questions for the file state's Ask row. kinds lists the staged
+// Canned questions for the file state's Ask row. icon is a Nerd Font
+// codepoint, checked in Inconsolata Nerd Font's cmap; a saved preset
+// without one gets FALLBACK. kinds lists the staged
// kinds a preset applies to (Files.js); ["many"] offers it whenever two or
// more files are staged. The list in use is Prefs.presets when one is
// saved (a settings page will edit it), these otherwise.
const DEFAULTS = [
- { label: "Summarize", kinds: ["pdf", "text"], prompt: "Summarize this in a few short paragraphs." },
- { label: "Key points", kinds: ["pdf"], prompt: "List the key points as bullets." },
- { label: "Explain simply", kinds: ["pdf", "text"], prompt: "Explain this simply, as to a non-expert." },
- { label: "Describe", kinds: ["image"], prompt: "Describe this image." },
- { label: "Extract text", kinds: ["image", "pdf"], prompt: "Transcribe all the text, keeping the layout where it helps." },
- { label: "What is this?", kinds: ["image"], prompt: "What is this, and what is it for?" },
- { label: "Explain", kinds: ["code"], prompt: "Explain what this code does." },
- { label: "Review", kinds: ["code"], prompt: "Review this code: bugs, risks, and simplifications." },
- { label: "Compare", kinds: ["many"], prompt: "Compare these files: what they share and how they differ." },
+ { label: "Summarize", icon: "\uf036", kinds: ["pdf", "text"], prompt: "Summarize this in a few short paragraphs." },
+ { label: "Key points", icon: "\uf0ca", kinds: ["pdf"], prompt: "List the key points as bullets." },
+ { label: "Explain simply", icon: "\uf0eb", kinds: ["pdf", "text"], prompt: "Explain this simply, as to a non-expert." },
+ { label: "Describe", icon: "\uf06e", kinds: ["image"], prompt: "Describe this image." },
+ { label: "Extract text", icon: "\uf031", kinds: ["image", "pdf"], prompt: "Transcribe all the text, keeping the layout where it helps." },
+ { label: "What is this?", icon: "\uf059", kinds: ["image"], prompt: "What is this, and what is it for?" },
+ { label: "Explain", icon: "\uf121", kinds: ["code"], prompt: "Explain what this code does." },
+ { label: "Review", icon: "\uf188", kinds: ["code"], prompt: "Review this code: bugs, risks, and simplifications." },
+ { label: "Compare", icon: "\uf0db", kinds: ["many"], prompt: "Compare these files: what they share and how they differ." },
];
+var FALLBACK = "\uf086";
+
// The saved list if it is one, else the defaults: a hand edit with a typo
// must not leave the row empty.
function list(saved) {