summaryrefslogtreecommitdiff
path: root/lib/apps
diff options
context:
space:
mode:
Diffstat (limited to 'lib/apps')
-rw-r--r--lib/apps/application.vala135
-rw-r--r--lib/apps/apps.vala128
-rw-r--r--lib/apps/fuzzy.vala73
l---------lib/apps/gir.py1
-rw-r--r--lib/apps/meson.build47
5 files changed, 299 insertions, 85 deletions
diff --git a/lib/apps/application.vala b/lib/apps/application.vala
index 5748fc6..0a2f73c 100644
--- a/lib/apps/application.vala
+++ b/lib/apps/application.vala
@@ -1,23 +1,68 @@
-namespace AstalApps {
-public class Application : Object {
+public class AstalApps.Application : Object {
+ /**
+ * The underlying DesktopAppInfo.
+ */
public DesktopAppInfo app { get; construct set; }
+
+ /**
+ * The number of times [[email protected]] was called on this Application.
+ */
public int frequency { get; set; default = 0; }
+
+ /**
+ * The name of this Application.
+ */
public string name { get { return app.get_name(); } }
+
+ /**
+ * Name of the .desktop of this Application.
+ */
public string entry { get { return app.get_id(); } }
+
+ /**
+ * Description of this Application.
+ */
public string description { get { return app.get_description(); } }
+
+ /**
+ * `StartupWMClass` field from the desktop file.
+ * This represents the `WM_CLASS` property of the main window of the application.
+ */
public string wm_class { get { return app.get_startup_wm_class(); } }
+
+ /**
+ * `Exec` field from the desktop file.
+ * Note that if you want to launch this Application you should use the [[email protected]] method.
+ */
public string executable { owned get { return app.get_string("Exec"); } }
+
+ /**
+ * `Icon` field from the desktop file.
+ * This is usually a named icon or a path to a file.
+ */
public string icon_name { owned get { return app.get_string("Icon"); } }
+ /**
+ * `Keywords` field from the desktop file.
+ */
+ public string[] keywords { owned get { return app.get_keywords(); } }
+
internal Application(string id, int? frequency = 0) {
Object(app: new DesktopAppInfo(id));
this.frequency = frequency;
}
+ /**
+ * Get a value from the .desktop file by its key.
+ */
public string get_key(string key) {
return app.get_string(key);
}
+ /**
+ * Launches this application.
+ * The launched application inherits the environment of the launching process
+ */
public bool launch() {
try {
var s = app.launch(null, null);
@@ -29,22 +74,36 @@ public class Application : Object {
}
}
+ /**
+ * Calculate a score for an application using fuzzy matching algorithm.
+ */
public Score fuzzy_match(string term) {
var score = Score();
+
if (name != null)
- score.name = levenshtein(term, name);
+ score.name = fuzzy_match_string(term, name);
if (entry != null)
- score.entry = levenshtein(term, entry);
+ score.entry = fuzzy_match_string(term, entry);
if (executable != null)
- score.executable = levenshtein(term, executable);
+ score.executable = fuzzy_match_string(term, executable);
if (description != null)
- score.description = levenshtein(term, description);
+ score.description = fuzzy_match_string(term, description);
+ foreach (var keyword in keywords) {
+ var s = fuzzy_match_string(term, keyword);
+ if (s > score.keywords) {
+ score.keywords = s;
+ }
+ }
return score;
}
+ /**
+ * Calculate a score using exact string algorithm.
+ */
public Score exact_match(string term) {
var score = Score();
+
if (name != null)
score.name = name.down().contains(term.down()) ? 1 : 0;
if (entry != null)
@@ -53,12 +112,17 @@ public class Application : Object {
score.executable = executable.down().contains(term.down()) ? 1 : 0;
if (description != null)
score.description = description.down().contains(term.down()) ? 1 : 0;
+ foreach (var keyword in keywords) {
+ if (score.keywords == 0) {
+ score.keywords = keyword.down().contains(term.down()) ? 1 : 0;
+ }
+ }
return score;
}
internal Json.Node to_json() {
- return new Json.Builder()
+ var builder = new Json.Builder()
.begin_object()
.set_member_name("name").add_string_value(name)
.set_member_name("entry").add_string_value(entry)
@@ -66,53 +130,24 @@ public class Application : Object {
.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;
- }
+ .set_member_name("keywords")
+ .begin_array();
- 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
- );
+ foreach (string keyword in keywords) {
+ builder.add_string_value(keyword);
}
- }
- var distance = d[len1, len2];
- int max_len = len1 > len2 ? len1 : len2;
-
- if (max_len == 0) {
- return 1.0;
+ return builder
+ .end_array()
+ .end_object()
+ .get_root();
}
-
- return 1.0 - ((double)distance / max_len);
}
-public struct Score {
- double name;
- double entry;
- double executable;
- double description;
-}
+public struct AstalApps.Score {
+ int name;
+ int entry;
+ int executable;
+ int description;
+ int keywords;
}
diff --git a/lib/apps/apps.vala b/lib/apps/apps.vala
index 2a0d507..dde7d44 100644
--- a/lib/apps/apps.vala
+++ b/lib/apps/apps.vala
@@ -1,25 +1,84 @@
-namespace AstalApps {
-public class Apps : Object {
+public class AstalApps.Apps : Object {
private string cache_directory;
private string cache_file;
private List<Application> _list;
private HashTable<string, int> frequents { get; private set; }
+ /**
+ * Indicates wether hidden applications should included in queries.
+ */
public bool show_hidden { get; set; }
+
+ /**
+ * Full list of available applications.
+ */
public List<weak Application> list { owned get { return _list.copy(); } }
- public double min_score { get; set; default = 0.5; }
+ /**
+ * The minimum score the application has to meet in order to be included in queries.
+ */
+ public double min_score { get; set; default = 0; }
+ /**
+ * Extra multiplier to apply when matching the `name` of an application.
+ * Defaults to `2`
+ */
public double name_multiplier { get; set; default = 2; }
+
+ /**
+ * Extra multiplier to apply when matching the entry of an application.
+ * Defaults to `1`
+ */
public double entry_multiplier { get; set; default = 1; }
+
+ /**
+ * Extra multiplier to apply when matching the executable of an application.
+ * Defaults to `1`
+ */
public double executable_multiplier { get; set; default = 1; }
+
+ /**
+ * Extra multiplier to apply when matching the description of an application.
+ * Defaults to `0.5`
+ */
public double description_multiplier { get; set; default = 0.5; }
+ /**
+ * Extra multiplier to apply when matching the keywords of an application.
+ * Defaults to `0.5`
+ */
+ public double keywords_multiplier { get; set; default = 0.5; }
+
+ /**
+ * Consider the name of an application during queries.
+ * Defaults to `true`
+ */
public bool include_name { get; set; default = true; }
+
+ /**
+ * Consider the entry of an application during queries.
+ * Defaults to `false`
+ */
public bool include_entry { get; set; default = false; }
+
+ /**
+ * Consider the executable of an application during queries.
+ * Defaults to `false`
+ */
public bool include_executable { get; set; default = false; }
+
+ /**
+ * Consider the description of an application during queries.
+ * Defaults to `false`
+ */
public bool include_description { get; set; default = false; }
+ /**
+ * Consider the keywords of an application during queries.
+ * Defaults to `false`
+ */
+ public bool include_keywords { get; set; default = false; }
+
construct {
cache_directory = Environment.get_user_cache_dir() + "/astal";
cache_file = cache_directory + "/apps-frequents.json";
@@ -49,23 +108,39 @@ public class Apps : Object {
reload();
}
- private double score (string search, Application a, bool exact) {
- var am = exact ? a.exact_match(search) : a.fuzzy_match(search);
+ private double score(string search, Application a, SearchAlgorithm alg) {
+ var s = Score();
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;
+ if (alg == FUZZY) s = a.fuzzy_match(search);
+ if (alg == EXACT) s = a.exact_match(search);
+
+ if (include_name) r += s.name * name_multiplier;
+ if (include_entry) r += s.entry * entry_multiplier;
+ if (include_executable) r += s.executable * executable_multiplier;
+ if (include_description) r += s.description * description_multiplier;
+ if (include_keywords) r += s.keywords * keywords_multiplier;
return r;
}
- public List<weak Application> query(string? search = "", bool exact = false) {
+ /**
+ * Calculate a score for an application using fuzzy matching algorithm.
+ * Taking this Apps' include settings into consideration .
+ */
+ public double fuzzy_score(string search, Application a) {
+ return score(search, a, FUZZY);
+ }
+
+ /**
+ * Calculate a score for an application using exact string algorithm.
+ * Taking this Apps' include settings into consideration .
+ */
+ public double exact_score(string search, Application a) {
+ return score(search, a, EXACT);
+ }
+
+ internal List<weak Application> query(string? search = "", SearchAlgorithm alg = FUZZY) {
if (search == null)
search = "";
@@ -83,7 +158,7 @@ public class Apps : Object {
// single character, sort by frequency and exact match
if (search.length == 1) {
foreach (var app in list) {
- if (score(search, app, true) == 0)
+ if (score(search, app, alg) == 0)
arr.remove(app);
}
@@ -96,14 +171,14 @@ public class Apps : Object {
// filter
foreach (var app in list) {
- if (score(search, app, exact) < min_score)
+ if (score(search, app, alg) < 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);
+ var s1 = score(search, a, alg);
+ var s2 = score(search, b, alg);
if (s1 == s2)
return (int)b.frequency - (int)a.frequency;
@@ -114,14 +189,23 @@ public class Apps : Object {
return arr;
}
+ /**
+ * Query the `list` of applications with a fuzzy matching algorithm.
+ */
public List<weak Application> fuzzy_query(string? search = "") {
- return query(search, false);
+ return query(search, FUZZY);
}
+ /**
+ * Query the `list` of applications with a simple string matching algorithm.
+ */
public List<weak Application> exact_query(string? search = "") {
- return query(search, true);
+ return query(search, EXACT);
}
+ /**
+ * Reload the `list` of Applications.
+ */
public void reload() {
var arr = AppInfo.get_all();
@@ -169,4 +253,8 @@ public class Apps : Object {
}
}
}
+
+private enum AstalApps.SearchAlgorithm {
+ EXACT,
+ FUZZY,
}
diff --git a/lib/apps/fuzzy.vala b/lib/apps/fuzzy.vala
new file mode 100644
index 0000000..7745320
--- /dev/null
+++ b/lib/apps/fuzzy.vala
@@ -0,0 +1,73 @@
+namespace AstalApps {
+private int fuzzy_match_string(string pattern, string str) {
+ const int unmatched_letter_penalty = -1;
+ int score = 100;
+
+ if (pattern.length == 0) return score;
+ if (str.length < pattern.length) return int.MIN;
+
+ bool found = fuzzy_match_recurse(pattern, str, score, true, out score);
+ score += unmatched_letter_penalty * (str.length - pattern.length);
+
+ if(!found) score = -10;
+
+ return score;
+}
+
+private bool fuzzy_match_recurse(string pattern, string str, int score, bool first_char, out int result) {
+ result = score;
+ if (pattern.length == 0) return true;
+
+ int match_idx = 0;
+ int offset = 0;
+ unichar search = pattern.casefold().get_char(0);
+ int best_score = int.MIN;
+
+ while ((match_idx = str.casefold().substring(offset).index_of_char(search)) >= 0) {
+ offset += match_idx;
+ int subscore;
+ bool found = fuzzy_match_recurse(
+ pattern.substring(1),
+ str.substring(offset + 1),
+ compute_score(offset, first_char, str, offset), false, out subscore);
+ if(!found) break;
+ best_score = int.max(best_score, subscore);
+ offset++;
+ }
+
+ if (best_score == int.MIN) return false;
+ result += best_score;
+ return true;
+}
+
+private int compute_score(int jump, bool first_char, string match, int idx) {
+ const int adjacency_bonus = 15;
+ const int separator_bonus = 30;
+ const int camel_bonus = 30;
+ const int first_letter_bonus = 15;
+ const int leading_letter_penalty = -5;
+ const int max_leading_letter_penalty = -15;
+
+ int score = 0;
+
+ if (!first_char && jump == 0) {
+ score += adjacency_bonus;
+ }
+ if (!first_char || jump > 0) {
+ if (match[idx].isupper() && match[idx-1].islower()) {
+ score += camel_bonus;
+ }
+ if (match[idx].isalnum() && !match[idx-1].isalnum()) {
+ score += separator_bonus;
+ }
+ }
+ if (first_char && jump == 0) {
+ score += first_letter_bonus;
+ }
+ if (first_char) {
+ score += int.max(leading_letter_penalty * jump, max_leading_letter_penalty);
+ }
+
+ return score;
+}
+}
diff --git a/lib/apps/gir.py b/lib/apps/gir.py
new file mode 120000
index 0000000..b5b4f1d
--- /dev/null
+++ b/lib/apps/gir.py
@@ -0,0 +1 @@
+../gir.py \ No newline at end of file
diff --git a/lib/apps/meson.build b/lib/apps/meson.build
index fb87e22..eb7a90b 100644
--- a/lib/apps/meson.build
+++ b/lib/apps/meson.build
@@ -39,34 +39,41 @@ deps = [
dependency('json-glib-1.0'),
]
-sources = [
- config,
- 'apps.vala',
+sources = [config] + files(
'application.vala',
+ 'apps.vala',
'cli.vala',
-]
+ 'fuzzy.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',
- vala_gir: gir,
version: meson.project_version(),
install: true,
- install_dir: [true, true, true, true],
+ install_dir: [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',
+ 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(
@@ -79,10 +86,20 @@ if get_option('lib')
],
input: lib,
output: typelib,
- depends: lib,
+ 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')