fix: PKGBUILD compositor-agnostisch, Beispiel-Configs bereinigt
Moongreet ist ein reiner GTK4-Greeter ohne eigenen Compositor. niri-greeter.kdl entfernt — der User konfiguriert seinen Compositor selbst (regreet → moongreet tauschen).
This commit is contained in:
parent
10b613b50b
commit
6400270a50
@ -44,7 +44,7 @@ package() {
|
||||
cd "$srcdir/greetd-moongreet"
|
||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||
|
||||
# Example config
|
||||
# Greeter config
|
||||
install -Dm644 config/moongreet.toml "$pkgdir/etc/moongreet/moongreet.toml"
|
||||
|
||||
# Cache directory
|
||||
|
||||
1
pkg/greetd-moongreet/HEAD
Normal file
1
pkg/greetd-moongreet/HEAD
Normal file
@ -0,0 +1 @@
|
||||
ref: refs/heads/main
|
||||
9
pkg/greetd-moongreet/config
Normal file
9
pkg/greetd-moongreet/config
Normal file
@ -0,0 +1,9 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
[remote "origin"]
|
||||
url = https://gitea.moonarch.de/nevaforget/greetd-moongreet.git
|
||||
tagOpt = --no-tags
|
||||
fetch = +refs/*:refs/*
|
||||
mirror = true
|
||||
1
pkg/greetd-moongreet/description
Normal file
1
pkg/greetd-moongreet/description
Normal file
@ -0,0 +1 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
15
pkg/greetd-moongreet/hooks/applypatch-msg.sample
Executable file
15
pkg/greetd-moongreet/hooks/applypatch-msg.sample
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
24
pkg/greetd-moongreet/hooks/commit-msg.sample
Executable file
24
pkg/greetd-moongreet/hooks/commit-msg.sample
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines.
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
exit 1
|
||||
}
|
||||
174
pkg/greetd-moongreet/hooks/fsmonitor-watchman.sample
Executable file
174
pkg/greetd-moongreet/hooks/fsmonitor-watchman.sample
Executable file
@ -0,0 +1,174 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $retry = 1;
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
$retry--;
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $output->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
$last_update_token = $o->{clock};
|
||||
|
||||
eval { launch_watchman() };
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
8
pkg/greetd-moongreet/hooks/post-update.sample
Executable file
8
pkg/greetd-moongreet/hooks/post-update.sample
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
14
pkg/greetd-moongreet/hooks/pre-applypatch.sample
Executable file
14
pkg/greetd-moongreet/hooks/pre-applypatch.sample
Executable file
@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
49
pkg/greetd-moongreet/hooks/pre-commit.sample
Executable file
49
pkg/greetd-moongreet/hooks/pre-commit.sample
Executable file
@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
13
pkg/greetd-moongreet/hooks/pre-merge-commit.sample
Executable file
13
pkg/greetd-moongreet/hooks/pre-merge-commit.sample
Executable file
@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
53
pkg/greetd-moongreet/hooks/pre-push.sample
Executable file
53
pkg/greetd-moongreet/hooks/pre-push.sample
Executable file
@ -0,0 +1,53 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
169
pkg/greetd-moongreet/hooks/pre-rebase.sample
Executable file
169
pkg/greetd-moongreet/hooks/pre-rebase.sample
Executable file
@ -0,0 +1,169 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
24
pkg/greetd-moongreet/hooks/pre-receive.sample
Executable file
24
pkg/greetd-moongreet/hooks/pre-receive.sample
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
42
pkg/greetd-moongreet/hooks/prepare-commit-msg.sample
Executable file
42
pkg/greetd-moongreet/hooks/prepare-commit-msg.sample
Executable file
@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
78
pkg/greetd-moongreet/hooks/push-to-checkout.sample
Executable file
78
pkg/greetd-moongreet/hooks/push-to-checkout.sample
Executable file
@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
77
pkg/greetd-moongreet/hooks/sendemail-validate.sample
Executable file
77
pkg/greetd-moongreet/hooks/sendemail-validate.sample
Executable file
@ -0,0 +1,77 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
128
pkg/greetd-moongreet/hooks/update.sample
Executable file
128
pkg/greetd-moongreet/hooks/update.sample
Executable file
@ -0,0 +1,128 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
1
pkg/greetd-moongreet/info/attributes
Normal file
1
pkg/greetd-moongreet/info/attributes
Normal file
@ -0,0 +1 @@
|
||||
* -export-subst -export-ignore
|
||||
6
pkg/greetd-moongreet/info/exclude
Normal file
6
pkg/greetd-moongreet/info/exclude
Normal file
@ -0,0 +1,6 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
3
pkg/greetd-moongreet/packed-refs
Normal file
3
pkg/greetd-moongreet/packed-refs
Normal file
@ -0,0 +1,3 @@
|
||||
# pack-refs with: peeled fully-peeled sorted
|
||||
99c016adbc27e6455463b47fc6e6b3d2eb157def refs/heads/main
|
||||
99c016adbc27e6455463b47fc6e6b3d2eb157def refs/tags/v0.1.0
|
||||
BIN
pkg/moongreet-0.1.0-1-any.pkg.tar.zst
Normal file
BIN
pkg/moongreet-0.1.0-1-any.pkg.tar.zst
Normal file
Binary file not shown.
@ -1,16 +1,14 @@
|
||||
# ABOUTME: pacman install hooks for Moongreet.
|
||||
# ABOUTME: Sets ownership on cache directory for the greeter user.
|
||||
# ABOUTME: Sets ownership on cache directory and prints setup instructions.
|
||||
|
||||
post_install() {
|
||||
if getent passwd greeter > /dev/null 2>&1; then
|
||||
chown greeter:greeter /var/cache/moongreet
|
||||
fi
|
||||
|
||||
echo "==> Copy /etc/moongreet/moongreet.toml and adjust the wallpaper path."
|
||||
echo "==> Configure greetd to use moongreet:"
|
||||
echo " [default_session]"
|
||||
echo " command = \"moongreet\""
|
||||
echo " user = \"greeter\""
|
||||
echo "==> Moongreet installed."
|
||||
echo "==> Add moongreet to your greeter compositor command in /etc/greetd/config.toml."
|
||||
echo "==> Adjust wallpaper: /etc/moongreet/moongreet.toml"
|
||||
}
|
||||
|
||||
post_upgrade() {
|
||||
|
||||
1122
pkg/pkg/moongreet/.BUILDINFO
Normal file
1122
pkg/pkg/moongreet/.BUILDINFO
Normal file
File diff suppressed because it is too large
Load Diff
18
pkg/pkg/moongreet/.INSTALL
Normal file
18
pkg/pkg/moongreet/.INSTALL
Normal file
@ -0,0 +1,18 @@
|
||||
# ABOUTME: pacman install hooks for Moongreet.
|
||||
# ABOUTME: Sets ownership on cache directory for the greeter user.
|
||||
|
||||
post_install() {
|
||||
if getent passwd greeter > /dev/null 2>&1; then
|
||||
chown greeter:greeter /var/cache/moongreet
|
||||
fi
|
||||
|
||||
echo "==> Copy /etc/moongreet/moongreet.toml and adjust the wallpaper path."
|
||||
echo "==> Configure greetd to use moongreet:"
|
||||
echo " [default_session]"
|
||||
echo " command = \"moongreet\""
|
||||
echo " user = \"greeter\""
|
||||
}
|
||||
|
||||
post_upgrade() {
|
||||
post_install
|
||||
}
|
||||
BIN
pkg/pkg/moongreet/.MTREE
Normal file
BIN
pkg/pkg/moongreet/.MTREE
Normal file
Binary file not shown.
22
pkg/pkg/moongreet/.PKGINFO
Normal file
22
pkg/pkg/moongreet/.PKGINFO
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by makepkg 7.1.0
|
||||
# using fakeroot version 1.37.2
|
||||
pkgname = moongreet
|
||||
pkgbase = moongreet
|
||||
xdata = pkgtype=pkg
|
||||
pkgver = 0.1.0-1
|
||||
pkgdesc = A greetd greeter for Wayland, built with Python + GTK4 + gtk4-layer-shell
|
||||
url = https://gitea.moonarch.de/nevaforget/greetd-moongreet
|
||||
builddate = 1774528075
|
||||
packager = Unknown Packager
|
||||
size = 567020
|
||||
arch = any
|
||||
license = MIT
|
||||
depend = python
|
||||
depend = python-gobject
|
||||
depend = gtk4
|
||||
depend = gtk4-layer-shell
|
||||
depend = greetd
|
||||
makedepend = git
|
||||
makedepend = python-build
|
||||
makedepend = python-installer
|
||||
makedepend = python-hatchling
|
||||
6
pkg/pkg/moongreet/etc/moongreet/moongreet.toml
Normal file
6
pkg/pkg/moongreet/etc/moongreet/moongreet.toml
Normal file
@ -0,0 +1,6 @@
|
||||
# ABOUTME: Example configuration for the Moongreet greeter.
|
||||
# ABOUTME: Copy to /etc/moongreet/moongreet.toml and adjust paths.
|
||||
|
||||
[appearance]
|
||||
# Absolute path to wallpaper image
|
||||
background = "/usr/share/backgrounds/wallpaper.jpg"
|
||||
8
pkg/pkg/moongreet/usr/bin/moongreet
Executable file
8
pkg/pkg/moongreet/usr/bin/moongreet
Executable file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from moongreet.main import main
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||
sys.exit(main())
|
||||
@ -0,0 +1,7 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: moongreet
|
||||
Version: 0.1.0
|
||||
Summary: A greetd greeter for Wayland with GTK4
|
||||
License-Expression: MIT
|
||||
Requires-Python: >=3.11
|
||||
Requires-Dist: pygobject>=3.46
|
||||
@ -0,0 +1,18 @@
|
||||
../../../bin/moongreet,sha256=fhnGyAMSfkZpGxKQbKkSeaRAge-a8Fggfv3Pu0coQFs,212
|
||||
moongreet/__init__.py,sha256=QXh8GSCKrfiZ7_H2eiWGhfpBpznWUo4pFoIBqcTh07U,116
|
||||
moongreet/config.py,sha256=_WUlBma-KPQUpxeepvQbpR3e2lTIuVBGVcyc_qTi4mA,2100
|
||||
moongreet/greeter.py,sha256=Zist1Utwd3SzMDynbWQZG2l36E5C7h-anxQ1TXl6Hu4,22735
|
||||
moongreet/i18n.py,sha256=RNGu2Yo5RxM8Cok1HZMPabY5X3t_Ga4M78MRTJHvs1c,3972
|
||||
moongreet/ipc.py,sha256=55Np1qyY_T4ceRBr2nhGa4Dwed3__MBHzM16at4c2ss,2111
|
||||
moongreet/main.py,sha256=WazQV9yncPMHTkfVqDQUYR6_KtPVqpf-h7HudmeWzaA,4648
|
||||
moongreet/power.py,sha256=RYVvJrkGk0KFuWdE64xRch3oX_p2-Q78ieS-K5ZMMmg,468
|
||||
moongreet/sessions.py,sha256=7abDbPeSEXZqhbphl_0AvY-4P8MqO_SlNl6pRVT-JFs,1947
|
||||
moongreet/style.css,sha256=qpGjJSvTxy16SlYBQAKpFkmX1xeot0lHWNlim0nOpSs,1673
|
||||
moongreet/users.py,sha256=jso4iswMJuxxlqMruga2NJMZbnEbQ_OzMPs78h8i5yY,3111
|
||||
moongreet/data/default-avatar.svg,sha256=FaRdOK0qf6iR37j7uyAS6sh6cLelmwK6S9apIjC96yQ,2684
|
||||
moongreet/data/wallpaper.jpg,sha256=wZEatvO-9VCSOgZ_H9bsre6m3vp5a63RljUK6ujXuKs,374226
|
||||
moongreet/data/icons/hicolor/scalable/apps/moongreet-default-avatar-symbolic.svg,sha256=Zx9vJBV4JvoIY8MTzw3Cbx_Wy2d-qLFdfHFWnYwqqGc,2679
|
||||
moongreet-0.1.0.dist-info/METADATA,sha256=5aE4QV5Xf60TwCt8HGpQGCXQvbPpLlXoUs3uBRk4m9A,180
|
||||
moongreet-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
||||
moongreet-0.1.0.dist-info/entry_points.txt,sha256=_QpPvWlESl0gCjntxS5V8JmsKWEz8AtkneySdVW2r08,50
|
||||
moongreet-0.1.0.dist-info/RECORD,,
|
||||
@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: hatchling 1.29.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
moongreet = moongreet.main:main
|
||||
@ -0,0 +1,2 @@
|
||||
# ABOUTME: Moongreet package — a greetd greeter for Wayland with GTK4.
|
||||
# ABOUTME: Part of the Moonarch ecosystem.
|
||||
@ -0,0 +1,75 @@
|
||||
# ABOUTME: Configuration loading from moongreet.toml.
|
||||
# ABOUTME: Parses appearance and behavior settings with wallpaper path resolution.
|
||||
|
||||
import tomllib
|
||||
from contextlib import AbstractContextManager
|
||||
from dataclasses import dataclass
|
||||
from importlib.resources import as_file, files
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_CONFIG_PATHS = [
|
||||
Path("/etc/moongreet/moongreet.toml"),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Greeter configuration loaded from moongreet.toml."""
|
||||
|
||||
background: Path | None = None
|
||||
|
||||
|
||||
def load_config(config_path: Path | None = None) -> Config:
|
||||
"""Load configuration from a TOML file.
|
||||
|
||||
Relative paths in the config are resolved against the config file's directory.
|
||||
"""
|
||||
if config_path is None:
|
||||
for path in DEFAULT_CONFIG_PATHS:
|
||||
if path.exists():
|
||||
config_path = path
|
||||
break
|
||||
if config_path is None:
|
||||
return Config()
|
||||
|
||||
if not config_path.exists():
|
||||
return Config()
|
||||
|
||||
try:
|
||||
with open(config_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
except (tomllib.TOMLDecodeError, OSError):
|
||||
return Config()
|
||||
|
||||
config = Config()
|
||||
appearance = data.get("appearance", {})
|
||||
|
||||
bg = appearance.get("background")
|
||||
if bg:
|
||||
bg_path = Path(bg)
|
||||
if not bg_path.is_absolute():
|
||||
bg_path = config_path.parent / bg_path
|
||||
config.background = bg_path
|
||||
|
||||
return config
|
||||
|
||||
|
||||
_PACKAGE_DATA = files("moongreet") / "data"
|
||||
_DEFAULT_WALLPAPER_PATH = _PACKAGE_DATA / "wallpaper.jpg"
|
||||
|
||||
|
||||
def resolve_wallpaper_path(
|
||||
config: Config,
|
||||
) -> tuple[Path, AbstractContextManager | None]:
|
||||
"""Resolve the wallpaper path from config or fall back to the package default.
|
||||
|
||||
Returns (path, context_manager). The context_manager is non-None when a
|
||||
package resource was extracted to a temporary file — the caller must keep
|
||||
it alive and call __exit__ when done.
|
||||
"""
|
||||
if config.background and config.background.exists():
|
||||
return config.background, None
|
||||
|
||||
ctx = as_file(_DEFAULT_WALLPAPER_PATH)
|
||||
path = ctx.__enter__()
|
||||
return path, ctx
|
||||
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 512 512"><path fill="#PLACEHOLDER" d="M256 23c-16.076 0-32.375 3.73-48.178 10.24c-2.126 6.525-3.877 14.76-4.877 23.754c-1.31 11.79-1.73 24.706-1.87 36.819c33.864-3.704 75.986-3.704 109.85 0c-.14-12.113-.56-25.03-1.87-36.82c-1-8.992-2.75-17.228-4.877-23.753C288.375 26.73 272.076 23 256 23m100.564 19.332c9.315 7.054 18.107 14.878 26.282 23.234c1.53-6.65 4.69-12.696 9.03-17.695zm-170.03 1.49c-34.675 20.22-65.047 52.714-82.552 86.334c-33.08 63.536-39.69 156.956-.53 214.8C132.786 388.278 200.276 405 256 405s123.215-16.72 152.547-60.045c39.162-57.843 32.55-151.263-.53-214.8c-17.504-33.62-47.876-66.112-82.55-86.333c.578 3.65 1.057 7.388 1.478 11.184c1.522 13.694 1.912 28.197 2.014 41.267C347.664 99.427 362 104 368 110c32 32 75.537 134.695 16 224c-37.654 56.48-218.346 56.48-256 0c-59.537-89.305-16-192 16-224c6-6 20.335-10.573 39.04-13.727c.103-13.07.493-27.573 2.015-41.267c.42-3.796.9-7.534 1.478-11.184zM64 48c-8.837 0-16 7.163-16 16a16 16 0 0 0 7 13.227V145.5L73 132V77.21A16 16 0 0 0 80 64c0-8.837-7.163-16-16-16m358.81 3.68c-12.81 0-23 10.19-23 23s10.19 23 23 23s23-10.19 23-23s-10.19-23-23-23m25.272 55.205c-6.98 5.497-15.758 8.795-25.27 8.795c-.745 0-1.48-.027-2.214-.067a217 217 0 0 1 2.38 4.37l29.852 22.39zm-238.822 2.5c-17.257.09-37.256 3.757-53.233 16.12c-26.634 20.608-43.034 114.763-33.49 146.763c16.584-61.767 31.993-124.02 107.92-161.274a133.5 133.5 0 0 0-21.197-1.61zm-135.055 44.21L40.15 179.138l-14.48 72.408l38.18 45.814c-10.947-46.523-5.776-98.723 10.355-143.764zm363.59 0c16.13 45.042 21.302 97.242 10.355 143.764l38.18-45.815l-14.48-72.408zM106.645 375.93c-3.583 1.17-7.252 3.406-10.282 6.435c-4.136 4.136-6.68 9.43-7.164 14.104c.21.364.603 1.157 1.73 2.162c2.453 2.188 6.693 5.17 12.127 8.358c10.867 6.38 26.55 13.757 44.205 20.623c21.177 8.237 45.35 15.704 67.738 20.38v-27.61c-39.47-5.12-79.897-18.325-108.355-44.452zm298.71 0C376.897 402.055 336.47 415.26 297 420.38v27.61c22.387-4.676 46.56-12.143 67.738-20.38c17.655-6.865 33.338-14.243 44.205-20.622c5.434-3.19 9.674-6.17 12.127-8.36c1.127-1.004 1.52-1.797 1.73-2.16c-.482-4.675-3.027-9.97-7.163-14.105c-3.03-3.03-6.7-5.264-10.282-6.435zM77.322 410.602L18 450.15V494h37v-18h18v18h366v-18h18v18h37v-43.85l-59.322-39.548c-.537.488-1.08.97-1.623 1.457c-3.922 3.497-8.932 6.89-14.998 10.452c-12.133 7.12-28.45 14.743-46.795 21.877C334.572 458.656 290.25 471 256 471s-78.572-12.343-115.262-26.61c-18.345-7.135-34.662-14.757-46.795-21.878c-6.066-3.56-11.076-6.955-14.998-10.453c-.543-.487-1.086-.97-1.623-1.458zM233 422.184v28.992c8.236 1.162 16.012 1.824 23 1.824s14.764-.662 23-1.824v-28.992a325 325 0 0 1-46 0"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 512 512"><path fill="#222222" d="M256 23c-16.076 0-32.375 3.73-48.178 10.24c-2.126 6.525-3.877 14.76-4.877 23.754c-1.31 11.79-1.73 24.706-1.87 36.819c33.864-3.704 75.986-3.704 109.85 0c-.14-12.113-.56-25.03-1.87-36.82c-1-8.992-2.75-17.228-4.877-23.753C288.375 26.73 272.076 23 256 23m100.564 19.332c9.315 7.054 18.107 14.878 26.282 23.234c1.53-6.65 4.69-12.696 9.03-17.695zm-170.03 1.49c-34.675 20.22-65.047 52.714-82.552 86.334c-33.08 63.536-39.69 156.956-.53 214.8C132.786 388.278 200.276 405 256 405s123.215-16.72 152.547-60.045c39.162-57.843 32.55-151.263-.53-214.8c-17.504-33.62-47.876-66.112-82.55-86.333c.578 3.65 1.057 7.388 1.478 11.184c1.522 13.694 1.912 28.197 2.014 41.267C347.664 99.427 362 104 368 110c32 32 75.537 134.695 16 224c-37.654 56.48-218.346 56.48-256 0c-59.537-89.305-16-192 16-224c6-6 20.335-10.573 39.04-13.727c.103-13.07.493-27.573 2.015-41.267c.42-3.796.9-7.534 1.478-11.184zM64 48c-8.837 0-16 7.163-16 16a16 16 0 0 0 7 13.227V145.5L73 132V77.21A16 16 0 0 0 80 64c0-8.837-7.163-16-16-16m358.81 3.68c-12.81 0-23 10.19-23 23s10.19 23 23 23s23-10.19 23-23s-10.19-23-23-23m25.272 55.205c-6.98 5.497-15.758 8.795-25.27 8.795c-.745 0-1.48-.027-2.214-.067a217 217 0 0 1 2.38 4.37l29.852 22.39zm-238.822 2.5c-17.257.09-37.256 3.757-53.233 16.12c-26.634 20.608-43.034 114.763-33.49 146.763c16.584-61.767 31.993-124.02 107.92-161.274a133.5 133.5 0 0 0-21.197-1.61zm-135.055 44.21L40.15 179.138l-14.48 72.408l38.18 45.814c-10.947-46.523-5.776-98.723 10.355-143.764zm363.59 0c16.13 45.042 21.302 97.242 10.355 143.764l38.18-45.815l-14.48-72.408zM106.645 375.93c-3.583 1.17-7.252 3.406-10.282 6.435c-4.136 4.136-6.68 9.43-7.164 14.104c.21.364.603 1.157 1.73 2.162c2.453 2.188 6.693 5.17 12.127 8.358c10.867 6.38 26.55 13.757 44.205 20.623c21.177 8.237 45.35 15.704 67.738 20.38v-27.61c-39.47-5.12-79.897-18.325-108.355-44.452zm298.71 0C376.897 402.055 336.47 415.26 297 420.38v27.61c22.387-4.676 46.56-12.143 67.738-20.38c17.655-6.865 33.338-14.243 44.205-20.622c5.434-3.19 9.674-6.17 12.127-8.36c1.127-1.004 1.52-1.797 1.73-2.16c-.482-4.675-3.027-9.97-7.163-14.105c-3.03-3.03-6.7-5.264-10.282-6.435zM77.322 410.602L18 450.15V494h37v-18h18v18h366v-18h18v18h37v-43.85l-59.322-39.548c-.537.488-1.08.97-1.623 1.457c-3.922 3.497-8.932 6.89-14.998 10.452c-12.133 7.12-28.45 14.743-46.795 21.877C334.572 458.656 290.25 471 256 471s-78.572-12.343-115.262-26.61c-18.345-7.135-34.662-14.757-46.795-21.878c-6.066-3.56-11.076-6.955-14.998-10.453c-.543-.487-1.086-.97-1.623-1.458zM233 422.184v28.992c8.236 1.162 16.012 1.824 23 1.824s14.764-.662 23-1.824v-28.992a325 325 0 0 1-46 0"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 366 KiB |
@ -0,0 +1,587 @@
|
||||
# ABOUTME: Main greeter window — builds the GTK4 UI layout for the Moongreet greeter.
|
||||
# ABOUTME: Handles user selection, session choice, password entry, and power actions.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import socket
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
|
||||
import gi
|
||||
gi.require_version("Gtk", "4.0")
|
||||
gi.require_version("Gdk", "4.0")
|
||||
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
|
||||
|
||||
from moongreet.config import load_config, resolve_wallpaper_path
|
||||
from moongreet.i18n import load_strings, Strings
|
||||
from moongreet.ipc import create_session, post_auth_response, start_session, cancel_session
|
||||
from moongreet.users import User, get_users, get_avatar_path, get_user_gtk_theme
|
||||
from moongreet.sessions import Session, get_sessions
|
||||
from moongreet.power import reboot, shutdown
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LAST_USER_PATH = Path("/var/cache/moongreet/last-user")
|
||||
FAILLOCK_MAX_ATTEMPTS = 3
|
||||
VALID_USERNAME = re.compile(r"^[a-zA-Z0-9_.-]+$")
|
||||
MAX_USERNAME_LENGTH = 256
|
||||
PACKAGE_DATA = files("moongreet") / "data"
|
||||
DEFAULT_AVATAR_PATH = PACKAGE_DATA / "default-avatar.svg"
|
||||
AVATAR_SIZE = 128
|
||||
|
||||
|
||||
def faillock_warning(attempt_count: int, strings: Strings | None = None) -> str | None:
|
||||
"""Return a warning if the user is approaching or has reached the faillock limit."""
|
||||
if strings is None:
|
||||
strings = load_strings()
|
||||
remaining = FAILLOCK_MAX_ATTEMPTS - attempt_count
|
||||
if remaining <= 0:
|
||||
return strings.faillock_locked
|
||||
if remaining == 1:
|
||||
return strings.faillock_attempts_remaining.format(n=remaining)
|
||||
return None
|
||||
|
||||
|
||||
def _build_wallpaper_widget(bg_path: Path | None) -> Gtk.Widget:
|
||||
"""Create a wallpaper widget that fills the available space."""
|
||||
if bg_path and bg_path.exists():
|
||||
background = Gtk.Picture()
|
||||
background.set_filename(str(bg_path))
|
||||
background.set_content_fit(Gtk.ContentFit.COVER)
|
||||
background.set_hexpand(True)
|
||||
background.set_vexpand(True)
|
||||
return background
|
||||
background = Gtk.Box()
|
||||
background.set_hexpand(True)
|
||||
background.set_vexpand(True)
|
||||
return background
|
||||
|
||||
|
||||
class WallpaperWindow(Gtk.ApplicationWindow):
|
||||
"""A window that shows only the wallpaper — used for secondary monitors."""
|
||||
|
||||
def __init__(self, bg_path: Path | None = None, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.add_css_class("greeter")
|
||||
self.set_default_size(1920, 1080)
|
||||
self.set_child(_build_wallpaper_widget(bg_path))
|
||||
|
||||
|
||||
class GreeterWindow(Gtk.ApplicationWindow):
|
||||
"""The main greeter window with login UI."""
|
||||
|
||||
def __init__(self, bg_path: Path | None = None, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.add_css_class("greeter")
|
||||
self.set_default_size(1920, 1080)
|
||||
|
||||
self._config = load_config()
|
||||
self._strings = load_strings()
|
||||
self._users = get_users()
|
||||
self._sessions = get_sessions()
|
||||
self._selected_user: User | None = None
|
||||
self._greetd_sock: socket.socket | None = None
|
||||
self._greetd_sock_lock = threading.Lock()
|
||||
self._default_avatar_pixbuf: GdkPixbuf.Pixbuf | None = None
|
||||
self._avatar_cache: dict[str, GdkPixbuf.Pixbuf] = {}
|
||||
self._failed_attempts: dict[str, int] = {}
|
||||
self._bg_path = bg_path
|
||||
|
||||
self._build_ui()
|
||||
self._setup_keyboard_navigation()
|
||||
# Defer initial user selection until the window is realized,
|
||||
# so get_color() returns the actual theme foreground for SVG tinting
|
||||
self.connect("realize", self._on_realize)
|
||||
|
||||
def _on_realize(self, widget: Gtk.Widget) -> None:
|
||||
"""Called when the window is realized — select initial user.
|
||||
|
||||
Deferred from __init__ so get_color() returns actual theme values
|
||||
for SVG tinting. Uses idle_add so the first frame renders before
|
||||
avatar loading blocks the main loop.
|
||||
"""
|
||||
GLib.idle_add(self._select_initial_user)
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
"""Build the complete greeter UI layout."""
|
||||
# Root overlay for layering
|
||||
overlay = Gtk.Overlay()
|
||||
self.set_child(overlay)
|
||||
|
||||
# Background wallpaper
|
||||
overlay.set_child(_build_wallpaper_widget(self._bg_path))
|
||||
|
||||
# Main layout: 3 rows (top spacer, center login, bottom bar)
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
main_box.set_hexpand(True)
|
||||
main_box.set_vexpand(True)
|
||||
overlay.add_overlay(main_box)
|
||||
|
||||
# Top spacer
|
||||
top_spacer = Gtk.Box()
|
||||
top_spacer.set_vexpand(True)
|
||||
main_box.append(top_spacer)
|
||||
|
||||
# Center: login box
|
||||
center_box = self._build_login_box()
|
||||
center_box.set_halign(Gtk.Align.CENTER)
|
||||
main_box.append(center_box)
|
||||
|
||||
# Bottom spacer
|
||||
bottom_spacer = Gtk.Box()
|
||||
bottom_spacer.set_vexpand(True)
|
||||
main_box.append(bottom_spacer)
|
||||
|
||||
# Bottom bar overlay (user list left, power buttons right)
|
||||
bottom_bar = self._build_bottom_bar()
|
||||
bottom_bar.set_valign(Gtk.Align.END)
|
||||
overlay.add_overlay(bottom_bar)
|
||||
|
||||
def _build_login_box(self) -> Gtk.Box:
|
||||
"""Build the central login area with avatar, name, session, password."""
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
box.add_css_class("login-box")
|
||||
box.set_halign(Gtk.Align.CENTER)
|
||||
box.set_valign(Gtk.Align.CENTER)
|
||||
box.set_spacing(12)
|
||||
|
||||
# Avatar — wrapped in a clipping frame for round shape
|
||||
avatar_frame = Gtk.Box()
|
||||
avatar_frame.set_size_request(AVATAR_SIZE, AVATAR_SIZE)
|
||||
avatar_frame.set_halign(Gtk.Align.CENTER)
|
||||
avatar_frame.set_overflow(Gtk.Overflow.HIDDEN)
|
||||
avatar_frame.add_css_class("avatar")
|
||||
self._avatar_image = Gtk.Image()
|
||||
self._avatar_image.set_pixel_size(AVATAR_SIZE)
|
||||
avatar_frame.append(self._avatar_image)
|
||||
box.append(avatar_frame)
|
||||
|
||||
# Username label
|
||||
self._username_label = Gtk.Label(label="")
|
||||
self._username_label.add_css_class("username-label")
|
||||
box.append(self._username_label)
|
||||
|
||||
# Session dropdown
|
||||
self._session_dropdown = Gtk.DropDown()
|
||||
self._session_dropdown.add_css_class("session-dropdown")
|
||||
self._session_dropdown.set_hexpand(True)
|
||||
if self._sessions:
|
||||
session_names = [s.name for s in self._sessions]
|
||||
string_list = Gtk.StringList.new(session_names)
|
||||
self._session_dropdown.set_model(string_list)
|
||||
box.append(self._session_dropdown)
|
||||
|
||||
# Password entry
|
||||
self._password_entry = Gtk.PasswordEntry()
|
||||
self._password_entry.set_hexpand(True)
|
||||
self._password_entry.set_property("placeholder-text", self._strings.password_placeholder)
|
||||
self._password_entry.set_property("show-peek-icon", True)
|
||||
self._password_entry.add_css_class("password-entry")
|
||||
self._password_entry.connect("activate", self._on_login_activate)
|
||||
box.append(self._password_entry)
|
||||
|
||||
# Error label (hidden by default)
|
||||
self._error_label = Gtk.Label(label="")
|
||||
self._error_label.add_css_class("error-label")
|
||||
self._error_label.set_visible(False)
|
||||
box.append(self._error_label)
|
||||
|
||||
return box
|
||||
|
||||
def _build_bottom_bar(self) -> Gtk.Box:
|
||||
"""Build the bottom bar with user list (left) and power buttons (right)."""
|
||||
bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
bar.set_hexpand(True)
|
||||
bar.set_margin_start(16)
|
||||
bar.set_margin_end(16)
|
||||
bar.set_margin_bottom(16)
|
||||
|
||||
# User list (left)
|
||||
user_list_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
user_list_box.add_css_class("user-list")
|
||||
user_list_box.set_halign(Gtk.Align.START)
|
||||
user_list_box.set_valign(Gtk.Align.END)
|
||||
|
||||
for user in self._users:
|
||||
btn = Gtk.Button(label=user.display_name)
|
||||
btn.add_css_class("user-list-item")
|
||||
btn.connect("clicked", self._on_user_clicked, user)
|
||||
user_list_box.append(btn)
|
||||
|
||||
bar.append(user_list_box)
|
||||
|
||||
# Spacer
|
||||
spacer = Gtk.Box()
|
||||
spacer.set_hexpand(True)
|
||||
bar.append(spacer)
|
||||
|
||||
# Power buttons (right)
|
||||
power_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
power_box.set_halign(Gtk.Align.END)
|
||||
power_box.set_valign(Gtk.Align.END)
|
||||
power_box.set_spacing(8)
|
||||
|
||||
reboot_btn = Gtk.Button()
|
||||
reboot_btn.set_icon_name("system-reboot-symbolic")
|
||||
reboot_btn.add_css_class("power-button")
|
||||
reboot_btn.set_tooltip_text(self._strings.reboot_tooltip)
|
||||
reboot_btn.connect("clicked", self._on_reboot_clicked)
|
||||
power_box.append(reboot_btn)
|
||||
|
||||
shutdown_btn = Gtk.Button()
|
||||
shutdown_btn.set_icon_name("system-shutdown-symbolic")
|
||||
shutdown_btn.add_css_class("power-button")
|
||||
shutdown_btn.set_tooltip_text(self._strings.shutdown_tooltip)
|
||||
shutdown_btn.connect("clicked", self._on_shutdown_clicked)
|
||||
power_box.append(shutdown_btn)
|
||||
|
||||
bar.append(power_box)
|
||||
|
||||
return bar
|
||||
|
||||
def _select_initial_user(self) -> None:
|
||||
"""Select the last user or the first available user."""
|
||||
if not self._users:
|
||||
return
|
||||
|
||||
# Try to load last user
|
||||
last_username = self._load_last_user()
|
||||
target_user = None
|
||||
|
||||
if last_username:
|
||||
for user in self._users:
|
||||
if user.username == last_username:
|
||||
target_user = user
|
||||
break
|
||||
|
||||
if target_user is None:
|
||||
target_user = self._users[0]
|
||||
|
||||
self._switch_to_user(target_user)
|
||||
|
||||
def _switch_to_user(self, user: User) -> None:
|
||||
"""Update the UI to show the selected user."""
|
||||
self._selected_user = user
|
||||
self._username_label.set_text(user.display_name)
|
||||
self._password_entry.set_text("")
|
||||
self._error_label.set_visible(False)
|
||||
|
||||
# Update avatar (use cache if available)
|
||||
if user.username in self._avatar_cache:
|
||||
self._avatar_image.set_from_pixbuf(self._avatar_cache[user.username])
|
||||
else:
|
||||
avatar_path = get_avatar_path(
|
||||
user.username, home_dir=user.home
|
||||
)
|
||||
if avatar_path and avatar_path.exists():
|
||||
self._set_avatar_from_file(avatar_path, user.username)
|
||||
else:
|
||||
# Default avatar — _set_default_avatar uses Traversable.read_text()
|
||||
# which works in ZIP wheels too, no exists() check needed
|
||||
self._set_default_avatar()
|
||||
|
||||
# Apply user's GTK theme if available
|
||||
self._apply_user_theme(user)
|
||||
|
||||
# Focus password entry
|
||||
self._password_entry.grab_focus()
|
||||
|
||||
def _apply_user_theme(self, user: User) -> None:
|
||||
"""Load the user's preferred GTK theme from their settings.ini."""
|
||||
gtk_config_dir = user.home / ".config" / "gtk-4.0"
|
||||
theme_name = get_user_gtk_theme(config_dir=gtk_config_dir)
|
||||
|
||||
settings = Gtk.Settings.get_default()
|
||||
if settings is None:
|
||||
return
|
||||
|
||||
current = settings.get_property("gtk-theme-name")
|
||||
if theme_name and current != theme_name:
|
||||
settings.set_property("gtk-theme-name", theme_name)
|
||||
elif not theme_name and current:
|
||||
settings.reset_property("gtk-theme-name")
|
||||
|
||||
def _get_foreground_color(self) -> str:
|
||||
"""Get the current GTK theme foreground color as a hex string."""
|
||||
rgba = self.get_color()
|
||||
r = int(rgba.red * 255)
|
||||
g = int(rgba.green * 255)
|
||||
b = int(rgba.blue * 255)
|
||||
return f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
||||
def _set_default_avatar(self) -> None:
|
||||
"""Load the default avatar SVG, tinted with the GTK foreground color."""
|
||||
if self._default_avatar_pixbuf:
|
||||
self._avatar_image.set_from_pixbuf(self._default_avatar_pixbuf)
|
||||
return
|
||||
try:
|
||||
svg_text = DEFAULT_AVATAR_PATH.read_text()
|
||||
fg_color = self._get_foreground_color()
|
||||
svg_text = svg_text.replace("#PLACEHOLDER", fg_color)
|
||||
svg_bytes = svg_text.encode("utf-8")
|
||||
loader = GdkPixbuf.PixbufLoader.new_with_type("svg")
|
||||
loader.set_size(AVATAR_SIZE, AVATAR_SIZE)
|
||||
loader.write(svg_bytes)
|
||||
loader.close()
|
||||
pixbuf = loader.get_pixbuf()
|
||||
if pixbuf:
|
||||
self._default_avatar_pixbuf = pixbuf
|
||||
self._avatar_image.set_from_pixbuf(pixbuf)
|
||||
except (GLib.Error, OSError):
|
||||
self._avatar_image.set_from_icon_name("avatar-default-symbolic")
|
||||
|
||||
def _set_avatar_from_file(self, path: Path, username: str | None = None) -> None:
|
||||
"""Load an image file and set it as the avatar, scaled to AVATAR_SIZE."""
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||
str(path), AVATAR_SIZE, AVATAR_SIZE, True
|
||||
)
|
||||
if username:
|
||||
self._avatar_cache[username] = pixbuf
|
||||
self._avatar_image.set_from_pixbuf(pixbuf)
|
||||
except GLib.Error:
|
||||
self._avatar_image.set_from_icon_name("avatar-default-symbolic")
|
||||
|
||||
def _setup_keyboard_navigation(self) -> None:
|
||||
"""Set up keyboard shortcuts."""
|
||||
controller = Gtk.EventControllerKey()
|
||||
controller.connect("key-pressed", self._on_key_pressed)
|
||||
self.add_controller(controller)
|
||||
|
||||
def _on_key_pressed(
|
||||
self,
|
||||
controller: Gtk.EventControllerKey,
|
||||
keyval: int,
|
||||
keycode: int,
|
||||
state: Gdk.ModifierType,
|
||||
) -> bool:
|
||||
"""Handle global key presses."""
|
||||
if keyval == Gdk.KEY_Escape:
|
||||
self._password_entry.set_text("")
|
||||
self._error_label.set_visible(False)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _on_user_clicked(self, button: Gtk.Button, user: User) -> None:
|
||||
"""Handle user selection from the user list."""
|
||||
self._cancel_pending_session()
|
||||
self._switch_to_user(user)
|
||||
|
||||
def _on_login_activate(self, entry: Gtk.PasswordEntry) -> None:
|
||||
"""Handle Enter key in the password field — attempt login."""
|
||||
if not self._selected_user:
|
||||
return
|
||||
|
||||
password = entry.get_text()
|
||||
session = self._get_selected_session()
|
||||
if not session:
|
||||
self._show_error(self._strings.no_session_selected)
|
||||
return
|
||||
|
||||
self._attempt_login(self._selected_user, password, session)
|
||||
|
||||
def _validate_greetd_sock(self, sock_path: str) -> bool:
|
||||
"""Validate that GREETD_SOCK points to an absolute path and a real socket."""
|
||||
path = Path(sock_path)
|
||||
if not path.is_absolute():
|
||||
self._show_error(self._strings.greetd_sock_not_absolute)
|
||||
return False
|
||||
try:
|
||||
mode = path.stat().st_mode
|
||||
if not stat.S_ISSOCK(mode):
|
||||
self._show_error(self._strings.greetd_sock_not_socket)
|
||||
return False
|
||||
except OSError:
|
||||
self._show_error(self._strings.greetd_sock_unreachable)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _close_greetd_sock(self) -> None:
|
||||
"""Close the greetd socket and reset the reference."""
|
||||
with self._greetd_sock_lock:
|
||||
if self._greetd_sock:
|
||||
try:
|
||||
self._greetd_sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
self._greetd_sock = None
|
||||
|
||||
def _set_login_sensitive(self, sensitive: bool) -> None:
|
||||
"""Enable or disable login controls during authentication."""
|
||||
self._password_entry.set_sensitive(sensitive)
|
||||
self._session_dropdown.set_sensitive(sensitive)
|
||||
|
||||
def _attempt_login(self, user: User, password: str, session: Session) -> None:
|
||||
"""Attempt to authenticate and start a session via greetd IPC."""
|
||||
sock_path = os.environ.get("GREETD_SOCK")
|
||||
if not sock_path:
|
||||
self._show_error(self._strings.greetd_sock_not_set)
|
||||
return
|
||||
|
||||
if not self._validate_greetd_sock(sock_path):
|
||||
return
|
||||
|
||||
# Disable UI while authenticating — the IPC runs in a background thread
|
||||
self._set_login_sensitive(False)
|
||||
thread = threading.Thread(
|
||||
target=self._login_worker,
|
||||
args=(user, password, session, sock_path),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
|
||||
def _login_worker(self, user: User, password: str, session: Session, sock_path: str) -> None:
|
||||
"""Run greetd IPC in a background thread to avoid blocking the GTK main loop."""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(10.0)
|
||||
sock.connect(sock_path)
|
||||
with self._greetd_sock_lock:
|
||||
self._greetd_sock = sock
|
||||
|
||||
# Step 1: Create session
|
||||
response = create_session(sock, user.username)
|
||||
|
||||
if response.get("type") == "error":
|
||||
GLib.idle_add(self._on_login_error, response, self._strings.auth_failed)
|
||||
return
|
||||
|
||||
# Step 2: Send password if auth message received
|
||||
if response.get("type") == "auth_message":
|
||||
response = post_auth_response(sock, password)
|
||||
|
||||
if response.get("type") == "error":
|
||||
self._failed_attempts[user.username] = self._failed_attempts.get(user.username, 0) + 1
|
||||
warning = faillock_warning(self._failed_attempts[user.username], self._strings)
|
||||
GLib.idle_add(self._on_login_auth_error, response, warning)
|
||||
return
|
||||
|
||||
if response.get("type") == "auth_message":
|
||||
# Multi-stage auth (e.g. TOTP) is not supported
|
||||
cancel_session(sock)
|
||||
GLib.idle_add(self._on_login_error, None, self._strings.multi_stage_unsupported)
|
||||
return
|
||||
|
||||
# Step 3: Start session
|
||||
if response.get("type") == "success":
|
||||
cmd = shlex.split(session.exec_cmd)
|
||||
if not cmd or not shutil.which(cmd[0]):
|
||||
cancel_session(sock)
|
||||
GLib.idle_add(self._on_login_error, None, self._strings.invalid_session_command)
|
||||
return
|
||||
response = start_session(sock, cmd)
|
||||
|
||||
if response.get("type") == "success":
|
||||
self._save_last_user(user.username)
|
||||
self._close_greetd_sock()
|
||||
GLib.idle_add(self.get_application().quit)
|
||||
return
|
||||
else:
|
||||
GLib.idle_add(self._on_login_error, response, self._strings.session_start_failed)
|
||||
|
||||
self._close_greetd_sock()
|
||||
|
||||
except ConnectionError as e:
|
||||
logger.error("greetd connection error: %s", e)
|
||||
self._close_greetd_sock()
|
||||
GLib.idle_add(self._on_login_error, None, self._strings.connection_error)
|
||||
except (OSError, ValueError) as e:
|
||||
logger.error("greetd socket error: %s", e)
|
||||
self._close_greetd_sock()
|
||||
GLib.idle_add(self._on_login_error, None, self._strings.socket_error)
|
||||
|
||||
def _on_login_error(self, response: dict | None, message: str) -> None:
|
||||
"""Handle login error on the GTK main thread."""
|
||||
if response:
|
||||
self._show_greetd_error(response, message)
|
||||
else:
|
||||
self._show_error(message)
|
||||
self._close_greetd_sock()
|
||||
self._set_login_sensitive(True)
|
||||
|
||||
def _on_login_auth_error(self, response: dict, warning: str | None) -> None:
|
||||
"""Handle authentication failure with optional faillock warning on the GTK main thread."""
|
||||
self._show_greetd_error(response, self._strings.wrong_password)
|
||||
if warning:
|
||||
current = self._error_label.get_text()
|
||||
self._error_label.set_text(f"{current}\n{warning}")
|
||||
self._close_greetd_sock()
|
||||
self._set_login_sensitive(True)
|
||||
|
||||
def _cancel_pending_session(self) -> None:
|
||||
"""Cancel any in-progress greetd session."""
|
||||
with self._greetd_sock_lock:
|
||||
if self._greetd_sock:
|
||||
try:
|
||||
cancel_session(self._greetd_sock)
|
||||
except (ConnectionError, OSError):
|
||||
pass
|
||||
self._close_greetd_sock()
|
||||
|
||||
def _get_selected_session(self) -> Session | None:
|
||||
"""Get the currently selected session from the dropdown."""
|
||||
if not self._sessions:
|
||||
return None
|
||||
idx = self._session_dropdown.get_selected()
|
||||
if idx < len(self._sessions):
|
||||
return self._sessions[idx]
|
||||
return None
|
||||
|
||||
MAX_GREETD_ERROR_LENGTH = 200
|
||||
|
||||
def _show_greetd_error(self, response: dict, fallback: str) -> None:
|
||||
"""Display an error from greetd, using a fallback for missing or oversized descriptions."""
|
||||
description = response.get("description", "")
|
||||
if description and len(description) <= self.MAX_GREETD_ERROR_LENGTH:
|
||||
self._show_error(description)
|
||||
else:
|
||||
self._show_error(fallback)
|
||||
|
||||
def _show_error(self, message: str) -> None:
|
||||
"""Display an error message below the password field."""
|
||||
self._error_label.set_text(message)
|
||||
self._error_label.set_visible(True)
|
||||
self._password_entry.set_text("")
|
||||
self._password_entry.grab_focus()
|
||||
|
||||
def _on_reboot_clicked(self, button: Gtk.Button) -> None:
|
||||
"""Handle reboot button click."""
|
||||
try:
|
||||
reboot()
|
||||
except subprocess.CalledProcessError:
|
||||
self._show_error(self._strings.reboot_failed)
|
||||
|
||||
def _on_shutdown_clicked(self, button: Gtk.Button) -> None:
|
||||
"""Handle shutdown button click."""
|
||||
try:
|
||||
shutdown()
|
||||
except subprocess.CalledProcessError:
|
||||
self._show_error(self._strings.shutdown_failed)
|
||||
|
||||
@staticmethod
|
||||
def _load_last_user() -> str | None:
|
||||
"""Load the last logged-in username from cache."""
|
||||
if LAST_USER_PATH.exists():
|
||||
try:
|
||||
username = LAST_USER_PATH.read_text().strip()
|
||||
except OSError:
|
||||
return None
|
||||
if len(username) > MAX_USERNAME_LENGTH or not VALID_USERNAME.match(username):
|
||||
return None
|
||||
return username
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _save_last_user(username: str) -> None:
|
||||
"""Save the last logged-in username to cache."""
|
||||
try:
|
||||
LAST_USER_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
LAST_USER_PATH.write_text(username)
|
||||
except OSError:
|
||||
pass # Non-critical — cache dir may not be writable
|
||||
@ -0,0 +1,117 @@
|
||||
# ABOUTME: Locale detection and string lookup for the greeter UI.
|
||||
# ABOUTME: Reads system locale (LANG or /etc/locale.conf) and provides DE or EN strings.
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_LOCALE_CONF = Path("/etc/locale.conf")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Strings:
|
||||
"""All user-visible strings for the greeter UI."""
|
||||
|
||||
# UI labels
|
||||
password_placeholder: str
|
||||
reboot_tooltip: str
|
||||
shutdown_tooltip: str
|
||||
|
||||
# Error messages
|
||||
no_session_selected: str
|
||||
greetd_sock_not_set: str
|
||||
greetd_sock_not_absolute: str
|
||||
greetd_sock_not_socket: str
|
||||
greetd_sock_unreachable: str
|
||||
auth_failed: str
|
||||
wrong_password: str
|
||||
multi_stage_unsupported: str
|
||||
invalid_session_command: str
|
||||
session_start_failed: str
|
||||
reboot_failed: str
|
||||
shutdown_failed: str
|
||||
|
||||
# Error messages (continued)
|
||||
connection_error: str
|
||||
socket_error: str
|
||||
|
||||
# Templates (use .format())
|
||||
faillock_attempts_remaining: str
|
||||
faillock_locked: str
|
||||
|
||||
|
||||
_STRINGS_DE = Strings(
|
||||
password_placeholder="Passwort",
|
||||
reboot_tooltip="Neustart",
|
||||
shutdown_tooltip="Herunterfahren",
|
||||
no_session_selected="Keine Session ausgewählt",
|
||||
greetd_sock_not_set="GREETD_SOCK nicht gesetzt",
|
||||
greetd_sock_not_absolute="GREETD_SOCK ist kein absoluter Pfad",
|
||||
greetd_sock_not_socket="GREETD_SOCK zeigt nicht auf einen Socket",
|
||||
greetd_sock_unreachable="GREETD_SOCK nicht erreichbar",
|
||||
auth_failed="Authentifizierung fehlgeschlagen",
|
||||
wrong_password="Falsches Passwort",
|
||||
multi_stage_unsupported="Mehrstufige Authentifizierung wird nicht unterstützt",
|
||||
invalid_session_command="Ungültiger Session-Befehl",
|
||||
session_start_failed="Session konnte nicht gestartet werden",
|
||||
reboot_failed="Neustart fehlgeschlagen",
|
||||
shutdown_failed="Herunterfahren fehlgeschlagen",
|
||||
connection_error="Verbindungsfehler",
|
||||
socket_error="Socket-Fehler",
|
||||
faillock_attempts_remaining="Noch {n} Versuch(e) vor Kontosperrung!",
|
||||
faillock_locked="Konto ist möglicherweise gesperrt",
|
||||
)
|
||||
|
||||
_STRINGS_EN = Strings(
|
||||
password_placeholder="Password",
|
||||
reboot_tooltip="Reboot",
|
||||
shutdown_tooltip="Shut down",
|
||||
no_session_selected="No session selected",
|
||||
greetd_sock_not_set="GREETD_SOCK not set",
|
||||
greetd_sock_not_absolute="GREETD_SOCK is not an absolute path",
|
||||
greetd_sock_not_socket="GREETD_SOCK does not point to a socket",
|
||||
greetd_sock_unreachable="GREETD_SOCK unreachable",
|
||||
auth_failed="Authentication failed",
|
||||
wrong_password="Wrong password",
|
||||
multi_stage_unsupported="Multi-stage authentication is not supported",
|
||||
invalid_session_command="Invalid session command",
|
||||
session_start_failed="Failed to start session",
|
||||
reboot_failed="Reboot failed",
|
||||
shutdown_failed="Shutdown failed",
|
||||
connection_error="Connection error",
|
||||
socket_error="Socket error",
|
||||
faillock_attempts_remaining="{n} attempt(s) remaining before lockout!",
|
||||
faillock_locked="Account may be locked",
|
||||
)
|
||||
|
||||
_LOCALE_MAP: dict[str, Strings] = {
|
||||
"de": _STRINGS_DE,
|
||||
"en": _STRINGS_EN,
|
||||
}
|
||||
|
||||
|
||||
def detect_locale(locale_conf_path: Path = DEFAULT_LOCALE_CONF) -> str:
|
||||
"""Determine the system language from LANG env var or /etc/locale.conf."""
|
||||
lang = os.environ.get("LANG")
|
||||
|
||||
if not lang and locale_conf_path.exists():
|
||||
for line in locale_conf_path.read_text().splitlines():
|
||||
if line.startswith("LANG="):
|
||||
lang = line.split("=", 1)[1].strip()
|
||||
break
|
||||
|
||||
if not lang or lang in ("C", "POSIX"):
|
||||
return "en"
|
||||
|
||||
# Extract language prefix: "de_DE.UTF-8" → "de"
|
||||
lang = lang.split("_")[0].split(".")[0].lower()
|
||||
if not lang.isalpha():
|
||||
return "en"
|
||||
return lang
|
||||
|
||||
|
||||
def load_strings(locale: str | None = None) -> Strings:
|
||||
"""Return the string table for the given locale, defaulting to English."""
|
||||
if locale is None:
|
||||
locale = detect_locale()
|
||||
return _LOCALE_MAP.get(locale, _STRINGS_EN)
|
||||
@ -0,0 +1,62 @@
|
||||
# ABOUTME: greetd IPC protocol implementation — communicates via Unix socket.
|
||||
# ABOUTME: Uses length-prefixed JSON encoding as specified by the greetd IPC protocol.
|
||||
|
||||
import json
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
MAX_PAYLOAD_SIZE = 65536
|
||||
|
||||
|
||||
def _recvall(sock: Any, n: int) -> bytes:
|
||||
"""Receive exactly n bytes from socket, looping on partial reads."""
|
||||
buf = bytearray()
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise ConnectionError("Connection closed while reading data")
|
||||
buf.extend(chunk)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def send_message(sock: Any, msg: dict) -> None:
|
||||
"""Send a length-prefixed JSON message to the greetd socket."""
|
||||
payload = json.dumps(msg).encode("utf-8")
|
||||
header = struct.pack("!I", len(payload))
|
||||
sock.sendall(header + payload)
|
||||
|
||||
|
||||
def recv_message(sock: Any) -> dict:
|
||||
"""Receive a length-prefixed JSON message from the greetd socket."""
|
||||
header = _recvall(sock, 4)
|
||||
length = struct.unpack("!I", header)[0]
|
||||
|
||||
if length > MAX_PAYLOAD_SIZE:
|
||||
raise ConnectionError(f"Payload too large: {length} bytes (max {MAX_PAYLOAD_SIZE})")
|
||||
|
||||
payload = _recvall(sock, length)
|
||||
return json.loads(payload.decode("utf-8"))
|
||||
|
||||
|
||||
def create_session(sock: Any, username: str) -> dict:
|
||||
"""Send a create_session request to greetd and return the response."""
|
||||
send_message(sock, {"type": "create_session", "username": username})
|
||||
return recv_message(sock)
|
||||
|
||||
|
||||
def post_auth_response(sock: Any, response: str | None) -> dict:
|
||||
"""Send an authentication response (e.g. password) to greetd."""
|
||||
send_message(sock, {"type": "post_auth_message_response", "response": response})
|
||||
return recv_message(sock)
|
||||
|
||||
|
||||
def start_session(sock: Any, cmd: list[str]) -> dict:
|
||||
"""Send a start_session request to launch the user's session."""
|
||||
send_message(sock, {"type": "start_session", "cmd": cmd})
|
||||
return recv_message(sock)
|
||||
|
||||
|
||||
def cancel_session(sock: Any) -> dict:
|
||||
"""Cancel the current authentication session."""
|
||||
send_message(sock, {"type": "cancel_session"})
|
||||
return recv_message(sock)
|
||||
@ -0,0 +1,126 @@
|
||||
# ABOUTME: Entry point for Moongreet — sets up GTK Application and Layer Shell.
|
||||
# ABOUTME: Handles multi-monitor setup: login UI on primary, wallpaper on secondary monitors.
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from importlib.resources import files
|
||||
|
||||
import gi
|
||||
gi.require_version("Gtk", "4.0")
|
||||
gi.require_version("Gdk", "4.0")
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
from moongreet.config import load_config, resolve_wallpaper_path
|
||||
from moongreet.greeter import GreeterWindow, WallpaperWindow
|
||||
|
||||
# gtk4-layer-shell is optional for development/testing
|
||||
try:
|
||||
gi.require_version("Gtk4LayerShell", "1.0")
|
||||
from gi.repository import Gtk4LayerShell
|
||||
HAS_LAYER_SHELL = True
|
||||
except (ValueError, ImportError):
|
||||
HAS_LAYER_SHELL = False
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MoongreetApp(Gtk.Application):
|
||||
"""GTK Application for the Moongreet greeter."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(application_id="dev.moonarch.moongreet")
|
||||
self._wallpaper_ctx = None
|
||||
self._secondary_windows: list[WallpaperWindow] = []
|
||||
|
||||
def do_activate(self) -> None:
|
||||
"""Create and present greeter windows on all monitors."""
|
||||
self._register_icons()
|
||||
self._load_css()
|
||||
|
||||
# Resolve wallpaper once, share across all windows
|
||||
config = load_config()
|
||||
bg_path, self._wallpaper_ctx = resolve_wallpaper_path(config)
|
||||
|
||||
display = Gdk.Display.get_default()
|
||||
monitors = display.get_monitors()
|
||||
primary_monitor = None
|
||||
|
||||
# Find primary monitor — fall back to first available
|
||||
for i in range(monitors.get_n_items()):
|
||||
monitor = monitors.get_item(i)
|
||||
if hasattr(monitor, 'is_primary') and monitor.is_primary():
|
||||
primary_monitor = monitor
|
||||
break
|
||||
if primary_monitor is None and monitors.get_n_items() > 0:
|
||||
primary_monitor = monitors.get_item(0)
|
||||
|
||||
# Main greeter window (login UI) on primary monitor
|
||||
greeter = GreeterWindow(bg_path=bg_path, application=self)
|
||||
if HAS_LAYER_SHELL:
|
||||
self._setup_layer_shell(greeter, keyboard=True)
|
||||
if primary_monitor is not None:
|
||||
Gtk4LayerShell.set_monitor(greeter, primary_monitor)
|
||||
greeter.present()
|
||||
|
||||
# Wallpaper-only windows on secondary monitors
|
||||
for i in range(monitors.get_n_items()):
|
||||
monitor = monitors.get_item(i)
|
||||
if monitor == primary_monitor:
|
||||
continue
|
||||
wallpaper_win = WallpaperWindow(bg_path=bg_path, application=self)
|
||||
if HAS_LAYER_SHELL:
|
||||
self._setup_layer_shell(wallpaper_win, keyboard=False)
|
||||
Gtk4LayerShell.set_monitor(wallpaper_win, monitor)
|
||||
wallpaper_win.present()
|
||||
self._secondary_windows.append(wallpaper_win)
|
||||
|
||||
def do_shutdown(self) -> None:
|
||||
"""Clean up wallpaper context manager on exit."""
|
||||
if self._wallpaper_ctx is not None:
|
||||
self._wallpaper_ctx.__exit__(None, None, None)
|
||||
self._wallpaper_ctx = None
|
||||
Gtk.Application.do_shutdown(self)
|
||||
|
||||
def _register_icons(self) -> None:
|
||||
"""Register custom icons from the package data/icons directory."""
|
||||
icons_dir = files("moongreet") / "data" / "icons"
|
||||
icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
|
||||
icon_theme.add_search_path(str(icons_dir))
|
||||
|
||||
def _load_css(self) -> None:
|
||||
"""Load the CSS stylesheet for the greeter."""
|
||||
css_provider = Gtk.CssProvider()
|
||||
css_path = files("moongreet") / "style.css"
|
||||
css_provider.load_from_path(str(css_path))
|
||||
Gtk.StyleContext.add_provider_for_display(
|
||||
Gdk.Display.get_default(),
|
||||
css_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
|
||||
def _setup_layer_shell(self, window: Gtk.Window, keyboard: bool = False) -> None:
|
||||
"""Configure gtk4-layer-shell for fullscreen display."""
|
||||
Gtk4LayerShell.init_for_window(window)
|
||||
Gtk4LayerShell.set_layer(window, Gtk4LayerShell.Layer.TOP)
|
||||
if keyboard:
|
||||
Gtk4LayerShell.set_keyboard_mode(
|
||||
window, Gtk4LayerShell.KeyboardMode.EXCLUSIVE
|
||||
)
|
||||
# Anchor to all edges for fullscreen
|
||||
for edge in [
|
||||
Gtk4LayerShell.Edge.TOP,
|
||||
Gtk4LayerShell.Edge.BOTTOM,
|
||||
Gtk4LayerShell.Edge.LEFT,
|
||||
Gtk4LayerShell.Edge.RIGHT,
|
||||
]:
|
||||
Gtk4LayerShell.set_anchor(window, edge, True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the Moongreet application."""
|
||||
app = MoongreetApp()
|
||||
app.run(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,17 @@
|
||||
# ABOUTME: Power actions — reboot and shutdown via loginctl.
|
||||
# ABOUTME: Simple wrappers around system commands for the greeter UI.
|
||||
|
||||
import subprocess
|
||||
|
||||
|
||||
POWER_TIMEOUT = 30
|
||||
|
||||
|
||||
def reboot() -> None:
|
||||
"""Reboot the system via loginctl."""
|
||||
subprocess.run(["loginctl", "reboot"], check=True, timeout=POWER_TIMEOUT)
|
||||
|
||||
|
||||
def shutdown() -> None:
|
||||
"""Shut down the system via loginctl."""
|
||||
subprocess.run(["loginctl", "poweroff"], check=True, timeout=POWER_TIMEOUT)
|
||||
@ -0,0 +1,63 @@
|
||||
# ABOUTME: Session detection — discovers available Wayland and X11 sessions.
|
||||
# ABOUTME: Parses .desktop files from standard session directories.
|
||||
|
||||
import configparser
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_WAYLAND_DIRS = (Path("/usr/share/wayland-sessions"),)
|
||||
DEFAULT_XSESSION_DIRS = (Path("/usr/share/xsessions"),)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Session:
|
||||
"""Represents an available login session."""
|
||||
|
||||
name: str
|
||||
exec_cmd: str
|
||||
session_type: str # "wayland" or "x11"
|
||||
|
||||
|
||||
def _parse_desktop_file(path: Path, session_type: str) -> Session | None:
|
||||
"""Parse a .desktop file and return a Session, or None if invalid."""
|
||||
config = configparser.ConfigParser(interpolation=None)
|
||||
config.read(path)
|
||||
|
||||
section = "Desktop Entry"
|
||||
if not config.has_section(section):
|
||||
return None
|
||||
|
||||
name = config.get(section, "Name", fallback=None)
|
||||
exec_cmd = config.get(section, "Exec", fallback=None)
|
||||
|
||||
if not name or not exec_cmd:
|
||||
return None
|
||||
|
||||
return Session(name=name, exec_cmd=exec_cmd, session_type=session_type)
|
||||
|
||||
|
||||
def get_sessions(
|
||||
wayland_dirs: Sequence[Path] = DEFAULT_WAYLAND_DIRS,
|
||||
xsession_dirs: Sequence[Path] = DEFAULT_XSESSION_DIRS,
|
||||
) -> list[Session]:
|
||||
"""Discover available sessions from .desktop files."""
|
||||
sessions: list[Session] = []
|
||||
|
||||
for directory in wayland_dirs:
|
||||
if not directory.exists():
|
||||
continue
|
||||
for desktop_file in sorted(directory.glob("*.desktop")):
|
||||
session = _parse_desktop_file(desktop_file, "wayland")
|
||||
if session:
|
||||
sessions.append(session)
|
||||
|
||||
for directory in xsession_dirs:
|
||||
if not directory.exists():
|
||||
continue
|
||||
for desktop_file in sorted(directory.glob("*.desktop")):
|
||||
session = _parse_desktop_file(desktop_file, "x11")
|
||||
if session:
|
||||
sessions.append(session)
|
||||
|
||||
return sessions
|
||||
@ -0,0 +1,87 @@
|
||||
/* ABOUTME: GTK4 CSS stylesheet for the Moongreet greeter. */
|
||||
/* ABOUTME: Defines styling for the login screen layout. */
|
||||
|
||||
/* Main window background */
|
||||
window.greeter {
|
||||
background-color: #1a1a2e;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* Central login area */
|
||||
.login-box {
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
background-color: alpha(@theme_bg_color, 0.7);
|
||||
}
|
||||
|
||||
/* Round avatar image — size is set via set_size_request() in code */
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
min-width: 128px;
|
||||
min-height: 128px;
|
||||
background-color: @theme_selected_bg_color;
|
||||
border: 3px solid alpha(white, 0.3);
|
||||
}
|
||||
|
||||
/* Username label */
|
||||
.username-label {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Session dropdown */
|
||||
.session-dropdown {
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
/* Password entry field */
|
||||
.password-entry {
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
/* Error message label */
|
||||
.error-label {
|
||||
color: #ff6b6b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* User list on the bottom left */
|
||||
.user-list {
|
||||
background-color: transparent;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.user-list-item {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-list-item:hover {
|
||||
background-color: alpha(white, 0.15);
|
||||
}
|
||||
|
||||
.user-list-item:selected {
|
||||
background-color: alpha(white, 0.2);
|
||||
}
|
||||
|
||||
/* Power buttons on the bottom right */
|
||||
.power-button {
|
||||
min-width: 48px;
|
||||
min-height: 48px;
|
||||
padding: 0px;
|
||||
border-radius: 24px;
|
||||
background-color: alpha(white, 0.1);
|
||||
color: white;
|
||||
border: none;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.power-button:hover {
|
||||
background-color: alpha(white, 0.25);
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
# ABOUTME: User detection — parses /etc/passwd for login users, finds avatars and GTK themes.
|
||||
# ABOUTME: Provides User dataclass and helper functions for the greeter UI.
|
||||
|
||||
import configparser
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
VALID_THEME_NAME = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||
|
||||
NOLOGIN_SHELLS = {"/usr/sbin/nologin", "/sbin/nologin", "/bin/false", "/usr/bin/nologin"}
|
||||
MIN_UID = 1000
|
||||
MAX_UID = 65533
|
||||
|
||||
DEFAULT_PASSWD = Path("/etc/passwd")
|
||||
DEFAULT_ACCOUNTSSERVICE_DIR = Path("/var/lib/AccountsService/icons")
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
"""Represents a system user suitable for login."""
|
||||
|
||||
username: str
|
||||
uid: int
|
||||
gecos: str
|
||||
home: Path
|
||||
shell: str
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
"""Return gecos if available, otherwise username."""
|
||||
return self.gecos if self.gecos else self.username
|
||||
|
||||
|
||||
def get_users(passwd_path: Path = DEFAULT_PASSWD) -> list[User]:
|
||||
"""Parse /etc/passwd and return users with UID in the login range."""
|
||||
users: list[User] = []
|
||||
|
||||
if not passwd_path.exists():
|
||||
return users
|
||||
|
||||
for line in passwd_path.read_text().splitlines():
|
||||
parts = line.split(":")
|
||||
if len(parts) < 7:
|
||||
continue
|
||||
|
||||
username, _, uid_str, _, gecos, home, shell = parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6]
|
||||
|
||||
try:
|
||||
uid = int(uid_str)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if uid < MIN_UID or uid > MAX_UID:
|
||||
continue
|
||||
if shell in NOLOGIN_SHELLS:
|
||||
continue
|
||||
if "/" in username or username.startswith("."):
|
||||
continue
|
||||
|
||||
users.append(User(
|
||||
username=username,
|
||||
uid=uid,
|
||||
gecos=gecos,
|
||||
home=Path(home),
|
||||
shell=shell,
|
||||
))
|
||||
|
||||
return users
|
||||
|
||||
|
||||
def get_avatar_path(
|
||||
username: str,
|
||||
accountsservice_dir: Path = DEFAULT_ACCOUNTSSERVICE_DIR,
|
||||
home_dir: Path | None = None,
|
||||
) -> Path | None:
|
||||
"""Find avatar for a user: AccountsService icon → ~/.face → None."""
|
||||
# AccountsService icon
|
||||
icon = accountsservice_dir / username
|
||||
if icon.exists() and not icon.is_symlink():
|
||||
return icon
|
||||
|
||||
# ~/.face fallback
|
||||
if home_dir is not None:
|
||||
face = home_dir / ".face"
|
||||
if face.exists() and not face.is_symlink():
|
||||
return face
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_user_gtk_theme(config_dir: Path | None = None) -> str | None:
|
||||
"""Read the GTK theme name from a user's gtk-4.0/settings.ini."""
|
||||
if config_dir is None:
|
||||
return None
|
||||
|
||||
settings_file = config_dir / "settings.ini"
|
||||
if not settings_file.exists():
|
||||
return None
|
||||
|
||||
config = configparser.ConfigParser(interpolation=None)
|
||||
try:
|
||||
config.read(settings_file)
|
||||
except configparser.Error:
|
||||
return None
|
||||
|
||||
if config.has_option("Settings", "gtk-theme-name"):
|
||||
theme = config.get("Settings", "gtk-theme-name")
|
||||
# Validate against path traversal — only allow safe theme names
|
||||
if theme and VALID_THEME_NAME.match(theme):
|
||||
return theme
|
||||
|
||||
return None
|
||||
1
pkg/src/greetd-moongreet
Submodule
1
pkg/src/greetd-moongreet
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 99c016adbc27e6455463b47fc6e6b3d2eb157def
|
||||
Loading…
x
Reference in New Issue
Block a user