summaryrefslogtreecommitdiff
path: root/lib/apps
diff options
context:
space:
mode:
authorAylur <[email protected]>2024-09-01 14:17:36 +0200
committerAylur <[email protected]>2024-09-01 14:17:36 +0200
commit3e3f045d650a839d21f7b649da7aa5c19bd2e38b (patch)
tree9a974eb0d38932d474940288c662bd1f01ea3088 /lib/apps
parent408faee16911ccfaa3e7dad69f9938fd4a696704 (diff)
monorepo structuring
Diffstat (limited to 'lib/apps')
-rw-r--r--lib/apps/application.vala118
-rw-r--r--lib/apps/apps.vala172
-rw-r--r--lib/apps/cli.vala66
-rw-r--r--lib/apps/config.vala.in6
-rw-r--r--lib/apps/meson.build95
-rw-r--r--lib/apps/meson_options.txt11
-rw-r--r--lib/apps/version1
7 files changed, 469 insertions, 0 deletions
diff --git a/lib/apps/application.vala b/lib/apps/application.vala
new file mode 100644
index 0000000..5748fc6
--- /dev/null
+++ b/lib/apps/application.vala
@@ -0,0 +1,118 @@
+namespace AstalApps {
+public class Application : Object {
+ public DesktopAppInfo app { get; construct set; }
+ public int frequency { get; set; default = 0; }
+ public string name { get { return app.get_name(); } }
+ public string entry { get { return app.get_id(); } }
+ public string description { get { return app.get_description(); } }
+ public string wm_class { get { return app.get_startup_wm_class(); } }
+ public string executable { owned get { return app.get_string("Exec"); } }
+ public string icon_name { owned get { return app.get_string("Icon"); } }
+
+ internal Application(string id, int? frequency = 0) {
+ Object(app: new DesktopAppInfo(id));
+ this.frequency = frequency;
+ }
+
+ public string get_key(string key) {
+ return app.get_string(key);
+ }
+
+ public bool launch() {
+ try {
+ var s = app.launch(null, null);
+ ++frequency;
+ return s;
+ } catch (Error err) {
+ critical(err.message);
+ return false;
+ }
+ }
+
+ public Score fuzzy_match(string term) {
+ var score = Score();
+ if (name != null)
+ score.name = levenshtein(term, name);
+ if (entry != null)
+ score.entry = levenshtein(term, entry);
+ if (executable != null)
+ score.executable = levenshtein(term, executable);
+ if (description != null)
+ score.description = levenshtein(term, description);
+
+ return score;
+ }
+
+ public Score exact_match(string term) {
+ var score = Score();
+ if (name != null)
+ score.name = name.down().contains(term.down()) ? 1 : 0;
+ if (entry != null)
+ score.entry = entry.down().contains(term.down()) ? 1 : 0;
+ if (executable != null)
+ score.executable = executable.down().contains(term.down()) ? 1 : 0;
+ if (description != null)
+ score.description = description.down().contains(term.down()) ? 1 : 0;
+
+ return score;
+ }
+
+ internal Json.Node to_json() {
+ return new Json.Builder()
+ .begin_object()
+ .set_member_name("name").add_string_value(name)
+ .set_member_name("entry").add_string_value(entry)
+ .set_member_name("executable").add_string_value(executable)
+ .set_member_name("description").add_string_value(description)
+ .set_member_name("icon_name").add_string_value(icon_name)
+ .set_member_name("frequency").add_int_value(frequency)
+ .end_object()
+ .get_root();
+ }
+}
+
+int min3(int a, int b, int c) {
+ return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c);
+}
+
+double levenshtein(string s1, string s2) {
+ int len1 = s1.length;
+ int len2 = s2.length;
+
+ int[, ] d = new int[len1 + 1, len2 + 1];
+
+ for (int i = 0; i <= len1; i++) {
+ d[i, 0] = i;
+ }
+ for (int j = 0; j <= len2; j++) {
+ d[0, j] = j;
+ }
+
+ for (int i = 1; i <= len1; i++) {
+ for (int j = 1; j <= len2; j++) {
+ int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1;
+ d[i, j] = min3(
+ d[i - 1, j] + 1, // deletion
+ d[i, j - 1] + 1, // insertion
+ d[i - 1, j - 1] + cost // substitution
+ );
+ }
+ }
+
+ var distance = d[len1, len2];
+ int max_len = len1 > len2 ? len1 : len2;
+
+ if (max_len == 0) {
+ return 1.0;
+ }
+
+ return 1.0 - ((double)distance / max_len);
+}
+
+public struct Score {
+ double name;
+ double entry;
+ double executable;
+ double description;
+}
+}
diff --git a/lib/apps/apps.vala b/lib/apps/apps.vala
new file mode 100644
index 0000000..2a0d507
--- /dev/null
+++ b/lib/apps/apps.vala
@@ -0,0 +1,172 @@
+namespace AstalApps {
+public class Apps : Object {
+ private string cache_directory;
+ private string cache_file;
+ private List<Application> _list;
+ private HashTable<string, int> frequents { get; private set; }
+
+ public bool show_hidden { get; set; }
+ public List<weak Application> list { owned get { return _list.copy(); } }
+
+ public double min_score { get; set; default = 0.5; }
+
+ public double name_multiplier { get; set; default = 2; }
+ public double entry_multiplier { get; set; default = 1; }
+ public double executable_multiplier { get; set; default = 1; }
+ public double description_multiplier { get; set; default = 0.5; }
+
+ public bool include_name { get; set; default = true; }
+ public bool include_entry { get; set; default = false; }
+ public bool include_executable { get; set; default = false; }
+ public bool include_description { get; set; default = false; }
+
+ construct {
+ cache_directory = Environment.get_user_cache_dir() + "/astal";
+ cache_file = cache_directory + "/apps-frequents.json";
+ frequents = new HashTable<string, int>(str_hash, str_equal);
+
+ AppInfoMonitor.get().changed.connect(() => {
+ reload();
+ });
+
+ if (FileUtils.test(cache_file, FileTest.EXISTS)) {
+ try {
+ uint8[] content;
+ File.new_for_path(cache_file).load_contents(null, out content, null);
+
+ var parser = new Json.Parser();
+ parser.load_from_data((string)content);
+ var obj = parser.get_root().get_object();
+ foreach (var member in obj.get_members()) {
+ var v = obj.get_member(member).get_value().get_int64();
+ frequents.set(member, (int)v);
+ }
+ } catch (Error err) {
+ critical("cannot read cache: %s\n", err.message);
+ }
+ }
+
+ reload();
+ }
+
+ private double score (string search, Application a, bool exact) {
+ var am = exact ? a.exact_match(search) : a.fuzzy_match(search);
+ double r = 0;
+
+ if (include_name)
+ r += am.name * name_multiplier;
+ if (include_entry)
+ r += am.entry * entry_multiplier;
+ if (include_executable)
+ r += am.executable * executable_multiplier;
+ if (include_description)
+ r += am.description * description_multiplier;
+
+ return r;
+ }
+
+ public List<weak Application> query(string? search = "", bool exact = false) {
+ if (search == null)
+ search = "";
+
+ var arr = list.copy();
+
+ // empty search, sort by frequency
+ if (search == "") {
+ arr.sort_with_data((a, b) => {
+ return (int)b.frequency - (int)a.frequency;
+ });
+
+ return arr;
+ }
+
+ // single character, sort by frequency and exact match
+ if (search.length == 1) {
+ foreach (var app in list) {
+ if (score(search, app, true) == 0)
+ arr.remove(app);
+ }
+
+ arr.sort_with_data((a, b) => {
+ return (int)b.frequency - (int)a.frequency;
+ });
+
+ return arr;
+ }
+
+ // filter
+ foreach (var app in list) {
+ if (score(search, app, exact) < min_score)
+ arr.remove(app);
+ }
+
+ // sort by score, frequency
+ arr.sort_with_data((a, b) => {
+ var s1 = score(search, a, exact);
+ var s2 = score(search, b, exact);
+
+ if (s1 == s2)
+ return (int)b.frequency - (int)a.frequency;
+
+ return s1 < s2 ? 1 : -1;
+ });
+
+ return arr;
+ }
+
+ public List<weak Application> fuzzy_query(string? search = "") {
+ return query(search, false);
+ }
+
+ public List<weak Application> exact_query(string? search = "") {
+ return query(search, true);
+ }
+
+ public void reload() {
+ var arr = AppInfo.get_all();
+
+ _list = new List<Application>();
+ foreach (var app in arr) {
+ if (!show_hidden && !app.should_show())
+ continue;
+
+ var a = new Application(
+ app.get_id(),
+ frequents.get(app.get_id())
+ );
+ a.notify.connect((pspec) => {
+ if (pspec.name != "frequency")
+ return;
+
+ var f = frequents.get(app.get_id());
+ frequents.set(app.get_id(), ++f);
+
+ _list.sort((a, b) => {
+ return (int)a.frequency - (int)b.frequency;
+ });
+ cache();
+ });
+ _list.append(a);
+ }
+
+ cache();
+ }
+
+ private void cache() {
+ var json = new Json.Builder().begin_object();
+ foreach (string key in frequents.get_keys())
+ json.set_member_name(key).add_int_value(frequents.get(key));
+
+ try {
+ if (!FileUtils.test(cache_directory, FileTest.EXISTS))
+ File.new_for_path(cache_directory).make_directory_with_parents(null);
+
+ var generator = new Json.Generator();
+ generator.set_root(json.end_object().get_root());
+ FileUtils.set_contents_full(cache_file, generator.to_data(null));
+ } catch (Error err) {
+ critical("cannot cache frequents: %s", err.message);
+ }
+ }
+}
+}
diff --git a/lib/apps/cli.vala b/lib/apps/cli.vala
new file mode 100644
index 0000000..d926c87
--- /dev/null
+++ b/lib/apps/cli.vala
@@ -0,0 +1,66 @@
+static bool help;
+static bool version;
+static string search;
+static string launch;
+static bool json;
+
+const OptionEntry[] options = {
+ { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null },
+ { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null },
+ { "search", 's', OptionFlags.NONE, OptionArg.STRING, ref search, null, null },
+ { "launch", 'l', OptionFlags.NONE, OptionArg.STRING, ref launch, null, null },
+ { "json", 'j', OptionFlags.NONE, OptionArg.NONE, ref json, null, null },
+ { null },
+};
+
+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(" -s, --search Sort by a search term\n");
+ print(" -l, --launch Launch an application\n");
+ print(" -j, --json Print list in json format\n");
+ return 0;
+ }
+
+ if (version) {
+ print(AstalApps.VERSION);
+ return 0;
+ }
+
+ var apps = new AstalApps.Apps();
+
+ if (launch != null) {
+ apps.query(launch).first().data.launch();
+ return 0;
+ }
+
+ if (json) {
+ var b = new Json.Builder().begin_array();
+ foreach (var app in apps.query(search))
+ b.add_value(app.to_json());
+
+ var generator = new Json.Generator();
+ generator.set_root(b.end_array().get_root());
+ stdout.printf(generator.to_data(null));
+ } else {
+ foreach (var app in apps.query(search))
+ stdout.printf("%s\n", app.entry);
+ }
+
+ return 0;
+}
diff --git a/lib/apps/config.vala.in b/lib/apps/config.vala.in
new file mode 100644
index 0000000..b3a9f49
--- /dev/null
+++ b/lib/apps/config.vala.in
@@ -0,0 +1,6 @@
+namespace AstalApps {
+ 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/apps/meson.build b/lib/apps/meson.build
new file mode 100644
index 0000000..fb87e22
--- /dev/null
+++ b/lib/apps/meson.build
@@ -0,0 +1,95 @@
+project(
+ 'astal-apps',
+ '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 = 'AstalApps-' + api_version + '.gir'
+typelib = 'AstalApps-' + 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,
+ 'apps.vala',
+ 'application.vala',
+ 'cli.vala',
+]
+
+if get_option('lib')
+ lib = library(
+ meson.project_name(),
+ sources,
+ dependencies: deps,
+ vala_header: meson.project_name() + '.h',
+ vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
+ vala_gir: gir,
+ version: meson.project_version(),
+ install: true,
+ install_dir: [true, true, true, true],
+ )
+
+ 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',
+ )
+
+ 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,
+ install: true,
+ install_dir: get_option('libdir') / 'girepository-1.0',
+ )
+endif
+
+if get_option('cli')
+ executable(
+ meson.project_name(),
+ ['cli.vala', sources],
+ dependencies: deps,
+ install: true,
+ )
+endif
diff --git a/lib/apps/meson_options.txt b/lib/apps/meson_options.txt
new file mode 100644
index 0000000..f110242
--- /dev/null
+++ b/lib/apps/meson_options.txt
@@ -0,0 +1,11 @@
+option(
+ 'lib',
+ type: 'boolean',
+ value: true,
+)
+
+option(
+ 'cli',
+ type: 'boolean',
+ value: true,
+)
diff --git a/lib/apps/version b/lib/apps/version
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/lib/apps/version
@@ -0,0 +1 @@
+0.1.0