Skip to content

Commit b825c0a

Browse files
committed
feat(plugins): Support built-in plugins, scripts, & convert session plugin to scripts
1 parent 19ebae0 commit b825c0a

File tree

14 files changed

+476
-359
lines changed

14 files changed

+476
-359
lines changed

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ endif
99
ifeq ($(strip $(DESTDIR)),)
1010
INSTALLBASE = $(XDG_DATA_HOME)/gnome-shell/extensions
1111
PLUGIN_BASE = $(XDG_DATA_HOME)/pop-shell/launcher
12+
SCRIPTS_BASE = $(XDG_DATA_HOME)/pop-shell/scripts
1213
else
1314
INSTALLBASE = $(DESTDIR)/usr/share/gnome-shell/extensions
1415
PLUGIN_BASE = $(DESTDIR)/usr/lib/pop-shell/launcher
16+
SCRIPTS_BASE = $(DESTDIR)/usr/lib/pop-shell/scripts
1517
endif
1618
INSTALLNAME = $(UUID)
1719

@@ -77,10 +79,11 @@ local-install: depcheck compile install configure enable restart-shell
7779

7880
install:
7981
rm -rf $(INSTALLBASE)/$(INSTALLNAME)
80-
mkdir -p $(INSTALLBASE)/$(INSTALLNAME) $(PLUGIN_BASE)
82+
mkdir -p $(INSTALLBASE)/$(INSTALLNAME) $(PLUGIN_BASE) $(SCRIPTS_BASE)
8183
cp -r _build/* $(INSTALLBASE)/$(INSTALLNAME)/
8284
cp -r src/plugins/* $(PLUGIN_BASE)
83-
chmod +x $(PLUGIN_BASE)/**/*.js
85+
cp -r src/scripts/* $(SCRIPTS_BASE)
86+
chmod +x $(PLUGIN_BASE)/**/*.js $(SCRIPTS_BASE)/*
8487

8588
uninstall:
8689
rm -rf $(INSTALLBASE)/$(INSTALLNAME)

debian/rules

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ override_dh_install:
1515

1616
override_dh_fixperms:
1717
dh_fixperms
18-
chmod +x debian/pop-shell/usr/lib/pop-shell/launcher/**/*.js
18+
chmod +x debian/pop-shell/usr/lib/pop-shell/launcher/**/*.js
19+
chmod +x debian/pop-shell/usr/lib/pop-shell/scripts/*

src/arena.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** Hop slot arena allocator */
22
export class Arena<T> {
33
private slots: Array<null | T> = new Array();
4+
45
private unused: Array<number> = new Array()
56

67
truncate(n: number) {

src/dialog_launcher.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import * as lib from 'lib';
99
import * as log from 'log';
1010
import * as result from 'result';
1111
import * as search from 'dialog_search';
12-
import * as launch from 'launcher';
12+
import * as launch from 'launcher_service';
13+
import * as plugins from 'launcher_plugins';
1314

1415
import type { ShellWindow } from 'window';
1516
import type { Ext } from 'extension';
@@ -39,7 +40,7 @@ export class Launcher extends search.Search {
3940
options: Array<launch.SearchOption>
4041
desktop_apps: Array<[string, AppInfo]>
4142
service: launch.LauncherService
42-
last_plugin: null | launch.Plugin.Source
43+
last_plugin: null | plugins.Plugin.Source
4344
mode: number;
4445

4546
constructor(ext: Ext) {
@@ -195,9 +196,9 @@ export class Launcher extends search.Search {
195196
}
196197
} else if ("plugin" in option) {
197198
const { plugin, id } = option
198-
launch.Plugin.submit(plugin, id)
199+
plugins.Plugin.submit(plugin, id)
199200

200-
const response = launch.Plugin.listen(plugin)
201+
const response = plugins.Plugin.listen(plugin)
201202
if (response) {
202203
if (response.event === "fill") {
203204
this.set_text(response.text)
@@ -212,8 +213,8 @@ export class Launcher extends search.Search {
212213

213214
let complete = () => {
214215
if (this.last_plugin) {
215-
launch.Plugin.complete(this.last_plugin)
216-
const res = launch.Plugin.listen(this.last_plugin)
216+
plugins.Plugin.complete(this.last_plugin)
217+
const res = plugins.Plugin.listen(this.last_plugin)
217218
if (res && res.event === "fill") {
218219
this.set_text(res.text)
219220
}

src/dialog_search.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
const Me = imports.misc.extensionUtils.getCurrentExtension();
33

44
import * as Lib from 'lib';
5-
import { SearchOption } from './launcher';
5+
import { SearchOption } from 'launcher_service';
66

77
const { Clutter, St } = imports.gi;
88
const { ModalDialog } = imports.ui.modalDialog;

src/launcher_plugins.ts

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// @ts-ignore
2+
const Me = imports.misc.extensionUtils.getCurrentExtension()
3+
4+
const { Gio, GLib } = imports.gi
5+
6+
import * as utils from 'utils'
7+
8+
/** The trait which all builtin plugins implement */
9+
export abstract class Builtin {
10+
/** Stores the last search result */
11+
last_response: null | Response.Response = null
12+
13+
/** Results of the last query */
14+
selections: Array<Response.Selection> = new Array()
15+
16+
/** Initializes default values and resets state */
17+
abstract init(): void
18+
19+
/** Uses the search input to query for search results */
20+
abstract query(query: string): Response.Response
21+
22+
/** Applies an option by its ID */
23+
abstract submit(id: number): Response.Response
24+
25+
/** Dispatches a launcher request, and stores the response */
26+
handle(event: Request.Request) {
27+
switch (event.event) {
28+
case "complete":
29+
this.last_response = { event: "noop" }
30+
break
31+
case "query":
32+
this.last_response = this.query(event.value)
33+
break
34+
case "submit":
35+
this.last_response = this.submit(event.id)
36+
break
37+
default:
38+
this.last_response = { event: "noop" }
39+
40+
}
41+
}
42+
}
43+
44+
export namespace Request {
45+
export type Request = Complete | Submit | Query | Quit
46+
47+
export interface Complete {
48+
event: 'complete',
49+
}
50+
51+
export interface Submit {
52+
event: 'submit',
53+
id: number
54+
}
55+
56+
export interface Quit {
57+
event: 'quit'
58+
}
59+
60+
export interface Query {
61+
event: 'query',
62+
value: string
63+
}
64+
}
65+
66+
export namespace Response {
67+
export interface Selection {
68+
id: number
69+
name: string
70+
description: null | string
71+
icon?: string
72+
content_type?: string
73+
}
74+
75+
export interface Query {
76+
event: "queried",
77+
selections: Array<Selection>
78+
}
79+
80+
export interface Fill {
81+
event: "fill",
82+
text: string
83+
}
84+
85+
export interface Close {
86+
event: "close"
87+
}
88+
89+
export interface NoOp {
90+
event: 'noop'
91+
}
92+
93+
export type Response = Query | Fill | Close | NoOp
94+
95+
export function parse(input: string): null | Response {
96+
try {
97+
let object = JSON.parse(input) as Response
98+
switch (object.event) {
99+
case "close":
100+
case "fill":
101+
case "queried":
102+
return object
103+
}
104+
} catch (e) {
105+
106+
}
107+
108+
return null
109+
}
110+
}
111+
112+
export namespace Plugin {
113+
export interface Config {
114+
name: string
115+
description: string
116+
pattern: string
117+
exec: string
118+
icon: string
119+
}
120+
121+
export function read(file: string): Config | null {
122+
global.log(`found plugin at ${file}`)
123+
try {
124+
let [ok, contents] = Gio.file_new_for_path(file)
125+
.load_contents(null)
126+
127+
if (ok) return parse(imports.byteArray.toString(contents))
128+
} catch (e) {
129+
130+
}
131+
132+
return null
133+
}
134+
135+
export function parse(input: string): Config | null {
136+
try {
137+
return JSON.parse(input)
138+
} catch (e) {
139+
return null
140+
}
141+
}
142+
143+
export interface External {
144+
cmd: string
145+
proc: null | utils.AsyncIPC
146+
}
147+
148+
export interface BuiltinVariant {
149+
builtin: Builtin
150+
}
151+
152+
export interface Source {
153+
config: Config
154+
backend: External | BuiltinVariant
155+
pattern: null | RegExp
156+
}
157+
158+
export function listen(plugin: Plugin.Source): null | Response.Response {
159+
if ('builtin' in plugin.backend) {
160+
return plugin.backend.builtin.last_response
161+
} else {
162+
const backend = plugin.backend
163+
if (!backend.proc) {
164+
const proc = Plugin.start(backend)
165+
if (proc) {
166+
backend.proc = proc
167+
} else {
168+
return null
169+
}
170+
}
171+
172+
try {
173+
let [bytes,] = backend.proc.stdout.read_line(null)
174+
return Response.parse(imports.byteArray.toString(bytes))
175+
} catch (e) {
176+
return null
177+
}
178+
}
179+
}
180+
181+
export function complete(plugin: Plugin.Source): boolean {
182+
return send(plugin, { event: "complete" })
183+
}
184+
185+
export function query(plugin: Plugin.Source, value: string): boolean {
186+
return send(plugin, { event: "query", value })
187+
}
188+
189+
export function quit(plugin: Plugin.Source) {
190+
if ('proc' in plugin.backend) {
191+
if (plugin.backend.proc) {
192+
send(plugin, { event: "quit" })
193+
plugin.backend.proc = null
194+
}
195+
} else {
196+
send(plugin, { event: "quit" })
197+
}
198+
}
199+
200+
export function submit(plugin: Plugin.Source, id: number): boolean {
201+
return send(plugin, { event: "submit", id })
202+
}
203+
204+
export function send(plugin: Plugin.Source, event: Request.Request): boolean {
205+
const backend = plugin.backend
206+
207+
if ('builtin' in backend) {
208+
backend.builtin.handle(event)
209+
return true
210+
} else {
211+
let string = JSON.stringify(event)
212+
213+
if (!backend.proc) {
214+
backend.proc = start(backend)
215+
}
216+
217+
function attempt(name: string, plugin: Plugin.External, string: string) {
218+
if (!plugin.proc) return false
219+
220+
try {
221+
plugin.proc.stdin.write_bytes(new GLib.Bytes(string + "\n"), null)
222+
return true
223+
} catch (e) {
224+
global.log(`failed to send message to ${name}: ${e}`)
225+
return false
226+
}
227+
}
228+
229+
if (!attempt(plugin.config.name, backend, string)) {
230+
backend.proc = start(backend)
231+
if (!attempt(plugin.config.name, backend, string)) return false
232+
}
233+
}
234+
235+
return true
236+
}
237+
238+
export function start(plugin: Plugin.External): null | utils.AsyncIPC {
239+
return utils.async_process_ipc([plugin.cmd])
240+
}
241+
}

0 commit comments

Comments
 (0)