Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions debug/run-memory-scenarios.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ usage() {
Usage: run-memory-scenarios.sh [OPTIONS]

Options:
--scenario NAME launcher, launcher-no-theme, wallpaper, wallpaper-no-theme, power-menu, theme-only, css-only, cc, cc-no-theme, date-weather, date-weather-no-theme, date-weather-css-only, notifications, notifications-date-weather, notifications-date-weather-repeat, notifications-date-weather-close-wait, or all (runs every scenario with AGS restart between each; default: all)
--scenario NAME launcher, launcher-no-theme, wallpaper, wallpaper-no-theme, power-menu, theme-only, css-only, cc, cc-no-theme, date-weather, date-weather-no-theme, date-weather-css-only, notifications, notifications-date-weather, notifications-date-weather-repeat, notifications-date-weather-close-wait, notifications-date-weather-hide-retained, or all (runs every scenario with AGS restart between each; default: all)
--iterations N Panel open/theme-change/close repetitions (default: 30)
--notifications N Number of random image notifications (default: 30)
--settle-seconds N Delay after UI and wallpaper operations (default: 2)
Expand Down Expand Up @@ -50,7 +50,7 @@ while (($#)); do
esac
done

case "$scenario" in launcher|launcher-no-theme|wallpaper|wallpaper-no-theme|power-menu|theme-only|css-only|cc|cc-no-theme|date-weather|date-weather-no-theme|date-weather-css-only|notifications|notifications-date-weather|notifications-date-weather-repeat|notifications-date-weather-close-wait|all) ;; *) printf 'Invalid scenario: %s\n' "$scenario" >&2; exit 2 ;; esac
case "$scenario" in launcher|launcher-no-theme|wallpaper|wallpaper-no-theme|power-menu|theme-only|css-only|cc|cc-no-theme|date-weather|date-weather-no-theme|date-weather-css-only|notifications|notifications-date-weather|notifications-date-weather-repeat|notifications-date-weather-close-wait|notifications-date-weather-hide-retained|all) ;; *) printf 'Invalid scenario: %s\n' "$scenario" >&2; exit 2 ;; esac
[[ $iterations =~ ^[1-9][0-9]*$ ]] || { printf '%s\n' '--iterations must be a positive integer' >&2; exit 2; }
[[ $notification_count =~ ^[1-9][0-9]*$ ]] || { printf '%s\n' '--notifications must be a positive integer' >&2; exit 2; }
[[ $settle_seconds =~ ^[0-9]+$ && $gc_wait_seconds =~ ^[0-9]+$ ]] || { printf '%s\n' 'wait values must be non-negative integers' >&2; exit 2; }
Expand Down Expand Up @@ -159,6 +159,7 @@ run_named_scenario() {
notifications-date-weather) run_notification_scenario true ;;
notifications-date-weather-repeat) run_notification_scenario true 2 ;;
notifications-date-weather-close-wait) run_notification_scenario true 1 true ;;
notifications-date-weather-hide-retained) run_notification_scenario true 1 false true ;;
*) printf 'Invalid scenario: %s\n' "$1" >&2; exit 2 ;;
esac
}
Expand All @@ -182,6 +183,7 @@ run_all_scenarios() {
notifications-date-weather
notifications-date-weather-repeat
notifications-date-weather-close-wait
notifications-date-weather-hide-retained
)

