aboutsummaryrefslogtreecommitdiff
path: root/.config/ags/widget/launcher
diff options
context:
space:
mode:
Diffstat (limited to '.config/ags/widget/launcher')
-rw-r--r--.config/ags/widget/launcher/AppLauncher.ts125
-rw-r--r--.config/ags/widget/launcher/Launcher.ts134
-rw-r--r--.config/ags/widget/launcher/NixRun.ts118
-rw-r--r--.config/ags/widget/launcher/ShRun.ts89
-rw-r--r--.config/ags/widget/launcher/launcher.scss143
5 files changed, 609 insertions, 0 deletions
diff --git a/.config/ags/widget/launcher/AppLauncher.ts b/.config/ags/widget/launcher/AppLauncher.ts
new file mode 100644
index 0000000..08258de
--- /dev/null
+++ b/.config/ags/widget/launcher/AppLauncher.ts
@@ -0,0 +1,125 @@
+import { type Application } from 'types/service/applications';
+import { launchApp, icon } from 'lib/utils';
+import options from 'options';
+import icons from 'lib/icons';
+
+const apps = await Service.import('applications');
+const { query } = apps;
+const { iconSize } = options.launcher.apps;
+
+const QuickAppButton = (app: Application) =>
+ Widget.Button({
+ hexpand: true,
+ tooltip_text: app.name,
+ on_clicked: () => {
+ App.closeWindow('launcher');
+ launchApp(app);
+ },
+ child: Widget.Icon({
+ size: iconSize.bind(),
+ icon: icon(app.icon_name, icons.fallback.executable),
+ }),
+ });
+
+const AppItem = (app: Application) => {
+ const title = Widget.Label({
+ class_name: 'title',
+ label: app.name,
+ hexpand: true,
+ xalign: 0,
+ vpack: 'center',
+ truncate: 'end',
+ });
+
+ const description = Widget.Label({
+ class_name: 'description',
+ label: app.description || '',
+ hexpand: true,
+ wrap: true,
+ max_width_chars: 30,
+ xalign: 0,
+ justification: 'left',
+ vpack: 'center',
+ });
+
+ const appicon = Widget.Icon({
+ icon: icon(app.icon_name, icons.fallback.executable),
+ size: iconSize.bind(),
+ });
+
+ const textBox = Widget.Box({
+ vertical: true,
+ vpack: 'center',
+ children: app.description ? [title, description] : [title],
+ });
+
+ return Widget.Button({
+ class_name: 'app-item',
+ attribute: { app },
+ child: Widget.Box({
+ children: [appicon, textBox],
+ }),
+ on_clicked: () => {
+ App.closeWindow('launcher');
+ launchApp(app);
+ },
+ });
+};
+export function Favorites() {
+ const favs = options.launcher.apps.favorites.bind();
+ return Widget.Revealer({
+ visible: favs.as(f => f.length > 0),
+ child: Widget.Box({
+ vertical: true,
+ children: favs.as(favs =>
+ favs.flatMap(fs => [
+ Widget.Separator(),
+ Widget.Box({
+ class_name: 'quicklaunch horizontal',
+ children: fs
+ .map(f => query(f)?.[0])
+ .filter(f => f)
+ .map(QuickAppButton),
+ }),
+ ]),
+ ),
+ }),
+ });
+}
+
+export function Launcher() {
+ const applist = Variable(query(''));
+ const max = options.launcher.apps.max;
+ let first = applist.value[0];
+
+ function SeparatedAppItem(app: Application) {
+ return Widget.Revealer({ attribute: { app } }, Widget.Box({ vertical: true }, Widget.Separator(), AppItem(app)));
+ }
+
+ const list = Widget.Box({
+ vertical: true,
+ children: applist.bind().as(list => list.map(SeparatedAppItem)),
+ setup: self => self.hook(apps, () => (applist.value = query('')), 'notify::frequents'),
+ });
+
+ return Object.assign(list, {
+ filter(text: string | null) {
+ first = query(text || '')[0];
+ list.children.reduce((i, item) => {
+ if (!text || i >= max.value) {
+ item.reveal_child = false;
+ return i;
+ }
+ if (item.attribute.app.match(text)) {
+ item.reveal_child = true;
+ return ++i;
+ }
+ item.reveal_child = false;
+ return i;
+ }, 0);
+ },
+ launchFirst() {
+ launchApp(first);
+ },
+ });
+}
diff --git a/.config/ags/widget/launcher/Launcher.ts b/.config/ags/widget/launcher/Launcher.ts
new file mode 100644
index 0000000..90b4d58
--- /dev/null
+++ b/.config/ags/widget/launcher/Launcher.ts
@@ -0,0 +1,134 @@
+import { type Binding } from 'lib/utils';
+import PopupWindow, { Padding } from 'widget/PopupWindow';
+import icons from 'lib/icons';
+import options from 'options';
+import nix from 'service/nix';
+import * as AppLauncher from './AppLauncher';
+import * as NixRun from './NixRun';
+import * as ShRun from './ShRun';
+
+const { width, margin } = options.launcher;
+const isnix = nix.available;
+
+function Launcher() {
+ const favs = AppLauncher.Favorites();
+ const applauncher = AppLauncher.Launcher();
+ const sh = ShRun.ShRun();
+ const shicon = ShRun.Icon();
+ const nix = NixRun.NixRun();
+ const nixload = NixRun.Spinner();
+
+ function HelpButton(cmd: string, desc: string | Binding<string>) {
+ return Widget.Box(
+ { vertical: true },
+ Widget.Separator(),
+ Widget.Button(
+ {
+ class_name: 'help',
+ on_clicked: () => {
+ entry.grab_focus();
+ entry.text = `:${cmd} `;
+ entry.set_position(-1);
+ },
+ },
+ Widget.Box([
+ Widget.Label({
+ class_name: 'name',
+ label: `:${cmd}`,
+ }),
+ Widget.Label({
+ hexpand: true,
+ hpack: 'end',
+ class_name: 'description',
+ label: desc,
+ }),
+ ]),
+ ),
+ );
+ }
+
+ const help = Widget.Revealer({
+ child: Widget.Box(
+ { vertical: true },
+ HelpButton('sh', 'run a binary'),
+ isnix
+ ? HelpButton(
+ 'nx',
+ options.launcher.nix.pkgs.bind().as(pkg => `run a nix package from ${pkg}`),
+ )
+ : Widget.Box(),
+ ),
+ });
+
+ const entry = Widget.Entry({
+ hexpand: true,
+ primary_icon_name: icons.ui.search,
+ on_accept: ({ text }) => {
+ if (text?.startsWith(':nx')) nix.run(text.substring(3));
+ else if (text?.startsWith(':sh')) sh.run(text.substring(3));
+ else applauncher.launchFirst();
+
+ App.toggleWindow('launcher');
+ entry.text = '';
+ },
+ on_change: ({ text }) => {
+ text ||= '';
+ favs.reveal_child = text === '';
+ help.reveal_child = text.split(' ').length === 1 && text?.startsWith(':');
+
+ if (text?.startsWith(':nx')) nix.filter(text.substring(3));
+ else nix.filter('');
+
+ if (text?.startsWith(':sh')) sh.filter(text.substring(3));
+ else sh.filter('');
+
+ if (!text?.startsWith(':')) applauncher.filter(text);
+ },
+ });
+
+ function focus() {
+ entry.text = '';
+ entry.set_position(-1);
+ entry.select_region(0, -1);
+ entry.grab_focus();
+ favs.reveal_child = true;
+ }
+
+ const layout = Widget.Box({
+ css: width.bind().as(v => `min-width: ${v}pt;`),
+ class_name: 'launcher',
+ vertical: true,
+ vpack: 'start',
+ setup: self =>
+ self.hook(App, (_, win, visible) => {
+ if (win !== 'launcher') return;
+
+ entry.text = '';
+ if (visible) focus();
+ }),
+ children: [
+ Widget.Box([entry, nixload, shicon]),
+ favs,
+ help,
+ applauncher,
+ //nix,
+ sh,
+ ],
+ });
+
+ return Widget.Box(
+ { vertical: true, css: 'padding: 1px' },
+ Padding('applauncher', {
+ css: margin.bind().as(v => `min-height: ${v}pt;`),
+ vexpand: false,
+ }),
+ layout,
+ );
+}
+
+export default () =>
+ PopupWindow({
+ name: 'launcher',
+ layout: 'top',
+ child: Launcher(),
+ });
diff --git a/.config/ags/widget/launcher/NixRun.ts b/.config/ags/widget/launcher/NixRun.ts
new file mode 100644
index 0000000..cec9e09
--- /dev/null
+++ b/.config/ags/widget/launcher/NixRun.ts
@@ -0,0 +1,118 @@
+import icons from "lib/icons"
+import nix, { type Nixpkg } from "service/nix"
+
+const iconVisible = Variable(false)
+
+function Item(pkg: Nixpkg) {
+ const name = Widget.Label({
+ class_name: "name",
+ label: pkg.name.split(".").at(-1),
+ })
+
+ const subpkg = pkg.name.includes(".") ? Widget.Label({
+ class_name: "description",
+ hpack: "end",
+ hexpand: true,
+ label: ` ${pkg.name.split(".").slice(0, -1).join(".")}`,
+ }) : null
+
+ const version = Widget.Label({
+ class_name: "version",
+ label: pkg.version,
+ hexpand: true,
+ hpack: "end",
+ })
+
+ const description = pkg.description ? Widget.Label({
+ class_name: "description",
+ label: pkg.description,
+ justification: "left",
+ wrap: true,
+ hpack: "start",
+ max_width_chars: 40,
+ }) : null
+
+ return Widget.Box(
+ {
+ attribute: { name: pkg.name },
+ vertical: true,
+ },
+ Widget.Separator(),
+ Widget.Button(
+ {
+ class_name: "nix-item",
+ on_clicked: () => {
+ nix.run(pkg.name)
+ App.closeWindow("launcher")
+ },
+ },
+ Widget.Box(
+ { vertical: true },
+ Widget.Box([name, version]),
+ Widget.Box([
+ description as ReturnType<typeof Widget.Label>,
+ subpkg as ReturnType<typeof Widget.Label>,
+ ]),
+ ),
+ ),
+ )
+}
+
+export function Spinner() {
+ const icon = Widget.Icon({
+ icon: icons.nix.nix,
+ class_name: "spinner",
+ css: `
+ @keyframes spin {
+ to { -gtk-icon-transform: rotate(1turn); }
+ }
+
+ image.spinning {
+ animation-name: spin;
+ animation-duration: 1s;
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+ }
+ `,
+ setup: self => self.hook(nix, () => {
+ self.toggleClassName("spinning", !nix.ready)
+ }),
+ })
+
+ return Widget.Revealer({
+ transition: "slide_left",
+ child: icon,
+ reveal_child: Utils.merge([
+ nix.bind("ready"),
+ iconVisible.bind(),
+ ], (ready, show) => !ready || show),
+ })
+}
+
+export function NixRun() {
+ const list = Widget.Box<ReturnType<typeof Item>>({
+ vertical: true,
+ })
+
+ const revealer = Widget.Revealer({
+ child: list,
+ })
+
+ async function filter(term: string) {
+ iconVisible.value = Boolean(term)
+
+ if (!term)
+ revealer.reveal_child = false
+
+ if (term.trim()) {
+ const found = await nix.query(term)
+ list.children = found.map(k => Item(nix.db[k]))
+ revealer.reveal_child = true
+ }
+ }
+
+ return Object.assign(revealer, {
+ filter,
+ run: nix.run,
+ })
+}
diff --git a/.config/ags/widget/launcher/ShRun.ts b/.config/ags/widget/launcher/ShRun.ts
new file mode 100644
index 0000000..c4215ef
--- /dev/null
+++ b/.config/ags/widget/launcher/ShRun.ts
@@ -0,0 +1,89 @@
+import icons from "lib/icons"
+import options from "options"
+import { bash, dependencies } from "lib/utils"
+
+const iconVisible = Variable(false)
+
+const MAX = options.launcher.sh.max
+const BINS = `${Utils.CACHE_DIR}/binaries`
+bash("{ IFS=:; ls -H $PATH; } | sort ")
+ .then(bins => Utils.writeFile(bins, BINS))
+
+async function query(filter: string) {
+ if (!dependencies("fzf"))
+ return [] as string[]
+
+ return bash(`cat ${BINS} | fzf -f ${filter} | head -n ${MAX}`)
+ .then(str => Array.from(new Set(str.split("\n").filter(i => i)).values()))
+ .catch(err => { print(err); return [] })
+}
+
+function run(args: string) {
+ Utils.execAsync(args)
+ .then(out => {
+ print(`:sh ${args.trim()}:`)
+ print(out)
+ })
+ .catch(err => {
+ Utils.notify("ShRun Error", err, icons.app.terminal)
+ })
+}
+
+function Item(bin: string) {
+ return Widget.Box(
+ {
+ attribute: { bin },
+ vertical: true,
+ },
+ Widget.Separator(),
+ Widget.Button({
+ child: Widget.Label({
+ label: bin,
+ hpack: "start",
+ }),
+ class_name: "sh-item",
+ on_clicked: () => {
+ Utils.execAsync(bin)
+ App.closeWindow("launcher")
+ },
+ }),
+ )
+}
+
+export function Icon() {
+ const icon = Widget.Icon({
+ icon: icons.app.terminal,
+ class_name: "spinner",
+ })
+
+ return Widget.Revealer({
+ transition: "slide_left",
+ child: icon,
+ reveal_child: iconVisible.bind(),
+ })
+}
+
+export function ShRun() {
+ const list = Widget.Box<ReturnType<typeof Item>>({
+ vertical: true,
+ })
+
+ const revealer = Widget.Revealer({
+ child: list,
+ })
+
+ async function filter(term: string) {
+ iconVisible.value = Boolean(term)
+
+ if (!term)
+ revealer.reveal_child = false
+
+ if (term.trim()) {
+ const found = await query(term)
+ list.children = found.map(Item)
+ revealer.reveal_child = true
+ }
+ }
+
+ return Object.assign(revealer, { filter, run })
+}
diff --git a/.config/ags/widget/launcher/launcher.scss b/.config/ags/widget/launcher/launcher.scss
new file mode 100644
index 0000000..926abc3
--- /dev/null
+++ b/.config/ags/widget/launcher/launcher.scss
@@ -0,0 +1,143 @@
+@use "sass:math";
+@use "sass:color";
+
+window#launcher .launcher {
+ @include floating_widget;
+
+ .quicklaunch {
+ @include spacing;
+
+ button {
+ @include button($flat: true);
+ padding: $padding;
+ }
+ }
+
+ entry {
+ @include button;
+ padding: $padding;
+ margin: $spacing;
+
+ selection {
+ color: color.mix($fg, $bg, 50%);
+ background-color: transparent;
+ }
+
+ label,
+ image {
+ color: $fg;
+ }
+ }
+
+ image.spinner {
+ color: $primary-bg;
+ margin-right: $spacing;
+ }
+
+ separator {
+ margin: 4pt 0;
+ background-color: $popover-border-color;
+ }
+
+ button.app-item {
+ @include button($flat: true, $reactive: false);
+
+ >box {
+ @include spacing(0.5);
+ }
+
+ transition: $transition;
+ padding: $padding;
+
+ label {
+ transition: $transition;
+
+ &.title {
+ color: $fg;
+ }
+
+ &.description {
+ color: transparentize($fg, 0.3);
+ }
+ }
+
+ image {
+ transition: $transition;
+ }
+
+ &:hover,
+ &:focus {
+ .title {
+ color: $primary-bg;
+ }
+
+ .description {
+ color: transparentize($primary-bg, .4);
+ }
+
+ image {
+ -gtk-icon-shadow: 2px 2px $primary-bg;
+ }
+ }
+
+ &:active {
+ background-color: transparentize($primary-bg, 0.5);
+ border-radius: $radius;
+ box-shadow: inset 0 0 0 $border-width $border-color;
+
+ .title {
+ color: $fg;
+ }
+ }
+ }
+
+ button.help,
+ button.nix-item {
+ @include button($flat: true, $reactive: false);
+ padding: 0 ($padding * .5);
+
+ label {
+ transition: $transition;
+ color: $fg;
+ }
+
+ .name {
+ font-size: 1.2em;
+ font-weight: bold;
+ }
+
+ .description {
+ color: transparentize($fg, .3)
+ }
+
+ &:hover,
+ &:focus {
+ label {
+ text-shadow: $text-shadow;
+ }
+
+ .name,
+ .version {
+ color: $primary-bg;
+ }
+
+ .description {
+ color: transparentize($primary-bg, .3)
+ }
+ }
+ }
+
+ button.sh-item {
+ @include button($flat: true, $reactive: false);
+ padding: 0 ($padding * .5);
+
+ transition: $transition;
+ color: $fg;
+
+ &:hover,
+ &:focus {
+ color: $primary-bg;
+ text-shadow: $text-shadow;
+ }
+ }
+}