Watch
1
0
Fork
You've already forked souveraine
0

sidebar: the garden's micro-view — Lens service, vault card in the right panel

The vault card lives where notifications were — header with sync pill
(clean/ahead/behind/dirty), today's note entry, five freshest notes,
cold start and push rows — over the notification list. Lens.qml owns
souveraine-lens the way Face owns the rig: one socket, event lines up,
no vault access of its own. Kiyoshi's slash grammar and tool registry
are noted for the fuller app; the micro view only opens and plants.
This commit is contained in:
Fimeg 2026-08-10 21:24:23 -04:00
commit cb7dd73d2b
5 changed files with 543 additions and 0 deletions

View file

@ -63,6 +63,7 @@ modules/souveraine/windowSheet/PowerMenu.qml souveraine/modules/souveraine/windo
modules/souveraine/windowSheet/PowerOptionRow.qml souveraine/modules/souveraine/windowSheet/PowerOptionRow.qml
modules/souveraine/windowSheet/qmldir souveraine/modules/souveraine/windowSheet/qmldir
services/Face.qml souveraine/services/Face.qml
services/Lens.qml souveraine/services/Lens.qml
services/FingerprintPreview.qml souveraine/services/FingerprintPreview.qml
services/ViewtopControl.qml souveraine/services/ViewtopControl.qml
services/ZoneTransition.qml souveraine/services/ZoneTransition.qml
@ -93,6 +94,8 @@ modules/ii/sidebarLeft/AiChat.qml souveraine/modules/ii/sidebarLeft/AiCh
modules/ii/sidebarRight/SidebarRight.qml souveraine/modules/ii/sidebarRight/SidebarRight.qml
modules/ii/sidebarRight/SidebarRightContent.qml souveraine/modules/ii/sidebarRight/SidebarRightContent.qml
modules/ii/sidebarRight/QuickSliders.qml souveraine/modules/ii/sidebarRight/QuickSliders.qml
modules/ii/sidebarRight/CenterWidgetGroup.qml souveraine/modules/ii/sidebarRight/CenterWidgetGroup.qml
modules/ii/sidebarRight/lens/LensWidget.qml souveraine/modules/ii/sidebarRight/lens/LensWidget.qml
modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml souveraine/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml
modules/ii/bar/UtilButtons.qml souveraine/modules/ii/bar/UtilButtons.qml
modules/common/Config.qml souveraine/modules/common/Config.qml

View file

