diff --git a/.gitignore b/.gitignore index 74b926f..bfd3200 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ docs/ # Code coverage *.lst + +bin/* \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..30f9068 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,20 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "dub", + "run": false, + "cwd": "${workspaceFolder}", + "compiler": "$current", + "archType": "$current", + "buildType": "$current", + "configuration": "$current", + "problemMatcher": [ + "$dmd" + ], + "group": "build", + "label": "dub: Build pkm", + "detail": "dub build --compiler=dmd -a=x86_64 -b=debug -c=application" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 2a1a762..273b6d3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ # pkm -Simple Yay wrapper +**P**ac**K**age**M**anager - Simple apt-style [Yay](https://github.com/Jguer/yay) wrapper + +## Description + +### Why? + +## Installation + +### Source + +### Binary + +### AUR + +## First Use + +## Commands + +## Config + +## FAQ + +## Images \ No newline at end of file diff --git a/dub.json b/dub.json new file mode 100644 index 0000000..207dd17 --- /dev/null +++ b/dub.json @@ -0,0 +1,14 @@ +{ + "authors": [ + "Alisa Lain" + ], + "copyright": "Copyright © 2022, Alisa Lain", + "dependencies": { + "dyaml": "~>0.9.2" + }, + "description": "Simple Yay wrapper", + "license": "MIT License", + "name": "pkm", + "targetName": "pkm", + "targetPath": "bin" +} \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json new file mode 100644 index 0000000..cac2919 --- /dev/null +++ b/dub.selections.json @@ -0,0 +1,7 @@ +{ + "fileVersion": 1, + "versions": { + "dyaml": "0.9.2", + "tinyendian": "0.2.0" + } +} diff --git a/src/pkm/app.d b/src/pkm/app.d new file mode 100644 index 0000000..757b586 --- /dev/null +++ b/src/pkm/app.d @@ -0,0 +1,113 @@ +import std.stdio; +import std.getopt; +import std.array: popFront, join; +import std.process: execute, environment, executeShell, Config, spawnProcess, wait; + +import pkm.search; + +import sily.getopt; + +// --aur -a +// --no-aur +// --version -v +// --help -h +// search | yay -Ss term +// list | yay -Q +// info | yay -Qi +// install | yay -S term +// reinstall | yay -R & yay -S +// remove | yay -R term +// checkupdates | yay -Qu +// update | yay -Sy +// upgrade | yay -Su +// clone +// build +// clean | yay -Yc +// -- custom -- +// stats | yay -Ps +// pkgbuild | yay -G term | yay -Gp term + +private const string _version = "pkm v1.0.0"; + +int main(string[] args) { + version(Windows) { + writefln("Unable to run on windows."); + return 1; + } + + bool optVersion = false; + bool optAur = false; + + auto help = getopt( + args, + config.bundling, config.passThrough, + "version", "print version", &optVersion, + "aur|a", "search only aur", &optAur + ); + + Commands[] coms = [ + Commands("search", "[option] "), + Commands("list", "[option]"), + Commands("info", "[option] "), + Commands("install", "[option] "), + // Commands("reinstall", "[option] "), + Commands("remove", "[option] "), + Commands("checkupdates", "[option]"), + Commands("update", "[option] "), + Commands("upgrade", "[option] "), + Commands("clean", "[option]"), + Commands("stats", "[option]"), + Commands("pkgbuild", "[option] "), + ]; + + if (optVersion) { + writeln(_version); + return 0; + } + + if (help.helpWanted || args.length == 1) { + printGetopt("", "pkm [...]", coms, help.options); + return 0; + } + + string yay = "/usr/bin/yay"; + + string[] ops = args.dup; + ops.popFront(); // removes [0] command + ops.popFront(); // removes 'command' + + if (optAur) { + ops ~= ["--aur"]; + } + + switch (args[1]) { + case "search": + return search(ops); + case "list": + return wait(spawnProcess([yay, "-Q"])); + case "info": + return wait(spawnProcess([yay, "-Qi"] ~ ops)); + case "install": + return wait(spawnProcess([yay, "-S"] ~ ops)); + // case "reinstall": + // return wait(spawnProcess([yay, "-R"] ~ ops)); + // return wait(spawnProcess([yay, "-S"] ~ ops)); + case "remove": + return wait(spawnProcess([yay, "-R"] ~ ops)); + case "checkupdates": + return wait(spawnProcess([yay, "-Qu"] ~ ops)); + case "update": + return wait(spawnProcess([yay, "-Sy"] ~ ops)); + case "upgrade": + return wait(spawnProcess([yay, "-Su"] ~ ops)); + case "clean": + return wait(spawnProcess([yay, "-Yc"])); + case "stats": + return wait(spawnProcess([yay, "-Ps"])); + case "pkgbuild": + return wait(spawnProcess([yay, "-Gp"] ~ ops)); + default: + writefln("Unknown command \"%s\".", args[1]); + return 1; + } +} diff --git a/src/pkm/package.d b/src/pkm/package.d new file mode 100644 index 0000000..d2c2aa6 --- /dev/null +++ b/src/pkm/package.d @@ -0,0 +1,29 @@ +module pkm.pkg; + +// repo +// name +// version +// (size (package size, installed size) | aur-votes (votes, popularity)) +// [group]? +// (orphaned) +// (outofdate) +// (installed: (version)) \n +// (description) + +struct Pkg { + string repo = "aur"; + string name = "Package"; + string ver = "v0.0.0"; + string pkgsize = "0 KB"; // not aur + string inssize = "0 KB"; + string group = ""; // community only + bool isOrphaned = false; + bool isOutdated = false; + string outdatedDate = ""; + bool isInstalled = false; + string installedVersion = ""; + string description = ""; + + alias aurvotes = pkgsize; // aur + alias aurpopul = inssize; +} \ No newline at end of file diff --git a/src/pkm/search.d b/src/pkm/search.d new file mode 100644 index 0000000..3de5733 --- /dev/null +++ b/src/pkm/search.d @@ -0,0 +1,215 @@ +module pkm.search; + +import std.process: execute, environment, executeShell, Config, spawnProcess, wait; +import std.file: tempDir, remove, readText; +// import std.file; +import std.stdio; +import std.regex; +import std.path: buildNormalizedPath, absolutePath; +import std.array: split; +import std.conv: to; +import std.range: repeat; +import std.algorithm: canFind, sort; +import std.numeric: gapWeightedSimilarityNormalized; + +import core.sys.posix.sys.ioctl; + +import pkm.pkg; +import sily.bashfmt; + +// yay regex: +// (.*?)\/(.*?)\s(.*?)\s\((.*?)\)(?:\s\[(.*)\])?(?:\s\((Orphaned)\))?(?:\s\(Out-of-date:\s(.*?)\))?(?:\s\((Installed)(?:\:\s(.*?))?\))?(?:\s{6}|\s{5})(.*)(?:\r|\n|\z) +// 1 2 3 4 5 6 7 8 9 10 +// repo/name version (size|aur-votes) [group]? (orphaned) (outofdate) (installed: (version)) \n (description) +private auto reg = regex( + r"(.*?)\/(.*?)\s(.*?)\s\((.*?)\)(?:\s\[(.*)\])?" ~ + r"(?:\s\((Orphaned)\))?(?:\s\(Out-of-date:\s(.*?)\))?" ~ + r"(?:\s\((Installed)(?:\:\s(.*?))?\))?(?:\s{6}|\s{5})(.*)(?:\r|\n|\z)", "gm"); + +int search(string[] terms) { + string yay = "/usr/bin/yay"; + string tmpFile = tempDir ~ "/" ~ "pkm-yay-search-output.txt"; + tmpFile = tmpFile.buildNormalizedPath.absolutePath; + + auto processOut = File(tmpFile, "w+"); + + auto pidErr = wait(spawnProcess([yay, "-Ss"] ~ terms, std.stdio.stdin, processOut)); + + processOut.close(); + + if (pidErr) { + remove(tmpFile); + writefln("yay exited with code \"%d\"", pidErr); + return pidErr; + } + + printPackages(tmpFile, terms); + + remove(tmpFile); + + return 0; +} + + +// 1 2 3 4 5 6 +// repo/name version (size|aur-votes) [group]? (orphaned) +// 7 8 9 10 +// (outofdate) (installed: (version)) \n (description) +void printPackages(string tmpFile, string[] searchTerms) { + string contents = readText(tmpFile); + + Pkg[] pkgs = []; + + auto packages = matchAll(contents, reg); + + foreach (pkg; packages) { + string pkgsize; + string inssize; + + if (pkg[1] == "aur") { + string[] _size = pkg[4].split(' '); + pkgsize = _size[0]; + inssize = _size[1]; + } else { + string[] _size = pkg[4].split(' '); + pkgsize = _size[0] ~ " " ~ _size[1]; + inssize = _size[2] ~ " " ~ _size[3]; + } + + pkgs ~= Pkg( + pkg[1], // repo + pkg[2], // name + pkg[3], // version + pkgsize, // package size / aur votes + inssize, // installation size / aur popularity + pkg[5], // group + pkg[6] != "" ? true : false, // is orphaned + pkg[7] != "" ? true : false, // is out of date + pkg[7], // out of date date + pkg[8] != "" ? true : false, // is installed + pkg[9], // installed version + pkg[10] // description + ); + } + + sort!((a,b) { + return gapWeightedSimilarityNormalized(a.name.split("-"), searchTerms, 0) < + gapWeightedSimilarityNormalized(b.name.split("-"), searchTerms, 0); + })(pkgs); + + foreach (pkg; pkgs) { + printPackage(pkg); + } +} + +void printPackage(Pkg pkg) { + // if (!(pkg.isOrphaned || pkg.isInstalled || pkg.isOutdated)) return; + winsize w; + ioctl(0, TIOCGWINSZ, &w); + int terminalWidth = w.ws_col; + + string installstr = "[i]"; + string orphanedstr = "[a]"; + string outdatedstr = "[o]"; + ulong flagsLength = installstr.length + orphanedstr.length + outdatedstr.length + 2; + + ulong installPos = 1; + if (pkg.name.length + flagsLength < terminalWidth / 2) { + installPos = (terminalWidth / 2) - pkg.name.length - flagsLength; + } + write(pkg.name); + + write(' '.repeat(installPos)); + + if (pkg.isOrphaned) { + write(FG.ltred ~ orphanedstr ~ FG.reset); + } else { + // write(' '.repeat(orphanedstr.length)); + write(FG.dkgray ~ orphanedstr ~ FG.reset); + } + + write(' '); + + if (pkg.isOutdated) { + write(FG.ltred ~ outdatedstr ~ FG.reset); + } else { + // write(' '.repeat(outdatedstr.length)); + write(FG.dkgray ~ outdatedstr ~ FG.reset); + } + + write(' '); + + if (pkg.isInstalled) { + write(FG.ltgreen ~ installstr ~ FG.reset); + } else { + // write(' '.repeat(installstr.length)); + write(FG.dkgray ~ installstr ~ FG.reset); + } + + write(' '); + + ulong verlen = pkg.installedVersion != "" ? pkg.installedVersion.length : pkg.ver.length; + if (pkg.installedVersion != "") { + write(FG.ltmagenta ~ pkg.installedVersion ~ FG.reset); + } else { + write(FG.cyan ~ pkg.ver ~ FG.reset); + } + + ulong reposize = "[aur]".length + 1; + ulong sizelen = pkg.pkgsize.length + pkg.inssize.length + 1; + + ulong wantedlen = verlen + sizelen + 1 + reposize; + ulong rside = 1; + if (wantedlen < terminalWidth / 2) { + rside = (terminalWidth / 2) - wantedlen; + } + + write(' '.repeat(rside)); + + if (pkg.repo == "aur") { + float votes = pkg.aurvotes.to!float; + FG colvot = FG.dkgray; + if (votes >= 100.0) colvot = FG.ltred; + if (votes >= 250.0) colvot = FG.ltyellow; + if (votes >= 500.0) colvot = FG.ltcyan; + if (votes >= 750.0) colvot = FG.ltgreen; + if (votes >= 1000.0) colvot = FG.ltblue; + + float popul = pkg.aurpopul.to!float; + FG colpop = FG.dkgray; + if (popul >= 1.0) colpop = FG.ltred; + if (popul >= 10.0) colpop = FG.ltyellow; + if (popul >= 20.0) colpop = FG.ltcyan; + if (popul >= 30.0) colpop = FG.ltgreen; + if (popul >= 40.0) colpop = FG.ltblue; + write(colvot ~ pkg.aurvotes ~ FG.reset); + write(' '); + write(colpop ~ pkg.aurpopul ~ FG.reset); + } else { + write(FG.reset ~ pkg.pkgsize ~ FG.reset); + write(' '); + write(FG.reset ~ pkg.inssize ~ FG.reset); + } + write(' '); + + switch (pkg.repo) { + case "aur": write(FG.ltblue ~ ""); break; + case "core": write(FG.ltyellow ~ ""); break; + case "extra": write(FG.ltgreen ~ ""); break; + case "community": write(FG.ltmagenta ~ ""); break; + default: + if (pkg.repo.canFind("testing")) { + write(FG.ltred ~ ""); + } else { + write(FG.ltcyan); + } + break; + } + + write("[" ~ pkg.repo[0..3] ~ "]"); + write(FG.reset ~ ""); + writeln(); + writeln(" " ~ pkg.description); + + // writeln(); +} diff --git a/src/sily/addons.d b/src/sily/addons.d new file mode 100644 index 0000000..8399808 --- /dev/null +++ b/src/sily/addons.d @@ -0,0 +1,69 @@ +module sily.addons; + +/** + * + * Params: + * val = Value to check + * vals = Array or sequence of values to check against + * Returns: if `val` is one of `vals` + */ +bool isOneOf(T)(T val, T[] vals ...) { + foreach (T i; vals) { + if (val == i) return true; + } + return false; +} + +/** + * Checks if `c` is letter or `_` + * Params: + * c = char + * Returns: isAlpha + */ +bool isAlpha(char c) { + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c == '_'); +} + +/** + * Checks if `c` is letter, `_` or digit + * Params: + * c = char + * Returns: isAlphaNumeric + */ +bool isAlphaNumeric(char c) { + return isAlpha(c) && isDigit(c); +} + +/** + * Checks if `c` is digit + * Params: + * c = char + * Returns: isDigit + */ +bool isDigit(char c) { + return c >= '0' && c <= '9'; +} + +/** + * Checks if `c` is hexadecimal (all digits & letters from A to F) + * Params: + * c = char + * Returns: isHex + */ +bool isHex(char c) { + return (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); +} + +/** + * Checks if `c` is octal number (0 <= c <= 7) + * Params: + * c = char + * Returns: isOct + */ +bool isOct(char c) { + return (c >= '0' && c <= '7'); +} \ No newline at end of file diff --git a/src/sily/bashfmt.d b/src/sily/bashfmt.d new file mode 100644 index 0000000..132e63d --- /dev/null +++ b/src/sily/bashfmt.d @@ -0,0 +1,76 @@ +module sily.bashfmt; + +alias FG = Foreground; +alias BG = Background; +alias FM = Formatting; +alias RS = FormattingReset; + +enum Foreground : string { + reset = "\033[39m", + black = "\033[30m", + red = "\033[31m", + green = "\033[32m", + yellow = "\033[33m", + blue = "\033[34m", + magenta = "\033[35m", + cyan = "\033[36m", + ltgray = "\033[37m", + dkgray = "\033[90m", + ltred = "\033[91m", + ltgreen = "\033[92m", + ltyellow = "\033[93m", + ltblue = "\033[94m", + ltmagenta = "\033[95m", + ltcyan = "\033[96m", + white = "\033[97m" +} + +enum Background : string { + reset = "\033[49m", + black = "\033[40m", + red = "\033[41m", + green = "\033[42m", + yellow = "\033[43m", + blue = "\033[44m", + magenta = "\033[45m", + cyan = "\033[46m", + ltgray = "\033[47m", + dkgray = "\033[100m", + ltred = "\033[101m", + ltgreen = "\033[102m", + ltyellow = "\033[103m", + ltblue = "\033[104m", + ltmagenta = "\033[105m", + ltcyan = "\033[106m", + white = "\033[107m" +} + +enum Formatting : string { + bold = "\033[1m", + dim = "\033[2m", + italics = "\033[3m", + uline = "\033[4m", + blink = "\033[5m", + inverse = "\033[7m", + hidden = "\033[8m", + striked = "\033[9m", + dline = "\033[21m", + cline = "\033[4:3m", + oline = "\033[53" +} + +enum FormattingReset : string { + reset = "\033[0m", + + bold = "\033[21m", + dim = "\033[22m", + italics = "\033[22m", + uline = "\033[24m", + blink = "\033[25m", + inverse = "\033[27m", + hidden = "\033[28m", + striked = "\033[29m", + dline = "\033[24m", + cline = "\033[4:0m", + oline = "\033[55m" +} \ No newline at end of file diff --git a/src/sily/getopt.d b/src/sily/getopt.d new file mode 100644 index 0000000..cc752f0 --- /dev/null +++ b/src/sily/getopt.d @@ -0,0 +1,79 @@ +module sily.getopt; + +import std.getopt: Option; +import std.algorithm: max; +import std.stdio: writefln; + +/** + * Prints passed **Option**s and text in aligned manner on stdout, i.e: + * ``` + * A simple cli tool + * + * Usage: + * scli [options] [script] \ + * scli run [script] + * + * Options: + * -h, --help This help information. \ + * -c, --check Check syntax without running. \ + * --quiet Run silently (no output). + * Commands: + * run Runs script. \ + * compile Compiles script. + * ``` + * Params: + * text = Text to be printed at the beginning of the help output + * usage = Usage string + * com = Commands + * opt = The **Option** extracted from the **getopt** parameter + */ +void printGetopt(string text, string usage, Commands[] com, Option[] opt) { + size_t maxLen = 0; + + foreach (it; opt) { + int sep = it.optShort == "" ? 0 : 2; + maxLen = max(maxLen, it.optShort.length + it.optLong.length + sep); + } + + foreach (it; com) { + maxLen = max(maxLen, it.name.length); + } + + if (text != "") { + writefln(text); + } + + if (usage != "") { + if (text != "") writefln(""); + writefln("Usage:"); + writefln(" " ~ usage); + } + + if (com.length != 0) { + if (text != "" || usage != "") writefln(""); + writefln("Options:"); + } + + foreach (it; opt) { + // writefln("%*s %*s%s%s", + // shortLen, it.optShort, + // longLen, it.optLong, + // it.required ? " Required: " : " ", it.help); + string opts = it.optShort ~ (it.optShort == "" ? "" : ", ") ~ it.optLong; + writefln(" %-*s %s", maxLen, opts, it.help); + } + + if (com.length != 0) { + if (text != "" || usage != "" || com.length != 0) writefln(""); + writefln("Commands:"); + } + + foreach (it; com) { + writefln(" %-*s %s", maxLen, it.name, it.help); + } +} + +struct Commands { + string name; + string help; +} \ No newline at end of file