diff options
Diffstat (limited to 'linux/home/.config/ags/widget/launcher')
| -rw-r--r-- | linux/home/.config/ags/widget/launcher/AppLauncher.ts | 125 | ||||
| -rw-r--r-- | linux/home/.config/ags/widget/launcher/Launcher.ts | 134 | ||||
| -rw-r--r-- | linux/home/.config/ags/widget/launcher/NixRun.ts | 118 | ||||
| -rw-r--r-- | linux/home/.config/ags/widget/launcher/ShRun.ts | 89 | ||||
| -rw-r--r-- | linux/home/.config/ags/widget/launcher/launcher.scss | 143 |
5 files changed, 609 insertions, 0 deletions
diff --git a/linux/home/.config/ags/widget/launcher/AppLauncher.ts b/linux/home/.config/ags/widget/launcher/AppLauncher.ts new file mode 100644 index 0000000..08258de --- /dev/null +++ b/linux/home/.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/linux/home/.config/ags/widget/launcher/Launcher.ts b/linux/home/.config/ags/widget/launcher/Launcher.ts new file mode 100644 index 0000000..90b4d58 --- /dev/null +++ b/linux/home/.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/linux/home/.config/ags/widget/launcher/NixRun.ts b/linux/home/.config/ags/widget/launcher/NixRun.ts new file mode 100644 index 0000000..cec9e09 --- /dev/null +++ b/linux/home/.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/linux/home/.config/ags/widget/launcher/ShRun.ts b/linux/home/.config/ags/widget/launcher/ShRun.ts new file mode 100644 index 0000000..c4215ef --- /dev/null +++ b/linux/home/.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/linux/home/.config/ags/widget/launcher/launcher.scss b/linux/home/.config/ags/widget/launcher/launcher.scss new file mode 100644 index 0000000..926abc3 --- /dev/null +++ b/linux/home/.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; + } + } +} |
