diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/application.vala | 63 | ||||
-rw-r--r-- | src/apps.vala | 106 | ||||
-rw-r--r-- | src/cli.vala | 66 | ||||
-rw-r--r-- | src/config.vala.in | 6 | ||||
-rw-r--r-- | src/match.vala | 103 | ||||
-rw-r--r-- | src/meson.build | 80 |
6 files changed, 424 insertions, 0 deletions
diff --git a/src/application.vala b/src/application.vala new file mode 100644 index 0000000..b5cdcfd --- /dev/null +++ b/src/application.vala @@ -0,0 +1,63 @@ +namespace AstalApps { +public class Application : Object { + public DesktopAppInfo app { get; construct set; } + public uint frequency { get; set; } + 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, uint? frequency = 0) { + Object(app: new DesktopAppInfo(id), frequency: frequency); + } + + public string get_key(string key) { + return app.get_string(key); + } + + public void launch() { + try { + app.launch(null, null); + ++frequency; + } catch (Error err) { + critical(err.message); + } + } + + public double match(string term) { + int n = 0; + double score = 0; + if (name != null) { + score += jaro_winkler_similarity(term, name); + ++n; + } + if (entry != null) { + score += jaro_winkler_similarity(term, entry); + ++n; + } + if (executable != null) { + score += jaro_winkler_similarity(term, executable); + ++n; + } + if (description != null) { + score += levenshtein_distance(term, description); + ++n; + } + return n > 0 ? score / n : 0; + } + + 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) + .end_object() + .get_root(); + } +} +} diff --git a/src/apps.vala b/src/apps.vala new file mode 100644 index 0000000..a9524b9 --- /dev/null +++ b/src/apps.vala @@ -0,0 +1,106 @@ +namespace AstalApps { +public class Apps : Object { + private string cache_directory; + private string cache_file; + private List<Application> _list; + private HashTable<string, uint> frequents { get; private set; } + + public bool show_hidden { get; set; } + public List<weak Application> list { owned get { return _list.copy(); } } + + construct { + cache_directory = Environment.get_user_cache_dir() + "/astal"; + cache_file = cache_directory + "/apps-frequents.json"; + frequents = new HashTable<string, uint>(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, (uint)v); + } + } catch (Error err) { + critical("cannot read cache: %s\n", err.message); + } + } + + reload(); + } + + private int compare (string search, Application a, Application b) { + var s1 = a.match(search) * 100; + var s2 = b.match(search) * 100; + return (int)s2 - (int)s1; + } + + public List<weak Application> query(string? search = "") { + if (search == null) + return list.copy(); + + var arr = list.copy(); + arr.sort_with_data((a, b) => { + return compare(search, a, b); + }); + return arr; + } + + 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()) { + uint v = frequents.get(key); + json.set_member_name(key).add_int_value((int)v); + } + + 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/src/cli.vala b/src/cli.vala new file mode 100644 index 0000000..119502d --- /dev/null +++ b/src/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("Cli client for astal-notifd\n\n"); + 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 %f\n", app.entry, app.match(search)); + } + + return 0; +} diff --git a/src/config.vala.in b/src/config.vala.in new file mode 100644 index 0000000..b3a9f49 --- /dev/null +++ b/src/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/src/match.vala b/src/match.vala new file mode 100644 index 0000000..8158521 --- /dev/null +++ b/src/match.vala @@ -0,0 +1,103 @@ +namespace AstalApps { +int max(int i1, int i2) { return i1 > i2 ? i1 : i2; } + +int min(int i1, int i2) { return i1 > i2 ? i2 : i1; } + +int min3(int a, int b, int c) { + return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c); +} + +double jaro_winkler_similarity(string s1, string s2) { + int len1 = s1.length; + int len2 = s2.length; + + if (len1 == 0 && len2 == 0) + return 1.0; + if (len1 == 0 || len2 == 0) + return 0.0; + + int match_distance = (max(len1, len2) / 2) - 1; + bool[] s1_matches = new bool[len1]; + bool[] s2_matches = new bool[len2]; + + int matches = 0; + int transpositions = 0; + + for (int i = 0; i < len1; i++) { + int start = max(0, i - match_distance); + int end = min(i + match_distance + 1, len2); + for (int j = start; j < end; j++) { + if (s2_matches[j]) + continue; + if (s1[i] != s2[j]) + continue; + s1_matches[i] = true; + s2_matches[j] = true; + matches++; + break; + } + } + + if (matches == 0) + return 0.0; + + int k = 0; + for (int i = 0; i < len1; i++) { + if (!s1_matches[i]) + continue; + while (!s2_matches[k]) + k++; + if (s1[i] != s2[k]) + transpositions++; + k++; + } + + double m = matches; + double jaro = + ((m / len1) + (m / len2) + ((m - transpositions / 2.0) / m)) / 3.0; + + int prefix = 0; + for (int i = 0; i < min(len1, len2); i++) { + if (s1[i] == s2[i]) + prefix++; + else + break; + } + + double jaro_winkler = jaro + (prefix * 0.1 * (1.0 - jaro)); + return jaro_winkler; +} + +double levenshtein_distance(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 = max(s1.length, s2.length); + + if (max_len == 0) { + return 1.0; + } + + return 1.0 - ((double)distance / max_len); +} +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..ae9e763 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,80 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalApps-' + api_version + '.gir' +typelib = 'AstalApps-' + api_version + '.typelib' +so = 'libastal-apps.so.' + meson.project_version() + +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, + 'match.vala', + '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() + '.vapi', + vala_gir: gir, + version: meson.project_version(), + install: true, + install_dir: [true, true, true, true], + ) + + import('pkgconfig').generate( + description: 'libastal-notifd', + libraries: 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 |