summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin <[email protected]>2024-11-06 14:03:31 -0300
committerGitHub <[email protected]>2024-11-06 14:03:31 -0300
commit5217df934bcd17941139af484e478a2bce48a2f8 (patch)
tree848833c3659ded1b5c87c37011f836e653eb9f50
parentb1da91dcced3803c764d22d4da1a49c23d7e97ed (diff)
parent84c02e54d3bd25958dafa67a7420cf29b3375de1 (diff)
Merge branch 'Aylur:main' into lua-refactor
-rw-r--r--CONTRIBUTING.md1
-rw-r--r--docs/default.nix6
-rw-r--r--docs/guide/libraries/greet.md94
-rw-r--r--docs/guide/typescript/cli-app.md4
-rw-r--r--docs/guide/typescript/first-widgets.md8
-rw-r--r--docs/guide/typescript/installation.md1
-rw-r--r--docs/vitepress.config.ts1
-rw-r--r--flake.nix1
-rw-r--r--lib/greet/cli.vala72
-rw-r--r--lib/greet/client.vala265
-rw-r--r--lib/greet/config.vala.in6
l---------lib/greet/gir.py1
-rw-r--r--lib/greet/meson.build109
-rw-r--r--lib/greet/meson_options.txt11
-rw-r--r--lib/greet/version1
15 files changed, 573 insertions, 8 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e09c3dc..7b29bcf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,7 +22,6 @@ Planned features, you could help with:
- [niri ipc library](https://github.com/Aylur/astal/issues/8)
- sway ipc library
-- greetd ipc library
- http request library abstraction over libsoup (mostly to be used in gjs and lua)
- notification sending libnotify clone [#26](https://github.com/Aylur/astal/issues/26)
- setting up [uncrustify](https://github.com/uncrustify/uncrustify) for Vala
diff --git a/docs/default.nix b/docs/default.nix
index ff7e8c9..3eae600 100644
--- a/docs/default.nix
+++ b/docs/default.nix
@@ -177,6 +177,12 @@ in
authors = "kotontrion";
}}
${genLib {
+ flakepkg = "greet";
+ gir = "Greet";
+ description = "IPC client for greetd";
+ version = ../lib/greet/version;
+ }}
+ ${genLib {
flakepkg = "hyprland";
gir = "Hyprland";
description = "IPC client for Hyprland";
diff --git a/docs/guide/libraries/greet.md b/docs/guide/libraries/greet.md
new file mode 100644
index 0000000..f0cd012
--- /dev/null
+++ b/docs/guide/libraries/greet.md
@@ -0,0 +1,94 @@
+# Greet
+
+Library and CLI tool for sending requests to [greetd](https://sr.ht/~kennylevinsen/greetd/).
+
+## Installation
+
+1. install dependencies
+
+:::code-group
+
+```sh [<i class="devicon-archlinux-plain"></i> Arch]
+sudo pacman -Syu meson vala json-glib gobject-introspection
+```
+
+```sh [<i class="devicon-fedora-plain"></i> Fedora]
+sudo dnf install meson vala valadoc json-glib-devel gobject-introspection-devel
+```
+
+```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
+sudo apt install meson valac libjson-glib-dev gobject-introspection
+```
+
+:::
+
+::: info
+Although `greetd` is not a direct build dependency,
+it should be self-explanatory that the daemon is required to be available at runtime.
+:::
+
+2. clone repo
+
+```sh
+git clone https://github.com/aylur/astal.git
+cd astal/lib/greet
+```
+
+3. install
+
+```sh
+meson setup --prefix /usr build
+meson install -C build
+```
+
+## Usage
+
+You can browse the [Greet reference](https://aylur.github.io/libastal/greet).
+
+### CLI
+
+```sh
+astal-greet --help
+```
+
+### Library
+
+:::code-group
+
+```js [<i class="devicon-javascript-plain"></i> JavaScript]
+import Greet from "gi://AstalGreet"
+
+Greet.login("username", "password", "compositor", (_, res) => {
+ try {
+ Greet.login_finish(res)
+ } catch (err) {
+ printerr(err)
+ }
+})
+```
+
+```py [<i class="devicon-python-plain"></i> Python]
+# Not yet documented
+
+```
+
+```lua [<i class="devicon-lua-plain"></i> Lua]
+local Greet = require("lgi").require("AstalGreet")
+
+Greet.login("username", "password", "compositor", function (_, res)
+ local err = Greet.login_finish(res)
+ if err ~= nil then
+ print(err)
+ end
+end)
+```
+
+```vala [<i class="devicon-vala-plain"></i> Vala]
+try {
+ yield AstalGreet.login("username", "password", "compositor");
+} catch (Error err) {
+ printerr(err.message);
+}
+```
+
+:::
diff --git a/docs/guide/typescript/cli-app.md b/docs/guide/typescript/cli-app.md
index f409176..85b117c 100644
--- a/docs/guide/typescript/cli-app.md
+++ b/docs/guide/typescript/cli-app.md
@@ -61,7 +61,7 @@ astal say hi
```
```sh [ags]
-ags -m "say hi"
+ags request "say hi"
# hi cli
```
@@ -135,7 +135,7 @@ astal -t Bar
```
```sh [ags]
-ags -t Bar
+ags toggle Bar
```
:::
diff --git a/docs/guide/typescript/first-widgets.md b/docs/guide/typescript/first-widgets.md
index a7372a8..96a1275 100644
--- a/docs/guide/typescript/first-widgets.md
+++ b/docs/guide/typescript/first-widgets.md
@@ -5,13 +5,13 @@
Start by initializing a project
```sh
-ags --init
+ags init
```
-then run `ags` in the terminal
+then run `ags run` in the terminal
```sh
-ags
+ags run
```
:::details Usage without AGS
@@ -363,7 +363,7 @@ by setting `noImplicityDestroy` property on the container widget.
:::info
The above example destroys and recreates every widget in the list **every time**
the value of the `Variable` changes. There might be cases where you would
-want to [handle child creation and deletion](/guide/typescript/faq#avoiding-unnecessary-re-rendering)
+want to handle child creation and deletion
yourself, because you don't want to lose the
inner state of widgets that does not need to be recreated. In this case
you can create a [custom reactive structure](./binding#example-custom-subscribable)
diff --git a/docs/guide/typescript/installation.md b/docs/guide/typescript/installation.md
index f1d20c5..e0f1bd5 100644
--- a/docs/guide/typescript/installation.md
+++ b/docs/guide/typescript/installation.md
@@ -71,7 +71,6 @@ sudo apt install golang-go npm gjs
git clone https://github.com/aylur/ags.git /tmp/ags
cd /tmp/ags
git checkout v2 # https://github.com/Aylur/ags/pull/504
-cd src
go install
```
diff --git a/docs/vitepress.config.ts b/docs/vitepress.config.ts
index f542a68..7e16eb7 100644
--- a/docs/vitepress.config.ts
+++ b/docs/vitepress.config.ts
@@ -105,6 +105,7 @@ export default defineConfig({
{ text: "Battery", link: "/guide/libraries/battery" },
{ text: "Bluetooth", link: "/guide/libraries/bluetooth" },
{ text: "Cava", link: "/guide/libraries/cava" },
+ { text: "Greet", link: "/guide/libraries/greet" },
{ text: "Hyprland", link: "/guide/libraries/hyprland" },
{ text: "Mpris", link: "/guide/libraries/mpris" },
{ text: "Network", link: "/guide/libraries/network" },
diff --git a/flake.nix b/flake.nix
index 734a110..d7c5229 100644
--- a/flake.nix
+++ b/flake.nix
@@ -55,6 +55,7 @@
battery = mkPkg "astal-battery" ./lib/battery [json-glib];
bluetooth = mkPkg "astal-bluetooth" ./lib/bluetooth [];
cava = mkPkg "astal-cava" ./lib/cava [(pkgs.callPackage ./nix/libcava.nix {})];
+ greet = mkPkg "astal-greet" ./lib/greet [json-glib];
hyprland = mkPkg "astal-hyprland" ./lib/hyprland [json-glib];
mpris = mkPkg "astal-mpris" ./lib/mpris [gvfs json-glib];
network = mkPkg "astal-network" ./lib/network [networkmanager];
diff --git a/lib/greet/cli.vala b/lib/greet/cli.vala
new file mode 100644
index 0000000..946ec72
--- /dev/null
+++ b/lib/greet/cli.vala
@@ -0,0 +1,72 @@
+static bool help;
+static bool version;
+static string username;
+static string password;
+static string cmd;
+[CCode (array_length = false, array_null_terminated = true)]
+static string[] env;
+
+const OptionEntry[] options = {
+ { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null },
+ { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null },
+ { "username", 'u', OptionFlags.NONE, OptionArg.STRING, ref username, null, null },
+ { "password", 'p', OptionFlags.NONE, OptionArg.STRING, ref password, null, null },
+ { "cmd", 'c', OptionFlags.NONE, OptionArg.STRING, ref cmd, null, null },
+ { "env", 'e', OptionFlags.NONE, OptionArg.STRING_ARRAY, ref env, null, null },
+ { null },
+};
+
+async int main(string[] argv) {
+ try {
+ var opts = new OptionContext();
+ opts.add_main_entries(options, null);
+ opts.set_help_enabled(false);
+ opts.set_ignore_unknown_options(false);
+ opts.parse(ref argv);
+ } catch (OptionError err) {
+ printerr (err.message);
+ return 1;
+ }
+
+ if (help) {
+ print("Usage:\n");
+ print(" %s [flags]\n\n", argv[0]);
+ print("Flags:\n");
+ print(" -h, --help Print this help and exit\n");
+ print(" -v, --version Print version number and exit\n");
+ print(" -u, --username User to login to\n");
+ print(" -p, --password Password of the user\n");
+ print(" -c, --cmd Command to start the session with\n");
+ print(" -e, --env Additional env vars to set for the session\n");
+ return 0;
+ }
+
+ if (version) {
+ printerr(AstalGreet.VERSION);
+ return 0;
+ }
+
+ if (username == null) {
+ printerr("missing username\n");
+ return 1;
+ }
+
+ if (password == null) {
+ printerr("missing password\n");
+ return 1;
+ }
+
+ if (cmd == null) {
+ printerr("missing cmd\n");
+ return 1;
+ }
+
+ try {
+ yield AstalGreet.login_with_env(username, password, cmd, env);
+ } catch (Error err) {
+ printerr(err.message);
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/lib/greet/client.vala b/lib/greet/client.vala
new file mode 100644
index 0000000..fac5898
--- /dev/null
+++ b/lib/greet/client.vala
@@ -0,0 +1,265 @@
+namespace AstalGreet {
+/**
+ * Shorthand for creating a session, posting the password,
+ * and starting the session with the given `cmd`
+ * which is parsed with [[email protected]_parse_argv].
+ *
+ * @param username User to login to
+ * @param password Password of the user
+ * @param cmd Command to start the session with
+ */
+public async void login(
+ string username,
+ string password,
+ string cmd
+) throws GLib.Error {
+ yield login_with_env(username, password, cmd, {});
+}
+
+/**
+ * Same as [[email protected]] but allow for setting additonal env
+ * in the form of `name=value` pairs.
+ *
+ * @param username User to login to
+ * @param password Password of the user
+ * @param cmd Command to start the session with
+ * @param env Additonal env vars to set for the session
+ */
+public async void login_with_env(
+ string username,
+ string password,
+ string cmd,
+ string[] env
+) throws GLib.Error {
+ string[] argv;
+ Shell.parse_argv(cmd, out argv);
+ try {
+ yield new CreateSession(username).send();
+ yield new PostAuthMesssage(password).send();
+ yield new StartSession(argv, env).send();
+ } catch (GLib.Error err) {
+ yield new CancelSession().send();
+ throw err;
+ }
+}
+
+/**
+ * Base Request type.
+ */
+public abstract class Request : Object {
+ protected abstract string type_name { get; }
+
+ private string serialize() {
+ var node = Json.gobject_serialize(this);
+ var obj = node.get_object();
+ obj.set_string_member("type", obj.get_string_member("type-name"));
+ obj.remove_member("type-name");
+
+ return Json.to_string(node, false);
+ }
+
+ private int bytes_to_int(Bytes bytes) {
+ uint8[] data = (uint8[]) bytes.get_data();
+ int value = 0;
+
+ for (int i = 0; i < data.length; i++) {
+ value = (value << 8) | data[i];
+ }
+
+ return value;
+ }
+
+ /**
+ * Send this request to greetd.
+ */
+ public async Response send() throws GLib.Error {
+ var sock = Environment.get_variable("GREETD_SOCK");
+ if (sock == null) {
+ throw new IOError.NOT_FOUND("greetd socket not found");
+ }
+
+ var addr = new UnixSocketAddress(sock);
+ var socket = new SocketClient();
+ var conn = socket.connect(addr);
+ var payload = serialize();
+ var ostream = new DataOutputStream(conn.get_output_stream()) {
+ byte_order = DataStreamByteOrder.HOST_ENDIAN,
+ };
+
+ ostream.put_int32(payload.length, null);
+ ostream.put_string(payload, null);
+ ostream.close(null);
+
+ var istream = conn.get_input_stream();
+
+ var response_head = yield istream.read_bytes_async(4, Priority.DEFAULT, null);
+ var response_length = bytes_to_int(response_head);
+ var response_body = yield istream.read_bytes_async(response_length, Priority.DEFAULT, null);
+
+ var response = (string)response_body.get_data();
+ conn.close(null);
+
+ var parser = new Json.Parser();
+ parser.load_from_data(response);
+ var obj = parser.get_root().get_object();
+ var type = obj.get_string_member("type");
+
+ switch (type) {
+ case Success.TYPE: return new Success(obj);
+ case Error.TYPE: return new Error(obj);
+ case AuthMessage.TYPE: return new AuthMessage(obj);
+ default: throw new IOError.NOT_FOUND("unknown response type");
+ }
+ }
+}
+
+/**
+ * Creates a session and initiates a login attempted for the given user.
+ * The session is ready to be started if a success is returned.
+ */
+public class CreateSession : Request {
+ protected override string type_name { get { return "create_session"; } }
+ public string username { get; set; }
+
+ public CreateSession(string username) {
+ Object(username: username);
+ }
+}
+
+/**
+ * Answers an authentication message.
+ * If the message was informative (info, error),
+ * then a response does not need to be set in this message.
+ * The session is ready to be started if a success is returned.
+ */
+public class PostAuthMesssage : Request {
+ protected override string type_name { get { return "post_auth_message_response"; } }
+ public string response { get; set; }
+
+ public PostAuthMesssage(string response) {
+ Object(response: response);
+ }
+}
+
+/**
+ * Requests for the session to be started using the provided command line,
+ * adding the supplied environment to that created by PAM.
+ * The session will start after the greeter process terminates
+ */
+public class StartSession : Request {
+ protected override string type_name { get { return "start_session"; } }
+ public string[] cmd { get; set; }
+ public string[] env { get; set; }
+
+ public StartSession(string[] cmd, string[] env = {}) {
+ Object(cmd: cmd, env: env);
+ }
+}
+
+/**
+ * Cancels the session that is currently under configuration.
+ */
+public class CancelSession : Request {
+ internal override string type_name { get { return "cancel_session"; } }
+}
+
+/**
+ * Base Response type.
+ */
+public abstract class Response : Object {
+ // nothing to do
+}
+
+/**
+ * Indicates that the request succeeded.
+ */
+public class Success : Response {
+ internal const string TYPE = "success";
+
+ internal Success(Json.Object obj) {
+ // nothing to do
+ }
+}
+
+/**
+ * Indicates that the request succeeded.
+ */
+public class Error : Response {
+ internal const string TYPE = "error";
+
+ public enum Type {
+ /**
+ * Indicates that authentication failed.
+ * This is not a fatal error, and is likely caused by incorrect credentials.
+ */
+ AUTH_ERROR,
+ /**
+ * A general error.
+ * See the error description for more information.
+ */
+ ERROR;
+
+ internal static Type from_string(string str) throws IOError {
+ switch (str) {
+ case "auth_error": return Type.AUTH_ERROR;
+ case "error": return Type.ERROR;
+ default: throw new IOError.FAILED(@"unknown error_type: $str");
+ }
+ }
+ }
+
+ public Type error_type { get; private set; }
+ public string description { get; private set; }
+
+ internal Error(Json.Object obj) throws IOError {
+ error_type = Type.from_string(obj.get_string_member("error_type"));
+ description = obj.get_string_member("description");
+ }
+}
+
+/**
+ * Indicates that the request succeeded.
+ */
+public class AuthMessage : Response {
+ internal const string TYPE = "auth_message";
+
+ public enum Type {
+ /**
+ * Indicates that input from the user should be
+ * visible when they answer this question.
+ */
+ VISIBLE,
+ /**
+ * Indicates that input from the user should be
+ * considered secret when they answer this question.
+ */
+ SECRET,
+ /**
+ * Indicates that this message is informative, not a question.
+ */
+ INFO,
+ /**
+ * Indicates that this message is an error, not a question.
+ */
+ ERROR;
+
+ internal static Type from_string(string str) throws IOError {
+ switch (str) {
+ case "visible": return VISIBLE;
+ case "secret": return Type.SECRET;
+ case "info": return Type.INFO;
+ case "error": return Type.ERROR;
+ default: throw new IOError.FAILED(@"unknown message_type: $str");
+ }
+ }
+ }
+
+ public Type message_type { get; private set; }
+ public string message { get; private set; }
+
+ internal AuthMessage(Json.Object obj) throws IOError {
+ message_type = Type.from_string(obj.get_string_member("auth_message_type"));
+ message = obj.get_string_member("auth_message");
+ }
+}
+}
diff --git a/lib/greet/config.vala.in b/lib/greet/config.vala.in
new file mode 100644
index 0000000..333d735
--- /dev/null
+++ b/lib/greet/config.vala.in
@@ -0,0 +1,6 @@
+namespace AstalGreet {
+ public const int MAJOR_VERSION = @MAJOR_VERSION@;
+ public const int MINOR_VERSION = @MINOR_VERSION@;
+ public const int MICRO_VERSION = @MICRO_VERSION@;
+ public const string VERSION = "@VERSION@";
+}
diff --git a/lib/greet/gir.py b/lib/greet/gir.py
new file mode 120000
index 0000000..b5b4f1d
--- /dev/null
+++ b/lib/greet/gir.py
@@ -0,0 +1 @@
+../gir.py \ No newline at end of file
diff --git a/lib/greet/meson.build b/lib/greet/meson.build
new file mode 100644
index 0000000..11321b0
--- /dev/null
+++ b/lib/greet/meson.build
@@ -0,0 +1,109 @@
+project(
+ 'astal-greet',
+ 'vala',
+ 'c',
+ version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(),
+ meson_version: '>= 0.62.0',
+ default_options: [
+ 'warning_level=2',
+ 'werror=false',
+ 'c_std=gnu11',
+ ],
+)
+
+assert(
+ get_option('lib') or get_option('cli'),
+ 'Either lib or cli option must be set to true.',
+)
+
+version_split = meson.project_version().split('.')
+api_version = version_split[0] + '.' + version_split[1]
+gir = 'AstalGreet-' + api_version + '.gir'
+typelib = 'AstalGreet-' + api_version + '.typelib'
+
+config = configure_file(
+ input: 'config.vala.in',
+ output: 'config.vala',
+ configuration: {
+ 'VERSION': meson.project_version(),
+ 'MAJOR_VERSION': version_split[0],
+ 'MINOR_VERSION': version_split[1],
+ 'MICRO_VERSION': version_split[2],
+ },
+)
+
+deps = [
+ dependency('glib-2.0'),
+ dependency('gobject-2.0'),
+ dependency('gio-unix-2.0'),
+ dependency('json-glib-1.0'),
+]
+
+sources = [config] + files(
+ 'client.vala',
+)
+
+if get_option('lib')
+ lib = library(
+ meson.project_name(),
+ sources,
+ dependencies: deps,
+ vala_args: ['--vapi-comments'],
+ vala_header: meson.project_name() + '.h',
+ vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
+ version: meson.project_version(),
+ install: true,
+ install_dir: [true, true, true],
+ )
+
+ pkgs = []
+ foreach dep : deps
+ pkgs += ['--pkg=' + dep.name()]
+ endforeach
+
+ gir_tgt = custom_target(
+ gir,
+ command: [find_program('python3'), files('gir.py'), meson.project_name(), gir]
+ + pkgs
+ + sources,
+ input: sources,
+ depends: lib,
+ output: gir,
+ install: true,
+ install_dir: get_option('datadir') / 'gir-1.0',
+ )
+
+ custom_target(
+ typelib,
+ command: [
+ find_program('g-ir-compiler'),
+ '--output', '@OUTPUT@',
+ '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@',
+ meson.current_build_dir() / gir,
+ ],
+ input: lib,
+ output: typelib,
+ depends: [lib, gir_tgt],
+ install: true,
+ install_dir: get_option('libdir') / 'girepository-1.0',
+ )
+
+ import('pkgconfig').generate(
+ lib,
+ name: meson.project_name(),
+ filebase: meson.project_name() + '-' + api_version,
+ version: meson.project_version(),
+ subdirs: meson.project_name(),
+ requires: deps,
+ install_dir: get_option('libdir') / 'pkgconfig',
+ )
+endif
+
+if get_option('cli')
+ executable(
+ meson.project_name(),
+ ['cli.vala', sources],
+ dependencies: deps,
+ install: true,
+ )
+endif
diff --git a/lib/greet/meson_options.txt b/lib/greet/meson_options.txt
new file mode 100644
index 0000000..f110242
--- /dev/null
+++ b/lib/greet/meson_options.txt
@@ -0,0 +1,11 @@
+option(
+ 'lib',
+ type: 'boolean',
+ value: true,
+)
+
+option(
+ 'cli',
+ type: 'boolean',
+ value: true,
+)
diff --git a/lib/greet/version b/lib/greet/version
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/lib/greet/version
@@ -0,0 +1 @@
+0.1.0