Added ags
TODO copy ags config
This commit is contained in:
130
roles/ags/files/widget/launcher/AppLauncher.ts
Normal file
130
roles/ags/files/widget/launcher/AppLauncher.ts
Normal file
@@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
139
roles/ags/files/widget/launcher/Launcher.ts
Normal file
139
roles/ags/files/widget/launcher/Launcher.ts
Normal file
@@ -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 = "Search"
|
||||
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(),
|
||||
})
|
||||
118
roles/ags/files/widget/launcher/NixRun.ts
Normal file
118
roles/ags/files/widget/launcher/NixRun.ts
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
89
roles/ags/files/widget/launcher/ShRun.ts
Normal file
89
roles/ags/files/widget/launcher/ShRun.ts
Normal file
@@ -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 })
|
||||
}
|
||||
143
roles/ags/files/widget/launcher/launcher.scss
Normal file
143
roles/ags/files/widget/launcher/launcher.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user