@ -0,0 +1,30 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs.modules.ii.sidebarRight.notifications
import qs.modules.ii.sidebarRight.lens
import QtQuick
import QtQuick.Layouts
Rectangle {
id: root
clip: true
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
LensWidget {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
}
NotificationList {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}

View file

@ -0,0 +1,281 @@
// The garden's micro-view — the vault card in the right panel.
//
// The micro view is a window into the lens window, not a second editor:
// it shows the freshest notes, the sync truth, a daily note entry point
// and a quick capture — and every tap either opens the garden at that
// note or plants one in it. The full app is the lens window itself; this
// is the pulse.
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
implicitHeight: column.implicitHeight
property var shown: Lens.joined ? Lens.notes.slice(0, 5) : []
function dailyRel() {
return new Date().toISOString().slice(0, 10) + ".md";
}
function openDaily() {
if (!Lens.joined) {
Lens.join();
return;
}
const rel = root.dailyRel();
const exists = Lens.notes.some(n => n.rel === rel);
if (exists)
Lens.openNote(rel);
else
Lens.createNote(rel.replace(/\.md$/, ""));
}
function relTime(mtime) {
const s = Math.max(0, Date.now() / 1000 - mtime);
if (s < 60) return Translation.tr("just now");
if (s < 3600) return Translation.tr("%1m ago").arg(Math.floor(s / 60));
if (s < 86400) return Translation.tr("%1h ago").arg(Math.floor(s / 3600));
if (s < 604800) return Translation.tr("%1d ago").arg(Math.floor(s / 86400));
return new Date(mtime * 1000).toLocaleDateString(Qt.locale(), "dd MMM");
}
onVisibleChanged: {
if (root.visible && Lens.joined)
Lens.requestState();
}
ColumnLayout {
id: column
anchors.fill: parent
spacing: 4
// ── header ──
RowLayout {
Layout.fillWidth: true
spacing: 6
MaterialSymbol {
text: "hub"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colPrimary
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
text: Translation.tr("Garden")
}
StyledText {
visible: Lens.joined && Lens.head.length > 0
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1Inactive
text: Lens.branch + " @" + Lens.head
}
}
Rectangle {
id: syncPill
Layout.alignment: Qt.AlignVCenter
radius: 8
color: Appearance.colors.colLayer0
implicitHeight: pillText.implicitHeight + 6
implicitWidth: pillText.implicitWidth + 14
Row {
anchors.centerIn: parent
spacing: 5
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 7
height: 7
radius: width / 2
color: root.pillColor
}
StyledText {
id: pillText
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer0
text: Lens.stateText
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.topMargin: 2
height: 1
color: Appearance.colors.colLayer0Border
}
// ── the daily note ──
Item {
Layout.fillWidth: true
Layout.topMargin: 2
implicitHeight: dailyRow.implicitHeight
RowLayout {
id: dailyRow
anchors.fill: parent
spacing: 6
MaterialSymbol {
text: "today"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer1Inactive
}
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer1
text: Translation.tr("Today's note")
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colPrimary
text: Translation.tr("open")
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.openDaily()
}
}
// ── fresh notes ──
Repeater {
model: root.shown
delegate: Item {
required property var modelData
required property int index
Layout.fillWidth: true
implicitHeight: noteRow.implicitHeight
RowLayout {
id: noteRow
anchors.fill: parent
spacing: 6
Rectangle {
Layout.alignment: Qt.AlignVCenter
width: 4
height: 4
radius: width / 2
color: index === 0 ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer1Inactive
}
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer1
text: modelData.title
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1Inactive
text: root.relTime(modelData.mtime)
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Lens.openNote(modelData.rel)
}
}
}
// ── cold card ──
Item {
Layout.fillWidth: true
visible: !Lens.joined
implicitHeight: coldRow.implicitHeight
RowLayout {
id: coldRow
anchors.fill: parent
spacing: 6
MaterialSymbol {
text: "crop_free"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer1Inactive
}
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer1Inactive
text: Translation.tr("The garden is closed")
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colPrimary
text: Translation.tr("Open")
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Lens.join()
}
}
// ── push the truth ──
Item {
Layout.fillWidth: true
visible: Lens.joined && (Lens.syncState === "ahead" || Lens.syncState === "diverged" || Lens.syncState === "dirty")
implicitHeight: pushRow.implicitHeight
RowLayout {
id: pushRow
anchors.fill: parent
spacing: 6
MaterialSymbol {
text: "upload"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colPrimary
}
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer1
text: Translation.tr("The garden is ahead — push it")
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colPrimary
text: Translation.tr("Push")
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Lens.syncNow()
}
}
}
// Pill color: one dot, one meaning.
readonly property color pillColor: {
if (!Lens.joined) return Appearance.colors.colOnLayer1Inactive;
switch (Lens.syncState) {
case "clean": return Appearance.colors.colPrimary;
case "dirty": return Appearance.m3colors.m3error;
case "ahead":
case "diverged": return Appearance.m3colors.m3tertiary;
case "behind": return Appearance.m3colors.m3error;
default: return Appearance.colors.colOnLayer1Inactive;
}
}
}

View file

@ -0,0 +1,228 @@
// The garden on the limb — the vault's micro-view service.
//
// One more process owned the Face way: this holds a `souveraine-lens`
// process with `--ipc` at a socket only the shell ever touches. The lens
// process has no connection to the server, and this service has no
// connection to the vault — the micro-state the widget renders arrives
// over the same line protocol the lens already speaks, one JSON object
// per line on stdout. The seam stays: nobody reads the vault but the lens
// and the user.
//
// `joined` is the whole state, as with the face. While it is false the
// garden window is not on glass and no webview holds memory; the widget
// shows a cold card until the user opens it.
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs
import qs.modules.common
Singleton {
id: root
property bool joined: false
property string socketPath: Quickshell.env("XDG_RUNTIME_DIR") + "/souveraine/lens.sock"
property string vaultDir: Quickshell.env("HOME") + "/vault"
readonly property string vaultRel: vaultDir.replace(Quickshell.env("HOME"), "~")
// The micro-state, exactly as the lens reports it.
property var notes: []
property int noteCount: 0
property int dirty: 0
property int ahead: 0
property int behind: 0
property string head: ""
property string branch: ""
property bool hasRemote: true
property string lastError: ""
// cold until the first state answers; then one of clean / ahead /
// behind / diverged / dirty — computed, so there is one place the
// widget reads.
property string syncState: "cold"
readonly property string stateText: {
if (!root.joined) return "offline";
switch (root.syncState) {
case "clean": return root.noteCount + " notes · clean";
case "ahead": return root.noteCount + " notes · push " + root.ahead;
case "behind": return root.noteCount + " notes · pull " + root.behind;
case "diverged": return root.noteCount + " notes · push " + root.ahead + " pull " + root.behind;
case "dirty": return root.noteCount + " notes · " + root.dirty + " uncommitted";
default: return root.noteCount + " notes";
}
}
onJoinedChanged: {
if (root.joined)
root.requestState();
}
function join() {
if (root.joined)
return;
host.running = true;
root.joined = true;
}
function leave() {
if (!root.joined)
return;
root._send({ op: "quit" });
host.running = false;
root.joined = false;
root.syncState = "cold";
root.notes = [];
root.noteCount = 0;
}
function toggle() {
if (root.joined)
root.leave();
else
root.join();
}
function requestState() {
root._send({ op: "state" });
}
function openNote(rel) {
root._send({ op: "open", rel: rel });
}
function createNote(title) {
root._send({ op: "create", title: title });
}
// The page owns its sync flow; the shell only pulls the trigger.
function syncNow() {
root._eval("window.__lens.sync()");
}
function _eval(script) {
root._send({ op: "eval", script: script });
}
function _send(msg) {
if (sock.connected)
sock.write(JSON.stringify(msg) + "\n");
}
Process {
id: host
command: ["souveraine-lens",
"--vault", root.vaultDir,
"--ipc", root.socketPath,
"--app-id", "org.souveraine.lens",
"--title", "Garden"]
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
let msg;
try {
msg = JSON.parse(line);
} catch (e) {
return;
}
if (msg.event === "state" && msg.state) {
root.notes = msg.state.notes ?? [];
root.noteCount = msg.state.status?.note_count ?? root.notes.length;
root.dirty = msg.state.status?.dirty ?? 0;
root.ahead = msg.state.status?.ahead ?? 0;
root.behind = msg.state.status?.behind ?? 0;
root.head = msg.state.status?.head ?? "";
root.branch = msg.state.status?.branch ?? "";
root.hasRemote = (msg.state.status?.remote?.length ?? 0) > 0;
root.syncState = root.dirty > 0
? "dirty"
: root.ahead > 0 && root.behind > 0
? "diverged"
: root.ahead > 0 ? "ahead"
: root.behind > 0 ? "behind" : "clean";
}
else if (msg.event === "synced")
refreshTimer.restart();
else if (msg.event === "note_opened" || msg.event === "note_created" || msg.event === "note_saved")
refreshTimer.restart();
else if (msg.event === "console")
console.log("[lens]", msg.level ?? "", msg.text ?? "");
else
console.log("[lens]", line);
}
}
onExited: {
root.joined = false;
root.syncState = "cold";
}
}
// One refresh after a burst of events, not one per line.
Timer {
id: refreshTimer
interval: 300
repeat: false
onTriggered: root.requestState()
}
// The garden's heartbeat while it is joined; cheap — one scan.
Timer {
id: pollTimer
running: root.joined
interval: 60000
repeat: true
triggeredOnStart: true
onTriggered: root.requestState()
}
Socket {
id: sock
path: root.socketPath
connected: root.joined
onConnectionStateChanged: {
if (sock.connected)
root.requestState();
}
}
// Reachable by name: the agent summons the garden the same way it
// summons the face, without guessing.
IpcHandler {
target: "lens"
function open(rel: string): void {
if (!root.joined)
root.join();
root.openNote(rel);
}
function create(title: string): void {
if (!root.joined)
root.join();
root.createNote(title);
}
function sync(): void {
root.syncNow();
}
function join(): void {
root.join();
}
function leave(): void {
root.leave();
}
function status(): string {
return JSON.stringify({
joined: root.joined,
syncState: root.syncState,
noteCount: root.noteCount,
head: root.head
});
}
}
}

View file

@ -35,6 +35,7 @@ singleton KeyringStorage 1.0 KeyringStorage.qml
singleton LatexRenderer 1.0 LatexRenderer.qml
singleton LauncherApps 1.0 LauncherApps.qml
singleton LauncherSearch 1.0 LauncherSearch.qml
singleton Lens 1.0 Lens.qml
singleton LockContentPolicy 1.0 LockContentPolicy.qml
singleton MaterialThemeLoader 1.0 MaterialThemeLoader.qml
singleton MprisController 1.0 MprisController.qml