Watch
1
0
Fork
You've already forked souveraine
0

quickshell: pin shared ii base and phone overlay

This commit is contained in:
Fimeg 2026-07-21 18:55:02 -04:00
commit b0a1304ba1
970 changed files with 88310 additions and 1 deletions

View file

@ -0,0 +1,77 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.panels.lock
import QtQuick
import Quickshell
import Quickshell.Hyprland
LockScreen {
id: root
// Monitor name -> workspace id to restore on unlock (set when locking)
property var savedWorkspaces: ({})
Timer {
id: restoreTimer
interval: 150
repeat: false
onTriggered: {
var batch = ""
for (var j = 0; j < Quickshell.screens.length; ++j) {
var monName = Quickshell.screens[j].name
var wsId = root.savedWorkspaces[monName]
if (wsId !== undefined) {
batch += `hyprctl dispatch 'hl.dsp.focus({monitor="${monName}"})'; hyprctl dispatch 'hl.dsp.focus({workspace=${wsId}})';`
}
}
if (batch.length > 0) {
Quickshell.execDetached(["bash", "-c", batch])
}
}
}
lockSurface: LockSurface {
context: root.context
}
// Single batch for lock and unlock so we don't race multiple hyprctl calls
Connections {
target: GlobalStates
function onScreenLockedChanged() {
if (GlobalStates.screenLocked) {
// Lock: save workspace per monitor and move all to temp workspace in one batch
var next = {}
var batch = "keyword animation workspaces,1,7,menu_decel,slidevert; "
for (var i = 0; i < Quickshell.screens.length; ++i) {
var mon = Quickshell.screens[i].name
var mData = HyprlandData.monitors.find(m => m.name === mon)
if (mData?.activeWorkspace == undefined) {
return;
}
var ws = (mData?.activeWorkspace?.id ?? 1)
next[mon] = ws
batch += `hyprctl dispatch 'hl.dsp.focus({monitor="${mon}"})'; hyprctl dispatch 'hl.dsp.focus({workspace=${2147483647 - ws}})';`
}
root.savedWorkspaces = next
Quickshell.execDetached(["bash", "-c", batch])
} else {
restoreTimer.start()
}
}
}
// Push everything down (visual only; workspace switch is in Connections above)
Variants {
model: Quickshell.screens
delegate: Scope {
required property ShellScreen modelData
property bool shouldPush: GlobalStates.screenLocked
property string targetMonitorName: modelData?.name ?? ""
property int verticalMovementDistance: modelData?.height ?? 0
property int horizontalSqueeze: (modelData?.width ?? 0) * 0.2
}
}
}

View file

