Autostart entries showed "Enabled" or "Disabled" as plain text. The row did toggle on click the whole time, so this is an affordance rather than a missing capability -- but a control that reads as static text is one nobody knows they have. It is a switch now, with removal alongside it behind a confirmation: disabling writes Hidden=true and can be undone, deleting the file cannot. remove-autostart is confined to files the autostart directory owns. It resolves the path and compares the parent, so a name like "../../.bashrc" cannot escape, and it refuses symlinks rather than following them -- deleting through one would remove whatever it points at, which is somewhere else and not ours. Each refusal was tested against a fixture directory, including a symlink aimed at /etc/hostname, which survived. Sharing says who is signed in from another machine: user, origin and since when. An empty list on this machine proves nothing, so the parser was checked against sample `who` output -- it picks out remote sessions and leaves out local seats and the :0 display, which would otherwise report the person at the keyboard as a remote login. Media sharing was "Available" and nothing else: rygel installed, rygel.service disabled, no way to change that from here. It is a switch now, and it says what it does before you touch it rather than afterwards -- turning it on publishes media folders to every device on the network with no password in front of them. Per-application camera and microphone permissions come from the portal's permission store, which is where an application that asked through the portal has its answer recorded. The page states the limit plainly instead of implying a protection that does not exist: a program installed outside the portal opens the device directly and nothing here stands in its way. Anything that is not an explicit "yes" is treated as withheld, because guessing generously about a camera is the wrong way to be wrong. The first version of the write silently did nothing -- SetPermission takes an array of strings and was being handed one string -- and the test did not notice, because it discarded the helper's output and only checked that state was unchanged afterwards, which was trivially true. The contract now requires the value to move, and was proven to fail by putting that exact bug back. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
210 lines
7.7 KiB
QML
210 lines
7.7 KiB
QML
// What this machine offers to other machines on the network.
|
|
//
|
|
// Every row states what is true right now, including "the software for this is
|
|
// not installed" -- which is the honest answer for file sharing here, and is
|
|
// what the panel this replaces hides behind a switch that silently does
|
|
// nothing.
|
|
//
|
|
// Turning remote login on changes the whole system and prompts. Remote desktop
|
|
// is a user service and does not.
|
|
|
|
import Quickshell
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
objectName: "sharing"
|
|
title: "Sharing"
|
|
lede: "What this machine offers to other machines on the network."
|
|
|
|
Component.onCompleted: Sharing.refresh()
|
|
|
|
TextRow {
|
|
visible: Sharing.lastError !== ""
|
|
label: "Sharing needs attention"
|
|
detail: Sharing.lastError
|
|
value: ""
|
|
divider: false
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "This machine"
|
|
subtitle: "The name other machines see."
|
|
|
|
TextFieldRow {
|
|
label: "Network name"
|
|
detail: "Used for ssh and for anything else that finds this machine by name"
|
|
text: Sharing.hostname
|
|
placeholder: "desktop"
|
|
enabled: !Sharing.busy
|
|
divider: false
|
|
onAccepted: value => Sharing.setHostname(value)
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Remote login"
|
|
subtitle: "Sign in to a terminal on this machine over SSH."
|
|
|
|
SwitchRow {
|
|
label: "Allow remote login"
|
|
detail: Sharing.remoteLogin?.installed === true
|
|
? (Sharing.remoteLoginOn
|
|
? "Running, and starts automatically at boot"
|
|
: "Not running")
|
|
: "OpenSSH server is not installed"
|
|
checked: Sharing.remoteLoginOn
|
|
enabled: !Sharing.busy && Sharing.remoteLogin?.installed === true
|
|
onToggled: value => Sharing.setRemoteLogin(value)
|
|
}
|
|
|
|
TextRow {
|
|
visible: Sharing.remoteLoginOn
|
|
label: "Connect with"
|
|
detail: "From another machine on your network"
|
|
value: "ssh " + Sharing.networkName
|
|
}
|
|
|
|
TextRow {
|
|
visible: Sharing.remoteLoginOn && Sharing.remoteSessions.length === 0
|
|
label: "Nobody is signed in"
|
|
detail: "Remote login is on, and no one is connected from another machine."
|
|
value: ""
|
|
}
|
|
|
|
Repeater {
|
|
model: Sharing.remoteSessions
|
|
|
|
delegate: TextRow {
|
|
required property var modelData
|
|
width: parent.width
|
|
label: String(modelData.user ?? "") + " is signed in from " + String(modelData.from ?? "")
|
|
detail: "Since " + String(modelData.since ?? "") + " · " + String(modelData.line ?? "")
|
|
value: ""
|
|
}
|
|
}
|
|
|
|
TextRow {
|
|
visible: Sharing.remoteLoginOn
|
|
label: "Port"
|
|
detail: "Where the SSH server is listening"
|
|
value: String(Sharing.remoteLogin?.port ?? "22")
|
|
}
|
|
|
|
// Reported from the configuration rather than assumed. Saying "keys
|
|
// only" on a machine that actually accepts passwords would be a
|
|
// security claim this page cannot back up.
|
|
TextRow {
|
|
visible: Sharing.remoteLoginOn
|
|
label: "Password sign-in"
|
|
detail: Sharing.passwordLoginSummary()
|
|
value: ""
|
|
divider: false
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Remote desktop"
|
|
subtitle: "See and control this desktop from another machine."
|
|
|
|
SwitchRow {
|
|
label: "Allow remote desktop"
|
|
detail: Sharing.remoteDesktop?.available === true
|
|
? (Sharing.remoteDesktopOn
|
|
? "Running for your session"
|
|
: (Sharing.remoteDesktop?.hasCredentials === true
|
|
? "Not running"
|
|
: "Set a username and password before turning this on"))
|
|
: "Remote desktop support is not installed"
|
|
checked: Sharing.remoteDesktopOn
|
|
enabled: !Sharing.busy
|
|
&& Sharing.remoteDesktop?.available === true
|
|
&& Sharing.remoteDesktop?.hasCredentials === true
|
|
onToggled: value => Sharing.setRemoteDesktop(value)
|
|
}
|
|
|
|
TextFieldRow {
|
|
visible: Sharing.remoteDesktop?.available === true
|
|
label: "Port"
|
|
detail: "The RDP port other machines connect to"
|
|
text: String(Sharing.remoteDesktop?.port ?? "")
|
|
placeholder: "3389"
|
|
enabled: !Sharing.busy
|
|
onAccepted: value => Sharing.setRdpPort(value)
|
|
}
|
|
|
|
SwitchRow {
|
|
visible: Sharing.remoteDesktop?.available === true
|
|
label: "View only"
|
|
detail: "Let someone watch this desktop without controlling the pointer or keyboard"
|
|
checked: Sharing.remoteDesktop?.viewOnly === true
|
|
enabled: !Sharing.busy
|
|
onToggled: value => Sharing.setRdpViewOnly(value)
|
|
}
|
|
|
|
// The password is typed into gnome-remote-desktop's own tool in a
|
|
// terminal, never into this page. grdctl prompts for it on a terminal
|
|
// and crashes without one, and passing it as an argument would publish
|
|
// it through /proc to every process on this machine.
|
|
ActionRow {
|
|
visible: Sharing.remoteDesktop?.available === true
|
|
label: "Credentials"
|
|
detail: Sharing.remoteDesktop?.hasCredentials === true
|
|
? "Stored in the login keyring · setting new ones opens a terminal to type into"
|
|
: "None stored yet · remote desktop cannot be turned on without them"
|
|
action: "Set…"
|
|
enabled: !Sharing.busy
|
|
onTriggered: Sharing.setRdpCredentials(Quickshell.env("USER") || "")
|
|
}
|
|
|
|
ActionRow {
|
|
visible: Sharing.remoteDesktop?.available === true
|
|
&& Sharing.remoteDesktop?.hasCredentials === true
|
|
label: "Forget the stored credentials"
|
|
detail: "Remote desktop cannot be turned on again until new ones are set"
|
|
action: "Clear"
|
|
enabled: !Sharing.busy
|
|
divider: false
|
|
onTriggered: Sharing.clearRdpCredentials()
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "File and media sharing"
|
|
subtitle: "Sharing folders and media needs software this machine does not necessarily have."
|
|
|
|
TextRow {
|
|
label: "Share folders on the network"
|
|
detail: Sharing.fileSharing?.installed === true
|
|
? "Samba is installed"
|
|
: "Needs Samba, which is not installed. Settings does not install software."
|
|
value: Sharing.fileSharing?.installed === true ? "Available" : "Not installed"
|
|
}
|
|
|
|
SwitchRow {
|
|
visible: Sharing.mediaSharing?.installed === true
|
|
label: "Share music and video to devices"
|
|
// Said before it happens, not after: this advertises on the network
|
|
// to anything that speaks DLNA, with no password in front of it.
|
|
detail: Sharing.mediaSharing?.active === true
|
|
? "Rygel is serving your media to devices on the network"
|
|
: "Publishes your media folders to every device on the network. No password is asked for."
|
|
checked: Sharing.mediaSharing?.active === true
|
|
enabled: !Sharing.busy
|
|
divider: false
|
|
onToggled: value => Sharing.setMediaSharing(value)
|
|
}
|
|
|
|
TextRow {
|
|
visible: Sharing.mediaSharing?.installed !== true
|
|
label: "Share music and video to devices"
|
|
detail: "Needs Rygel, which is not installed."
|
|
value: "Not installed"
|
|
divider: false
|
|
}
|
|
}
|
|
}
|