fix: Build-Artefakte aus Repo entfernt, .gitignore ergänzt
makepkg-Artefakte (pkg/src, pkg/pkg, .pkg.tar.zst) waren versehentlich committed. Entfernt und per .gitignore geschützt.
This commit is contained in:
parent
6400270a50
commit
d089fa201c
6
.gitignore
vendored
6
.gitignore
vendored
@ -8,3 +8,9 @@ build/
|
|||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.pyright/
|
.pyright/
|
||||||
*.egg
|
*.egg
|
||||||
|
|
||||||
|
# makepkg build artifacts
|
||||||
|
pkg/src/
|
||||||
|
pkg/pkg/
|
||||||
|
pkg/*.pkg.tar*
|
||||||
|
pkg/greetd-moongreet/
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
ref: refs/heads/main
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
[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 +0,0 @@
|
|||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
#!/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+"$@"}
|
|
||||||
:
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
#!/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
|
|
||||||
}
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
#!/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;
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
#!/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+"$@"}
|
|
||||||
:
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
#!/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 --
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
:
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
#!/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 +0,0 @@
|
|||||||
* -export-subst -export-ignore
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
# 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.
@ -1,3 +0,0 @@
|
|||||||
# pack-refs with: peeled fully-peeled sorted
|
|
||||||
99c016adbc27e6455463b47fc6e6b3d2eb157def refs/heads/main
|
|
||||||
99c016adbc27e6455463b47fc6e6b3d2eb157def refs/tags/v0.1.0
|
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,18 +0,0 @@
|
|||||||
# 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
|
|
||||||
}
|
|
||||||
Binary file not shown.
@ -1,22 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
# 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"
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
#!/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())
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
../../../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,,
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: hatchling 1.29.0
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py3-none-any
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
[console_scripts]
|
|
||||||
moongreet = moongreet.main:main
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
# ABOUTME: Moongreet package — a greetd greeter for Wayland with GTK4.
|
|
||||||
# ABOUTME: Part of the Moonarch ecosystem.
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 366 KiB |
@ -1,587 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
# 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()
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
/* 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);
|
|
||||||
}
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
# 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 +0,0 @@
|
|||||||
Subproject commit 99c016adbc27e6455463b47fc6e6b3d2eb157def
|
|
||||||
Loading…
x
Reference in New Issue
Block a user