forked from poz/modfetch
Compare commits
47 commits
Author | SHA1 | Date | |
---|---|---|---|
2e57ca9b3f | |||
e405ccdf2a | |||
fa46131ee5 | |||
a610d63088 | |||
e2ac9aa4e6 | |||
b990fabd4f | |||
4eb0ddca86 | |||
e0edbc83d6 | |||
e3bdb80e91 | |||
a6a1d0c65e | |||
d8a0393fe1 | |||
55414046ea | |||
c50902e45e | |||
6eaf260d8d | |||
f3ef45abda | |||
022f0462e9 | |||
4ef40c8aed | |||
79615a946b | |||
d24c1d3767 | |||
5601945db1 | |||
c128b281d8 | |||
8bfacabcd0 | |||
41d9f87eab | |||
8081a8d302 | |||
d6961a75f3 | |||
7d62a8acc4 | |||
c08e2398ab | |||
cca7224559 | |||
c013f078ab | |||
274df36583 | |||
53ff1074ef | |||
49cf173286 | |||
147f372a9b | |||
6b8e1087bd | |||
1640be4666 | |||
2695a19da4 | |||
0e9a7d6bd8 | |||
ac6c61e10a | |||
dfa4ea2f44 | |||
92a9fb163f | |||
d6b05cd7b7 | |||
ce5cd7ac79 | |||
b15ab051f0 | |||
4baf04cda0 | |||
24e2d04c68 | |||
727556295e | |||
7afaaa2376 |
22 changed files with 797 additions and 337 deletions
22
.ccls
Normal file
22
.ccls
Normal file
|
@ -0,0 +1,22 @@
|
|||
gcc
|
||||
%c -std=c99
|
||||
%h
|
||||
-Iinclude
|
||||
-D_GNU_SOURCE
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wpedantic
|
||||
-Wstrict-aliasing
|
||||
-Wfloat-equal
|
||||
-Wundef
|
||||
-Wshadow
|
||||
-Wpointer-arith
|
||||
-Wcast-align
|
||||
-Wstrict-prototypes
|
||||
-Wstrict-overflow=5
|
||||
-Wwrite-strings
|
||||
-Wcast-qual
|
||||
-Wswitch-default
|
||||
-Wswitch-enum
|
||||
-Wconversion
|
||||
-Wunreachable-code
|
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
use flake
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1,3 @@
|
|||
target/
|
||||
.ccls-cache/
|
||||
.direnv/
|
||||
|
|
46
Makefile
46
Makefile
|
@ -6,36 +6,46 @@ CFLAGS += -Wcast-qual -Wswitch-default -Wswitch-enum
|
|||
CFLAGS += -Wconversion -Wunreachable-code
|
||||
# for asprintf() and getline()
|
||||
CFLAGS += -D_GNU_SOURCE
|
||||
CFLAGS += -Iinclude
|
||||
LDFLAGS =
|
||||
|
||||
SRC = modfetch.c
|
||||
OBJ = $(BIN)/$(SRC:.c=.o)
|
||||
NAME = modfetch
|
||||
|
||||
SRC = src
|
||||
BIN = target
|
||||
TEMPMOD = $(wildcard modules/*.c)
|
||||
MOD = $(TEMPMOD:modules/%=%)
|
||||
PROGRAM = modfetch
|
||||
|
||||
_PROG = config.c fetch.c main.c util.c
|
||||
PROG = $(addprefix $(SRC)/, $(_PROG))
|
||||
|
||||
_LIB = semver.c
|
||||
LIB = $(addprefix $(SRC)/, $(_LIB))
|
||||
|
||||
OBJ = $(_PROG:.c=.o) $(_LIB:.c=.o)
|
||||
|
||||
_MOD = $(wildcard modules/*.c)
|
||||
MOD = $(_MOD:modules/%=%)
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: dirs modules $(PROGRAM)
|
||||
all: dirs modules $(NAME)
|
||||
|
||||
modules: dirs $(MOD:.c=.so)
|
||||
|
||||
dirs:
|
||||
mkdir -p ./$(BIN)
|
||||
mkdir -p $(BIN)
|
||||
|
||||
run: all
|
||||
$(BIN)/$(PROGRAM)
|
||||
$(NAME): $(OBJ)
|
||||
$(CC) $(addprefix $(BIN)/, $^) $(LDFLAGS) -o $(BIN)/$@
|
||||
|
||||
$(PROGRAM): $(OBJ)
|
||||
$(CC) -o $(BIN)/$(PROGRAM) $^ $(LDFLAGS)
|
||||
%.so: modules/%.c $(LIB)
|
||||
$(CC) $^ -shared -fPIC $(CFLAGS) $(LDFLAGS) -o $(BIN)/$@
|
||||
|
||||
$(BIN)/%.o: %.c
|
||||
$(CC) -o $@ -c $< $(CFLAGS)
|
||||
%.o: src/%.c
|
||||
$(CC) -c $< $(CFLAGS) -o $(BIN)/$@
|
||||
|
||||
%.so: modules/%.c
|
||||
$(CC) -o $(BIN)/$@ $^ -shared -fPIC $(CFLAGS)
|
||||
clean: clean_modules
|
||||
rm -rf $(addprefix $(BIN)/, $(OBJ))
|
||||
rm -rf $(BIN)/$(NAME)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJ)
|
||||
rm -rf $(PROGRAM)
|
||||
clean_modules:
|
||||
rm -rf $(wildcard $(BIN)/*.so)
|
||||
|
|
13
README.md
13
README.md
|
@ -2,9 +2,15 @@
|
|||
|
||||
modular fetch
|
||||
|
||||
each printable thing is a separate module in a shared object
|
||||
|
||||
modfetch loads and calls them at runtime to print system info
|
||||
|
||||
## building
|
||||
|
||||
`make` for the main binary
|
||||
`make` to build everything
|
||||
|
||||
`make modfetch` for the main binary
|
||||
|
||||
`make modules` for all the default modules
|
||||
|
||||
|
@ -12,7 +18,7 @@ write your own using `mod.h` if you want to
|
|||
|
||||
## config
|
||||
|
||||
by default `$XDG_CURRENT_DESKTOP/modfetch.conf`
|
||||
by default `$XDG_CONFIG_HOME/modfetch.conf`, `~/.config/modfetch.conf` if `$XDG_CONFIG_HOME` doesn't exist
|
||||
|
||||
format:
|
||||
```
|
||||
|
@ -26,10 +32,11 @@ format:
|
|||
|
||||
## TODO
|
||||
|
||||
- [x] ~~module api versioning~~
|
||||
- [ ] module manager (url to module source, compiles and puts the binary somewhere)
|
||||
- [ ] better config parsing
|
||||
- [ ] more text positioning options aside from printing text vertically
|
||||
- [ ] some way for module to throw errors
|
||||
- [ ] some way for modules to throw errors
|
||||
- [ ] module related options (list module, print specific module info, etc.)
|
||||
|
||||
## license
|
||||
|
|
26
flake.lock
Normal file
26
flake.lock
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1707824078,
|
||||
"narHash": "sha256-Au3wLi2d06bU7TDvahP2jIEeKwmjAxKHqi8l2uiBkGA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "99d7b32e4cfdaf763d9335b4d9ecf4415cbdc405",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
36
flake.nix
Normal file
36
flake.nix
Normal file
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
description = "modular fetch";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
nixpkgs,
|
||||
...
|
||||
}: let
|
||||
systems = [ "x86_64-linux" "aarch64-linux" ];
|
||||
forEachSystem = nixpkgs.lib.genAttrs systems;
|
||||
pkgsForEach = nixpkgs.legacyPackages;
|
||||
in {
|
||||
devShells = forEachSystem (system:
|
||||
let
|
||||
pkgs = pkgsForEach.${system};
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
name = "modfetch";
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
gcc
|
||||
gnumake
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
curl.dev
|
||||
xorg.libX11.dev
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
25
include/config.h
Normal file
25
include/config.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
#ifndef _MODFETCH_CONFIG_H
|
||||
#define _MODFETCH_CONFIG_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
typedef struct {
|
||||
// path to the binary of this module
|
||||
char *path;
|
||||
// name=value strings for each value in config
|
||||
char **config;
|
||||
} Module;
|
||||
|
||||
typedef struct {
|
||||
size_t module_count;
|
||||
Module *modules;
|
||||
} Config;
|
||||
|
||||
char *default_config_path(void);
|
||||
|
||||
void parsing_error(size_t line);
|
||||
|
||||
Config parse_config(FILE *config_file);
|
||||
|
||||
#endif // _MODFETCH_CONFIG_H
|
8
include/fetch.h
Normal file
8
include/fetch.h
Normal file
|
@ -0,0 +1,8 @@
|
|||
#ifndef _MODFETCH_FETCH_H
|
||||
#define _MODFETCH_FETCH_H
|
||||
|
||||
#include <config.h>
|
||||
|
||||
void fetch(Config config);
|
||||
|
||||
#endif // _MODFETCH_FETCH_H
|
22
include/mod.h
Normal file
22
include/mod.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
#ifndef _MODFETCH_MOD_H
|
||||
#define _MODFETCH_MOD_H
|
||||
|
||||
#include <semver.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
static const semver API_VERSION = {
|
||||
.major = 0,
|
||||
.minor = 2,
|
||||
.patch = 0,
|
||||
};
|
||||
|
||||
static const uint8_t MFERR_APIVER = 1;
|
||||
|
||||
semver version(void);
|
||||
const char *name(void);
|
||||
|
||||
uint8_t init(semver api_ver, char **config);
|
||||
const char *get(void);
|
||||
|
||||
#endif // _MODFETCH_MOD_H
|
17
include/semver.h
Normal file
17
include/semver.h
Normal file
|
@ -0,0 +1,17 @@
|
|||
#ifndef _MODFETCH_SEMVER_H
|
||||
#define _MODFETCH_SEMVER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t major;
|
||||
uint8_t minor;
|
||||
uint8_t patch;
|
||||
} semver;
|
||||
|
||||
bool sveq(semver s1, semver s2);
|
||||
|
||||
char *svtoa(semver s);
|
||||
|
||||
#endif // _MODFETCH_SEMVER_H
|
20
include/util.h
Normal file
20
include/util.h
Normal file
|
@ -0,0 +1,20 @@
|
|||
#ifndef _MODFETCH_UTIL_H
|
||||
#define _MODFETCH_UTIL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
static const uint8_t MAX_MODULE_NAME_LENGTH = 128;
|
||||
static const uint8_t MAX_MODULES = 128;
|
||||
|
||||
static const uint8_t INITIAL_CONFIG_SIZE = 8;
|
||||
|
||||
static const uint16_t MAX_PATH_LENGTH = 4096;
|
||||
|
||||
|
||||
char *remove_whitespaces(const char *str);
|
||||
|
||||
char *resolve_env_vars(const char *str);
|
||||
|
||||
char *process_str(const char *str);
|
||||
|
||||
#endif // _MODFETCH_UTIL_H
|
10
mod.h
10
mod.h
|
@ -1,10 +0,0 @@
|
|||
#include <stdint.h>
|
||||
|
||||
uint8_t version_major(void);
|
||||
uint8_t version_minor(void);
|
||||
uint8_t version_patch(void);
|
||||
|
||||
const char *module_name(void);
|
||||
const char *get(void);
|
||||
|
||||
void init(char **config);
|
292
modfetch.c
292
modfetch.c
|
@ -1,292 +0,0 @@
|
|||
#include <ctype.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <wordexp.h>
|
||||
|
||||
static const char *PNAME = "modfetch";
|
||||
|
||||
static const uint8_t VERSION_MAJOR = 0;
|
||||
static const uint8_t VERSION_MINOR = 1;
|
||||
static const uint8_t VERSION_PATCH = 0;
|
||||
|
||||
static const uint8_t MAX_MODULE_NAME_LENGTH = 128;
|
||||
static const uint8_t MAX_MODULES = 128;
|
||||
|
||||
static const uint8_t INITIAL_CONFIG_SIZE = 8;
|
||||
|
||||
static const uint16_t MAX_PATH_LENGTH = 4096;
|
||||
|
||||
static const char *version_str(void) {
|
||||
char *ver;
|
||||
if (asprintf(&ver, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
return ver;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
// path to the binary of this module
|
||||
char *path;
|
||||
// name=value strings for each value in config
|
||||
char **config;
|
||||
} Module;
|
||||
|
||||
typedef struct {
|
||||
size_t module_count;
|
||||
Module *modules;
|
||||
} Config;
|
||||
|
||||
void parsing_error(size_t line) {
|
||||
fprintf(stderr, "error: failed parsing config at line %zu", line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
char *remove_whitespaces(const char *str) {
|
||||
char tmp[strlen(str)];
|
||||
size_t i = 0;
|
||||
size_t j = 0;
|
||||
while (str[i] != '\0') {
|
||||
if (isspace(str[i])) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
tmp[j] = str[i];
|
||||
i += 1;
|
||||
j += 1;
|
||||
}
|
||||
tmp[j] = '\0';
|
||||
|
||||
char *ret = malloc(j * sizeof(char));
|
||||
strncpy(ret, tmp, j);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
char *resolve_env_vars(const char *str) {
|
||||
size_t len = strlen(str);
|
||||
char *ret = malloc(MAX_PATH_LENGTH * sizeof(char));
|
||||
size_t ret_offset = 0;
|
||||
bool in_env_var = false;
|
||||
char env_var[MAX_PATH_LENGTH];
|
||||
memset(env_var, 0, sizeof(env_var));
|
||||
size_t env_var_offset = 0;
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (str[i] == '~') {
|
||||
char *home = getenv("HOME");
|
||||
strcat(ret, home);
|
||||
ret_offset += strlen(home);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str[i] == '$') {
|
||||
in_env_var = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// end of current env var
|
||||
if (in_env_var && !(isalpha(str[i]) || isdigit(str[i]) || str[i] == '_')) {
|
||||
in_env_var = false;
|
||||
|
||||
char *env = getenv(env_var);
|
||||
strcat(ret, env);
|
||||
ret_offset += strlen(env);
|
||||
memset(env_var, 0, sizeof(env_var));
|
||||
env_var_offset = 0;
|
||||
}
|
||||
|
||||
if (in_env_var) {
|
||||
env_var[env_var_offset] = str[i];
|
||||
env_var_offset += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
ret[ret_offset] = str[i];
|
||||
ret_offset += 1;
|
||||
}
|
||||
ret[ret_offset] = '\0';
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
char *process_str(const char *str) {
|
||||
char *whitespaceless = remove_whitespaces(str);
|
||||
char *ret = resolve_env_vars(whitespaceless);
|
||||
free(whitespaceless);
|
||||
return ret;
|
||||
}
|
||||
|
||||
Config parse_config(FILE *config_file) {
|
||||
size_t len = 0;
|
||||
ssize_t read;
|
||||
char *line = NULL;
|
||||
size_t line_index = 0;
|
||||
bool in_config = false;
|
||||
|
||||
char current_path[MAX_MODULE_NAME_LENGTH];
|
||||
Module current_module = {
|
||||
.path = NULL,
|
||||
.config = NULL,
|
||||
};
|
||||
size_t config_index = 0;
|
||||
size_t config_multiplier = 1;
|
||||
|
||||
Config config = {
|
||||
.modules = malloc(MAX_MODULES * sizeof(Module)),
|
||||
.module_count = 0,
|
||||
};
|
||||
|
||||
// TODO handle errors of all the function inside the while
|
||||
errno = 0;
|
||||
|
||||
// TODO rewrite this to read the config char by char
|
||||
while ((read = getline(&line, &len, config_file)) != -1) {
|
||||
if (read <= 1)
|
||||
continue;
|
||||
|
||||
// if config is passed to this module
|
||||
if (line[read - 2u] == '{') {
|
||||
if (in_config) {
|
||||
fclose(config_file);
|
||||
parsing_error(line_index);
|
||||
}
|
||||
in_config = true;
|
||||
// get rid of the '{'
|
||||
strncpy(current_path, line, (size_t)read - 2);
|
||||
current_path[read - 2] = '\0';
|
||||
|
||||
current_module.path = process_str(current_path);
|
||||
current_module.config = malloc(INITIAL_CONFIG_SIZE * sizeof(char*));
|
||||
continue;
|
||||
}
|
||||
|
||||
// end of config for current module
|
||||
if (line[read - 2u] == '}') {
|
||||
if (!in_config) {
|
||||
fclose(config_file);
|
||||
parsing_error(line_index);
|
||||
}
|
||||
in_config = false;
|
||||
|
||||
if (config_index >= INITIAL_CONFIG_SIZE * config_multiplier) {
|
||||
config_multiplier *= 2;
|
||||
current_module.config = realloc(current_module.config, INITIAL_CONFIG_SIZE * config_multiplier);
|
||||
}
|
||||
current_module.config[config_index] = NULL;
|
||||
config.modules[config.module_count] = current_module;
|
||||
config.module_count += 1;
|
||||
config_index = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_config) {
|
||||
if (config_index >= INITIAL_CONFIG_SIZE * config_multiplier) {
|
||||
config_multiplier *= 2;
|
||||
current_module.config = realloc(current_module.config, INITIAL_CONFIG_SIZE * config_multiplier);
|
||||
}
|
||||
current_module.config[config_index] = process_str(line);
|
||||
config_index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// no config passed to this module
|
||||
current_module.path = process_str(line);
|
||||
|
||||
current_module.config = malloc(sizeof(void*));
|
||||
current_module.config[0] = NULL;
|
||||
|
||||
config.modules[config.module_count] = current_module;
|
||||
config.module_count += 1;
|
||||
|
||||
free(line);
|
||||
}
|
||||
if (read == -1 && errno != 0)
|
||||
free(line);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
|
||||
char *config_path;
|
||||
|
||||
if (asprintf(&config_path, "%s/modfetch.conf", getenv("XDG_CONFIG_HOME")) < 0) {
|
||||
fprintf(stderr, "error: failed formatting config path (this shouldn't happen)");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
for (size_t i = 1; i < (size_t)argc; ++i) {
|
||||
if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--config") == 0) {
|
||||
if (i == (size_t)argc - 1) {
|
||||
fprintf(stderr, "error: no config path passed\n");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
config_path = argv[i];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||
fprintf(stderr, "%s: modular fetch [%s]\n", PNAME, version_str());
|
||||
fprintf(stderr, "\n");
|
||||
fprintf(stderr, "OPTIONS\n");
|
||||
fprintf(stderr, "\t-h, --help\t\t\tdisplays this help text\n");
|
||||
fprintf(stderr, "\t-c, --config /path/to/config\tchanges config path from the default ($XDG_CONFIG_HOME/%s.conf)\n", PNAME);
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
FILE *config_file = fopen(config_path, "r");
|
||||
|
||||
if (config_file == NULL) {
|
||||
fprintf(stderr, "error: failed to open config at: %s\n", config_path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
Config config = parse_config(config_file);
|
||||
|
||||
fclose(config_file);
|
||||
|
||||
for (size_t i = 0; i < config.module_count; ++i) {
|
||||
Module current_module = config.modules[i];
|
||||
void *handle = dlopen(current_module.path, RTLD_NOW);
|
||||
if (handle == NULL) {
|
||||
fprintf(stderr, "error: failed opening module %s\n", current_module.path);
|
||||
fprintf(stderr, "%s\n", dlerror());
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
dlerror();
|
||||
|
||||
// stupid fucking c standard issue
|
||||
// I'm not fixing this
|
||||
// https://stackoverflow.com/questions/14134245/iso-c-void-and-function-pointers
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
uint8_t (*version_major)(void) = dlsym(handle, "version_major");
|
||||
uint8_t (*version_minor)(void) = dlsym(handle, "version_minor");
|
||||
uint8_t (*version_patch)(void) = dlsym(handle, "version_patch");
|
||||
|
||||
const char *(*module_name)(void) = dlsym(handle, "module_name");
|
||||
const char *(*get)(void) = dlsym(handle, "get");
|
||||
void (*init)(char**) = dlsym(handle, "init");
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
init(current_module.config);
|
||||
|
||||
(void)module_name;
|
||||
(void)version_major;
|
||||
(void)version_minor;
|
||||
(void)version_patch;
|
||||
//printf("%s: %d.%d.%d\n", module_name(), version_major(), version_minor(), version_patch());
|
||||
printf("%s\n", get());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -1,20 +1,60 @@
|
|||
#include "../mod.h"
|
||||
/* DESKTOP - desktop environment / window manager / compositor module for modfetch
|
||||
*
|
||||
* author: jacekpoz
|
||||
* 09 Feb 2024
|
||||
*/
|
||||
|
||||
#include <mod.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#ifdef MF_DESKTOP_X11
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
|
||||
uint8_t version_major(void) { return 0; }
|
||||
uint8_t version_minor(void) { return 1; }
|
||||
uint8_t version_patch(void) { return 0; }
|
||||
const char *module_name(void) { return "desktop"; }
|
||||
static const semver _version = {
|
||||
.major = 0,
|
||||
.minor = 1,
|
||||
.patch = 0,
|
||||
};
|
||||
|
||||
semver version(void) { return _version; }
|
||||
const char *name(void) { return "desktop"; }
|
||||
|
||||
uint8_t init(semver api_ver, char **config) {
|
||||
if (!sveq(api_ver, API_VERSION)) {
|
||||
return MFERR_APIVER;
|
||||
}
|
||||
|
||||
void init(char **config) {
|
||||
(void)config;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *get(void) {
|
||||
char *ret;
|
||||
|
||||
if (asprintf(&ret, "Desktop: %s", getenv("XDG_CURRENT_DESKTOP")) < 0) {
|
||||
char *current_desktop = getenv("XDG_CURRENT_DESKTOP");
|
||||
|
||||
if (current_desktop == NULL) {
|
||||
#ifdef MF_DESKTOP_X11
|
||||
Display *disp = XOpenDisplay(NULL);
|
||||
Window root = XDefaultRootWindow(disp);
|
||||
Atom atom = XInternAtom(disp, "_NET_WM_NAME", 0);
|
||||
Atom type;
|
||||
int fmt;
|
||||
unsigned long num_items;
|
||||
unsigned long bytes_after;
|
||||
unsigned char *prop;
|
||||
XGetWindowProperty(disp, root, atom, 0, 4096 / sizeof(int32_t), 0, AnyPropertyType,
|
||||
&type, &fmt, &num_items, &bytes_after, &prop);
|
||||
current_desktop = prop;
|
||||
#else
|
||||
current_desktop = "Unknown";
|
||||
#endif
|
||||
}
|
||||
|
||||
if (asprintf(&ret, "Desktop: %s", current_desktop) < 0) {
|
||||
fprintf(stderr, "error: failed formatting (this shouldn't happen)");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
|
37
modules/os.c
37
modules/os.c
|
@ -1,16 +1,33 @@
|
|||
#include "../mod.h"
|
||||
/* OS - operating system module for modfetch
|
||||
*
|
||||
* author: jacekpoz
|
||||
* 09 Feb 2024
|
||||
*/
|
||||
|
||||
#include <mod.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
uint8_t version_major(void) { return 0; }
|
||||
uint8_t version_minor(void) { return 1; }
|
||||
uint8_t version_patch(void) { return 0; }
|
||||
const char *module_name(void) { return "os"; }
|
||||
static const semver _version = {
|
||||
.major = 0,
|
||||
.minor = 1,
|
||||
.patch = 0,
|
||||
};
|
||||
|
||||
semver version(void) { return _version; }
|
||||
const char *name(void) { return "os"; }
|
||||
|
||||
uint8_t init(semver api_ver, char **config) {
|
||||
if (!sveq(api_ver, API_VERSION)) {
|
||||
return MFERR_APIVER;
|
||||
}
|
||||
|
||||
void init(char **config) {
|
||||
(void)config;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *get(void) {
|
||||
|
@ -27,7 +44,7 @@ const char *get(void) {
|
|||
ssize_t read;
|
||||
char *line = NULL;
|
||||
|
||||
char *name = malloc(1024 * sizeof(char));
|
||||
char *name = NULL;
|
||||
const char *name_label = "PRETTY_NAME=";
|
||||
const size_t name_label_len = strlen(name_label);
|
||||
|
||||
|
@ -44,6 +61,12 @@ const char *get(void) {
|
|||
break;
|
||||
}
|
||||
|
||||
fclose(os_release);
|
||||
|
||||
if (name == NULL) {
|
||||
name = "Unknown";
|
||||
}
|
||||
|
||||
if (asprintf(&ret, "OS: %s", name) < 0) {
|
||||
fprintf(stderr, "error: failed formatting string (this shouldn't happen)");
|
||||
exit(EXIT_FAILURE);
|
||||
|
|
80
modules/sep.c
Normal file
80
modules/sep.c
Normal file
|
@ -0,0 +1,80 @@
|
|||
/* SEP - separator module for modfetch
|
||||
*
|
||||
* options: <type> name (default value)
|
||||
* <number> sep (8) specifies how long the separator should be
|
||||
* <string> char (-) specifies the character (or string) to use for the separator
|
||||
*
|
||||
* author: krizej
|
||||
* 09 Feb 2024
|
||||
*/
|
||||
|
||||
#include <mod.h>
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static const semver _version = {
|
||||
.major = 1,
|
||||
.minor = 0,
|
||||
.patch = 0,
|
||||
};
|
||||
|
||||
semver version(void) { return _version; }
|
||||
const char *name(void) { return "sep"; }
|
||||
|
||||
static size_t length = 8;
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdiscarded-qualifiers"
|
||||
static char *sep_char = "-";
|
||||
#pragma GCC diagnostic pop
|
||||
static size_t sep_char_length = 1;
|
||||
|
||||
uint8_t init(semver api_ver, char **config) {
|
||||
if (!sveq(api_ver, API_VERSION)) {
|
||||
return MFERR_APIVER;
|
||||
}
|
||||
|
||||
for (char *opt = *config; opt != NULL; opt = *++config) {
|
||||
if (strncmp(opt, "sep", 3) == 0) {
|
||||
char *valstr = opt + 4; // skip 'sep='
|
||||
long value = strtol(valstr, NULL, 10);
|
||||
|
||||
if (value >= 0) {
|
||||
length = (size_t) value;
|
||||
}
|
||||
} else if (strncmp(opt, "char", 4) == 0) {
|
||||
char *value = opt + 5; // skip 'char='
|
||||
|
||||
if (*value != 0) { // todo: see if there are any more characters that should be rejected
|
||||
sep_char_length = strlen(value);
|
||||
sep_char = malloc((sep_char_length + 1) * sizeof(char));
|
||||
strncpy(sep_char, value, sep_char_length);
|
||||
sep_char[sep_char_length] = '\0';
|
||||
} else {
|
||||
sep_char = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *get(void) {
|
||||
char *ret;
|
||||
|
||||
if (sep_char == NULL) {
|
||||
ret = malloc(1 * sizeof(char));
|
||||
*ret = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = malloc((length * sep_char_length + 1) * sizeof(char));
|
||||
memset(ret, 0, length * sep_char_length + 1);
|
||||
|
||||
for (size_t i = 0; i < length; i++)
|
||||
strncat(ret, sep_char, sep_char_length);
|
||||
|
||||
ret[length * sep_char_length] = '\0'; // just to be sure :-D
|
||||
|
||||
return ret;
|
||||
}
|
126
src/config.c
Normal file
126
src/config.c
Normal file
|
@ -0,0 +1,126 @@
|
|||
#include <config.h>
|
||||
|
||||
#include <util.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
char *default_config_path(void) {
|
||||
char *config_path = NULL;
|
||||
|
||||
char *config_dir = getenv("XDG_CONFIG_HOME");
|
||||
if (config_dir == NULL) {
|
||||
char *home = getenv("HOME");
|
||||
if (asprintf(&config_dir, "%s/.config", home) < 0) {
|
||||
fprintf(stderr, "error: failed formatting config dir (this shouldn't happen)");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
if (asprintf(&config_path, "%s/modfetch.conf", config_dir) < 0) {
|
||||
fprintf(stderr, "error: failed formatting config path (this shouldn't happen)");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return config_path;
|
||||
}
|
||||
|
||||
void parsing_error(size_t line) {
|
||||
fprintf(stderr, "error: failed parsing config at line %zu", line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
Config parse_config(FILE *config_file) {
|
||||
size_t len = 0;
|
||||
ssize_t read;
|
||||
char *line = NULL;
|
||||
size_t line_index = 0;
|
||||
bool in_config = false;
|
||||
|
||||
char current_path[MAX_MODULE_NAME_LENGTH];
|
||||
Module current_module = {
|
||||
.path = NULL,
|
||||
.config = NULL,
|
||||
};
|
||||
size_t config_index = 0;
|
||||
size_t config_multiplier = 1;
|
||||
|
||||
Config config = {
|
||||
.modules = malloc(MAX_MODULES * sizeof(Module)),
|
||||
.module_count = 0,
|
||||
};
|
||||
|
||||
// TODO handle errors of all the function inside the while
|
||||
errno = 0;
|
||||
|
||||
// TODO rewrite this to read the config char by char
|
||||
while ((read = getline(&line, &len, config_file)) != -1) {
|
||||
if (read <= 1)
|
||||
continue;
|
||||
|
||||
// if config is passed to this module
|
||||
if (line[read - 2u] == '{') {
|
||||
if (in_config) {
|
||||
fclose(config_file);
|
||||
parsing_error(line_index);
|
||||
}
|
||||
in_config = true;
|
||||
// get rid of the '{'
|
||||
strncpy(current_path, line, (size_t)read - 2);
|
||||
current_path[read - 2] = '\0';
|
||||
|
||||
current_module.path = process_str(current_path);
|
||||
current_module.config = malloc(INITIAL_CONFIG_SIZE * sizeof(char*));
|
||||
continue;
|
||||
}
|
||||
|
||||
// end of config for current module
|
||||
if (line[read - 2u] == '}') {
|
||||
if (!in_config) {
|
||||
fclose(config_file);
|
||||
parsing_error(line_index);
|
||||
}
|
||||
in_config = false;
|
||||
|
||||
if (config_index >= INITIAL_CONFIG_SIZE * config_multiplier) {
|
||||
config_multiplier *= 2;
|
||||
current_module.config = realloc(current_module.config, INITIAL_CONFIG_SIZE * config_multiplier);
|
||||
}
|
||||
current_module.config[config_index] = NULL;
|
||||
config.modules[config.module_count] = current_module;
|
||||
config.module_count += 1;
|
||||
config_index = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_config) {
|
||||
if (config_index >= INITIAL_CONFIG_SIZE * config_multiplier) {
|
||||
config_multiplier *= 2;
|
||||
current_module.config = realloc(current_module.config, INITIAL_CONFIG_SIZE * config_multiplier);
|
||||
}
|
||||
current_module.config[config_index] = process_str(line);
|
||||
config_index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// no config passed to this module
|
||||
current_module.path = process_str(line);
|
||||
|
||||
current_module.config = malloc(sizeof(void*));
|
||||
current_module.config[0] = NULL;
|
||||
|
||||
config.modules[config.module_count] = current_module;
|
||||
config.module_count += 1;
|
||||
|
||||
free(line);
|
||||
}
|
||||
if (read == -1 && errno != 0)
|
||||
free(line);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
38
src/fetch.c
Normal file
38
src/fetch.c
Normal file
|
@ -0,0 +1,38 @@
|
|||
#include <fetch.h>
|
||||
|
||||
#include <mod.h>
|
||||
#include <semver.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
void fetch(Config config) {
|
||||
for (size_t i = 0; i < config.module_count; ++i) {
|
||||
Module current_module = config.modules[i];
|
||||
void *handle = dlopen(current_module.path, RTLD_NOW);
|
||||
if (handle == NULL) {
|
||||
fprintf(stderr, "error: failed opening module %s\n", current_module.path);
|
||||
fprintf(stderr, "%s\n", dlerror());
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
dlerror();
|
||||
|
||||
// stupid fucking c standard issue
|
||||
// I'm not fixing this
|
||||
// https://stackoverflow.com/questions/14134245/iso-c-void-and-function-pointers
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
semver (*mod_version)(void) = dlsym(handle, "version");
|
||||
const char *(*mod_name)(void) = dlsym(handle, "name");
|
||||
const char *(*mod_get)(void) = dlsym(handle, "get");
|
||||
void (*mod_init)(semver, char**) = dlsym(handle, "init");
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
mod_init(API_VERSION, current_module.config);
|
||||
|
||||
(void)mod_name;
|
||||
(void)mod_version;
|
||||
// printf("%s: %s\n", mod_name(), svtoa(mod_version()));
|
||||
printf("%s\n", mod_get());
|
||||
}
|
||||
}
|
149
src/main.c
Normal file
149
src/main.c
Normal file
|
@ -0,0 +1,149 @@
|
|||
#include <config.h>
|
||||
#include <fetch.h>
|
||||
#include <mod.h>
|
||||
#include <semver.h>
|
||||
#include <util.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <wordexp.h>
|
||||
|
||||
static const char *PNAME = "modfetch";
|
||||
|
||||
static const semver VERSION = {
|
||||
.major = 0,
|
||||
.minor = 1,
|
||||
.patch = 0,
|
||||
};
|
||||
|
||||
void print_mod_help(FILE *stream) {
|
||||
fprintf(stream, "%s mod: module information\n", PNAME);
|
||||
fprintf(stream, "\n");
|
||||
fprintf(stream, "SUBCOMMANDS\n");
|
||||
fprintf(stream, "\tlist\t\t\t\tlists all available modules\n");
|
||||
fprintf(stream, "\tinfo <module>\t\t\tprints all info about module\n");
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
|
||||
char *config_path = default_config_path();
|
||||
|
||||
FILE *config_file = fopen(config_path, "r");
|
||||
|
||||
if (config_file == NULL) {
|
||||
fprintf(stderr, "error: failed to open config at: %s\n", config_path);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
Config config = parse_config(config_file);
|
||||
|
||||
fclose(config_file);
|
||||
free(config_path);
|
||||
|
||||
bool hit_mod_cmd = false;
|
||||
bool hit_mod_info = false;
|
||||
|
||||
for (size_t i = 1; i < (size_t)argc; ++i) {
|
||||
if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--config") == 0) {
|
||||
// no more args
|
||||
if (i == (size_t)argc - 1) {
|
||||
fprintf(stderr, "error: no config path passed\n");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
config_path = argv[i];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||
printf("%s: modular fetch [%s]\n", PNAME, svtoa(VERSION));
|
||||
printf("\n");
|
||||
printf("SUBCOMMANDS\n");
|
||||
printf("\tmod\t\t\t\tmodule information\n");
|
||||
printf("\n");
|
||||
printf("OPTIONS\n");
|
||||
printf("\t-h, --help\t\t\tdisplays this help text\n");
|
||||
printf("\t-c, --config </path/to/config>\tchanges config path from the default ($XDG_CONFIG_HOME/%s.conf)\n", PNAME);
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
if (strncmp(argv[i], "mod", 3) == 0 || hit_mod_cmd) {
|
||||
// no more args
|
||||
if (i == (size_t)argc - 1) {
|
||||
print_mod_help(stderr);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
|
||||
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||
print_mod_help(stdout);
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
// some other -/-- option in between the mod and the specific mod command
|
||||
if (argv[i][0] == '-') {
|
||||
hit_mod_cmd = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(argv[i], "list") == 0) {
|
||||
for (size_t ii = 0; ii < config.module_count; ++ii) {
|
||||
Module current_module = config.modules[ii];
|
||||
void *handle = dlopen(current_module.path, RTLD_NOW);
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
semver (*mod_version)(void) = dlsym(handle, "version");
|
||||
const char *(*mod_name)(void) = dlsym(handle, "name");
|
||||
#pragma GCC diagnostic pop
|
||||
printf("%s-%s\n", mod_name(), svtoa(mod_version()));
|
||||
}
|
||||
}
|
||||
|
||||
if (strcmp(argv[i], "info") == 0 || hit_mod_info) {
|
||||
// no more args
|
||||
if (i == (size_t)argc - 1) {
|
||||
fprintf(stderr, "error: no module passed\n");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// some other -/-- option in between the info and the module
|
||||
if (argv[i][0] == '-') {
|
||||
hit_mod_info = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
|
||||
for (size_t ii = 0; ii < config.module_count; ++ii) {
|
||||
Module current_module = config.modules[ii];
|
||||
void *handle = dlopen(current_module.path, RTLD_NOW);
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
semver (*mod_version)(void) = dlsym(handle, "version");
|
||||
const char *(*mod_name)(void) = dlsym(handle, "name");
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
const char *_mod_name = mod_name();
|
||||
if (strcmp(argv[i], _mod_name) != 0)
|
||||
continue;
|
||||
|
||||
printf("%s-%s\n", _mod_name, svtoa(mod_version()));
|
||||
}
|
||||
}
|
||||
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
fetch(config);
|
||||
|
||||
return 0;
|
||||
}
|
29
src/semver.c
Normal file
29
src/semver.c
Normal file
|
@ -0,0 +1,29 @@
|
|||
#include <semver.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
uint8_t num_digits(uint8_t x) {
|
||||
return ((x) / 100) != 0 ? 3 :
|
||||
((x) / 10) != 0 ? 2 :
|
||||
1;
|
||||
}
|
||||
|
||||
bool sveq(semver s1, semver s2) {
|
||||
return s1.major == s2.major &&
|
||||
s1.minor == s2.minor &&
|
||||
s1.patch == s2.patch;
|
||||
}
|
||||
|
||||
char *svtoa(semver s) {
|
||||
uint8_t major_len = num_digits(s.major);
|
||||
uint8_t minor_len = num_digits(s.minor);
|
||||
uint8_t patch_len = num_digits(s.patch);
|
||||
// \0 dots
|
||||
size_t len = (size_t)1 + 2 + major_len + minor_len + patch_len;
|
||||
char *ret = malloc(len * sizeof(char));
|
||||
|
||||
snprintf(ret, len, "%d.%d.%d", s.major, s.minor, s.patch);
|
||||
|
||||
return ret;
|
||||
}
|
81
src/util.c
Normal file
81
src/util.c
Normal file
|
@ -0,0 +1,81 @@
|
|||
#include <util.h>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
char *remove_whitespaces(const char *str) {
|
||||
char tmp[strlen(str)];
|
||||
size_t i = 0;
|
||||
size_t j = 0;
|
||||
while (str[i] != '\0') {
|
||||
if (isspace(str[i])) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
tmp[j] = str[i];
|
||||
i += 1;
|
||||
j += 1;
|
||||
}
|
||||
|
||||
char *ret = malloc(j * sizeof(char));
|
||||
strncpy(ret, tmp, j);
|
||||
ret[j] = '\0';
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
char *resolve_env_vars(const char *str) {
|
||||
size_t len = strlen(str);
|
||||
char *ret = malloc(MAX_PATH_LENGTH * sizeof(char));
|
||||
size_t ret_offset = 0;
|
||||
bool in_env_var = false;
|
||||
char env_var[MAX_PATH_LENGTH];
|
||||
memset(env_var, 0, sizeof(env_var));
|
||||
size_t env_var_offset = 0;
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (str[i] == '~') {
|
||||
char *home = getenv("HOME");
|
||||
strcat(ret, home);
|
||||
ret_offset += strlen(home);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str[i] == '$') {
|
||||
in_env_var = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// end of current env var
|
||||
if (in_env_var && !(isalpha(str[i]) || isdigit(str[i]) || str[i] == '_')) {
|
||||
in_env_var = false;
|
||||
|
||||
char *env = getenv(env_var);
|
||||
strcat(ret, env);
|
||||
ret_offset += strlen(env);
|
||||
memset(env_var, 0, sizeof(env_var));
|
||||
env_var_offset = 0;
|
||||
}
|
||||
|
||||
if (in_env_var) {
|
||||
env_var[env_var_offset] = str[i];
|
||||
env_var_offset += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
ret[ret_offset] = str[i];
|
||||
ret_offset += 1;
|
||||
}
|
||||
ret[ret_offset] = '\0';
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
char *process_str(const char *str) {
|
||||
char *whitespaceless = remove_whitespaces(str);
|
||||
char *ret = resolve_env_vars(whitespaceless);
|
||||
free(whitespaceless);
|
||||
return ret;
|
||||
}
|
Loading…
Reference in a new issue