This commit is contained in:
암냥 2026-09-14 12:09:52 +09:00
commit 15091ac422
No known key found for this signature in database
19 changed files with 2328 additions and 263 deletions

View file

@ -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; [

View file

@ -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

View 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;
}
}

View file

@ -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"

View file

@ -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
]

View 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();
}
}

View 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

View 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";
};
}