diff --git a/src/components/menus/apps/index.tsx b/src/components/menus/apps/index.tsx
new file mode 100644
index 000000000..729a759ba
--- /dev/null
+++ b/src/components/menus/apps/index.tsx
@@ -0,0 +1,169 @@
+import DropdownMenu from '../shared/dropdown/index.js';
+import options from 'src/options.js';
+import Variable from 'astal/variable';
+import { bind } from 'astal/binding';
+import { RevealerTransitionMap } from 'src/lib/constants/options.js';
+import { App, Gtk } from 'astal/gtk3';
+import Separator from 'src/components/shared/Separator.js';
+import AstalApps from 'gi://AstalApps?version=0.1'
+import { icon, launchApp } from 'src/lib/utils.js';
+import { Entry, EntryProps, Scrollable } from 'astal/gtk3/widget';
+
+import PopupWindow from '../shared/popup/index.js';
+
+const apps = new AstalApps.Apps({
+ nameMultiplier: 2,
+ keywordsMultiplier: 2,
+ executableMultiplier: 1,
+ entryMultiplier: 0.5,
+ categoriesMultiplier: 0.5,
+});
+
+interface ApplicationItemProps {
+ app: AstalApps.Application;
+ onLaunched?: () => void;
+}
+
+const ApplicationItem = ({ app, onLaunched }: ApplicationItemProps): JSX.Element => {
+ return (
+
+ );
+}
+
+
+function useRef() {
+ let ref: T | null = null;
+
+ return {
+ set: (r: T) => { ref = r },
+ get: () => ref
+ }
+}
+
+function useApplicationsFilter() {
+ const filter = Variable('')
+
+ const list = bind(filter).as((f) => {
+ // show all apps by default
+ if (!f) return apps.get_list()
+ // if the filter is a single character, show all apps that start with that character
+ if (f.length === 1) return apps.get_list().filter((app) => app.name.toLowerCase().startsWith(f.toLowerCase()))
+ // otherwise, do a fuzzy search (this method wont filter with a single character)
+ return apps.fuzzy_query(f)
+ })
+
+ return { filter, list }
+}
+
+interface ApplicationLauncherProps {
+ visible: Variable;
+ onLaunched?: () => void;
+}
+
+const SearchBar = ({ value, setup, onActivate }: { value?: Variable; setup?: (self: Entry) => void; onActivate?: EntryProps['onActivate'] }) => {
+ return (
+
+
+ value.set(entry.text))} />
+
+
+
+
+
+
+ )
+}
+
+
+const ApplicationLauncher = ({ visible, onLaunched }: ApplicationLauncherProps): JSX.Element => {
+ const entry = useRef()
+ const scrollable = useRef()
+
+ const { filter, list } = useApplicationsFilter()
+
+ const onFilterReturn = () => {
+ const first = list.get()[0]
+ if (!first) return;
+ launchApp(first)
+ onLaunched?.()
+ }
+
+ // focus the entry when the menu is shown
+ const onShow = () => {
+ entry.get()?.grab_focus()
+ }
+ visible.subscribe(v => v && onShow());
+
+ const onHide = () => {
+ // clear the filter when the menu is hidden
+ filter.set('')
+ // TODO: reset scroll position
+ }
+ visible.subscribe(v => !v && onHide);
+
+ return (
+
+
+
+
+
+ {list.as(apps => apps.map((app) => ))}
+
+
+
+
+ )
+}
+
+/**
+ * track the visibility of a window
+ * this is necessary because menu are realized at startup and never destroyed
+ * making onRealize and onDestroy unreliable for lifecycle management
+ */
+function useWindowVisibility(windowName: string) {
+ const visible = Variable(!!App.get_window(windowName)?.visible);
+
+ App.connect('window-toggled', (_, window) => {
+ if (window.name !== windowName) return;
+ visible.set(window.visible);
+ })
+
+ return visible;
+}
+
+export const ApplicationsDropdownMenu = (): JSX.Element => {
+ const visible = useWindowVisibility('applicationsdropdownmenu');
+
+ const close = () => App.get_window('applicationsdropdownmenu')?.set_visible(false);
+
+ return (
+ RevealerTransitionMap[transition])}
+ >
+
+
+ );
+};
+
+
+export const ApplicationsMenu = (): JSX.Element => {
+ const visible = useWindowVisibility('applicationsmenu');
+
+ const close = () => App.get_window('applicationsmenu')?.set_visible(false);
+
+ return (
+ RevealerTransitionMap[transition])}>
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/menus/exports.ts b/src/components/menus/exports.ts
index 913a42f7b..0b3bb149d 100644
--- a/src/components/menus/exports.ts
+++ b/src/components/menus/exports.ts
@@ -9,6 +9,7 @@ import CalendarMenu from './calendar/index.js';
import EnergyMenu from './energy/index.js';
import DashboardMenu from './dashboard/index.js';
import PowerDropdown from './powerDropdown/index.js';
+import {ApplicationsDropdownMenu, ApplicationsMenu} from './apps/index';
export const DropdownMenus = [
AudioMenu,
@@ -20,6 +21,7 @@ export const DropdownMenus = [
EnergyMenu,
DashboardMenu,
PowerDropdown,
+ ApplicationsDropdownMenu
];
-export const StandardWindows = [PowerMenu, Verification];
+export const StandardWindows = [PowerMenu, Verification, ApplicationsMenu];