wow
This commit is contained in:
parent
1ab0263e27
commit
15091ac422
19 changed files with 2328 additions and 263 deletions
|
|
@ -21,6 +21,7 @@
|
|||
"${inputs.self}/modules/mizukios/features/steam.nix"
|
||||
"${inputs.self}/modules/mizukios/features/_1password.nix"
|
||||
"${inputs.self}/modules/mizukios/features/hyprland.nix"
|
||||
./sddm.nix
|
||||
];
|
||||
|
||||
catppuccin.flavor = lib.mkForce "mocha";
|
||||
|
|
@ -31,9 +32,21 @@
|
|||
|
||||
services.printing.enable = true;
|
||||
services.power-profiles-daemon.enable = true;
|
||||
services.upower.enable = true;
|
||||
services.asusd.enable = true;
|
||||
services.netbird.enable = true;
|
||||
|
||||
# Quickshell stores the AI API key through the Secret Service API. Hyprland
|
||||
# does not start GNOME Keyring by itself, so enable it explicitly for the
|
||||
# SDDM/PAM session as well as D-Bus activation.
|
||||
services.gnome.gnome-keyring.enable = true;
|
||||
|
||||
# Quickshell's on-screen keyboard sends virtual key events through ydotool.
|
||||
# Keep the daemon and uinput device available to the desktop session.
|
||||
programs.ydotool.enable = true;
|
||||
hardware.uinput.enable = true;
|
||||
users.users.imnyang.extraGroups = [ "ydotool" ];
|
||||
|
||||
systemd.services.asusctl-battery-limit = {
|
||||
description = "Set ASUS battery charge limit";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
|
@ -47,6 +60,10 @@
|
|||
};
|
||||
|
||||
services.cloudflare-warp.enable = true;
|
||||
boot.plymouth = {
|
||||
enable = true;
|
||||
theme = "spinner";
|
||||
};
|
||||
|
||||
# Wine needs both 64-bit and 32-bit userspace for typical Windows programs.
|
||||
environment.systemPackages = with pkgs; [
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ nixpkgs.lib.nixosSystem {
|
|||
{
|
||||
home-manager.useGlobalPkgs = true;
|
||||
home-manager.useUserPackages = true;
|
||||
home-manager.backupFileExtension = "backup";
|
||||
home-manager.backupFileExtension = "backup2";
|
||||
home-manager.sharedModules = [
|
||||
plasma-manager.homeModules.plasma-manager
|
||||
spicetify-nix.homeManagerModules.default
|
||||
|
|
@ -33,6 +33,7 @@ nixpkgs.lib.nixosSystem {
|
|||
];
|
||||
home-manager.extraSpecialArgs = { inherit inputs; };
|
||||
home-manager.users.imnyang = import ./home;
|
||||
catppuccin.flavor = nixpkgs.lib.mkForce "mocha";
|
||||
}
|
||||
];
|
||||
specialArgs = {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
303
hosts/machine/ena/home/config/quickshell/OpenAiApiStrategy.qml
Normal file
303
hosts/machine/ena/home/config/quickshell/OpenAiApiStrategy.qml
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
import QtQuick
|
||||
|
||||
ApiStrategy {
|
||||
// GPT-5.6 Luna uses the Responses API so reasoning and function tools can
|
||||
// be enabled together. Reasoning summaries are intentionally not rendered
|
||||
// in the chat bubble; only the final output text belongs in the transcript.
|
||||
property string functionName: ""
|
||||
property string functionCallId: ""
|
||||
property string functionItemId: ""
|
||||
property string functionArguments: ""
|
||||
property bool functionCallEmitted: false
|
||||
|
||||
function buildEndpoint(model: AiModel): string {
|
||||
return model.endpoint;
|
||||
}
|
||||
|
||||
function responseTool(tool) {
|
||||
const functionData = tool?.function ?? tool ?? {};
|
||||
let parameters = functionData.parameters;
|
||||
if (!parameters || Object.keys(parameters).length === 0) {
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
};
|
||||
}
|
||||
return {
|
||||
"type": "function",
|
||||
"name": functionData.name,
|
||||
"description": functionData.description ?? "",
|
||||
"parameters": parameters,
|
||||
"strict": false,
|
||||
};
|
||||
}
|
||||
|
||||
function previousFunctionCallId(messages, index, name) {
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const call = messages[i].functionCall;
|
||||
if (call && typeof call === "object" && (!name || call.name === name)) {
|
||||
return call.id ?? call.call_id ?? "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function buildInput(messages) {
|
||||
let input = [];
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i];
|
||||
const functionCall = message.functionCall;
|
||||
|
||||
// Responses requires the model's function_call item to be kept in
|
||||
// the input before its corresponding function_call_output item.
|
||||
if (message.role === "assistant" && functionCall && typeof functionCall === "object") {
|
||||
const callId = functionCall.id ?? functionCall.call_id ?? "";
|
||||
const name = message.functionName || functionCall.name || "";
|
||||
if (callId && name) {
|
||||
input.push({
|
||||
"type": "function_call",
|
||||
"id": functionCall.item_id ?? callId,
|
||||
"call_id": callId,
|
||||
"name": name,
|
||||
"arguments": JSON.stringify(functionCall.args ?? {}),
|
||||
"status": "completed",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.functionResponse !== undefined && message.functionName) {
|
||||
const callId = message.functionCall?.id
|
||||
?? message.functionCall?.call_id
|
||||
?? previousFunctionCallId(messages, i, message.functionName);
|
||||
if (callId) {
|
||||
input.push({
|
||||
"type": "function_call_output",
|
||||
"call_id": callId,
|
||||
"output": String(message.functionResponse),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.rawContent && message.rawContent.length > 0) {
|
||||
input.push({
|
||||
"role": message.role === "assistant" ? "assistant" : "user",
|
||||
"content": message.rawContent,
|
||||
});
|
||||
}
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>, filePath: string) {
|
||||
let baseData = {
|
||||
"model": model.model,
|
||||
"instructions": systemPrompt,
|
||||
"input": buildInput(messages),
|
||||
"stream": true,
|
||||
"tools": tools.map(responseTool),
|
||||
// Keep Luna's normal reasoning quality. The previous Chat
|
||||
// Completions workaround used reasoning_effort=none, which made
|
||||
// the model appear responsive but substantially worse at answers.
|
||||
"reasoning": {
|
||||
"effort": "medium",
|
||||
},
|
||||
// Luna currently rejects the temperature parameter even though
|
||||
// it is accepted by the general Responses schema.
|
||||
};
|
||||
return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
|
||||
}
|
||||
|
||||
function buildAuthorizationHeader(apiKeyEnvVarName: string): string {
|
||||
return `-H "Authorization: Bearer \$\{${apiKeyEnvVarName}\}"`;
|
||||
}
|
||||
|
||||
function appendText(text, message) {
|
||||
if (!text || text.length === 0) return;
|
||||
message.content += text;
|
||||
message.rawContent += text;
|
||||
}
|
||||
|
||||
function appendError(error, message) {
|
||||
const errorMsg = `**Error**: ${error?.message || JSON.stringify(error)}`;
|
||||
appendText(errorMsg, message);
|
||||
return { finished: true };
|
||||
}
|
||||
|
||||
function appendOutputItemText(item, message) {
|
||||
if (message.content.length > 0 || !item || !Array.isArray(item.content)) return;
|
||||
for (let i = 0; i < item.content.length; i++) {
|
||||
const content = item.content[i];
|
||||
if ((content?.type === "output_text" || content?.type === "text") && content.text) {
|
||||
appendText(content.text, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function appendResponseOutput(response, message) {
|
||||
if (message.content.length > 0 || !response || !Array.isArray(response.output)) return;
|
||||
for (let i = 0; i < response.output.length; i++) {
|
||||
const item = response.output[i];
|
||||
if (item?.type === "message") appendOutputItemText(item, message);
|
||||
}
|
||||
}
|
||||
|
||||
function parseUsage(response) {
|
||||
const usage = response?.usage;
|
||||
if (!usage) return undefined;
|
||||
return {
|
||||
input: usage.input_tokens ?? -1,
|
||||
output: usage.output_tokens ?? -1,
|
||||
total: usage.total_tokens ?? -1,
|
||||
};
|
||||
}
|
||||
|
||||
function parseFunctionArguments(message) {
|
||||
let args = {};
|
||||
try {
|
||||
args = JSON.parse(functionArguments || "{}");
|
||||
} catch (e) {
|
||||
appendText(`\n\n[[ Invalid function arguments: ${functionArguments} ]]\n`, message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = functionName;
|
||||
const callId = functionCallId || functionItemId;
|
||||
if (!name || !callId || functionCallEmitted) return null;
|
||||
|
||||
const call = {
|
||||
name: name,
|
||||
args: args,
|
||||
id: callId,
|
||||
call_id: callId,
|
||||
item_id: functionItemId,
|
||||
};
|
||||
message.functionName = name;
|
||||
message.functionCall = call;
|
||||
appendText(`\n\n[[ Function: ${name}(${JSON.stringify(args, null, 2)}) ]]\n`, message);
|
||||
functionCallEmitted = true;
|
||||
return { functionCall: call };
|
||||
}
|
||||
|
||||
function parseEventLine(line, message) {
|
||||
let cleanData = line.trim();
|
||||
|
||||
// Responses streams have both `event:` and `data:` SSE lines. The
|
||||
// event name is useful for humans but the JSON type is authoritative.
|
||||
if (cleanData.startsWith("event:")) return {};
|
||||
if (cleanData.startsWith("data:")) cleanData = cleanData.slice(5).trim();
|
||||
if (!cleanData || cleanData.startsWith(":")) return {};
|
||||
if (cleanData === "[DONE]") return { finished: true };
|
||||
|
||||
let dataJson;
|
||||
try {
|
||||
dataJson = JSON.parse(cleanData);
|
||||
} catch (e) {
|
||||
// A partial/non-JSON SSE line must not be rendered as assistant
|
||||
// text. SplitParser normally gives us complete data lines, but
|
||||
// ignoring malformed metadata is safer than corrupting the UI.
|
||||
return {};
|
||||
}
|
||||
|
||||
if (dataJson.error) return appendError(dataJson.error, message);
|
||||
if (dataJson.response?.error) return appendError(dataJson.response.error, message);
|
||||
|
||||
const eventType = dataJson.type || "";
|
||||
if (eventType === "response.output_item.added") {
|
||||
const item = dataJson.item;
|
||||
if (item?.type === "function_call") {
|
||||
functionItemId = item.id ?? "";
|
||||
functionCallId = item.call_id ?? "";
|
||||
functionName = item.name ?? "";
|
||||
functionArguments = item.arguments ?? "";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
if (eventType === "response.function_call_arguments.delta") {
|
||||
functionArguments += dataJson.delta ?? "";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (eventType === "response.function_call_arguments.done") {
|
||||
functionArguments = dataJson.arguments ?? functionArguments;
|
||||
return parseFunctionArguments(message) ?? {};
|
||||
}
|
||||
|
||||
if (eventType === "response.output_item.done") {
|
||||
const item = dataJson.item;
|
||||
appendOutputItemText(item, message);
|
||||
if (item?.type === "function_call") {
|
||||
functionItemId = item.id ?? functionItemId;
|
||||
functionCallId = item.call_id ?? functionCallId;
|
||||
functionName = item.name ?? functionName;
|
||||
functionArguments = item.arguments ?? functionArguments;
|
||||
return parseFunctionArguments(message) ?? {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
if (eventType === "response.output_text.delta" || eventType === "response.refusal.delta") {
|
||||
appendText(dataJson.delta, message);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (eventType === "response.output_text.done" || eventType === "response.refusal.done") {
|
||||
// The done event contains the complete text after delta events.
|
||||
// Only use it as a fallback when no delta was received.
|
||||
if (message.content.length === 0) appendText(dataJson.text ?? dataJson.output_text, message);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (eventType === "response.completed") {
|
||||
appendResponseOutput(dataJson.response, message);
|
||||
const tokenUsage = parseUsage(dataJson.response);
|
||||
return tokenUsage ? { tokenUsage: tokenUsage, finished: true } : { finished: true };
|
||||
}
|
||||
|
||||
if (eventType === "response.failed" || eventType === "response.incomplete") {
|
||||
const response = dataJson.response ?? {};
|
||||
return response.error
|
||||
? appendError(response.error, message)
|
||||
: { finished: true };
|
||||
}
|
||||
|
||||
// Some compatible gateways return a complete response object even
|
||||
// when streaming was requested. Handle only its actual text field;
|
||||
// never dump unknown response metadata into the chat bubble.
|
||||
if (dataJson.output_text) appendText(dataJson.output_text, message);
|
||||
appendResponseOutput(dataJson, message);
|
||||
if (dataJson.response?.output_text) appendText(dataJson.response.output_text, message);
|
||||
appendResponseOutput(dataJson.response, message);
|
||||
const tokenUsage = parseUsage(dataJson);
|
||||
return tokenUsage ? { tokenUsage: tokenUsage } : {};
|
||||
}
|
||||
|
||||
function parseResponseLine(line, message) {
|
||||
// SplitParser usually emits one line, but curl/SSE buffering can hand
|
||||
// us several records at once. Parse every physical line so a metadata
|
||||
// event cannot hide the following output_text event.
|
||||
const lines = String(line).split(/\r?\n/);
|
||||
let result = {};
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const parsed = parseEventLine(lines[i], message);
|
||||
if (parsed.functionCall) result.functionCall = parsed.functionCall;
|
||||
if (parsed.tokenUsage) result.tokenUsage = parsed.tokenUsage;
|
||||
if (parsed.finished) result.finished = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function onRequestFinished(message) {
|
||||
return {};
|
||||
}
|
||||
|
||||
function reset() {
|
||||
functionName = "";
|
||||
functionCallId = "";
|
||||
functionItemId = "";
|
||||
functionArguments = "";
|
||||
functionCallEmitted = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,49 @@
|
|||
home.sessionVariables = {
|
||||
EDITOR = "nano";
|
||||
VISUAL = "nano";
|
||||
GTK_THEME = "Adwaita:dark";
|
||||
};
|
||||
|
||||
dconf.settings = {
|
||||
"org/gnome/desktop/interface" = {
|
||||
color-scheme = "prefer-dark";
|
||||
gtk-theme = "Adwaita-dark";
|
||||
};
|
||||
};
|
||||
|
||||
# Quickshell's Kirigami icons follow the KDE icon theme setting. Keep the
|
||||
# Breeze theme available even though the rest of the session uses Adwaita.
|
||||
xdg.configFile."kdeglobals".text = ''
|
||||
[Icons]
|
||||
Theme=breeze-dark
|
||||
'';
|
||||
|
||||
programs.zed-editor.userSettings.theme.mode = lib.mkForce "dark";
|
||||
programs.ghostty.settings.theme = lib.mkForce "Mizuki Dark";
|
||||
programs.spicetify.colorScheme = lib.mkForce "mocha";
|
||||
|
||||
# Keep Hangul available in every application and make the Hangul key switch
|
||||
# between Korean and the US keyboard layout.
|
||||
xdg.configFile."fcitx5/profile" = {
|
||||
force = true;
|
||||
text = lib.generators.toINI { } {
|
||||
"Groups/0" = {
|
||||
Name = "Default";
|
||||
"Default Layout" = "us";
|
||||
DefaultIM = "hangul";
|
||||
};
|
||||
"Groups/0/Items/0" = {
|
||||
Name = "keyboard-us";
|
||||
Layout = "";
|
||||
};
|
||||
"Groups/0/Items/1" = {
|
||||
Name = "hangul";
|
||||
Layout = "";
|
||||
};
|
||||
GroupOrder = {
|
||||
"0" = "Default";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
xdg.configFile."fontconfig/conf.d/10-hm-fonts.conf".force = true;
|
||||
|
|
@ -31,6 +74,7 @@
|
|||
"${inputs.self}/modules/home/firefox-devedition.nix"
|
||||
"${inputs.self}/modules/home/discord/linux.nix"
|
||||
"${inputs.self}/modules/home/devtool/vscode.nix"
|
||||
"${inputs.self}/modules/home/devtool/vicinae.nix"
|
||||
"${inputs.self}/modules/home/ssh.nix"
|
||||
"${inputs.self}/modules/home/vicinae.nix"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ with pkgs;
|
|||
# inputs.waterfox.packages.${pkgs.system}.waterfox
|
||||
gnome-network-displays
|
||||
element-desktop
|
||||
# Provides secret-tool, used by Quickshell to load/save API keys.
|
||||
libsecret
|
||||
inputs.imnyang.packages.${stdenv.hostPlatform.system}.figma-linux
|
||||
inputs.hyprmod.packages.${pkgs.system}.default
|
||||
# hoffice
|
||||
]
|
||||
|
|
|
|||
489
hosts/machine/ena/sddm-theme/Main.qml
Normal file
489
hosts/machine/ena/sddm-theme/Main.qml
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Window 2.15
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
width: Screen.width
|
||||
height: Screen.height
|
||||
color: colors.background
|
||||
|
||||
QtObject {
|
||||
id: colors
|
||||
|
||||
// Keep these in sync with the Mizuki Dark palette used by hyprlock.
|
||||
readonly property color background: "#191017"
|
||||
readonly property color black: "#000000"
|
||||
readonly property color text: "#fafafa"
|
||||
readonly property color pink: "#f5c2e7"
|
||||
readonly property color secondary: "#bac2de"
|
||||
readonly property color error: "#f38ba8"
|
||||
readonly property color input: "#1affffff"
|
||||
readonly property color inputActive: "#2affffff"
|
||||
}
|
||||
|
||||
property int sessionIndex: Math.max(0, sessionModel.lastIndex)
|
||||
property bool sessionMenuOpen: false
|
||||
property bool loginInProgress: false
|
||||
property string errorText: ""
|
||||
property date now: new Date()
|
||||
// Keep the interactive layout in the same coordinate system as hyprlock.
|
||||
// Fixed logical margins prevent fractional scaling from pulling elements
|
||||
// apart on the greeter screen.
|
||||
property real edgeMargin: 32
|
||||
property real loginBottomMargin: 120
|
||||
|
||||
function sessionCount() {
|
||||
return sessionModel.rowCount();
|
||||
}
|
||||
|
||||
function sessionName(index) {
|
||||
if (index === undefined)
|
||||
index = sessionIndex;
|
||||
|
||||
var count = sessionCount();
|
||||
if (count === 0)
|
||||
return "No session";
|
||||
|
||||
index = Math.max(0, Math.min(index, count - 1));
|
||||
var label = sessionModel.data(sessionModel.index(index, 0), Qt.UserRole + 4);
|
||||
return label ? label.toString() : "Desktop";
|
||||
}
|
||||
|
||||
function selectSession(index) {
|
||||
sessionIndex = index;
|
||||
sessionMenuOpen = false;
|
||||
}
|
||||
|
||||
function login() {
|
||||
if (loginInProgress || username.text.trim().length === 0)
|
||||
return;
|
||||
|
||||
errorText = "";
|
||||
loginInProgress = true;
|
||||
sddm.login(username.text.trim(), password.text, sessionIndex);
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: root.now = new Date()
|
||||
}
|
||||
|
||||
// Hyprlock uses the current wallpaper with a dark, low-brightness treatment.
|
||||
// SDDM cannot read the logged-in user's current-wallpaper symlink, so it uses
|
||||
// the same default wallpaper shipped with this host.
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: Qt.resolvedUrl("wallpaper.png")
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: true
|
||||
smooth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: colors.black
|
||||
opacity: 0.40
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: colors.background
|
||||
opacity: 0.08
|
||||
}
|
||||
|
||||
// One clock container owns all four labels. Keeping the labels in one
|
||||
// column avoids independent anchor calculations drifting at fractional
|
||||
// display scales, while the whole cluster stays above the login form.
|
||||
Item {
|
||||
id: clockCluster
|
||||
width: 420
|
||||
height: 360
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -80
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: -18
|
||||
|
||||
Text {
|
||||
width: clockCluster.width
|
||||
text: Qt.formatTime(root.now, "HH")
|
||||
color: colors.text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: "Adwaita Sans"
|
||||
font.pixelSize: 112
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
Text {
|
||||
width: clockCluster.width
|
||||
text: Qt.formatTime(root.now, "mm")
|
||||
color: colors.text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: "Adwaita Sans"
|
||||
font.pixelSize: 112
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
Item { width: 1; height: 8 }
|
||||
|
||||
Text {
|
||||
width: clockCluster.width
|
||||
text: Qt.formatDate(root.now, "dddd")
|
||||
color: colors.pink
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: "JetBrainsMono NFM"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
width: clockCluster.width
|
||||
text: Qt.formatDate(root.now, "dd MMM")
|
||||
color: colors.pink
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: "JetBrainsMono NFM"
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User selection stays immediately above the password field, as requested.
|
||||
Column {
|
||||
id: loginArea
|
||||
width: 250
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.loginBottomMargin
|
||||
spacing: 10
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 60
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
text: "USER"
|
||||
color: colors.pink
|
||||
font.family: "JetBrainsMono NFM"
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: userField
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 42
|
||||
radius: 20
|
||||
color: username.activeFocus ? colors.inputActive : colors.input
|
||||
border.color: colors.pink
|
||||
border.width: username.activeFocus ? 3 : 2
|
||||
|
||||
TextInput {
|
||||
id: username
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
color: colors.text
|
||||
selectionColor: colors.pink
|
||||
selectedTextColor: colors.background
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
font.family: "Adwaita Sans"
|
||||
font.pixelSize: 16
|
||||
text: userModel.lastUser
|
||||
selectByMouse: true
|
||||
clip: true
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
password.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
onTextChanged: root.errorText = ""
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Username..."
|
||||
color: colors.secondary
|
||||
font.family: "Adwaita Sans"
|
||||
font.pixelSize: 16
|
||||
font.italic: true
|
||||
visible: username.text.length === 0 && !username.activeFocus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: passwordField
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: 22
|
||||
color: password.activeFocus ? colors.inputActive : colors.input
|
||||
border.color: colors.pink
|
||||
border.width: 3
|
||||
|
||||
TextInput {
|
||||
id: password
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
color: colors.pink
|
||||
selectionColor: colors.pink
|
||||
selectedTextColor: colors.background
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
font.family: "Adwaita Sans"
|
||||
font.pixelSize: 17
|
||||
echoMode: TextInput.Password
|
||||
passwordCharacter: "•"
|
||||
selectByMouse: true
|
||||
clip: true
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
root.login();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
onTextChanged: root.errorText = ""
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "Password..."
|
||||
color: colors.secondary
|
||||
font.family: "Adwaita Sans"
|
||||
font.pixelSize: 16
|
||||
font.italic: true
|
||||
visible: password.text.length === 0 && !password.activeFocus
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
height: visible ? implicitHeight : 0
|
||||
text: root.errorText
|
||||
color: colors.error
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: "Adwaita Sans"
|
||||
font.pixelSize: 12
|
||||
visible: root.errorText.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// A quiet, centered failure message does not change the lockscreen-like layout.
|
||||
Connections {
|
||||
target: sddm
|
||||
|
||||
function onLoginFailed() {
|
||||
root.loginInProgress = false;
|
||||
root.errorText = "That password did not work. Try again.";
|
||||
password.text = "";
|
||||
password.forceActiveFocus();
|
||||
}
|
||||
|
||||
function onLoginSucceeded() {
|
||||
root.loginInProgress = false;
|
||||
}
|
||||
|
||||
function onInformationMessage(message) {
|
||||
root.loginInProgress = false;
|
||||
root.errorText = message;
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom-left desktop/session selector.
|
||||
Item {
|
||||
id: sessionSelector
|
||||
z: 100
|
||||
width: 250
|
||||
height: 58
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.edgeMargin
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.edgeMargin
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
text: "SESSION"
|
||||
color: colors.pink
|
||||
font.family: "JetBrainsMono NFM"
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: sessionButton
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 38
|
||||
radius: 19
|
||||
color: sessionMouse.containsMouse ? "#32ffffff" : "#1affffff"
|
||||
border.color: colors.pink
|
||||
border.width: 2
|
||||
opacity: root.sessionMenuOpen ? 1 : 0.9
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 16
|
||||
anchors.right: arrow.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.sessionName()
|
||||
color: colors.text
|
||||
elide: Text.ElideRight
|
||||
font.family: "Adwaita Sans"
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
Text {
|
||||
id: arrow
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 15
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.sessionMenuOpen ? "⌃" : "⌄"
|
||||
color: colors.pink
|
||||
font.family: "Adwaita Sans"
|
||||
font.pixelSize: 18
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sessionMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: root.sessionCount() > 0
|
||||
onClicked: root.sessionMenuOpen = !root.sessionMenuOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sessionMenuDismiss
|
||||
anchors.fill: parent
|
||||
z: 110
|
||||
visible: root.sessionMenuOpen
|
||||
onClicked: root.sessionMenuOpen = false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: sessionMenu
|
||||
z: 111
|
||||
width: sessionSelector.width
|
||||
height: Math.min(Math.max(46, root.sessionCount() * 46), root.height * 0.38)
|
||||
anchors.left: sessionSelector.left
|
||||
// sessionButton is inside sessionSelector, so anchor to the sibling
|
||||
// container instead of crossing item hierarchies.
|
||||
anchors.bottom: sessionSelector.top
|
||||
anchors.bottomMargin: 8
|
||||
radius: 18
|
||||
color: "#f2191017"
|
||||
border.color: colors.pink
|
||||
border.width: 2
|
||||
visible: root.sessionMenuOpen && root.sessionCount() > 0
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: sessionList
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
clip: true
|
||||
model: root.sessionCount()
|
||||
currentIndex: root.sessionIndex
|
||||
|
||||
delegate: Rectangle {
|
||||
width: sessionList.width
|
||||
height: 42
|
||||
radius: 14
|
||||
color: index === root.sessionIndex ? "#32f5c2e7" : (sessionDelegateMouse.containsMouse ? "#22f5c2e7" : "transparent")
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 14
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.sessionName(index)
|
||||
color: colors.text
|
||||
elide: Text.ElideRight
|
||||
font.family: "Adwaita Sans"
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sessionDelegateMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: root.selectSession(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SDDM-only controls; the lock screen itself has no power controls.
|
||||
Row {
|
||||
z: 100
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.edgeMargin
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.edgeMargin + 6
|
||||
spacing: 22
|
||||
|
||||
Text {
|
||||
text: "POWER OFF"
|
||||
color: powerMouse.containsMouse ? colors.text : colors.secondary
|
||||
font.family: "JetBrainsMono NFM"
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.1
|
||||
|
||||
MouseArea {
|
||||
id: powerMouse
|
||||
anchors.fill: parent
|
||||
anchors.margins: -10
|
||||
hoverEnabled: true
|
||||
onClicked: sddm.powerOff()
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "REBOOT"
|
||||
color: rebootMouse.containsMouse ? colors.text : colors.secondary
|
||||
font.family: "JetBrainsMono NFM"
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.1
|
||||
|
||||
MouseArea {
|
||||
id: rebootMouse
|
||||
anchors.fill: parent
|
||||
anchors.margins: -10
|
||||
hoverEnabled: true
|
||||
onClicked: sddm.reboot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (username.text.length > 0)
|
||||
password.forceActiveFocus();
|
||||
else
|
||||
username.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
11
hosts/machine/ena/sddm-theme/metadata.desktop
Normal file
11
hosts/machine/ena/sddm-theme/metadata.desktop
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[SddmGreeterTheme]
|
||||
Name=ENA
|
||||
Description=Mizuki glass login theme for ena
|
||||
Author=imnyang
|
||||
License=MIT
|
||||
Type=sddm-theme
|
||||
Version=1.0
|
||||
Theme-Id=ena
|
||||
Theme-API=2.0
|
||||
QtVersion=6
|
||||
MainScript=Main.qml
|
||||
25
hosts/machine/ena/sddm.nix
Normal file
25
hosts/machine/ena/sddm.nix
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{ inputs, pkgs, ... }:
|
||||
let
|
||||
enaSddmTheme = pkgs.stdenvNoCC.mkDerivation {
|
||||
pname = "ena-sddm-theme";
|
||||
version = "1.0.0";
|
||||
|
||||
src = ./sddm-theme;
|
||||
dontBuild = true;
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
install -d $out/share/sddm/themes/ena
|
||||
install -Dm644 Main.qml metadata.desktop $out/share/sddm/themes/ena/
|
||||
install -Dm644 ${inputs.self}/assets/wallpaper/wallpaper.png $out/share/sddm/themes/ena/wallpaper.png
|
||||
install -Dm644 ${inputs.self}/assets/avatar.webp $out/share/sddm/themes/ena/avatar.webp
|
||||
runHook postInstall
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
services.displayManager.sddm = {
|
||||
enable = true;
|
||||
theme = "${enaSddmTheme}/share/sddm/themes/ena";
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue