aboutsummaryrefslogtreecommitdiff
path: root/.config/ags/widget/launcher
diff options
context:
space:
mode:
authorsrdusr <trevorgray@srdusr.com>2024-06-13 13:11:05 +0200
committersrdusr <trevorgray@srdusr.com>2024-06-13 13:11:05 +0200
commitd0fbb19623e4fb6097e1ff3ee49c6a76a0928d0e (patch)
tree937531ddf423d3935c6e20c8a9227e39ce782241 /.config/ags/widget/launcher
parent4ccbe0270c25ecab492508b5b0209ae53b9c35bd (diff)
downloaddotfiles-d0fbb19623e4fb6097e1ff3ee49c6a76a0928d0e.tar.gz
dotfiles-d0fbb19623e4fb6097e1ff3ee49c6a76a0928d0e.zip
Add ags
Diffstat (limited to '.config/ags/widget/launcher')
-rw-r--r--.config/ags/widget/launcher/AppLauncher.ts130
-rw-r--r--.config/ags/widget/launcher/Launcher.ts139
-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, 619 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..4d7ca73
--- /dev/null
+++ b/.config/ags/widget/launcher/AppLauncher.ts
@@ -0,0 +1,130 @@
+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..3b73dc5
--- /dev/null
+++ b/.config/ags/widget/launcher/Launcher.ts
@@ -0,0 +1,139 @@
+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;
+ }
+ }
+}