for scenario_name in "${scenarios[@]}"; do
Expand Down Expand Up @@ -265,6 +267,7 @@ run_notification_scenario() {
local with_date_weather=${1:-false}
local batch_count=${2:-1}
local observe_close=${3:-false}
local hide_with_notifications=${4:-false}
local scenario_name=notifications
local -a images=()
local image size i batch added_phase cleared_phase settled_phase
Expand All @@ -276,6 +279,9 @@ run_notification_scenario() {
if "$observe_close"; then
scenario_name=notifications-date-weather-close-wait
fi
if "$hide_with_notifications"; then
scenario_name=notifications-date-weather-hide-retained
fi

image_count=$(find "$HOME/Pictures" -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' \) -print | wc -l)
if ((image_count < notification_count)); then
Expand Down Expand Up @@ -319,6 +325,24 @@ run_notification_scenario() {
fi
wait_for_settle "$settle_seconds"
snapshot "$scenario_name" "$batch" "$added_phase"
if "$hide_with_notifications"; then
ags_request toggle-notif
snapshot "$scenario_name" "$batch" date_weather_close_requested
wait_for_settle 1
snapshot "$scenario_name" "$batch" date_weather_hidden_1s
wait_for_settle 4
snapshot "$scenario_name" "$batch" date_weather_hidden_5s
wait_for_settle 10
snapshot "$scenario_name" "$batch" date_weather_hidden_15s
wait_for_settle 15
snapshot "$scenario_name" "$batch" date_weather_hidden_30s
ags_request clear-notifications
wait_for_settle "$settle_seconds"
snapshot "$scenario_name" "$batch" cleared_after_hide
wait_for_settle "$gc_wait_seconds"
snapshot "$scenario_name" "$batch" cleared_gc_settled
return
fi
ags_request clear-notifications
wait_for_settle "$settle_seconds"
snapshot "$scenario_name" "$batch" "$cleared_phase"
Expand Down
10 changes: 9 additions & 1 deletion src/stores/application/appLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,31 @@ import Apps from 'gi://AstalApps';
import {searchApps} from '@/stores/application/applicationCatalog';

export interface AppLauncherState {
contentLoaded: Accessor<boolean>;
text: Accessor<string>;
setText: Setter<string>;
selectedIndex: Accessor<number>;
setSelectedIndex: Setter<number>;
results: Accessor<Apps.Application[]>;
loadContent: () => void;
}

export function createAppLauncherState(): AppLauncherState {
const [contentLoaded, setContentLoaded] = createState(false);
const [text, setText] = createState('');
const [selectedIndex, setSelectedIndex] = createState(0);
const results = createComputed(() => searchApps(text().trim().toLowerCase()));
const results = createComputed<Apps.Application[]>(() => {
if (!contentLoaded()) return [];
return searchApps(text().trim().toLowerCase());
});

return {
contentLoaded,
text,
setText,
selectedIndex,
setSelectedIndex,
results,
loadContent: () => setContentLoaded(true),
};
}
24 changes: 21 additions & 3 deletions src/stores/application/applicationCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,24 @@ import Apps from 'gi://AstalApps';
import {ApplicationHistory} from '@/stores/application/applicationHistory';

const MAX_APP_RESULTS = 30;
const applications = new Apps.Apps();
const history = new ApplicationHistory(applications);

interface ApplicationCatalog {
applications: Apps.Apps;
history: ApplicationHistory;
}

let catalog: ApplicationCatalog | null = null;

function getCatalog() {
if (catalog) return catalog;

const applications = new Apps.Apps();
catalog = {
applications,
history: new ApplicationHistory(applications),
};
return catalog;
}

function getResultKey(application: Apps.Application) {
return application.name + (application.description || '') + (application.iconName || '');
Expand All @@ -27,6 +43,7 @@ function getUniqueResults(applicationList: Apps.Application[]) {
}

function getApplicationList() {
const {applications, history} = getCatalog();
return applications.get_list().sort((applicationA, applicationB) => {
const scoreA = history.getScore(applicationA);
const scoreB = history.getScore(applicationB);
Expand All @@ -36,10 +53,11 @@ function getApplicationList() {
}

export function recordAppLaunch(application: Apps.Application) {
history.recordLaunch(application);
getCatalog().history.recordLaunch(application);
}

export function searchApps(query: string) {
const {history} = getCatalog();
const allApplications = getApplicationList();
if (query === '') return getUniqueResults(allApplications);
const keywords = query.split(/\s+/);
Expand Down
177 changes: 177 additions & 0 deletions src/stores/notification/notificationImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import system from 'system';

import {Gtk} from 'ags/gtk4';
import {type Timer, timeout} from 'ags/time';

import Notifd from 'gi://AstalNotifd';

import {type SharedTexture, acquireNotificationTexture} from '@/stores/notification/imageCache';
import {subscribeNotificationThumbnail} from '@/stores/notification/notificationThumbnail';

const IMAGE_RELEASE_DELAY_MS = 300;
const GC_DELAY_MS = 100;

interface PictureResource {
picture: Gtk.Picture;
uri: string;
logicalWidth: number;
logicalHeight: number;
mapHook: number;
destroyHook: number;
destroyed: boolean;
root: Gtk.Widget | null;
rootUnmapHook: number | null;
thumbnail: boolean;
thumbnailUnsubscribe: (() => void) | null;
texture: SharedTexture | null;
}

let garbageCollectionTimer: Timer | null = null;

function scheduleGarbageCollection() {
garbageCollectionTimer?.cancel();
garbageCollectionTimer = timeout(GC_DELAY_MS, () => {
garbageCollectionTimer = null;
try {
system.gc();
} catch (error) {
console.error(error);
}
});
}

export class NotificationImageResources {
private readonly pictures: PictureResource[] = [];
private releaseTimer: Timer | null = null;
private disposed = false;
private imagesEnabled = true;
private readonly resolvedHook: number;

constructor(private readonly notification: Notifd.Notification) {
this.resolvedHook = notification.connect('resolved', () => this.scheduleRelease());
}

bindPicture(
picture: Gtk.Picture,
uri: string,
logicalWidth: number,
logicalHeight: number,
thumbnail = false
) {
const resource: PictureResource = {
picture,
uri,
logicalWidth,
logicalHeight,
mapHook: 0,
destroyHook: 0,
destroyed: false,
root: null,
rootUnmapHook: null,
thumbnail,
thumbnailUnsubscribe: null,
texture: null,
};
resource.mapHook = picture.connect('map', () => this.load(resource));
resource.destroyHook = picture.connect('destroy', () => {
resource.destroyed = true;
this.release(resource);
this.disconnectRoot(resource);
});
this.pictures.push(resource);

if (picture.get_mapped()) this.load(resource);
}

dispose() {
if (this.disposed) return;
this.disposed = true;
this.imagesEnabled = false;
this.releaseTimer?.cancel();
this.releaseTimer = null;

for (const resource of this.pictures) {
this.release(resource);
if (resource.destroyed) continue;
resource.picture.disconnect(resource.mapHook);
resource.picture.disconnect(resource.destroyHook);
this.disconnectRoot(resource);
}
this.pictures.length = 0;
this.notification.disconnect(this.resolvedHook);
scheduleGarbageCollection();
}

private load(resource: PictureResource) {
if (this.disposed || !this.imagesEnabled || resource.texture || resource.thumbnailUnsubscribe)
return;

try {
this.connectRoot(resource);
const scaleFactor = Math.max(1, resource.picture.get_scale_factor());
const width = resource.logicalWidth * scaleFactor;
const height = resource.logicalHeight * scaleFactor;
if (!resource.thumbnail) {
this.loadTexture(resource, resource.uri, width, height);
return;
}

let completed = false;
const unsubscribe = subscribeNotificationThumbnail(resource.uri, width, height, path => {
completed = true;
resource.thumbnailUnsubscribe = null;
if (this.disposed || !this.imagesEnabled || resource.destroyed) return;
if (path) this.loadTexture(resource, `file://${path}`, width, height);
else this.loadTexture(resource, resource.uri, width, height);
});
if (!completed) resource.thumbnailUnsubscribe = unsubscribe;
} catch (error) {
console.error(error);
}
Comment on lines +128 to +130
}

private loadTexture(resource: PictureResource, uri: string, width: number, height: number) {
resource.texture = acquireNotificationTexture(uri, width, height);
resource.picture.set_paintable(resource.texture.texture);
}

private release(resource: PictureResource) {
resource.thumbnailUnsubscribe?.();
resource.thumbnailUnsubscribe = null;
if (!resource.texture) return;
if (!resource.destroyed) resource.picture.set_paintable(null);
resource.texture.release();
resource.texture = null;
scheduleGarbageCollection();
}

private releaseImages() {
for (const resource of this.pictures) this.release(resource);
}

private connectRoot(resource: PictureResource) {
const root = resource.picture.get_root() as Gtk.Widget | null;
if (!root || resource.root === root) return;

this.disconnectRoot(resource);
resource.root = root;
resource.rootUnmapHook = root.connect('unmap', () => this.release(resource));
}

private disconnectRoot(resource: PictureResource) {
if (resource.root && resource.rootUnmapHook !== null) {
resource.root.disconnect(resource.rootUnmapHook);
}
resource.root = null;
resource.rootUnmapHook = null;
}

private scheduleRelease() {
this.imagesEnabled = false;
this.releaseTimer?.cancel();
this.releaseTimer = timeout(IMAGE_RELEASE_DELAY_MS, () => {
this.releaseTimer = null;
this.releaseImages();
});
}
}
Loading