@ -0,0 +1,500 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Services.UPower
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import qs.modules.common.panels.lock
import qs.modules.ii.bar as Bar
import Quickshell
import Quickshell.Io
import Quickshell.Services.SystemTray
import qs.modules.ii.mediaControls
MouseArea {
id: root
required property LockContext context
property bool active: false
property bool showInputField: active || context.currentText.length > 0
readonly property bool requirePasswordToPower: Config.options.lock.security.requirePasswordToPower
// Whether a player with a title is currently active
readonly property bool mediaPlayerAvailable: MprisController.activePlayer !== null && MprisController.activePlayer.trackTitle
// Gate that keeps the Loader alive during exit animation.
// Set to true when player appears, set to false only after exit anim finishes.
property bool mediaLoaderActive: false
onMediaPlayerAvailableChanged: {
if (mediaPlayerAvailable) {
// Player appeared: activate loader (entry anim fires via onLoaded)
mediaLoaderActive = true
} else {
// Player disappeared: run exit anim, deactivate loader when done
if (lockscreenMediaController.item) {
entryAnim.stop()
mediaExitAnim.restart()
} else {
mediaLoaderActive = false
}
}
}
// Visualizer data for lockscreen media controls
property list<real> visualizerPoints: []
Process {
id: cavaProc
running: root.mediaLoaderActive && root.mediaPlayerAvailable
onRunningChanged: {
if (!cavaProc.running)
root.visualizerPoints = [];
}
command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.scriptPath)}/cava/raw_output_config.txt`]
stdout: SplitParser {
onRead: data => {
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p));
root.visualizerPoints = points;
}
}
}
// Force focus on entry
function forceFieldFocus() {
passwordBox.forceActiveFocus();
}
Connections {
target: context
function onShouldReFocus() {
forceFieldFocus();
}
}
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onPressed: mouse => {
forceFieldFocus();
}
onPositionChanged: mouse => {
forceFieldFocus();
}
// Toolbar appearing animation
property real toolbarScale: 0.9
property real toolbarOpacity: 0
Behavior on toolbarScale {
NumberAnimation {
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
}
}
Behavior on toolbarOpacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
// Init
Component.onCompleted: {
forceFieldFocus();
toolbarScale = 1;
toolbarOpacity = 1;
}
// Key presses
property bool ctrlHeld: false
Keys.onPressed: event => {
root.context.resetClearTimer();
if (event.key === Qt.Key_Control) {
root.ctrlHeld = true;
}
if (event.key === Qt.Key_Escape) { // Esc to clear
root.context.currentText = "";
}
forceFieldFocus();
}
Keys.onReleased: event => {
if (event.key === Qt.Key_Control) {
root.ctrlHeld = false;
}
forceFieldFocus();
}
// RippleButton {
// anchors {
// top: parent.top
// left: parent.left
// leftMargin: 10
// topMargin: 10
// }
// implicitHeight: 40
// colBackground: Appearance.colors.colLayer2
// onClicked: {
// context.unlocked(LockContext.ActionEnum.Unlock);
// GlobalStates.screenLocked = false;
// }
// contentItem: StyledText {
// text: "[[ DEBUG BYPASS ]]"
// }
// }
Loader {
id: lockscreenMediaController
// Controlled manually via mediaLoaderActive so exit anim can finish
// before the item is destroyed.
active: root.mediaLoaderActive
anchors {
horizontalCenter: parent.horizontalCenter
bottom: mainIsland.top
bottomMargin: 15
}
// Driven imperatively no Behavior, so start values apply instantly.
property real mediaScale: 0.85
property real mediaOpacity: 0.0
scale: mediaScale * root.toolbarScale
opacity: mediaOpacity * root.toolbarOpacity
// Entry: fires after sourceComponent is fully instantiated.
onLoaded: {
mediaScale = 0.85
mediaOpacity = 0.0
entryAnim.restart()
}
ParallelAnimation {
id: entryAnim
NumberAnimation {
target: lockscreenMediaController
property: "mediaScale"
to: 1.0
duration: Appearance.animation.elementMove.duration * 1.2
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
NumberAnimation {
target: lockscreenMediaController
property: "mediaOpacity"
to: 1.0
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.OutCubic
}
}
// Exit: deactivates loader when animation finishes.
SequentialAnimation {
id: mediaExitAnim
ParallelAnimation {
NumberAnimation {
target: lockscreenMediaController
property: "mediaScale"
to: 0.85
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.InBack
easing.overshoot: 1.2
}
NumberAnimation {
target: lockscreenMediaController
property: "mediaOpacity"
to: 0.0
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.InCubic
}
}
// Only destroy the item after the animation has fully completed.
ScriptAction {
script: root.mediaLoaderActive = false
}
}
sourceComponent: PlayerControl {
player: MprisController.activePlayer
visualizerPoints: root.visualizerPoints
implicitWidth: Appearance.sizes.mediaControlsWidth
implicitHeight: Appearance.sizes.mediaControlsHeight
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
}
}
// Main toolbar: password box
Toolbar {
id: mainIsland
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 20
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
scale: root.toolbarScale
opacity: root.toolbarOpacity
// Fingerprint
Loader {
Layout.leftMargin: 10
Layout.rightMargin: 6
Layout.alignment: Qt.AlignVCenter
active: root.context.fingerprintsConfigured
visible: active
sourceComponent: MaterialSymbol {
id: fingerprintIcon
fill: 1
text: "fingerprint"
iconSize: Appearance.font.pixelSize.hugeass
color: Appearance.colors.colOnSurfaceVariant
}
}
ToolbarTextField {
id: passwordBox
Layout.rightMargin: -Layout.leftMargin
placeholderText: GlobalStates.screenUnlockFailed ? Translation.tr("Incorrect password") : Translation.tr("Enter password")
// Style
clip: true
font.pixelSize: Appearance.font.pixelSize.small
selectedTextColor: materialShapeChars ? "transparent" : Appearance.colors.colOnSecondaryContainer
selectionColor: materialShapeChars ? "transparent" : Appearance.colors.colSecondaryContainer
// Password
enabled: !root.context.unlockInProgress
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
// Synchronizing (across monitors) and unlocking
onTextChanged: root.context.currentText = this.text
onAccepted: {
root.context.tryUnlock(ctrlHeld);
}
Connections {
target: root.context
function onCurrentTextChanged() {
passwordBox.text = root.context.currentText;
}
}
Keys.onPressed: event => {
root.context.resetClearTimer();
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: passwordBox.width - 8
height: passwordBox.height
radius: height / 2
}
}
// Shake when wrong password
ErrorShakeAnimation {
id: wrongPasswordShakeAnim
target: passwordBox
}
Connections {
target: GlobalStates
function onScreenUnlockFailedChanged() {
if (GlobalStates.screenUnlockFailed) wrongPasswordShakeAnim.restart();
}
}
// We're drawing dots manually
property bool materialShapeChars: Config.options.lock.materialShapeChars
color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, materialShapeChars ? 1 : 0)
Loader {
active: passwordBox.materialShapeChars
anchors {
fill: parent
leftMargin: passwordBox.padding
rightMargin: passwordBox.padding
}
sourceComponent: PasswordChars {
length: root.context.currentText.length
selectionStart: passwordBox.selectionStart
selectionEnd: passwordBox.selectionEnd
cursorPosition: passwordBox.cursorPosition
}
}
}
ToolbarButton {
id: confirmButton
implicitWidth: height
toggled: true
enabled: !root.context.unlockInProgress
colBackgroundToggled: Appearance.colors.colPrimary
onClicked: root.context.tryUnlock()
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 24
text: {
if (root.context.targetAction === LockContext.ActionEnum.Unlock) {
return root.ctrlHeld ? "coffee" : "arrow_right_alt";
} else if (root.context.targetAction === LockContext.ActionEnum.Poweroff) {
return "power_settings_new";
} else if (root.context.targetAction === LockContext.ActionEnum.Reboot) {
return "restart_alt";
}
}
color: confirmButton.enabled ? Appearance.colors.colOnPrimary : Appearance.colors.colSubtext
}
}
}
// Left toolbar
Toolbar {
id: leftIsland
anchors {
right: mainIsland.left
top: mainIsland.top
bottom: mainIsland.bottom
rightMargin: 10
}
scale: root.toolbarScale
opacity: root.toolbarOpacity
// Username
IconAndTextPair {
Layout.leftMargin: 8
icon: "account_circle"
text: SystemInfo.username
}
// Keyboard layout (Xkb)
Loader {
Layout.rightMargin: 8
Layout.fillHeight: true
active: true
visible: active
sourceComponent: Row {
spacing: 8
MaterialSymbol {
id: keyboardIcon
anchors.verticalCenter: parent.verticalCenter
fill: 1
text: "keyboard_alt"
iconSize: Appearance.font.pixelSize.huge
color: Appearance.colors.colOnSurfaceVariant
}
Loader {
anchors.verticalCenter: parent.verticalCenter
sourceComponent: StyledText {
text: HyprlandXkb.currentLayoutCode
color: Appearance.colors.colOnSurfaceVariant
animateChange: true
}
}
}
}
// Keyboard layout (Fcitx)
Bar.SysTray {
Layout.rightMargin: 10
Layout.alignment: Qt.AlignVCenter
showSeparator: false
showOverflowMenu: false
pinnedItems: SystemTray.items.values.filter(i => i.id == "Fcitx")
visible: pinnedItems.length > 0
}
}
// Right toolbar
Toolbar {
id: rightIsland
anchors {
left: mainIsland.right
top: mainIsland.top
bottom: mainIsland.bottom
leftMargin: 10
}
scale: root.toolbarScale
opacity: root.toolbarOpacity
IconAndTextPair {
visible: Battery.available
icon: Battery.isCharging ? "bolt" : "battery_android_full"
text: Math.round(Battery.percentage * 100)
color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant
}
IconToolbarButton {
id: sleepButton
onClicked: Session.suspend()
text: "dark_mode"
}
PasswordGuardedIconToolbarButton {
id: powerButton
text: "power_settings_new"
targetAction: LockContext.ActionEnum.Poweroff
}
PasswordGuardedIconToolbarButton {
id: rebootButton
text: "restart_alt"
targetAction: LockContext.ActionEnum.Reboot
}
}
component PasswordGuardedIconToolbarButton: IconToolbarButton {
id: guardedBtn
required property var targetAction
toggled: root.context.targetAction === guardedBtn.targetAction
onClicked: {
if (!root.requirePasswordToPower) {
root.context.unlocked(guardedBtn.targetAction);
return;
}
if (root.context.targetAction === guardedBtn.targetAction) {
root.context.resetTargetAction();
} else {
root.context.targetAction = guardedBtn.targetAction;
root.context.shouldReFocus();
}
}
}
component IconAndTextPair: Row {
id: pair
required property string icon
required property string text
property color color: Appearance.colors.colOnSurfaceVariant
spacing: 4
Layout.fillHeight: true
Layout.leftMargin: 10
Layout.rightMargin: 10
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 1
text: pair.icon
iconSize: Appearance.font.pixelSize.huge
animateChange: true
color: pair.color
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: pair.text
color: pair.color
}
}
}

View file

@ -0,0 +1,128 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Quickshell
StyledFlickable {
id: root
required property int length
property int selectionStart
property int selectionEnd
property int cursorPosition
property color color: Appearance.colors.colPrimary
property color selectedTextColor: Appearance.colors.colOnSecondaryContainer
property color selectionColor: Appearance.colors.colSecondaryContainer
property int charSize: 20
contentWidth: dotsRow.implicitWidth
contentX: (Math.max(contentWidth - width, 0))
Behavior on contentX {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Rectangle {
id: cursor
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: root.charSize * root.cursorPosition
}
color: root.color
implicitWidth: 2
implicitHeight: root.charSize
Behavior on anchors.leftMargin {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(cursor)
}
}
Row {
id: dotsRow
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: 4 - 5 // -5 to account for spacing being simulated by char item width
}
spacing: 0
Repeater {
model: ScriptModel { // TODO: use proper custom object model to insert new char at the correct pos
values: Array(root.length)
}
delegate: Rectangle {
id: charItem
required property int index
implicitWidth: root.charSize
implicitHeight: root.charSize
property bool selected: index >= root.selectionStart && index < root.selectionEnd
color: ColorUtils.transparentize(root.selectionColor, selected ? 0 : 1)
MaterialShape {
id: materialShape
anchors.centerIn: parent
property list<var> charShapes: [
MaterialShape.Shape.Clover4Leaf,
MaterialShape.Shape.Arrow,
MaterialShape.Shape.Pill,
MaterialShape.Shape.SoftBurst,
MaterialShape.Shape.Diamond,
MaterialShape.Shape.ClamShell,
MaterialShape.Shape.Pentagon,
]
shape: charShapes[charItem.index % charShapes.length]
// Animate on appearance
color: charItem.selected ? root.selectedTextColor : root.color
implicitSize: 0
opacity: 0
scale: 0.5
Component.onCompleted: {
appearAnim.start();
}
ParallelAnimation {
id: appearAnim
NumberAnimation {
target: materialShape
properties: "opacity"
to: 1
duration: 50
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
NumberAnimation {
target: materialShape
properties: "scale"
to: 1
duration: 200
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
}
NumberAnimation {
target: materialShape
properties: "implicitSize"
to: 18
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
}
ColorAnimation {
target: materialShape
properties: "color"
from: Appearance.colors.colPrimary
to: Appearance.colors.colOnLayer1
duration: 1000
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
}
}
}
}
}