summaryrefslogtreecommitdiff
path: root/lib/apps/apps.vala
blob: 999643c3543ab884122e5d60ef98267126ed8e6d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
/**
 * This object can be used to query applications.
 * Multipliers can be set to customize [[email protected]] results
 * from queries which then are summed and sorted accordingly.
 */
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(); } }

    /**
     * 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 `0`
     */
    public double entry_multiplier { get; set; default = 0; }

    /**
     * Extra multiplier to apply when matching the executable of an application.
     * Defaults to `0.5`
     */
    public double executable_multiplier { get; set; default = 0.5; }

    /**
     * Extra multiplier to apply when matching the description of an application.
     * Defaults to `0`
     */
    public double description_multiplier { get; set; default = 0; }

    /**
     * Extra multiplier to apply when matching the keywords of an application.
     * Defaults to `0.5`
     */
    public double keywords_multiplier { get; set; default = 0.5; }

    /**
     * Extra multiplier to apply when matching the categories of an application.
     * Defaults to `0`
     */
    public double categories_multiplier { get; set; default = 0; }

    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, SearchAlgorithm alg) {
        var s = Score();
        double r = 0;

        if (alg == FUZZY) s = a.fuzzy_match(search);
        if (alg == EXACT) s = a.exact_match(search);

        r += s.name * name_multiplier;
        r += s.entry * entry_multiplier;
        r += s.executable * executable_multiplier;
        r += s.description * description_multiplier;
        r += s.keywords * keywords_multiplier;
        r += s.categories * categories_multiplier;

        return r;
    }

    /**
     * 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 = "";

        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, alg) == 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, alg) < min_score)
                arr.remove(app);
        }

        // sort by score, frequency
        arr.sort_with_data((a, b) => {
            var s1 = score(search, a, alg);
            var s2 = score(search, b, alg);

            if (s1 == s2)
                return (int)b.frequency - (int)a.frequency;

            return s1 < s2 ? 1 : -1;
        });

        return arr;
    }

    /**
     * Query the `list` of applications with a fuzzy matching algorithm.
     */
    public List<weak Application> fuzzy_query(string? search = "") {
        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, EXACT);
    }

    /**
     * Reload the `list` of Applications.
     */
    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);
        }
    }
}

private enum AstalApps.SearchAlgorithm {
    EXACT,
    FUZZY,
}