diff --git a/src/app.ts b/src/app.ts index e64c2a5..031108d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -180,8 +180,8 @@ export default class App extends Gtk.Application { }); this.emit('config-parsed'); - } catch (error) { - logError(error as Error); + } catch (_) { + print(`No config found at ${App.configPath}`); } } diff --git a/src/service/apps.ts b/src/service/apps.ts index 4ea8abb..c45c046 100644 --- a/src/service/apps.ts +++ b/src/service/apps.ts @@ -1,79 +1,121 @@ import Gio from 'gi://Gio'; import Service from './service.js'; +import GObject from 'gi://GObject'; +import { CACHE_DIR, ensureDirectory, readFile, writeFile } from '../utils.js'; -interface App { - app: Gio.DesktopAppInfo - name: string - desktop: string | null - description: string | null - wmClass: string | null - executable: string - iconName: string - launch: () => void - match: (term: string) => boolean +const APPS_CACHE_DIR = `${CACHE_DIR}/apps`; +const CACHE_FILE = APPS_CACHE_DIR + '/apps_frequency.json'; + +class Application extends GObject.Object { + static { GObject.registerClass(this); } + + app: Gio.DesktopAppInfo; + frequency: number; + name: string; + desktop: string | null; + description: string | null; + wmClass: string | null; + executable: string; + iconName: string; + service: ApplicationsService; + + constructor(app: Gio.DesktopAppInfo, service: ApplicationsService) { + super(); + this.service = service; + this.app = app; + this.name = app.get_name(); + this.desktop = app.get_id(); + this.executable = app.get_executable(); + this.description = app.get_description(); + this.iconName = this._iconName(app); + this.wmClass = app.get_startup_wm_class(); + this.frequency = this.desktop && service.frequents[this.desktop] || 0; + } + + _iconName(app: any): string { + if (!app.get_icon()) + return ''; + + if (typeof app.get_icon()?.get_names !== 'function') + return ''; + + const name = app.get_icon()?.get_names()[0]; + return name || ''; + } + + _match(prop: string | null, search: string) { + if (!prop) + return false; + + if (!search) + return true; + + return prop?.toLowerCase().includes(search.toLowerCase()); + } + + match(term: string) { + const { name, desktop, description, executable } = this; + return this._match(name, term) || + this._match(desktop, term) || + this._match(executable, term) || + this._match(description, term); + } + + launch() { + this.frequency++; + this.service._launched(this.desktop); + this.app.launch([], null); + } } -function _appIconName(app: any): string { - if (!app.get_icon()) - return ''; - - if (typeof app.get_icon()?.get_names !== 'function') - return ''; - - const name = app.get_icon()?.get_names()[0]; - return name || ''; -} - -function _match(prop: string | null, search: string): boolean { - if (!prop) - return false; - - if (!search) - return true; - - return prop?.toLowerCase().includes(search.toLowerCase()); -} - -function _search(app: App, search: string): boolean { - const { name, desktop, description, executable } = app; - return _match(name, search) || - _match(desktop, search) || - _match(executable, search) || - _match(description, search); -} - -const _wrapper = (app: Gio.DesktopAppInfo): App => ({ - app, - name: app.get_name(), - desktop: app.get_id(), - executable: app.get_executable(), - description: app.get_description(), - iconName: _appIconName(app), - wmClass: app.get_startup_wm_class(), - launch: () => app.launch([], null), - match: (term: string) => _search(_wrapper(app), term), -}); - class ApplicationsService extends Service { - static { Service.register(this); } - private _list!: App[]; + static { + Service.register(this, { + 'launched': ['string'], + }); + } + + private _list!: Application[]; + frequents: { [app: string]: number }; query(term: string) { - return this._list.filter(app => _search(app, term)); + return this._list.filter(app => app.match(term)).sort((a, b) => { + return a.frequency < b.frequency ? 1 : 0; + }); } constructor() { super(); Gio.AppInfoMonitor.get().connect('changed', this._sync.bind(this)); + + try { + this.frequents = JSON.parse(readFile(CACHE_FILE)) as { [app: string]: number }; + } catch (_) { + this.frequents = {}; + } + this._sync(); } + _launched(id: string | null) { + if (!id) + return; + + typeof this.frequents[id] === 'number' + ? this.frequents[id] += 1 + : this.frequents[id] = 1; + + ensureDirectory(APPS_CACHE_DIR); + const json = JSON.stringify(this.frequents, null, 2); + writeFile(json, CACHE_FILE).catch(logError); + } + _sync() { this._list = Gio.AppInfo.get_all() .filter(app => app.should_show()) .map(app => Gio.DesktopAppInfo.new(app.get_id() || '')) .filter(app => app) - .map(app => _wrapper(app)); + .map(app => new Application(app, this)); this.emit('changed'); } @@ -88,6 +130,10 @@ export default class Applications { return Applications._instance; } + static frequents() { + return Applications.instance.frequents; + } + static query(term: string) { return Applications.instance.query(term); } diff --git a/src/service/mpris.ts b/src/service/mpris.ts index a1b4c27..9631aec 100644 --- a/src/service/mpris.ts +++ b/src/service/mpris.ts @@ -5,7 +5,9 @@ import Service from './service.js'; import { ensureDirectory, timeout } from '../utils.js'; import { MprisPlayerProxy, MprisProxy, TMprisProxy, TPlayerProxy, MprisMetadata } from '../dbus/mpris.js'; import { DBusProxy, TDBusProxy } from '../dbus/dbus.js'; -import { MEDIA_CACHE_PATH } from '../utils.js'; +import { CACHE_DIR } from '../utils.js'; + +const MEDIA_CACHE_PATH = `${CACHE_DIR}/media`; type PlaybackStatus = 'Playing' | 'Paused' | 'Stopped'; type LoopStatus = 'None' | 'Track' | 'Playlist'; @@ -144,8 +146,7 @@ class MprisPlayer extends GObject.Object { if (GLib.file_test(coverPath, GLib.FileTest.EXISTS)) return; - ensureDirectory(); - + ensureDirectory(MEDIA_CACHE_PATH); Gio.File.new_for_uri(trackCoverUrl).copy_async( Gio.File.new_for_path(coverPath), Gio.FileCopyFlags.OVERWRITE, diff --git a/src/service/notifications.ts b/src/service/notifications.ts index 3e4fd8c..dda650b 100644 --- a/src/service/notifications.ts +++ b/src/service/notifications.ts @@ -4,7 +4,10 @@ import GLib from 'gi://GLib'; import Service from './service.js'; import App from '../app.js'; import { NotificationIFace } from '../dbus/notifications.js'; -import { NOTIFICATIONS_CACHE_PATH, ensureDirectory, readFileAsync, timeout, writeFile } from '../utils.js'; +import { CACHE_DIR, ensureDirectory, readFileAsync, timeout, writeFile } from '../utils.js'; + +const NOTIFICATIONS_CACHE_PATH = `${CACHE_DIR}/notifications`; +const CACHE_FILE = NOTIFICATIONS_CACHE_PATH + '/notifications.json'; interface action { id: string @@ -183,8 +186,7 @@ class NotificationsService extends Service { async _readFromFile() { try { - const path = NOTIFICATIONS_CACHE_PATH + '/notifications.json'; - const file = await readFileAsync(path); + const file = await readFileAsync(CACHE_FILE); const notifications = JSON.parse(file as string) as Notification[]; notifications.forEach(n => { if (n.id > this._idCount) @@ -207,7 +209,7 @@ class NotificationsService extends Service { if (!image_data) return null; - ensureDirectory(); + ensureDirectory(NOTIFICATIONS_CACHE_PATH); const fileName = NOTIFICATIONS_CACHE_PATH + '/' + name.replace(/[^a-zA-Z0-9]/g, ''); const image = image_data.recursiveUnpack(); const pixbuf = GdkPixbuf.Pixbuf.new_from_bytes( @@ -237,8 +239,8 @@ class NotificationsService extends Service { notifications.push(n); } - ensureDirectory(); - writeFile(JSON.stringify(notifications, null, 2), NOTIFICATIONS_CACHE_PATH + '/notifications.json').catch(); + ensureDirectory(NOTIFICATIONS_CACHE_PATH); + writeFile(JSON.stringify(notifications, null, 2), CACHE_FILE).catch(logError); } } diff --git a/src/utils.ts b/src/utils.ts index fea6ac3..7117535 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,8 +5,6 @@ import GObject from 'gi://GObject'; export const USER = GLib.get_user_name(); export const CACHE_DIR = `${GLib.get_user_cache_dir()}/${pkg.name}`; -export const MEDIA_CACHE_PATH = `${CACHE_DIR}/media`; -export const NOTIFICATIONS_CACHE_PATH = `${CACHE_DIR}/notifications`; export function readFile(path: string) { const f = Gio.File.new_for_path(path); @@ -129,19 +127,8 @@ export function lookUpIcon(name?: string, size = 16) { } export function ensureDirectory(path?: string) { - if (path && !GLib.file_test(path, GLib.FileTest.EXISTS)) { + if (path && !GLib.file_test(path, GLib.FileTest.EXISTS)) Gio.File.new_for_path(path).make_directory_with_parents(null); - } - else { - [ - MEDIA_CACHE_PATH, - NOTIFICATIONS_CACHE_PATH, - ] - .forEach(path => { - if (!GLib.file_test(path, GLib.FileTest.EXISTS)) - Gio.File.new_for_path(path).make_directory_with_parents(null); - }); - } } export function execAsync(cmd: string | string[]): Promise {