feat: make AGS colorshell configuration fully declarative

- Add complete colorshell v2.0.3 configuration to home/ags-config/
- Disable runner plugin and NightLight tile (incompatible with NixOS)
- Customize SCSS with full opacity (no transparency)
- Add dark pale blue color scheme in home/pywal-colors/
- Configure Papirus-Dark icon theme via home-manager
- Make ~/.config/ags/ immutable and managed by Nix store
- Auto-deploy pywal colors to ~/.cache/wal/colors.json

All AGS configuration is now reproducible and version controlled.
This commit is contained in:
2025-11-04 21:36:38 +00:00
parent 593735370a
commit b2ae32a078
240 changed files with 1024921 additions and 3 deletions

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Kirollos Risk
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,64 @@
# Fuse.js
![Node.js CI](https://github.com/krisk/Fuse/workflows/Node.js%20CI/badge.svg)
[![Version](https://img.shields.io/npm/v/fuse.js.svg)](https://www.npmjs.com/package/fuse.js)
[![Downloads](https://img.shields.io/npm/dm/fuse.js.svg)](https://npmcharts.com/compare/fuse.js?minimal=tru)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
[![Contributors](https://img.shields.io/github/contributors/krisk/fuse.svg)](https://github.com/krisk/Fuse/graphs/contributors)
![License](https://img.shields.io/npm/l/fuse.js.svg)
## Supporting Fuse.js
Through contributions, donations, and sponsorship, you allow Fuse.js to thrive. Also, you will be recognized as a beacon of support to open-source developers.
- [Become a backer or sponsor on **GitHub**.](https://github.com/sponsors/krisk)
- [Become a backer or sponsor on **Patreon**.](https://patreon.com/fusejs)
- [One-time donation via **PayPal**.](https://www.paypal.me/kirorisk)
---
<h3 align="center">Sponsors</h3>
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://www.worksome.com" target="_blank">
<img width="222px" src="https://raw.githubusercontent.com/krisk/Fuse/7a0d77d85ac90063575613b6a738f418b624357f/docs/.vuepress/public/assets/img/sponsors/worksome.svg">
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.bairesdev.com/sponsoring-open-source-projects/" target="_blank">
<img width="222px" src="https://github.com/krisk/Fuse/blob/gh-pages/assets/img/sponsors/bairesdev.png?raw=true">
</a>
</td>
<td align="center" valign="middle">
<a href="https://litslink.com/" target="_blank">
<img width="222px" src="https://github.com/krisk/Fuse/blob/gh-pages/assets/img/sponsors/litslink.svg?raw=true">
</a>
</td>
</tr>
</body>
</table>
---
## Introduction
Fuse.js is a lightweight fuzzy-search, in JavaScript, with zero dependencies.
### Browser Compatibility
Fuse.js supports all browsers that are [ES5-compliant](http://kangax.github.io/compat-table/es5/) (IE8 and below are not supported).
## Documentation
To check out a [live demo](https://fusejs.io/demo.html) and docs, visit [fusejs.io](https://fusejs.io).
## Develop
Here's a separate document for [developers](https://github.com/krisk/Fuse/blob/master/DEVELOPERS.md).
## Contribute
We've set up a separate document for our
[contribution guidelines](https://github.com/krisk/Fuse/blob/master/CONTRIBUTING.md).

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,373 @@
// Type definitions for Fuse.js v7.1.0
// TypeScript v4.9.5
declare class Fuse<T> {
public constructor(
list: ReadonlyArray<T>,
options?: IFuseOptions<T>,
index?: FuseIndex<T>
)
/**
* Search function for the Fuse instance.
*
* ```typescript
* const list: MyType[] = [myType1, myType2, etc...]
* const options: Fuse.IFuseOptions<MyType> = {
* keys: ['key1', 'key2']
* }
*
* const myFuse = new Fuse(list, options)
* let result = myFuse.search('pattern')
* ```
*
* @param pattern The pattern to search
* @param options `Fuse.FuseSearchOptions`
* @returns An array of search results
*/
public search<R = T>(
pattern: string | Expression,
options?: FuseSearchOptions
): FuseResult<R>[]
public setCollection(docs: ReadonlyArray<T>, index?: FuseIndex<T>): void
/**
* Adds a doc to the end the list.
*/
public add(doc: T): void
/**
* Removes all documents from the list which the predicate returns truthy for,
* and returns an array of the removed docs.
* The predicate is invoked with two arguments: (doc, index).
*/
public remove(predicate: (doc: T, idx: number) => boolean): T[]
/**
* Removes the doc at the specified index.
*/
public removeAt(idx: number): void
/**
* Returns the generated Fuse index
*/
public getIndex(): FuseIndex<T>
/**
* Return the current version.
*/
public static version: string
/**
* Use this method to pre-generate the index from the list, and pass it
* directly into the Fuse instance.
*
* _Note that Fuse will automatically index the table if one isn't provided
* during instantiation._
*
* ```typescript
* const list: MyType[] = [myType1, myType2, etc...]
*
* const index = Fuse.createIndex<MyType>(
* keys: ['key1', 'key2']
* list: list
* )
*
* const options: Fuse.IFuseOptions<MyType> = {
* keys: ['key1', 'key2']
* }
*
* const myFuse = new Fuse(list, options, index)
* ```
* @param keys The keys to index
* @param list The list from which to create an index
* @param options?
* @returns An indexed list
*/
public static createIndex<U>(
keys: Array<FuseOptionKey<U>>,
list: ReadonlyArray<U>,
options?: FuseIndexOptions<U>
): FuseIndex<U>
public static parseIndex<U>(
index: {
keys: ReadonlyArray<string>
records: FuseIndexRecords
},
options?: FuseIndexOptions<U>
): FuseIndex<U>
public static config: Required<IFuseOptions<any>>
}
declare class FuseIndex<T> {
public constructor(options?: FuseIndexOptions<T>)
public setSources(docs: ReadonlyArray<T>): void
public setKeys(keys: ReadonlyArray<string>): void
public setIndexRecords(records: FuseIndexRecords): void
public create(): void
public add(doc: T): void
public toJSON(): {
keys: ReadonlyArray<string>
records: FuseIndexRecords
}
}
type FuseGetFunction<T> = (
obj: T,
path: string | string[]
) => ReadonlyArray<string> | string
type FuseIndexOptions<T> = {
getFn: FuseGetFunction<T>
}
/**
* @example
* ```ts
* {
* title: { '$': "Old Man's War" },
* 'author.firstName': { '$': 'Codenar' }
* }
* ```
* @example
* ```ts
* {
* tags: [
* { $: 'nonfiction', idx: 0 },
* { $: 'web development', idx: 1 },
* ]
* }
* ```
*/
type FuseSortFunctionItem = {
[key: string]: { $: string } | { $: string; idx: number }[]
}
/**
* @example
* ```ts
* {
* score: 0.001,
* key: 'author.firstName',
* value: 'Codenar',
* indices: [ [ 0, 3 ] ]
* }
* ```
*/
type FuseSortFunctionMatch = {
score: number
key: string
value: string
indices: ReadonlyArray<number>[]
}
/**
* @example
* ```ts
* {
* score: 0,
* key: 'tags',
* value: 'nonfiction',
* idx: 1,
* indices: [ [ 0, 9 ] ]
* }
* ```
*/
type FuseSortFunctionMatchList = FuseSortFunctionMatch & {
idx: number
}
type FuseSortFunctionArg = {
idx: number
item: FuseSortFunctionItem
score: number
matches?: (FuseSortFunctionMatch | FuseSortFunctionMatchList)[]
}
type FuseSortFunction = (
a: FuseSortFunctionArg,
b: FuseSortFunctionArg
) => number
/**
* @example
* ```ts
* title: {
* '$': "Old Man's War",
* 'n': 0.5773502691896258
* }
* ```
*/
type RecordEntryObject = {
/** The text value */
v: string
/** The field-length norm */
n: number
}
/**
* @example
* ```ts
* 'author.tags.name': [{
* 'v': 'pizza lover',
* 'i': 2,
* 'n: 0.7071067811865475
* }
* ```
*/
type RecordEntryArrayItem = ReadonlyArray<
RecordEntryObject & { i: number }
>
// TODO: this makes it difficult to infer the type. Need to think more about this
type RecordEntry = {
[key: string]: RecordEntryObject | RecordEntryArrayItem
}
/**
* @example
* ```ts
* {
* i: 0,
* '$': {
* '0': { v: "Old Man's War", n: 0.5773502691896258 },
* '1': { v: 'Codenar', n: 1 },
* '2': [
* { v: 'pizza lover', i: 2, n: 0.7071067811865475 },
* { v: 'helo wold', i: 1, n: 0.7071067811865475 },
* { v: 'hello world', i: 0, n: 0.7071067811865475 }
* ]
* }
* }
* ```
*/
type FuseIndexObjectRecord = {
/** The index of the record in the source list */
i: number
$: RecordEntry
}
/**
* @example
* ```ts
* {
* keys: null,
* list: [
* { v: 'one', i: 0, n: 1 },
* { v: 'two', i: 1, n: 1 },
* { v: 'three', i: 2, n: 1 }
* ]
* }
* ```
*/
type FuseIndexStringRecord = {
/** The index of the record in the source list */
i: number
/** The text value */
v: string
/** The field-length norm */
n: number
}
type FuseIndexRecords =
| ReadonlyArray<FuseIndexObjectRecord>
| ReadonlyArray<FuseIndexStringRecord>
/**
* @example
* ```ts
* {
* name: 'title',
* weight: 0.7
* }
* ```
*/
type FuseOptionKeyObject<T> = {
name: string | string[]
weight?: number
getFn?: (obj: T) => ReadonlyArray<string> | string
}
type FuseOptionKey<T> = FuseOptionKeyObject<T> | string | string[]
interface IFuseOptions<T> {
/** Indicates whether comparisons should be case sensitive. */
isCaseSensitive?: boolean
/** Indicates whether comparisons should ignore diacritics (accents). */
ignoreDiacritics?: boolean
/** Determines how close the match must be to the fuzzy location (specified by `location`). An exact letter match which is `distance` characters away from the fuzzy location would score as a complete mismatch. A `distance` of `0` requires the match be at the exact `location` specified. A distance of `1000` would require a perfect match to be within `800` characters of the `location` to be found using a `threshold` of `0.8`. */
distance?: number
/** When true, the matching function will continue to the end of a search pattern even if a perfect match has already been located in the string. */
findAllMatches?: boolean
/** The function to use to retrieve an object's value at the provided path. The default will also search nested paths. */
getFn?: FuseGetFunction<T>
/** When `true`, search will ignore `location` and `distance`, so it won't matter where in the string the pattern appears. */
ignoreLocation?: boolean
/** When `true`, the calculation for the relevance score (used for sorting) will ignore the `field-length norm`. */
ignoreFieldNorm?: boolean
/** Determines how much the `field-length norm` affects scoring. A value of `0` is equivalent to ignoring the field-length norm. A value of `0.5` will greatly reduce the effect of field-length norm, while a value of `2.0` will greatly increase it. */
fieldNormWeight?: number
/** Whether the matches should be included in the result set. When `true`, each record in the result set will include the indices of the matched characters. These can consequently be used for highlighting purposes. */
includeMatches?: boolean
/** Whether the score should be included in the result set. A score of `0`indicates a perfect match, while a score of `1` indicates a complete mismatch. */
includeScore?: boolean
/** List of keys that will be searched. This supports nested paths, weighted search, searching in arrays of `strings` and `objects`. */
keys?: Array<FuseOptionKey<T>>
/** Determines approximately where in the text is the pattern expected to be found. */
location?: number
/** Only the matches whose length exceeds this value will be returned. (For instance, if you want to ignore single character matches in the result, set it to `2`). */
minMatchCharLength?: number
/** Whether to sort the result list, by score. */
shouldSort?: boolean
/** The function to use to sort all the results. The default will sort by ascending relevance score, ascending index. */
sortFn?: FuseSortFunction
/** At what point does the match algorithm give up. A threshold of `0.0` requires a perfect match (of both letters and location), a threshold of `1.0` would match anything. */
threshold?: number
/** When `true`, it enables the use of unix-like search commands. See [example](/examples.html#extended-search). */
useExtendedSearch?: boolean
}
/**
* Denotes the start/end indices of a match
*
* @example
*
* ```ts
* const startIndex = 0;
* const endIndex = 5;
*
* const range: RangeTuple = [startIndex, endIndex];
* ```
*/
type RangeTuple = [number, number]
type FuseResultMatch = {
indices: ReadonlyArray<RangeTuple>
key?: string
refIndex?: number
value?: string
}
type FuseSearchOptions = {
limit: number
}
type FuseResult<T> = {
item: T
refIndex: number
score?: number
matches?: ReadonlyArray<FuseResultMatch>
}
type Expression =
| { [key: string]: string }
| {
$path: ReadonlyArray<string>
$val: string
}
| { $and?: Expression[] }
| { $or?: Expression[] }
export { Expression, FuseGetFunction, FuseIndex, FuseIndexObjectRecord, FuseIndexOptions, FuseIndexRecords, FuseIndexStringRecord, FuseOptionKey, FuseOptionKeyObject, FuseResult, FuseResultMatch, FuseSearchOptions, FuseSortFunction, FuseSortFunctionArg, FuseSortFunctionItem, FuseSortFunctionMatch, FuseSortFunctionMatchList, IFuseOptions, RangeTuple, RecordEntry, RecordEntryArrayItem, RecordEntryObject, Fuse as default };

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,118 @@
{
"name": "fuse.js",
"author": {
"name": "Kiro Risk",
"email": "kirollos@gmail.com",
"url": "http://kiro.me"
},
"type": "module",
"main": "./dist/fuse.cjs",
"module": "./dist/fuse.mjs",
"unpkg": "./dist/fuse.js",
"jsdelivr": "./dist/fuse.js",
"types": "./dist/fuse.d.ts",
"exports": {
".": {
"types": "./dist/fuse.d.ts",
"import": "./dist/fuse.mjs",
"require": "./dist/fuse.cjs"
},
"./min": {
"types": "./dist/fuse.d.ts",
"import": "./dist/fuse.min.mjs",
"require": "./dist/fuse.min.cjs"
},
"./basic": {
"types": "./dist/fuse.d.ts",
"import": "./dist/fuse.basic.mjs",
"require": "./dist/fuse.basic.cjs"
},
"./min-basic": {
"types": "./dist/fuse.d.ts",
"import": "./dist/fuse.basic.min.mjs",
"require": "./dist/fuse.basic.min.cjs"
}
},
"sideEffects": false,
"files": [
"dist"
],
"version": "7.1.0",
"description": "Lightweight fuzzy-search",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/krisk/Fuse.git"
},
"homepage": "http://fusejs.io",
"keywords": [
"fuzzy",
"search",
"bitap"
],
"scripts": {
"dev": "rollup -w -c scripts/configs.cjs --environment TARGET:umd-dev-full",
"dev:cjs": "rollup -w -c scripts/configs.cjs --environment TARGET:commonjs-full",
"dev:esm": "rollup -w -c scripts/configs.cjs --environment TARGET:esm-dev-full",
"build": "rm -r dist && mkdir dist && node ./scripts/build.main.cjs",
"test": "vitest run",
"lint": "eslint src test",
"release": "./scripts/release.sh",
"docs:bump": "node ./scripts/bump-docs.cjs",
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs",
"docs:release": "./scripts/deploy-docs.sh",
"prepare": "husky install"
},
"standard-version": {
"scripts": {
"postbump": "npm run build && npm run lint && npm run test 2>/dev/null",
"precommit": "git add dist/*.js dist/*.cjs dist/*.mjs dist/*.d.ts"
}
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"devDependencies": {
"@babel/cli": "^7.20.7",
"@babel/core": "^7.20.12",
"@babel/eslint-parser": "^7.19.1",
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-syntax-import-assertions": "^7.22.5",
"@babel/preset-env": "7.20.2",
"@babel/preset-typescript": "7.18.6",
"@commitlint/cli": "^17.4.2",
"@commitlint/config-conventional": "^17.4.2",
"@monaco-editor/loader": "^1.3.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-replace": "^5.0.2",
"@sapphire/stopwatch": "^1.5.0",
"@sapphire/utilities": "^3.11.0",
"@snippetors/vuepress-plugin-tabs": "1.2.3",
"@vuepress/plugin-google-analytics": "2.0.0-beta.60",
"@vuepress/plugin-pwa": "2.0.0-beta.60",
"@vuepress/plugin-register-components": "2.0.0-beta.60",
"@vuepress/plugin-search": "2.0.0-beta.60",
"babel-loader": "^9.1.2",
"eslint": "8.32.0",
"eslint-config-prettier": "8.6.0",
"husky": "^8.0.3",
"monaco-editor": "^0.34.1",
"prettier": "2.8.3",
"replace-in-file": "^6.3.5",
"rollup": "^2.79.1",
"rollup-plugin-dts": "^5.3.0",
"standard-version": "^9.5.0",
"terser": "^5.16.1",
"typescript": "^4.9.4",
"vitest": "^0.28.3",
"vuepress": "2.0.0-beta.60",
"vuepress-plugin-google-adsense2": "1.0.2"
},
"engines": {
"node": ">=10"
}
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 João Dias
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,297 @@
import { Accessor, For, getScope, Node, With } from "gnim";
import GObject from "gnim/gobject";
/** re-exported gnim's jsx node type */
export type JSXNode = Node;
/** subscribes to an accessor, with the extra of when
* the scope is disposed, the subscription is also disposed */
export function createSubscription<T = any>(
accessor: Accessor<T>,
callback: () => void
): void {
const scope = getScope();
const sub = accessor.subscribe(callback);
scope.onCleanup(sub);
}
/** convert a normal value or accessor to a boolean/Accessor<boolean> value.
* equivalent to Boolean(value), but adds support to Accessor and arrays.
*
* @returns false when the value is falsy("", 0, false, undefined, null) or an empty array,
* if something else, true */
export function toBoolean(variable: any|Array<any>|Accessor<Array<any>|any>): boolean|Accessor<boolean> {
return (variable instanceof Accessor) ?
variable.as(v => Array.isArray(v) ?
(v as Array<any>).length > 0
: Boolean(v))
: Array.isArray(variable) ?
variable.length > 0
: Boolean(variable);
}
/** securely bind to a GObject property. works the same as gnim's createBinding, but
* allows setting a value to return when things go wrong.
*
* @param gobj the gobject to bind a property of
* @param prop the property to bind
* @param defaultValue the value to return when something goes wrong
*
* @example
* the gobject is disposed/destroyed, return the default value.
* the property is removed, return the default value */
export function createSecureBinding<
GObj extends GObject.Object,
Prop extends keyof GObj,
Returns extends unknown
>(
gobj: GObj,
prop: Prop,
defaultValue: Returns
): Accessor<GObj[Prop]|Returns> {
const get = () => gobj && Object.hasOwn(gobj, prop) ? gobj[prop] : defaultValue;
return new Accessor<GObj[Prop]|Returns>(
get,
(notify) => {
const gobjectProp = (prop as string).replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`);
const id = gobj.connect(`notify::${gobjectProp}`, () => notify());
return () => {
try {
gobj.disconnect(id);
} catch(e) {}
}
}
);
}
/** bind to a property inside an existing accessor.
* use this when the property you want to bind is inside another
* property of a GObject.
*
* @param accessorObject the gobject property that links to a gobject
* @param prop the property to bind from the gobject accessor
*
* You need to provide the GObject type in a generic type format.
* @example
* \/\/ MainGObject is a GObject here
* \/\/ It has the property "subGObject", which points to a existing GObject(AnotherGObject)
* const mainGObject = new MainGObject();
*
* \/\/ The AnotherGObject GObject has the property "exampleProperty", which is a string
* createAccessorBinding<AnotherGObject>(
* createBinding(mainGObject, "subGObject"),
* "exampleProperty"
* );
* */
export function createAccessorBinding<
T extends GObject.Object = GObject.Object,
Prop extends keyof T = keyof T
>(
accessorObject: Accessor<T>,
prop: Prop
): Accessor<T[Prop]> {
let gobj: T|undefined = accessorObject.get();
let notify: () => void;
const baseSub = accessorObject.subscribe(() => {
const newBase = accessorObject.get();
if(!newBase) {
gobj = undefined;
notify!();
return;
}
gobj = newBase;
notify!();
});
const accessor = new Accessor<T[Prop]>(
() => gobj![prop],
(notifyFun) => {
notify = notifyFun;
const id = gobj?.connect(
`notify::${(prop as string).replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`)}`,
() => notify()
);
return () => {
id && gobj?.disconnect(id);
baseSub();
}
}
);
return accessor;
}
/** securely bind to a property of an existing gobject wrapped with an accessor.
*
* It follows the same idea of secureBinding: allows setting
* a default value to return when the base gobject is null.
*
* @param accessor a binding to the constantly updated property
* that points to the gobject
* @param prop the property to bind
* @param defaultValue the value to return when the baseObject is
* null/undefined
*
* @returns a bind to the specified property of the constantly-updated
* object or the default value.
* */
export function createSecureAccessorBinding<
T extends GObject.Object = GObject.Object,
Prop extends keyof T = keyof T,
Default = any
>(
baseObject: Accessor<T>,
prop: Prop,
defaultValue: Default
): Accessor<T[Prop]|Default> {
let gobj: T|undefined = baseObject.get();
let notify: () => void;
const baseSub = baseObject.subscribe(() => {
const newBase = baseObject.get();
if(!newBase) {
gobj = undefined;
notify!();
return;
}
gobj = newBase;
notify!();
});
const accessor = new Accessor<T[Prop]|Default>(
() => gobj ? gobj[prop] : defaultValue,
(notifyFun) => {
notify = notifyFun;
const id = gobj?.connect(
`notify::${(prop as string).replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`)}`,
() => notify()
);
return () => {
id && gobj?.disconnect(id);
baseSub();
}
}
);
return accessor;
}
/** transform a normal value or an accessor to something else
* @returns the transformation result */
export function transform<ValueType = any|Array<any>, RType = any>(
v: Accessor<ValueType>|ValueType, fn: (v: ValueType) => RType
): RType|Accessor<RType> {
return (v instanceof Accessor) ?
v.as(fn)
: fn(v);
}
/** transform data or accessor containing data to widget(s)
* if an array is provided, the callback will act like a forEach function.
*
* @returns the baked widgets */
export function transformWidget<ValueType = unknown>(
v: Accessor<ValueType|Array<ValueType>>|ValueType|Array<ValueType>,
fn: (v: ValueType, i?: Accessor<number>|number) => JSX.Element
): JSXNode {
return (v instanceof Accessor) ?
Array.isArray(v.get()) ?
For({
each: v as Accessor<Array<ValueType>>,
children: (cval, i) => fn(cval, i)
})
: With({
value: v as Accessor<ValueType>,
children: fn
})
: (Array.isArray(v) ?
v.map(val => fn(val))
: fn(v));
}
/** filter normal data types or an array wrapped inside an accessor
* @returns the filtered data */
export function filter<ValueType = unknown, FilterReturnType = unknown>(
v: Accessor<Array<ValueType>>|Array<ValueType>,
fn: (v: ValueType, i: number, array: Array<ValueType>) => FilterReturnType
): Array<ValueType>|Accessor<Array<ValueType>> {
return ((v instanceof Accessor) ?
v(v => v.filter((it, i, arr) => fn(it, i, arr)))
: v.filter((it, i, arr) => fn(it, i, arr)));
}
/** initialize class fields with a props object and dispose subscriptions together with
* the current scope.
* class fields need to have the same name as in the property object to make this work.
*
* this should be used when the class constructor contains props that are
* defined as accessors and normal values together.
*
* it's not recommended to use this method, instead, you can subclass widgets and use
* property decorators and JSX syntax, which allows you to use accessors as property
* values without explicitly allowing them to be accessors.
*
* @param klass the class to apply the field values to
* @param props the props object containing the keys and their values
*
* @returns an array containing all the subscriptions */
export function construct<Class extends object>(klass: Class, props: Record<string|number|symbol, any|Accessor<any>>): Array<() => void> {
const subs: Array<() => void> = [];
const isGObject = klass instanceof GObject.Object;
Object.keys(props).forEach(k => {
const v = props[k as keyof typeof props];
if(v === undefined) return;
if(v instanceof Accessor) {
subs.push(v.subscribe(() => {
klass[k as keyof Class] = v.get() as Class[keyof Class];
if(isGObject)
klass.notify(k.replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`));
}));
klass[k as keyof Class] = v.get() as Class[keyof Class];
return;
}
klass[k as keyof Class] = v as Class[keyof Class];
});
return subs;
}
/** works the same as connecting to a signal of the gobject, but
* it's disposed as soon as the current scope is disposed. */
export function createScopedConnection<
GObj extends GObject.Object,
Signal extends keyof GObj["$signals"]
>(
gobj: GObj,
signal: Signal,
callback: GObj["$signals"][Signal]
): void {
const scope = getScope();
const id = gobj.connect(signal as string, (_, ...args) =>
(callback as Function)(...args)
);
scope.onCleanup(() => gobj.disconnect(id));
}

View File

@@ -0,0 +1,29 @@
{
"$schema": "https://schemastore.org/package.json",
"name": "gnim-utils",
"version": "1.0.0",
"description": "util functions for development using gnim",
"type": "module",
"keywords": [
"gnim",
"library",
"util",
"gnim-library"
],
"author": {
"name": "retrozinndev",
"url": "https://github.com/retrozinndev",
"email": "joaovodias@gmail.com"
},
"scripts": {
"types": "sh scripts/types.sh"
},
"exports": {
".": "./index.ts"
},
"license": "MIT",
"packageManager": "pnpm@10.12.1",
"peerDependencies": {
"gnim": "^1.8.2"
}
}

View File

@@ -0,0 +1,9 @@
if [[ -d "@types" ]] && [[ ! "$1" == "-f" ]]; then
echo "Types skipped(already built). To force-build, run \`types\`"
exit 0
fi
echo "Building types, this can take long..."
pnpx @ts-for-gir/cli generate --ignoreVersionConflicts -o ./@types

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://www.schemastore.org/tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"module": "ES2022",
"lib": ["ES2024"],
"strict": true,
"types": [
"./@types/gi.d.ts"
],
"moduleResolution": "bundler",
"skipLibCheck": true,
"jsx": "react-jsx",
"jsxImportSource": "gnim/gtk4"
}
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Aylur
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,39 @@
# Gnim
Library which brings JSX and reactivity to GNOME JavaScript.
If you are not already familiar with GJS and GObject, you should read
[gjs.guide](https://gjs.guide/) first.
This library provides:
- [JSX and reactivity](https://aylur.github.io/gnim/jsx) for both Gtk
Applications and Gnome extensions
- [GObject decorators](https://aylur.github.io/gnim/gobject) for a convenient
and type safe way for subclassing GObjects
- [DBus decorators](https://aylur.github.io/gnim/dbus) for a convenient and type
safe way for implementing DBus services and proxies.
## Obligatory Counter Example
```tsx
function Counter() {
const [counter, setCounter] = createState(0)
function increment() {
setCounter((v) => v + 1)
}
return (
<Gtk.Box spacing={8}>
<Gtk.Label label={counter((c) => c.toString())} />
<Gtk.Button onClicked={increment}>Increment</Gtk.Button>
</Gtk.Box>
)
}
```
## Templates
- [gnome-extension](https://github.com/Aylur/gnome-shell-extension-template/)
- [gtk4](https://github.com/Aylur/gnim-gtk4-template/)

View File

@@ -0,0 +1,864 @@
/**
* A {@link Service} currently only allows interfacing with a single interface of a remote object.
* In the future I want to come up with an API to be able to create Service objects for multiple
* interfaces of an object at the same time. Example usage would be for example combining
* "org.mpris.MediaPlayer2" and "org.mpris.MediaPlayer2.Player" into a single object.
*/
import Gio from "gi://Gio"
import GLib from "gi://GLib"
import GObject from "gi://GObject"
import { definePropertyGetter, kebabify, xml } from "./util.js"
import type { DeepInfer } from "./variant.js"
import {
register,
property as gproperty,
signal as gsignal,
getter as ggetter,
setter as gsetter,
} from "./gobject.js"
const DEFAULT_TIMEOUT = 10_000
export const Variant = GLib.Variant
export type Variant<T extends string> = GLib.Variant<T>
const info = Symbol("dbus interface info")
const internals = Symbol("dbus interface internals")
const remoteMethod = Symbol("proxy remoteMethod")
const remoteMethodAsync = Symbol("proxy remoteMethodAsync")
const remotePropertySet = Symbol("proxy remotePropertySet")
type Ctx = { private: false; static: false; name: string }
/**
* Base type for DBus services and proxies. Interface name is set with
* the {@link iface} decorator which also register it as a GObject type.
*/
export class Service extends GObject.Object {
static [info]?: Gio.DBusInterfaceInfo
static {
GObject.registerClass(this)
}
[internals]: {
dbusObject?: Gio.DBusExportedObject
proxy?: Gio.DBusProxy
priv: Record<string | symbol, unknown>
onStop: Set<() => void>
} = {
priv: {},
onStop: new Set<() => void>(),
}
#info: Gio.DBusInterfaceInfo
constructor() {
super()
const service = this.constructor as unknown as typeof Service
if (!service[info]) throw Error("missing interface info")
this.#info = service[info]
}
notify(propertyName: Extract<keyof this, string> | (string & {})): void {
const prop = this.#info.lookup_property(propertyName)
if (prop && this[internals].dbusObject) {
this[internals].dbusObject.emit_property_changed(
propertyName,
new GLib.Variant(prop.signature, this[propertyName as keyof this]),
)
}
super.notify(prop ? kebabify(propertyName) : propertyName)
}
emit(name: string, ...params: unknown[]): void {
const signal = this.#info.lookup_signal(name)
if (signal && this[internals].dbusObject) {
const signature = `(${signal.args.map((a) => a.signature).join("")})`
this[internals].dbusObject.emit_signal(name, new GLib.Variant(signature, params))
}
return super.emit(signal ? kebabify(name) : name, ...params)
}
// server
#handlePropertyGet(_: Gio.DBusExportedObject, propertyName: Extract<keyof this, string>) {
const prop = this.#info.lookup_property(propertyName)
if (!prop) {
throw Error(`${this.constructor.name} has no exported property: "${propertyName}"`)
}
const value = this[propertyName]
if (typeof value !== "undefined") {
return new GLib.Variant(prop.signature, value)
} else {
return null
}
}
// server
#handlePropertySet(
_: Gio.DBusExportedObject,
propertyName: Extract<keyof this, string>,
value: GLib.Variant,
) {
const newValue = value.deepUnpack()
const prop = this.#info.lookup_property(propertyName)
if (!prop) {
throw Error(`${this.constructor.name} has no property: "${propertyName}"`)
}
if (this[propertyName] !== newValue) {
this[propertyName] = value.deepUnpack<any>()
}
}
// server
#returnError(error: unknown, invocation: Gio.DBusMethodInvocation) {
console.error(error)
if (error instanceof GLib.Error) {
return invocation.return_gerror(error)
}
if (error instanceof Error) {
return invocation.return_dbus_error(
error.name.includes(".") ? error.name : `gjs.JSError.${error.name}`,
error.message,
)
}
invocation.return_dbus_error("gjs.DBusService.UnknownError", `${error}`)
}
// server
#returnValue(value: unknown, methodName: string, invocation: Gio.DBusMethodInvocation) {
if (value === null || value === undefined) {
return invocation.return_value(new GLib.Variant("()", []))
}
const args = this.#info.lookup_method(methodName)?.out_args ?? []
const signature = `(${args.map((arg) => arg.signature).join("")})`
if (!Array.isArray(value)) throw Error("value has to be a tuple")
invocation.return_value(new GLib.Variant(signature, value))
}
// server
#handleMethodCall(
_: Gio.DBusExportedObject,
methodName: Extract<keyof this, string>,
parameters: GLib.Variant,
invocation: Gio.DBusMethodInvocation,
): void {
try {
const value = (this[methodName] as (...args: unknown[]) => unknown)(
...parameters.deepUnpack<Array<unknown>>(),
)
if (value instanceof GLib.Variant) {
invocation.return_value(value)
} else if (value instanceof Promise) {
value
.then((value) => this.#returnValue(value, methodName, invocation))
.catch((error) => this.#returnError(error, invocation))
} else {
this.#returnValue(value, methodName, invocation)
}
} catch (error) {
this.#returnError(error, invocation)
}
}
// server
async serve({
busType = Gio.BusType.SESSION,
name = this.#info.name,
objectPath = "/" + this.#info.name.split(".").join("/"),
flags = Gio.BusNameOwnerFlags.NONE,
timeout = DEFAULT_TIMEOUT,
}: {
busType?: Gio.BusType
name?: string
objectPath?: string
flags?: Gio.BusNameOwnerFlags
timeout?: number
} = {}): Promise<this> {
const impl = new Gio.DBusExportedObject(
// @ts-expect-error missing constructor type
{ g_interface_info: this.#info },
)
impl.connect("handle-method-call", this.#handleMethodCall.bind(this))
impl.connect("handle-property-get", this.#handlePropertyGet.bind(this))
impl.connect("handle-property-set", this.#handlePropertySet.bind(this))
this.#info.cache_build()
return new Promise((resolve, reject) => {
let source =
timeout > 0
? setTimeout(() => {
reject(Error(`serve timed out`))
source = null
}, timeout)
: null
const clear = () => {
if (source) {
clearTimeout(source)
source = null
}
}
const busId = Gio.bus_own_name(
busType,
name,
flags,
(conn: Gio.DBusConnection) => {
try {
impl.export(conn, objectPath)
this[internals].dbusObject = impl
this[internals].onStop.add(() => {
Gio.bus_unown_name(busId)
impl.unexport()
this.#info.cache_release()
delete this[internals].dbusObject
})
resolve(this)
} catch (error) {
reject(error)
}
},
clear,
clear,
)
})
}
// proxy
#handlePropertiesChanged(
_: Gio.DBusProxy,
changed: GLib.Variant<"a{sv}">,
invalidated: string[],
) {
const set = new Set([...Object.keys(changed.deepUnpack()), ...invalidated])
for (const prop of set.values()) {
this.notify(prop as Extract<keyof this, string>)
}
}
// proxy
#handleSignal(
_: Gio.DBusProxy,
_sender: string | null,
signal: string,
parameters: GLib.Variant,
) {
this.emit(kebabify(signal), ...parameters.deepUnpack<Array<unknown>>())
}
// proxy
#remoteMethodParams(
methodName: string,
args: unknown[],
): Parameters<Gio.DBusProxy["call_sync"]> {
const { proxy } = this[internals]
if (!proxy) throw Error("invalid remoteMethod invocation: not a proxy")
const method = this.#info.lookup_method(methodName)
if (!method) throw Error("method not found")
const signature = `(${method.in_args.map((a) => a.signature).join("")})`
return [
methodName,
new GLib.Variant(signature, args),
Gio.DBusCallFlags.NONE,
DEFAULT_TIMEOUT,
null,
]
}
// proxy
[remoteMethod](methodName: string, args: unknown[]): GLib.Variant {
const params = this.#remoteMethodParams(methodName, args)
return this[internals].proxy!.call_sync(...params)
}
// proxy
[remoteMethodAsync](methodName: string, args: unknown[]): Promise<GLib.Variant> {
return new Promise((resolve, reject) => {
try {
const params = this.#remoteMethodParams(methodName, args)
this[internals].proxy!.call(...params, (_, res) => {
try {
resolve(this[internals].proxy!.call_finish(res))
} catch (error) {
reject(error)
}
})
} catch (error) {
reject(error)
}
})
}
// proxy
[remotePropertySet](name: string, value: unknown) {
const proxy = this[internals].proxy!
const prop = this.#info.lookup_property(name)!
const variant = new GLib.Variant(prop.signature, value)
proxy.set_cached_property(name, variant)
proxy.call(
"org.freedesktop.DBus.Properties.Set",
new GLib.Variant("(ssv)", [proxy.gInterfaceName, name, variant]),
Gio.DBusCallFlags.NONE,
-1,
null,
(_, res) => {
try {
proxy.call_finish(res)
} catch (e) {
console.error(e)
}
},
)
}
// proxy
async proxy({
bus = Gio.DBus.session,
name = this.#info.name,
objectPath = "/" + this.#info.name.split(".").join("/"),
flags = Gio.DBusProxyFlags.NONE,
timeout = DEFAULT_TIMEOUT,
}: {
bus?: Gio.DBusConnection
name?: string
objectPath?: string
flags?: Gio.DBusProxyFlags
timeout?: number
} = {}): Promise<this> {
const proxy = new Gio.DBusProxy({
gConnection: bus,
gInterfaceName: this.#info.name,
gInterfaceInfo: this.#info,
gName: name,
gFlags: flags,
gObjectPath: objectPath,
})
return new Promise((resolve, reject) => {
const cancallable = new Gio.Cancellable()
let source =
timeout > 0
? setTimeout(() => {
reject(Error(`proxy timed out`))
source = null
cancallable.cancel()
}, timeout)
: null
proxy.init_async(GLib.PRIORITY_DEFAULT, cancallable, (_, res) => {
try {
if (source) {
clearTimeout(source)
source = null
}
proxy.init_finish(res)
this[internals].proxy = proxy
const ids = [
proxy.connect("g-signal", this.#handleSignal.bind(this)),
proxy.connect(
"g-properties-changed",
this.#handlePropertiesChanged.bind(this),
),
]
this[internals].onStop.add(() => {
ids.forEach((id) => proxy.disconnect(id))
delete this[internals].proxy
})
resolve(this)
} catch (error) {
reject(error)
}
})
})
}
stop() {
const { onStop } = this[internals]
for (const cb of onStop.values()) {
onStop.delete(cb)
cb()
}
}
}
type InterfaceMeta = {
dbusMethods?: Record<
string,
Array<{
name?: string
type: string
direction: "in" | "out"
}>
>
dbusSignals?: Record<
string,
Array<{
name?: string
type: string
}>
>
dbusProperties?: Record<
string,
{
name: string
type: string
read?: true
write?: true
}
>
}
/**
* Registers a {@link Service} as a dbus interface.
*
* @param name Interface name of the object. For example "org.gnome.Shell.SearchProvider2"
* @param options optional properties to pass to {@link register}
*/
export function iface(name: string, options?: Parameters<typeof register>[0]) {
return function (cls: { new (...args: any[]): Service }, ctx: ClassDecoratorContext) {
const meta = ctx.metadata
if (!meta) throw Error(`${cls.name} is not an interface`)
const { dbusMethods = {}, dbusSignals = {}, dbusProperties = {} } = meta as InterfaceMeta
const infoXml = xml({
name: "node",
children: [
{
name: "interface",
attributes: { name },
children: [
...Object.entries(dbusMethods).map(([name, args]) => ({
name: "method",
attributes: { name },
children: args.map((arg) => ({ name: "arg", attributes: arg })),
})),
...Object.entries(dbusSignals).map(([name, args]) => ({
name: "signal",
attributes: { name },
children: args.map((arg) => ({ name: "arg", attributes: arg })),
})),
...Object.values(dbusProperties).map(({ name, type, read, write }) => ({
name: "property",
attributes: {
...(name && { name }),
type,
access: (read ? "read" : "") + (write ? "write" : ""),
},
})),
],
},
],
})
Object.assign(cls, { [info]: Gio.DBusInterfaceInfo.new_for_xml(infoXml) })
register(options)(cls, ctx)
}
}
type DBusType = string | { type: string; name: string }
type InferVariantTypes<T extends Array<DBusType>> = {
[K in keyof T]: T[K] extends string
? DeepInfer<T[K]>
: T[K] extends { type: infer S }
? S extends string
? DeepInfer<S>
: never
: unknown
}
function installMethod<Args extends Array<DBusType>>(
args: Args | [Args, Args?],
method: (...args: any[]) => unknown,
ctx: ClassMethodDecoratorContext<Service, typeof method>,
) {
const name = ctx.name
const meta = ctx.metadata! as InterfaceMeta
const methods = (meta.dbusMethods ??= {})
if (typeof name !== "string") {
throw Error("only string named methods are allowed")
}
const [inArgs, outArgs = []] = (Array.isArray(args[0]) ? args : [args]) as [Args, Args]
methods[name] = [
...inArgs.map((arg) => ({
direction: "in" as const,
...(typeof arg === "string" ? { type: arg } : arg),
})),
...outArgs.map((arg) => ({
direction: "out" as const,
...(typeof arg === "string" ? { type: arg } : arg),
})),
]
return name
}
function installProperty<T extends string>(
type: T,
ctx: ClassFieldDecoratorContext | ClassGetterDecoratorContext | ClassSetterDecoratorContext,
) {
const kind = ctx.kind
const name = ctx.name
const meta = ctx.metadata! as InterfaceMeta
const properties = (meta.dbusProperties ??= {})
if (typeof name !== "string") {
throw Error("only string named properties are allowed")
}
const read = kind === "field" || kind === "getter"
const write = kind === "field" || kind === "setter"
if (name in properties) {
if (write) properties[name].write = true
if (read) properties[name].read = true
} else {
properties[name] = {
name,
type,
...(read && { read }),
...(write && { write }),
}
}
return name
}
function installSignal<Params extends Array<DBusType>>(
params: Params,
ctx: ClassMethodDecoratorContext<Service>,
) {
const name = ctx.name
const meta = ctx.metadata! as InterfaceMeta
const signals = (meta.dbusSignals ??= {})
if (typeof name === "symbol") {
throw Error("symbols are not valid signals")
}
signals[name] = params.map((arg) => (typeof arg === "string" ? { type: arg } : arg))
return name
}
function inferGTypeFromVariant(type: DBusType): GObject.GType<any> {
if (typeof type !== "string") return inferGTypeFromVariant(type.type)
if (type.startsWith("a") || type.startsWith("(")) {
return GObject.TYPE_JSOBJECT
}
switch (type) {
case "v":
return GObject.TYPE_VARIANT
case "b":
return GObject.TYPE_BOOLEAN
case "y":
return GObject.TYPE_UINT
case "n":
return GObject.TYPE_INT
case "q":
return GObject.TYPE_UINT
case "i":
return GObject.TYPE_INT
case "u":
return GObject.TYPE_UINT
case "x":
return GObject.TYPE_INT64
case "t":
return GObject.TYPE_UINT64
case "h":
return GObject.TYPE_INT
case "d":
return GObject.TYPE_DOUBLE
case "s":
case "g":
case "o":
return GObject.TYPE_STRING
default:
break
}
throw Error(`cannot infer GType from variant "${type}"`)
}
/**
* Registers a method.
* You should prefer using {@link methodAsync} when proxying, due to IO blocking.
* Note that this is functionally the same as {@link methodAsync} on exported objects.
* ```
*/
export function method<const InArgs extends Array<DBusType>, const OutArgs extends Array<DBusType>>(
inArgs: InArgs,
outArgs: OutArgs,
): (
method: (this: Service, ...args: any[]) => InferVariantTypes<OutArgs>,
ctx: ClassMethodDecoratorContext<Service, typeof method>,
) => (this: Service, ...args: InferVariantTypes<InArgs>) => any
/**
* Registers a method.
* You should prefer using {@link methodAsync} when proxying, due to IO blocking.
* Note that this is functionally the same as {@link methodAsync} on exported objects.
* ```
*/
export function method<const InArgs extends Array<DBusType>>(
...inArgs: InArgs
): (
method: (this: Service, ...args: any[]) => void,
ctx: ClassMethodDecoratorContext<Service, typeof method>,
) => (this: Service, ...args: InferVariantTypes<InArgs>) => void
export function method<const InArgs extends Array<DBusType>, const OutArgs extends Array<DBusType>>(
...args: InArgs | [inArgs: InArgs, outArgs?: OutArgs]
) {
return function (
method: (
this: Service,
...args: InferVariantTypes<InArgs>
) => InferVariantTypes<OutArgs> | void,
ctx: ClassMethodDecoratorContext<Service, typeof method>,
): (this: Service, ...args: InferVariantTypes<InArgs>) => any {
const name = installMethod(args, method, ctx)
return function (...args) {
if (this[internals].proxy) {
const value = this[remoteMethod](name, args)
return value.deepUnpack<InferVariantTypes<OutArgs>>()
} else {
return method.apply(this, args)
}
}
}
}
/**
* Registers a method.
* You should prefer using this over {@link method} when proxying, since this does not block IO.
* Note that this is functionally the same as {@link method} on exported objects.
* ```
*/
export function methodAsync<
const InArgs extends Array<DBusType>,
const OutArgs extends Array<DBusType>,
>(
inArgs: InArgs,
outArgs: OutArgs,
): (
method: (this: Service, ...args: any[]) => Promise<InferVariantTypes<OutArgs>>,
ctx: ClassMethodDecoratorContext<Service, typeof method>,
) => (this: Service, ...args: InferVariantTypes<InArgs>) => Promise<any>
/**
* Registers a method.
* You should prefer using this over {@link method} when proxying, since this does not block IO.
* Note that this is functionally the same as {@link method} on exported objects.
* ```
*/
export function methodAsync<const InArgs extends Array<DBusType>>(
...inArgs: InArgs
): (
method: (this: Service, ...args: any[]) => Promise<void>,
ctx: ClassMethodDecoratorContext<Service, typeof method>,
) => (this: Service, ...args: InferVariantTypes<InArgs>) => Promise<void>
export function methodAsync<
const InArgs extends Array<DBusType>,
const OutArgs extends Array<DBusType>,
>(...args: InArgs | [inArgs: InArgs, outArgs?: OutArgs]) {
return function (
method: (
this: Service,
...args: InferVariantTypes<InArgs>
) => Promise<InferVariantTypes<OutArgs> | void>,
ctx: ClassMethodDecoratorContext<Service, typeof method>,
): (this: Service, ...args: InferVariantTypes<InArgs>) => Promise<any> {
const name = installMethod(args, method, ctx)
return async function (...args) {
if (this[internals].proxy) {
const value = await this[remoteMethodAsync](name, args)
return value.deepUnpack<InferVariantTypes<OutArgs>>()
} else {
return method.apply(this, args)
}
}
}
}
/**
* Registers a read-write property. When a new value is assigned the notify signal
* is automatically emitted on the local and exported object.
*
* Note that new values are checked by reference so assigning the same object will
* not emit the notify signal.
* ```
*/
export function property<T extends string>(type: T) {
return function (
_: void,
ctx: ClassFieldDecoratorContext<Service, DeepInfer<T>>,
): (this: Service, init: DeepInfer<T>) => any {
const name = installProperty(type, ctx)
void gproperty({ $gtype: inferGTypeFromVariant(type) })(
_,
ctx as ClassFieldDecoratorContext<GObject.Object> & Ctx,
{ metaOnly: true },
)
ctx.addInitializer(function () {
Object.defineProperty(this, name, {
configurable: false,
enumerable: true,
set(value: DeepInfer<T>) {
const { proxy, priv } = this[internals]
if (proxy) {
this[remotePropertySet](name, value)
return
}
if (priv[name] !== value) {
priv[name] = value
this.notify(name as Extract<keyof Service, string>)
}
},
get(): DeepInfer<T> {
const { proxy, priv } = this[internals]
return proxy
? proxy.get_cached_property(name)!.deepUnpack<DeepInfer<T>>()
: (priv[name] as DeepInfer<T>)
},
} satisfies ThisType<Service>)
})
return function (init) {
const priv = this[internals].priv
priv[name] = init
// we don't need to store the value on the object itself
}
}
}
/**
* Registers a read-only property. Can be used in conjuction with {@link setter} to define
* read-write properties as accessors.
*
* Note that you will need to explicitly emit the notify signal.
*/
export function getter<T extends string>(type: T) {
return function (
method: (this: Service) => DeepInfer<T>,
ctx: ClassGetterDecoratorContext<Service, DeepInfer<T>>,
): (this: Service) => any {
const name = installProperty(type, ctx)
ctx.addInitializer(function () {
definePropertyGetter(this, name as Extract<keyof Service, string>)
})
void ggetter({ $gtype: inferGTypeFromVariant(type) })(
() => {},
ctx as ClassGetterDecoratorContext<GObject.Object> & Ctx,
)
return function get(): DeepInfer<T> {
const { proxy } = this[internals]
return proxy
? proxy.get_cached_property(name)!.deepUnpack<DeepInfer<T>>()
: method.call(this)
}
}
}
/**
* Registers a write-only property. Can be used in conjuction with {@link getter} to define
* read-write properties as accessors.
*
* Note that you will need to explicitly emit the notify signal.
*/
export function setter<T extends string>(type: T) {
return function (
setter: (this: Service, value: any) => void,
ctx: ClassSetterDecoratorContext<Service, DeepInfer<T>>,
): (this: Service, value: DeepInfer<T>) => void {
const name = installProperty(type, ctx)
void gsetter({ $gtype: inferGTypeFromVariant(type) })(
() => {},
ctx as ClassSetterDecoratorContext<GObject.Object> & Ctx,
)
return function (value: DeepInfer<T>) {
const { proxy } = this[internals]
if (proxy) {
this[remotePropertySet](name, value)
} else {
setter.call(this, value)
}
}
}
}
/**
* Registers a signal which when invoked will emit the signal
* on the local object and the exported object.
*
* **Note**: its not possible to emit signals on remote objects through proxies.
*/
export function signal<const Params extends Array<DBusType>>(...params: Params) {
return function (
method: (this: Service, ...params: any) => void,
ctx: ClassMethodDecoratorContext<Service, typeof method>,
): (this: Service, ...params: InferVariantTypes<Params>) => void {
const name = installSignal(params, ctx)
void gsignal(...params.map(inferGTypeFromVariant))(
() => {},
ctx as ClassMethodDecoratorContext<GObject.Object> & Ctx,
)
return function (...params) {
if (this[internals].proxy) {
console.warn(`cannot emit signal "${name}" on remote object`)
}
if (this[internals].dbusObject || !this[internals].proxy) {
method.apply(this, params)
}
return this.emit(name, ...params)
}
}
}

View File

@@ -0,0 +1,9 @@
import "@girs/adw-1"
import "@girs/gtk-3.0"
import "@girs/gtk-4.0"
import "@girs/soup-3.0"
import "@girs/clutter-16"
import "@girs/shell-16"
import "@girs/st-16"
import "@girs/gjs"
import "@girs/gjs/dom"

View File

@@ -0,0 +1,411 @@
import GLib from "gi://GLib"
import Gio from "gi://Gio"
import Soup from "gi://Soup?version=3.0"
type ResponseType = "basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"
export type HeadersInit = Headers | Record<string, string> | [string, string][]
export type ResponseInit = {
headers?: HeadersInit
status?: number
statusText?: string
}
export type RequestInit = {
body?: string
headers?: HeadersInit
method?: string
}
export class Headers {
private headers: Map<string, string[]> = new Map()
constructor(init: HeadersInit = {}) {
if (Array.isArray(init)) {
for (const [name, value] of init) {
this.append(name, value)
}
} else if (init instanceof Headers) {
init.forEach((value, name) => this.set(name, value))
} else if (typeof init === "object") {
for (const name in init) {
this.set(name, init[name])
}
}
}
append(name: string, value: string): void {
name = name.toLowerCase()
if (!this.headers.has(name)) {
this.headers.set(name, [])
}
this.headers.get(name)!.push(value)
}
delete(name: string): void {
this.headers.delete(name.toLowerCase())
}
get(name: string): string | null {
const values = this.headers.get(name.toLowerCase())
return values ? values.join(", ") : null
}
getAll(name: string): string[] {
return this.headers.get(name.toLowerCase()) || []
}
has(name: string): boolean {
return this.headers.has(name.toLowerCase())
}
set(name: string, value: string): void {
this.headers.set(name.toLowerCase(), [value])
}
forEach(
callbackfn: (value: string, name: string, parent: Headers) => void,
thisArg?: any,
): void {
for (const [name, values] of this.headers.entries()) {
callbackfn.call(thisArg, values.join(", "), name, this)
}
}
*entries(): IterableIterator<[string, string]> {
for (const [name, values] of this.headers.entries()) {
yield [name, values.join(", ")]
}
}
*keys(): IterableIterator<string> {
for (const name of this.headers.keys()) {
yield name
}
}
*values(): IterableIterator<string> {
for (const values of this.headers.values()) {
yield values.join(", ")
}
}
[Symbol.iterator](): IterableIterator<[string, string]> {
return this.entries()
}
}
export class URLSearchParams {
private params = new Map<string, Array<string>>()
constructor(init: string[][] | Record<string, string> | string | URLSearchParams = "") {
if (typeof init === "string") {
this.parseString(init)
} else if (Array.isArray(init)) {
for (const [key, value] of init) {
this.append(key, value)
}
} else if (init instanceof URLSearchParams) {
init.forEach((value, key) => this.append(key, value))
} else if (typeof init === "object") {
for (const key in init) {
this.set(key, init[key])
}
}
}
private parseString(query: string) {
query
.replace(/^\?/, "")
.split("&")
.forEach((pair) => {
if (!pair) return
const [key, value] = pair.split("=").map(decodeURIComponent)
this.append(key, value ?? "")
})
}
get size() {
return this.params.size
}
append(name: string, value: string): void {
if (!this.params.has(name)) {
this.params.set(name, [])
}
this.params.get(name)!.push(value)
}
delete(name: string, value?: string): void {
if (value === undefined) {
this.params.delete(name)
} else {
const values = this.params.get(name) || []
this.params.set(
name,
values.filter((v) => v !== value),
)
if (this.params.get(name)!.length === 0) {
this.params.delete(name)
}
}
}
get(name: string): string | null {
const values = this.params.get(name)
return values ? values[0] : null
}
getAll(name: string): Array<string> {
return this.params.get(name) || []
}
has(name: string, value?: string): boolean {
if (!this.params.has(name)) return false
if (value === undefined) return true
return this.params.get(name)?.includes(value) || false
}
set(name: string, value: string): void {
this.params.set(name, [value])
}
sort(): void {
this.params = new Map([...this.params.entries()].sort())
}
toString(): string {
return [...this.params.entries()]
.flatMap(([key, values]) =>
values.map((value) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`),
)
.join("&")
}
forEach(
callbackfn: (value: string, key: string, parent: URLSearchParams) => void,
thisArg?: any,
): void {
for (const [key, values] of this.params.entries()) {
for (const value of values) {
callbackfn.call(thisArg, value, key, this)
}
}
}
[Symbol.iterator](): MapIterator<[string, Array<string>]> {
return this.params.entries()
}
}
// TODO: impl setters
export class URL {
readonly uri: GLib.Uri
readonly searchParams: URLSearchParams
constructor(url: string | URL, base?: string | URL) {
if (base) {
url = GLib.Uri.resolve_relative(
base instanceof URL ? base.toString() : base,
url instanceof URL ? url.toString() : url,
GLib.UriFlags.HAS_PASSWORD,
)
}
this.uri = GLib.Uri.parse(
url instanceof URL ? url.toString() : url,
GLib.UriFlags.HAS_PASSWORD,
)
this.searchParams = new URLSearchParams(this.uri.get_query() ?? "")
}
get href(): string {
const uri = GLib.Uri.build_with_user(
GLib.UriFlags.HAS_PASSWORD,
this.uri.get_scheme(),
this.uri.get_user(),
this.uri.get_password(),
null,
this.uri.get_host(),
this.uri.get_port(),
this.uri.get_path(),
this.searchParams.toString(),
this.uri.get_fragment(),
)
return uri.to_string()
}
get origin(): string {
return "null" // TODO:
}
get protocol(): string {
return this.uri.get_scheme() + ":"
}
get username(): string {
return this.uri.get_user() ?? ""
}
get password(): string {
return this.uri.get_password() ?? ""
}
get host(): string {
const host = this.hostname
const port = this.port
return host ? host + (port ? ":" + port : "") : ""
}
get hostname(): string {
return this.uri.get_host() ?? ""
}
get port(): string {
const p = this.uri.get_port()
return p >= 0 ? p.toString() : ""
}
get pathname(): string {
return this.uri.get_path()
}
get hash(): string {
const frag = this.uri.get_fragment()
return frag ? "#" + frag : ""
}
get search(): string {
const q = this.searchParams.toString()
return q ? "?" + q : ""
}
toString(): string {
return this.href
}
toJSON(): string {
return this.href
}
}
export class Response {
readonly body: Gio.InputStream | null = null
readonly bodyUsed: boolean = false
readonly headers: Headers
readonly ok: boolean
readonly redirected: boolean = false
readonly status: number
readonly statusText: string
readonly type: ResponseType = "default"
readonly url: string = ""
static error(): Response {
throw Error("Not yet implemented")
}
static json(_data: any, _init?: ResponseInit): Response {
throw Error("Not yet implemented")
}
static redirect(_url: string | URL, _status?: number): Response {
throw Error("Not yet implemented")
}
constructor(body: Gio.InputStream | null = null, options: ResponseInit = {}) {
this.body = body
this.headers = new Headers(options.headers ?? {})
this.status = options.status ?? 200
this.statusText = options.statusText ?? ""
this.ok = this.status >= 200 && this.status < 300
}
async blob(): Promise<never> {
throw Error("Not implemented")
}
async bytes() {
const { CLOSE_SOURCE, CLOSE_TARGET } = Gio.OutputStreamSpliceFlags
const outputStream = Gio.MemoryOutputStream.new_resizable()
if (!this.body) return null
await new Promise((resolve, reject) => {
outputStream.splice_async(
this.body!,
CLOSE_TARGET | CLOSE_SOURCE,
GLib.PRIORITY_DEFAULT,
null,
(_, res) => {
try {
resolve(outputStream.splice_finish(res))
} catch (error) {
reject(error)
}
},
)
})
Object.assign(this, { bodyUsed: true })
return outputStream.steal_as_bytes()
}
async formData(): Promise<never> {
throw Error("Not yet implemented")
}
async arrayBuffer() {
const blob = await this.bytes()
if (!blob) return null
return blob.toArray().buffer
}
async text() {
const blob = await this.bytes()
return blob ? new TextDecoder().decode(blob.toArray()) : ""
}
async json() {
const text = await this.text()
return JSON.parse(text)
}
clone(): Response {
throw Error("Not yet implemented")
}
}
export async function fetch(url: string | URL, { method, headers, body }: RequestInit = {}) {
const session = new Soup.Session()
const message = new Soup.Message({
method: method || "GET",
uri: url instanceof URL ? url.uri : GLib.Uri.parse(url, GLib.UriFlags.NONE),
})
if (headers) {
for (const [key, value] of Object.entries(headers))
message.get_request_headers().append(key, String(value))
}
if (typeof body === "string") {
message.set_request_body_from_bytes(null, new GLib.Bytes(new TextEncoder().encode(body)))
}
const inputStream: Gio.InputStream = await new Promise((resolve, reject) => {
session.send_async(message, 0, null, (_, res) => {
try {
resolve(session.send_finish(res))
} catch (error) {
reject(error)
}
})
})
return new Response(inputStream, {
statusText: message.reason_phrase,
status: message.status_code,
})
}
export default fetch

View File

@@ -0,0 +1,80 @@
import Clutter from "gi://Clutter"
import St from "gi://St"
import { configue } from "../jsx/env.js"
import { onCleanup, Accessor, Fragment } from "../index.js"
const { intrinsicElements } = configue({
setCss(object, css) {
if (!(object instanceof St.Widget)) {
return console.warn(Error(`cannot set css on ${object}`))
}
if (css instanceof Accessor) {
object.style = css.get()
const dispose = css.subscribe(() => (object.style = css.get()))
onCleanup(dispose)
} else {
object.set_style(css)
}
},
setClass(object, className) {
if (!(object instanceof St.Widget)) {
return console.warn(Error(`cannot set className on ${object}`))
}
if (className instanceof Accessor) {
object.styleClass = className.get()
const dispose = className.subscribe(() => (object.styleClass = className.get()))
onCleanup(dispose)
} else {
object.set_style_class_name(className)
}
},
textNode(text) {
return St.Label.new(text.toString())
},
removeChild(parent, child) {
if (parent instanceof Clutter.Actor) {
if (child instanceof Clutter.Action) {
return parent.remove_action(child)
}
if (child instanceof Clutter.Actor) {
return parent.remove_child(child)
}
if (child instanceof Clutter.Constraint) {
return parent.remove_constraint(child)
}
if (child instanceof Clutter.LayoutManager) {
return parent.set_layout_manager(null)
}
}
throw Error(`cannot remove ${child} from ${parent}`)
},
appendChild(parent, child) {
if (parent instanceof Clutter.Actor) {
if (child instanceof Clutter.Action) {
return parent.add_action(child)
}
if (child instanceof Clutter.Constraint) {
return parent.add_constraint(child)
}
if (child instanceof Clutter.LayoutManager) {
return parent.set_layout_manager(child)
}
if (child instanceof Clutter.Actor) {
return parent.add_child(child)
}
}
throw Error(`cannot add ${child} to ${parent}`)
},
defaultCleanup(object) {
if (object instanceof Clutter.Actor) {
object.destroy()
}
},
})
export { Fragment, intrinsicElements }
export { jsx, jsxs } from "../jsx/jsx.js"

View File

@@ -0,0 +1,496 @@
/**
* In the future I would like to make type declaration in decorators optional
* and infer it from typescript types at transpile time. Currently, we could
* either use stage 2 decorators with the "emitDecoratorMetadata" and
* "experimentalDecorators" tsconfig options. However, metadata is not supported
* by esbuild which is what I'm mostly targeting as the bundler for performance
* reasons. https://github.com/evanw/esbuild/issues/257
* However, I believe that we should not use stage 2 anymore,
* so I'm waiting for a better alternative.
*/
import GObject from "gi://GObject"
import GLib from "gi://GLib"
import { definePropertyGetter, kebabify } from "./util.js"
const priv = Symbol("gobject private")
const { defineProperty, fromEntries, entries } = Object
const { Object: GObj, registerClass } = GObject
export { GObject as default }
export { GObj as Object }
export const SignalFlags = GObject.SignalFlags
export type SignalFlags = GObject.SignalFlags
export const AccumulatorType = GObject.AccumulatorType
export type AccumulatorType = GObject.AccumulatorType
export type ParamSpec<T = unknown> = GObject.ParamSpec<T>
export const ParamSpec = GObject.ParamSpec
export type ParamFlags = GObject.ParamFlags
export const ParamFlags = GObject.ParamFlags
export type GType<T = unknown> = GObject.GType<T>
type GObj = GObject.Object
interface GObjPrivate extends GObj {
[priv]: Record<string, any>
}
type Meta = {
properties?: {
[fieldName: string]: {
flags: ParamFlags
type: PropertyTypeDeclaration<unknown>
}
}
signals?: {
[key: string]: {
default?: boolean
flags?: SignalFlags
accumulator?: AccumulatorType
return_type?: GType
param_types?: Array<GType>
method: (...arg: any[]) => unknown
}
}
}
type Context = { private: false; static: false; name: string }
type PropertyContext<T> = ClassFieldDecoratorContext<GObj, T> & Context
type GetterContext<T> = ClassGetterDecoratorContext<GObj, T> & Context
type SetterContext<T> = ClassSetterDecoratorContext<GObj, T> & Context
type SignalContext<T extends () => any> = ClassMethodDecoratorContext<GObj, T> & Context
type SignalOptions = {
default?: boolean
flags?: SignalFlags
accumulator?: AccumulatorType
}
type PropertyTypeDeclaration<T> =
| ((name: string, flags: ParamFlags) => ParamSpec<T>)
| ParamSpec<T>
| { $gtype: GType<T> }
function assertField(
ctx: ClassFieldDecoratorContext | ClassGetterDecoratorContext | ClassSetterDecoratorContext,
): string {
if (ctx.private) throw Error("private fields are not supported")
if (ctx.static) throw Error("static fields are not supported")
if (typeof ctx.name !== "string") {
throw Error("only strings can be gobject property keys")
}
return ctx.name
}
/**
* Defines a readable *and* writeable property to be registered when using the {@link register} decorator.
*
* Example:
* ```ts
* class {
* \@property(String) myProp = ""
* }
* ```
*/
export function property<T>(typeDeclaration: PropertyTypeDeclaration<T>) {
return function (
_: void,
ctx: PropertyContext<T>,
options?: { metaOnly: true },
): (this: GObj, init: T) => any {
const fieldName = assertField(ctx)
const key = kebabify(fieldName)
const meta: Partial<Meta> = ctx.metadata!
meta.properties ??= {}
meta.properties[fieldName] = { flags: ParamFlags.READWRITE, type: typeDeclaration }
ctx.addInitializer(function () {
definePropertyGetter(this, fieldName as Extract<keyof GObj, string>)
if (options && options.metaOnly) return
defineProperty(this, fieldName, {
enumerable: true,
configurable: false,
set(v: T) {
if (this[priv][key] !== v) {
this[priv][key] = v
this.notify(key)
}
},
get(): T {
return this[priv][key]
},
} satisfies ThisType<GObjPrivate>)
})
return function (init: T) {
const dict = ((this as GObjPrivate)[priv] ??= {})
dict[key] = init
return init
}
}
}
/**
* Defines a read-only property to be registered when using the {@link register} decorator.
* If the getter has a setter pair decorated with the {@link setter} decorator the property will be readable *and* writeable.
*
* Example:
* ```ts
* class {
* \@setter(String)
* set myProp(value: string) {
* //
* }
*
* \@getter(String)
* get myProp(): string {
* return ""
* }
* }
* ```
*/
export function getter<T>(typeDeclaration: PropertyTypeDeclaration<T>) {
return function (get: (this: GObj) => any, ctx: GetterContext<T>) {
const fieldName = assertField(ctx)
const meta: Partial<Meta> = ctx.metadata!
const props = (meta.properties ??= {})
if (fieldName in props) {
const { flags, type } = props[fieldName]
props[fieldName] = { flags: flags | ParamFlags.READABLE, type }
} else {
props[fieldName] = { flags: ParamFlags.READABLE, type: typeDeclaration }
}
return get
}
}
/**
* Defines a write-only property to be registered when using the {@link register} decorator.
* If the setter has a getter pair decorated with the {@link getter} decorator the property will be writeable *and* readable.
*
* Example:
* ```ts
* class {
* \@setter(String)
* set myProp(value: string) {
* //
* }
*
* \@getter(String)
* get myProp(): string {
* return ""
* }
* }
* ```
*/
export function setter<T>(typeDeclaration: PropertyTypeDeclaration<T>) {
return function (set: (this: GObj, value: any) => void, ctx: SetterContext<T>) {
const fieldName = assertField(ctx)
const meta: Partial<Meta> = ctx.metadata!
const props = (meta.properties ??= {})
if (fieldName in props) {
const { flags, type } = props[fieldName]
props[fieldName] = { flags: flags | ParamFlags.WRITABLE, type }
} else {
props[fieldName] = { flags: ParamFlags.WRITABLE, type: typeDeclaration }
}
return set
}
}
type ParamType<P> = P extends { $gtype: GType<infer T> } ? T : P extends GType<infer T> ? T : never
type ParamTypes<Params> = {
[K in keyof Params]: ParamType<Params[K]>
}
/**
* Defines a signal to be registered when using the {@link register} decorator.
*
* Example:
* ```ts
* class {
* \@signal([String, Number], Boolean, {
* accumulator: AccumulatorType.FIRST_WINS
* })
* mySignal(str: string, n: number): boolean {
* // default handler
* return false
* }
* }
* ```
*/
export function signal<
const Params extends Array<{ $gtype: GType } | GType>,
Return extends { $gtype: GType } | GType,
>(
params: Params,
returnType: Return,
options?: SignalOptions,
): (
method: (this: GObj, ...args: any) => ParamType<Return>,
ctx: SignalContext<typeof method>,
) => (this: GObj, ...args: ParamTypes<Params>) => any
/**
* Defines a signal to be registered when using the {@link register} decorator.
*
* Example:
* ```ts
* class {
* \@signal(String, Number)
* mySignal(str: string, n: number): void {
* // default handler
* }
* }
* ```
*/
export function signal<Params extends Array<{ $gtype: GType } | GType>>(
...params: Params
): (
method: (this: GObject.Object, ...args: any) => void,
ctx: SignalContext<typeof method>,
) => (this: GObject.Object, ...args: ParamTypes<Params>) => void
export function signal<
Params extends Array<{ $gtype: GType } | GType>,
Return extends { $gtype: GType } | GType,
>(
...args: Params | [params: Params, returnType?: Return, options?: SignalOptions]
): (
method: (this: GObj, ...args: ParamTypes<Params>) => ParamType<Return> | void,
ctx: SignalContext<typeof method>,
) => typeof method {
return function (method, ctx) {
if (ctx.private) throw Error("private fields are not supported")
if (ctx.static) throw Error("static fields are not supported")
if (typeof ctx.name !== "string") {
throw Error("only strings can be gobject signals")
}
const signalName = kebabify(ctx.name)
const meta: Partial<Meta> = ctx.metadata!
const signals = (meta.signals ??= {})
if (Array.isArray(args[0])) {
const [params, returnType, options] = args as [
params: Params,
returnType?: Return,
options?: SignalOptions,
]
signals[signalName] = {
method,
default: options?.default ?? true,
param_types: params.map((i) => ("$gtype" in i ? i.$gtype : i)),
...(returnType && {
return_type: "$gtype" in returnType ? returnType.$gtype : returnType,
}),
...(options?.flags && {
flags: options.flags,
}),
...(typeof options?.accumulator === "number" && {
accumulator: options.accumulator,
}),
}
} else {
const params = args as Params
signals[signalName] = {
method,
default: true,
param_types: params.map((i) => ("$gtype" in i ? i.$gtype : i)),
}
}
return function (...params) {
return this.emit(signalName, ...params) as ParamType<Return>
}
}
}
const MAXINT = 2 ** 31 - 1
const MININT = -(2 ** 31)
const MAXUINT = 2 ** 32 - 1
const MAXFLOAT = 3.4028235e38
const MINFLOAT = -3.4028235e38
const MININT64 = Number.MIN_SAFE_INTEGER
const MAXINT64 = Number.MAX_SAFE_INTEGER
function pspecFromGType(type: GType<unknown>, name: string, flags: ParamFlags) {
switch (type) {
case GObject.TYPE_BOOLEAN:
return ParamSpec.boolean(name, "", "", flags, false)
case GObject.TYPE_STRING:
return ParamSpec.string(name, "", "", flags, "")
case GObject.TYPE_INT:
return ParamSpec.int(name, "", "", flags, MININT, MAXINT, 0)
case GObject.TYPE_UINT:
return ParamSpec.uint(name, "", "", flags, 0, MAXUINT, 0)
case GObject.TYPE_INT64:
return ParamSpec.int64(name, "", "", flags, MININT64, MAXINT64, 0)
case GObject.TYPE_UINT64:
return ParamSpec.uint64(name, "", "", flags, 0, Number.MAX_SAFE_INTEGER, 0)
case GObject.TYPE_FLOAT:
return ParamSpec.float(name, "", "", flags, MINFLOAT, MAXFLOAT, 0)
case GObject.TYPE_DOUBLE:
return ParamSpec.double(name, "", "", flags, Number.MIN_VALUE, Number.MIN_VALUE, 0)
case GObject.TYPE_JSOBJECT:
return ParamSpec.jsobject(name, "", "", flags)
case GObject.TYPE_VARIANT:
return ParamSpec.object(name, "", "", flags as any, GLib.Variant)
case GObject.TYPE_ENUM:
case GObject.TYPE_INTERFACE:
case GObject.TYPE_BOXED:
case GObject.TYPE_POINTER:
case GObject.TYPE_PARAM:
case GObject.type_from_name("GType"):
throw Error(`cannot guess ParamSpec from GType "${type}"`)
case GObject.TYPE_OBJECT:
default:
return ParamSpec.object(name, "", "", flags as any, type)
}
}
function pspec(name: string, flags: ParamFlags, declaration: PropertyTypeDeclaration<unknown>) {
if (declaration instanceof ParamSpec) return declaration
if (declaration === Object || declaration === Function || declaration === Array) {
return ParamSpec.jsobject(name, "", "", flags)
}
if (declaration === String) {
return ParamSpec.string(name, "", "", flags, "")
}
if (declaration === Number) {
return ParamSpec.double(name, "", "", flags, -Number.MAX_VALUE, Number.MAX_VALUE, 0)
}
if (declaration === Boolean) {
return ParamSpec.boolean(name, "", "", flags, false)
}
if ("$gtype" in declaration) {
return pspecFromGType(declaration.$gtype, name, flags)
}
if (typeof declaration === "function") {
return declaration(name, flags)
}
throw Error("invalid PropertyTypeDeclaration")
}
type MetaInfo = GObject.MetaInfo<never, Array<{ $gtype: GType<unknown> }>, never>
/**
* Replacement for {@link GObject.registerClass}
* This decorator consumes metadata needed to register types where the provided decorators are used:
* - {@link signal}
* - {@link property}
* - {@link getter}
* - {@link setter}
*
* Example:
* ```ts
* \@register({ GTypeName: "MyClass" })
* class MyClass extends GObject.Object { }
* ```
*/
export function register<Cls extends { new (...args: any): GObj }>(options: MetaInfo = {}) {
return function (cls: Cls, ctx: ClassDecoratorContext<Cls>) {
const t = options.Template
if (typeof t === "string" && !t.startsWith("resource://") && !t.startsWith("file://")) {
options.Template = new TextEncoder().encode(t)
}
const meta = ctx.metadata! as Meta
const props: Record<string, ParamSpec<unknown>> = fromEntries(
entries(meta.properties ?? {}).map(([fieldName, { flags, type }]) => {
const key = kebabify(fieldName)
const spec = pspec(key, flags, type)
return [key, spec]
}),
)
const signals = fromEntries(
entries(meta.signals ?? {}).map(([signalName, { default: def, method, ...signal }]) => {
if (def) {
defineProperty(cls.prototype, `on_${signalName.replaceAll("-", "_")}`, {
enumerable: false,
configurable: false,
value: method,
})
}
return [signalName, signal]
}),
)
delete meta.properties
delete meta.signals
registerClass({ ...options, Properties: props, Signals: signals }, cls)
}
}
/**
* @experimental
* Asserts a gtype in cases where the type is too loose/strict.
*
* Example:
* ```ts
* type Tuple = [number, number]
* const Tuple = gtype<Tuple>(Array)
*
* class {
* \@property(Tuple) value = [1, 2] as Tuple
* }
* ```
*/
export function gtype<Assert>(type: GType<any> | { $gtype: GType<any> }): {
$gtype: GType<Assert>
} {
return "$gtype" in type ? type : { $gtype: type }
}
declare global {
interface FunctionConstructor {
$gtype: GType<(...args: any[]) => any>
}
interface ArrayConstructor {
$gtype: GType<any[]>
}
interface DateConstructor {
$gtype: GType<Date>
}
interface MapConstructor {
$gtype: GType<Map<any, any>>
}
interface SetConstructor {
$gtype: GType<Set<any>>
}
}
Function.$gtype = Object.$gtype as FunctionConstructor["$gtype"]
Array.$gtype = Object.$gtype as ArrayConstructor["$gtype"]
Date.$gtype = Object.$gtype as DateConstructor["$gtype"]
Map.$gtype = Object.$gtype as MapConstructor["$gtype"]
Set.$gtype = Object.$gtype as SetConstructor["$gtype"]

View File

@@ -0,0 +1,119 @@
import Gtk from "gi://Gtk?version=3.0"
import { configue } from "../jsx/env.js"
import { getType, onCleanup, Accessor, Fragment } from "../index.js"
const dummyBuilder = new Gtk.Builder()
const { intrinsicElements } = configue({
initProps(ctor, props) {
props.visible ??= true
if (ctor === Gtk.Stack) {
const keys: Array<Extract<keyof Gtk.Stack, string>> = [
"visibleChildName",
"visible_child_name",
]
return keys
}
},
setCss(object, css) {
if (!(object instanceof Gtk.Widget)) {
return console.warn(Error(`cannot set css on ${object}`))
}
const ctx = object.get_style_context()
let provider: Gtk.CssProvider
const setter = (css: string) => {
if (!css.includes("{") || !css.includes("}")) css = `* { ${css} }`
if (provider) ctx.remove_provider(provider)
provider = new Gtk.CssProvider()
provider.load_from_data(new TextEncoder().encode(css))
ctx.add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
}
if (css instanceof Accessor) {
setter(css.get())
const dispose = css.subscribe(() => setter(css.get()))
onCleanup(dispose)
} else {
setter(css)
}
},
setClass(object, className) {
if (!(object instanceof Gtk.Widget)) {
return console.warn(Error(`cannot set className on ${object}`))
}
const ctx = object.get_style_context()
const setter = (names: string) => {
for (const name of ctx.list_classes()) {
ctx.remove_class(name)
}
for (const name of names.split(/\s+/)) {
ctx.add_class(name)
}
}
if (className instanceof Accessor) {
setter(className.get())
const dispose = className.subscribe(() => setter(className.get()))
onCleanup(dispose)
} else {
setter(className)
}
},
textNode(text) {
return new Gtk.Label({ label: text.toString(), visible: true })
},
removeChild(parent, child) {
if (parent instanceof Gtk.Container && child instanceof Gtk.Widget) {
return parent.remove(child)
}
throw Error(`cannot remove ${child} from ${parent}`)
},
appendChild(parent, child) {
if (
child instanceof Gtk.Adjustment &&
"set_adjustment" in parent &&
typeof parent.set_adjustment === "function"
) {
return parent.set_adjustment(child)
}
if (
child instanceof Gtk.Widget &&
parent instanceof Gtk.Stack &&
child.name !== "" &&
child.name !== null &&
getType(child) === "named"
) {
return parent.add_named(child, child.name)
}
if (child instanceof Gtk.Window && parent instanceof Gtk.Application) {
return parent.add_window(child)
}
if (child instanceof Gtk.TextBuffer && parent instanceof Gtk.TextView) {
return parent.set_buffer(child)
}
if (parent instanceof Gtk.Buildable) {
return parent.vfunc_add_child(dummyBuilder, child, getType(child))
}
throw Error(`cannot add ${child} to ${parent}`)
},
defaultCleanup(object) {
if (object instanceof Gtk.Widget) {
object.destroy()
}
},
})
export { Fragment, intrinsicElements }
export { jsx, jsxs } from "../jsx/jsx.js"

View File

@@ -0,0 +1,149 @@
import Gtk from "gi://Gtk?version=4.0"
import Gio from "gi://Gio?version=2.0"
import { configue } from "../jsx/env.js"
import { getType, onCleanup, Accessor, Fragment } from "../index.js"
import type Adw from "gi://Adw"
const adw = await import("gi://Adw").then((m) => m.default).catch(() => null)
const dummyBuilder = new Gtk.Builder()
const { intrinsicElements } = configue({
initProps(ctor) {
if (ctor === Gtk.Stack) {
const keys: Array<Extract<keyof Gtk.Stack, string>> = [
"visibleChildName",
"visible_child_name",
]
return keys
}
if (adw && ctor === adw.ToggleGroup) {
const keys: Array<Extract<keyof Adw.ToggleGroup, string>> = [
"active",
"activeName",
"active_name",
]
return keys
}
},
setCss(object, css) {
if (!(object instanceof Gtk.Widget)) {
return console.warn(Error(`cannot set css on ${object}`))
}
const ctx = object.get_style_context()
let provider: Gtk.CssProvider
const setter = (css: string) => {
if (!css.includes("{") || !css.includes("}")) {
css = `* { ${css} }`
}
if (provider) ctx.remove_provider(provider)
provider = new Gtk.CssProvider()
provider.load_from_string(css)
ctx.add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
}
if (css instanceof Accessor) {
setter(css.get())
const dispose = css.subscribe(() => setter(css.get()))
onCleanup(dispose)
} else {
setter(css)
}
},
setClass(object, className) {
if (!(object instanceof Gtk.Widget)) {
return console.warn(Error(`cannot set className on ${object}`))
}
if (className instanceof Accessor) {
object.cssClasses = className.get().split(/\s+/)
const dispose = className.subscribe(
() => (object.cssClasses = className.get().split(/\s+/)),
)
onCleanup(dispose)
} else {
object.set_css_classes(className.split(/\s+/))
}
},
textNode(text) {
return Gtk.Label.new(text.toString())
},
// `set_child` and especially `remove` might be way too generic and there might
// be cases where it does not actually do what we want it to do
//
// if there is a usecase for either of these two that does something else than
// we expect it to do here in a JSX context we have to check for known instances
removeChild(parent, child) {
if (parent instanceof Gtk.Widget && child instanceof Gtk.EventController) {
return parent.remove_controller(child)
}
if ("set_child" in parent && typeof parent.set_child == "function") {
return parent.set_child(null)
}
if ("remove" in parent && typeof parent.remove == "function") {
return parent.remove(child)
}
throw Error(`cannot remove ${child} from ${parent}`)
},
appendChild(parent, child) {
if (
child instanceof Gtk.Adjustment &&
"set_adjustment" in parent &&
typeof parent.set_adjustment === "function"
) {
return parent.set_adjustment(child)
}
if (
child instanceof Gtk.Widget &&
parent instanceof Gtk.Stack &&
child.name !== "" &&
child.name !== null &&
getType(child) === "named"
) {
return parent.add_named(child, child.name)
}
if (child instanceof Gtk.Popover && parent instanceof Gtk.MenuButton) {
return parent.set_popover(child)
}
if (
child instanceof Gio.MenuModel &&
(parent instanceof Gtk.MenuButton || parent instanceof Gtk.PopoverMenu)
) {
return parent.set_menu_model(child)
}
if (child instanceof Gio.MenuItem && parent instanceof Gio.Menu) {
// TODO:
}
if (child instanceof Gtk.Window && parent instanceof Gtk.Application) {
return parent.add_window(child)
}
if (child instanceof Gtk.TextBuffer && parent instanceof Gtk.TextView) {
return parent.set_buffer(child)
}
if (parent instanceof Gtk.Buildable) {
return parent.vfunc_add_child(dummyBuilder, child, getType(child))
}
throw Error(`cannot add ${child} to ${parent}`)
},
})
export { Fragment, intrinsicElements }
export { jsx, jsxs } from "../jsx/jsx.js"

View File

@@ -0,0 +1,34 @@
export {
type Node,
type CCProps,
type FCProps,
getType,
jsx,
appendChild,
removeChild,
} from "./jsx/jsx.js"
export { Fragment } from "./jsx/Fragment.js"
export { For } from "./jsx/For.js"
export { With } from "./jsx/With.js"
export { This } from "./jsx/This.js"
export {
type Context,
type Scope,
createRoot,
getScope,
onCleanup,
onMount,
createContext,
} from "./jsx/scope.js"
export {
type Accessed,
type State,
type Setter,
Accessor,
createState,
createComputed,
createBinding,
createConnection,
createExternal,
createSettings,
} from "./jsx/state.js"

View File

@@ -0,0 +1,110 @@
import { Fragment } from "./Fragment.js"
import { Accessor, type State, createState } from "./state.js"
import { env } from "./env.js"
import { getScope, onCleanup, Scope } from "./scope.js"
interface ForProps<Item, El extends JSX.Element, Key> {
each: Accessor<Iterable<Item>>
children: (item: Item, index: Accessor<number>) => El
/**
* Function to run for each removed element.
* The default value depends on the environment:
*
* - **Gtk4**: null
* - **Gtk3**: Gtk.Widget.prototype.destroy
* - **Gnome**: Clutter.Actor.prototype.destroy
*/
cleanup?: null | ((element: El, item: Item, index: number) => void)
/**
* Function that generates the key for each item.
*
* By default items are mapped by:
* - value in case of primitive values
* - reference otherwise
*/
id?: (item: Item) => Key | Item
}
// TODO: support Gio.ListModel
export function For<Item, El extends JSX.Element, Key>({
each,
children: mkChild,
cleanup,
id = (item: Item) => item,
}: ForProps<Item, El, Key>): Fragment<El> {
type MapItem = { item: Item; child: El; index: State<number>; scope: Scope }
const currentScope = getScope()
const map = new Map<Item | Key, MapItem>()
const fragment = new Fragment<El>()
function remove({ item, child, index: [index], scope }: MapItem) {
scope.dispose()
if (typeof cleanup === "function") {
cleanup(child, item, index.get())
} else if (cleanup !== null) {
env.defaultCleanup(child)
}
}
function callback(itareable: Iterable<Item>) {
const items = [...itareable]
const ids = items.map(id)
const idSet = new Set(ids)
// cleanup children missing from arr
for (const [key, value] of map.entries()) {
// there is no generic way to insert child at index
// so we sort by removing every child and reappending in order
fragment.remove(value.child)
if (!idSet.has(key)) {
remove(value)
map.delete(key)
}
}
// update index and add new items
items.map((item, i) => {
const key = ids[i]
if (map.has(key)) {
const {
index: [, setIndex],
child,
} = map.get(key)!
setIndex(i)
if ([...fragment].some((ch) => ch === child)) {
console.warn(`duplicate keys found: ${key}`)
} else {
fragment.append(child)
}
} else {
const [index, setIndex] = createState(i)
const scope = new Scope(currentScope)
const child = scope.run(() => mkChild(item, index))
map.set(key, { item, child, index: [index, setIndex], scope })
fragment.append(child)
}
})
}
const dispose = each.subscribe(() => {
callback(each.get())
})
callback(each.get())
onCleanup(() => {
dispose()
for (const value of map.values()) {
remove(value)
}
map.clear()
})
return fragment
}

View File

@@ -0,0 +1,59 @@
import GObject from "gi://GObject"
interface FragmentSignals<T> extends GObject.Object.SignalSignatures {
append: (child: T) => void
remove: (child: T) => void
}
export class Fragment<T = any> extends GObject.Object {
declare $signals: FragmentSignals<T>
static [GObject.signals] = {
append: { param_types: [GObject.TYPE_OBJECT] },
remove: { param_types: [GObject.TYPE_OBJECT] },
}
static [GObject.properties] = {
children: GObject.ParamSpec.jsobject("children", "", "", GObject.ParamFlags.READABLE),
}
static {
GObject.registerClass(this)
}
*[Symbol.iterator]() {
yield* this._children
}
private _children: Array<T>
append(child: T): void {
if (child instanceof Fragment) {
throw Error(`nesting Fragments are not yet supported`)
}
this._children.push(child)
this.emit("append", child)
this.notify("children")
}
remove(child: T): void {
const index = this._children.findIndex((i) => i === child)
this._children.splice(index, 1)
this.emit("remove", child)
this.notify("children")
}
constructor({ children = [] }: Partial<{ children: Array<T> | T }> = {}) {
super()
this._children = Array.isArray(children) ? children : [children]
}
connect<S extends keyof FragmentSignals<T>>(
signal: S,
callback: GObject.SignalCallback<this, FragmentSignals<T>[S]>,
): number {
return super.connect(signal, callback)
}
}

View File

@@ -0,0 +1,75 @@
import GObject from "gi://GObject"
import { env } from "./env.js"
import { Accessor } from "./state.js"
import { set } from "../util.js"
import { onCleanup } from "./scope.js"
import { append, setType, signalName, type CCProps } from "./jsx.js"
type ThisProps<Self extends GObject.Object> = Partial<
Omit<CCProps<Self, { [K in keyof Self]: Self[K] }>, "$" | "$constructor">
> & {
this: Self
}
/** @experimental */
export function This<T extends GObject.Object>({
this: self,
children,
$type,
...props
}: ThisProps<T>) {
const cleanup = new Array<() => void>()
if ($type) setType(self, $type)
for (const [key, value] of Object.entries(props)) {
if (key === "css") {
if (value instanceof Accessor) {
env.setCss(self, value.get())
cleanup.push(value.subscribe(() => env.setCss(self, value.get())))
} else if (typeof value === "string") {
env.setCss(self, value)
}
} else if (key === "class") {
if (value instanceof Accessor) {
env.setClass(self, value.get())
cleanup.push(value.subscribe(() => env.setClass(self, value.get())))
} else if (typeof value === "string") {
env.setClass(self, value)
}
} else if (key.startsWith("on")) {
const id = self.connect(signalName(key), value)
cleanup.push(() => self.disconnect(id))
} else if (value instanceof Accessor) {
const dispose = value.subscribe(() => set(self, key, value.get()))
set(self, key, value.get())
cleanup.push(dispose)
} else {
set(self, key, value)
}
}
for (let child of Array.isArray(children) ? children : [children]) {
if (child === true) {
console.warn(Error("Trying to add boolean value of `true` as a child."))
continue
}
if (Array.isArray(child)) {
for (const ch of child) {
append(self, ch)
}
} else if (child) {
if (!(child instanceof GObject.Object)) {
child = env.textNode(child)
}
append(self, child)
}
}
if (cleanup.length > 0) {
onCleanup(() => cleanup.forEach((cb) => cb()))
}
return self
}

View File

@@ -0,0 +1,73 @@
import { Fragment } from "./Fragment.js"
import { Accessor } from "./state.js"
import { env } from "./env.js"
import { getScope, onCleanup, Scope } from "./scope.js"
interface WithProps<T, E extends JSX.Element> {
value: Accessor<T>
children: (value: T) => E | "" | false | null | undefined
/**
* Function to run for each removed element.
* The default value depends on the environment:
*
* - **Gtk4**: null
* - **Gtk3**: Gtk.Widget.prototype.destroy
* - **Gnome**: Clutter.Actor.prototype.destroy
*/
cleanup?: null | ((element: E) => void)
}
export function With<T, E extends JSX.Element>({
value,
children: mkChild,
cleanup,
}: WithProps<T, E>): Fragment<E> {
const currentScope = getScope()
const fragment = new Fragment<E>()
let currentValue: T
let scope: Scope
function remove(child: E) {
fragment.remove(child)
if (scope) scope.dispose()
if (typeof cleanup === "function") {
cleanup(child)
} else if (cleanup !== null) {
env.defaultCleanup(child)
}
}
function callback(v: T) {
for (const child of fragment) {
remove(child)
}
scope = new Scope(currentScope)
const ch = scope.run(() => mkChild(v))
if (ch !== "" && ch !== false && ch !== null && ch !== undefined) {
fragment.append(ch)
}
}
const dispose = value.subscribe(() => {
const newValue = value.get()
if (currentValue !== newValue) {
callback((currentValue = newValue))
}
})
currentValue = value.get()
callback(currentValue)
onCleanup(() => {
dispose()
for (const child of fragment) {
remove(child)
}
})
return fragment
}

View File

@@ -0,0 +1,40 @@
import type GObject from "gi://GObject"
import { type Accessor } from "./state.js"
type GObj = GObject.Object
export type CC<T extends GObj = GObj> = { new (props: any): T }
export type FC<T extends GObj = GObj> = (props: any) => T
type CssSetter = (object: GObj, css: string | Accessor<string>) => void
export function configue(conf: Partial<JsxEnv>) {
return Object.assign(env, conf)
}
type JsxEnv = {
intrinsicElements: Record<string, CC | FC>
textNode(node: string | number): GObj
appendChild(parent: GObj, child: GObj): void
removeChild(parent: GObj, child: GObj): void
setCss: CssSetter
setClass: CssSetter
// string[] can be use to delay setting props after children
// e.g Gtk.Stack["visibleChildName"] depends on children
initProps(ctor: unknown, props: any): void | string[]
defaultCleanup(object: GObj): void
}
function missingImpl(): any {
throw Error("missing impl")
}
export const env: JsxEnv = {
intrinsicElements: {},
textNode: missingImpl,
appendChild: missingImpl,
removeChild: missingImpl,
setCss: missingImpl,
setClass: missingImpl,
initProps: () => void 0,
defaultCleanup: () => void 0,
}

View File

@@ -0,0 +1,373 @@
import GObject from "gi://GObject"
import { Fragment } from "./Fragment.js"
import { Accessor } from "./state.js"
import { type CC, type FC, env } from "./env.js"
import { kebabify, type Pascalify, set } from "../util.js"
import { onCleanup } from "./scope.js"
/**
* Represents all of the things that can be passed as a child to class components.
*/
export type Node =
| Array<GObject.Object>
| GObject.Object
| number
| string
| boolean
| null
| undefined
export const gtkType = Symbol("gtk builder type")
/**
* Special symbol which lets you implement how widgets are appended in JSX.
*
* Example:
*
* ```ts
* class MyComponent extends GObject.Object {
* [appendChild](child: GObject.Object, type: string | null) {
* // implement
* }
* }
* ```
*/
export const appendChild = Symbol("JSX add child method")
/**
* Special symbol which lets you implement how widgets are removed in JSX.
*
* Example:
*
* ```ts
* class MyComponent extends GObject.Object {
* [removeChild](child: GObject.Object) {
* // implement
* }
* }
* ```
*/
export const removeChild = Symbol("JSX add remove method")
/**
* Get the type of the object specified through the `$type` property
*/
export function getType(object: GObject.Object) {
return gtkType in object ? (object[gtkType] as string) : null
}
/**
* Function Component Properties
*/
export type FCProps<Self, Props> = Props & {
/**
* Gtk.Builder type
* its consumed internally and not actually passed as a parameters
*/
$type?: string
/**
* setup function
* its consumed internally and not actually passed as a parameters
*/
$?(self: Self): void
}
/**
* Class Component Properties
*/
export type CCProps<Self extends GObject.Object, Props> = {
/**
* @internal children elements
* its consumed internally and not actually passed to class component constructors
*/
children?: Array<Node> | Node
/**
* Gtk.Builder type
* its consumed internally and not actually passed to class component constructors
*/
$type?: string
/**
* function to use as a constructor,
* its consumed internally and not actually passed to class component constructors
*/
$constructor?(props: Partial<Props>): Self
/**
* setup function,
* its consumed internally and not actually passed to class component constructors
*/
$?(self: Self): void
/**
* CSS class names
*/
class?: string | Accessor<string>
/**
* inline CSS
*/
css?: string | Accessor<string>
} & {
[K in keyof Props]: Accessor<NonNullable<Props[K]>> | Props[K]
} & {
[S in keyof Self["$signals"] as S extends `notify::${infer P}`
? `onNotify${Pascalify<P>}`
: S extends `${infer E}::${infer D}`
? `on${Pascalify<`${E}:${D}`>}`
: S extends string
? `on${Pascalify<S>}`
: never]?: GObject.SignalCallback<Self, Self["$signals"][S]>
}
// prettier-ignore
type JsxProps<C, Props> =
C extends typeof Fragment ? (Props & {})
// intrinsicElements always resolve as FC
// so we can't narrow it down, and in some cases
// the setup function is typed as a union of Object and actual type
// as a fix users can and should use FCProps
: C extends FC ? Props & Omit<FCProps<ReturnType<C>, Props>, "$">
: C extends CC ? CCProps<InstanceType<C>, Props>
: never
function isGObjectCtor(ctor: any): ctor is CC {
return ctor.prototype instanceof GObject.Object
}
function isFunctionCtor(ctor: any): ctor is FC {
return typeof ctor === "function" && !isGObjectCtor(ctor)
}
// onNotifyPropName -> notify::prop-name
// onPascalName:detailName -> pascal-name::detail-name
export function signalName(key: string): string {
const [sig, detail] = kebabify(key.slice(2)).split(":")
if (sig.startsWith("notify-")) {
return `notify::${sig.slice(7)}`
}
return detail ? `${sig}::${detail}` : sig
}
export function remove(parent: GObject.Object, child: GObject.Object) {
if (parent instanceof Fragment) {
parent.remove(child)
return
}
if (removeChild in parent && typeof parent[removeChild] === "function") {
parent[removeChild](child)
return
}
env.removeChild(parent, child)
}
export function append(parent: GObject.Object, child: GObject.Object) {
if (parent instanceof Fragment) {
parent.append(child)
return
}
if (child instanceof Fragment) {
for (const ch of child) {
append(parent, ch)
}
const appendHandler = child.connect("append", (_, ch) => {
if (!(ch instanceof GObject.Object)) {
return console.error(TypeError(`cannot add ${ch} to ${parent}`))
}
append(parent, ch)
})
const removeHandler = child.connect("remove", (_, ch) => {
if (!(ch instanceof GObject.Object)) {
return console.error(TypeError(`cannot remove ${ch} from ${parent}`))
}
remove(parent, ch)
})
onCleanup(() => {
child.disconnect(appendHandler)
child.disconnect(removeHandler)
})
return
}
if (appendChild in parent && typeof parent[appendChild] === "function") {
parent[appendChild](child, getType(child))
return
}
env.appendChild(parent, child)
}
/** @internal */
export function setType(object: object, type: string) {
if (gtkType in object && object[gtkType] !== "") {
console.warn(`type overriden from ${object[gtkType]} to ${type} on ${object}`)
}
Object.assign(object, { [gtkType]: type })
}
export function jsx<T extends (props: any) => GObject.Object>(
ctor: T,
props: JsxProps<T, Parameters<T>[0]>,
): ReturnType<T>
export function jsx<T extends new (props: any) => GObject.Object>(
ctor: T,
props: JsxProps<T, ConstructorParameters<T>[0]>,
): InstanceType<T>
export function jsx<T extends GObject.Object>(
ctor: keyof (typeof env)["intrinsicElements"] | (new (props: any) => T) | ((props: any) => T),
inprops: any,
// key is a special prop in jsx which is passed as a third argument and not in props
key?: string,
): T {
const { $, $type, $constructor, children, ...rest } = inprops as CCProps<T, any>
const props = rest as Record<string, any>
if (key) Object.assign(props, { key })
const deferProps = env.initProps(ctor, props) ?? []
const deferredProps: Record<string, unknown> = {}
for (const [key, value] of Object.entries(props)) {
if (value === undefined) {
delete props[key]
}
if (deferProps.includes(key)) {
deferredProps[key] = props[key]
delete props[key]
}
}
if (typeof ctor === "string") {
if (ctor in env.intrinsicElements) {
ctor = env.intrinsicElements[ctor] as FC<T> | CC<T>
} else {
throw Error(`unknown intrinsic element "${ctor}"`)
}
}
if (isFunctionCtor(ctor)) {
const object = ctor({ children, ...props })
if ($type) setType(object, $type)
$?.(object)
return object
}
// collect css and className
const { css, class: className } = props
delete props.css
delete props.class
const signals: Array<[string, (...props: unknown[]) => unknown]> = []
const bindings: Array<[string, Accessor<unknown>]> = []
// collect signals and bindings
for (const [key, value] of Object.entries(props)) {
if (key.startsWith("on")) {
signals.push([key, value as () => unknown])
delete props[key]
}
if (value instanceof Accessor) {
bindings.push([key, value])
props[key] = value.get()
}
}
// construct
const object = $constructor ? $constructor(props) : new (ctor as CC<T>)(props)
if ($constructor) Object.assign(object, props)
if ($type) setType(object, $type)
if (css) env.setCss(object, css)
if (className) env.setClass(object, className)
// add children
for (let child of Array.isArray(children) ? children : [children]) {
if (child === true) {
console.warn(Error("Trying to add boolean value of `true` as a child."))
continue
}
if (Array.isArray(child)) {
for (const ch of child) {
append(object, ch)
}
} else if (child) {
if (!(child instanceof GObject.Object)) {
child = env.textNode(child)
}
append(object, child)
}
}
// handle signals
const disposeHandlers = signals.map(([sig, handler]) => {
const id = object.connect(signalName(sig), handler)
return () => object.disconnect(id)
})
// deferred props
for (const [key, value] of Object.entries(deferredProps)) {
if (value instanceof Accessor) {
bindings.push([key, value])
} else {
Object.assign(object, { [key]: value })
}
}
// handle bindings
const disposeBindings = bindings.map(([prop, binding]) => {
const dispose = binding.subscribe(() => {
set(object, prop, binding.get())
})
set(object, prop, binding.get())
return dispose
})
// cleanup
if (disposeBindings.length > 0 || disposeHandlers.length > 0) {
onCleanup(() => {
disposeHandlers.forEach((cb) => cb())
disposeBindings.forEach((cb) => cb())
})
}
$?.(object)
return object
}
// TODO: make use of jsxs
export const jsxs = jsx
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
type ElementType = keyof IntrinsicElements | FC | CC
type Element = GObject.Object
type ElementClass = GObject.Object
type LibraryManagedAttributes<C, Props> = JsxProps<C, Props> & {
// FIXME: why does an intrinsic element always resolve as FC?
// __type?: C extends CC ? "CC" : C extends FC ? "FC" : never
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface IntrinsicElements {
// cc: CCProps<Gtk.Box, Gtk.Box.ConstructorProps, Gtk.Box.SignalSignatures>
// fc: FCProps<Gtk.Widget, FnProps>
}
interface ElementChildrenAttribute {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
children: {}
}
}
}

View File

@@ -0,0 +1,185 @@
export class Scope {
static current?: Scope | null
parent?: Scope | null
contexts = new Map<Context, unknown>()
private cleanups = new Set<() => void>()
private mounts = new Set<() => void>()
private mounted = false
constructor(parent?: Scope | null) {
this.parent = parent
}
onCleanup(callback: () => void) {
this.cleanups?.add(callback)
}
onMount(callback: () => void) {
if (this.parent && !this.parent.mounted) {
this.parent.onMount(callback)
} else {
this.mounts.add(callback)
}
}
run<T>(fn: () => T) {
const prev = Scope.current
Scope.current = this
try {
return fn()
} finally {
this.mounts.forEach((cb) => cb())
this.mounts.clear()
this.mounted = true
Scope.current = prev
}
}
dispose() {
this.cleanups.forEach((cb) => cb())
this.cleanups.clear()
this.contexts.clear()
delete this.parent
}
}
export type Context<T = any> = {
use(): T
provide<R>(value: T, fn: () => R): R
(props: { value: T; children: () => JSX.Element }): JSX.Element
}
/**
* Example Usage:
* ```tsx
* const MyContext = createContext("fallback-value")
*
* function ConsumerComponent() {
* const value = MyContext.use()
*
* return <Gtk.Label label={value} />
* }
*
* function ProviderComponent() {
* return (
* <Gtk.Box>
* <MyContext value="my-value">
* {() => <ConsumerComponent />}
* </MyContext>
* </Gtk.Box>
* )
* }
* ```
*/
export function createContext<T>(defaultValue: T): Context<T> {
let ctx: Context<T>
function provide<R>(value: T, fn: () => R): R {
const scope = getScope()
scope.contexts.set(ctx, value)
return scope.run(fn)
}
function use(): T {
let scope = Scope.current
while (scope) {
const value = scope.contexts.get(ctx)
if (value !== undefined) return value as T
scope = scope.parent
}
return defaultValue
}
function context({ value, children }: { value: T; children: () => JSX.Element }) {
return provide(value, children)
}
return (ctx = Object.assign(context, {
provide,
use,
}))
}
/**
* Gets the scope that owns the currently running code.
*
* Example:
* ```ts
* const scope = getScope()
* setTimeout(() => {
* // This callback gets run without an owner scope.
* // Restore owner via scope.run:
* scope.run(() => {
* const foo = FooContext.use()
* onCleanup(() => {
* print("some cleanup")
* })
* })
* }, 1000)
* ```
*/
export function getScope(): Scope {
const scope = Scope.current
if (!scope) {
throw Error("cannot get scope: out of tracking context")
}
return scope
}
/**
* Attach a cleanup callback to the current {@link Scope}.
*/
export function onCleanup(cleanup: () => void) {
if (!Scope.current) {
console.error(Error("out of tracking context: will not be able to cleanup"))
}
Scope.current?.onCleanup(cleanup)
}
/**
* Attach a callback to run when the currently running {@link Scope} returns.
*/
export function onMount(cleanup: () => void) {
if (!Scope.current) {
console.error(Error("cannot attach onMount: out of tracking context"))
}
Scope.current?.onMount(cleanup)
}
/**
* Creates a root {@link Scope} that when disposed will remove
* any child signal handler or state subscriber.
*
* Example:
* ```tsx
* createRoot((dispose) => {
* let root: Gtk.Window
*
* const [state] = createState("value")
*
* const remove = () => {
* root.destroy()
* dispose()
* }
*
* return (
* <Gtk.Window $={(self) => (root = self)}>
* <Gtk.Box>
* <Gtk.Label label={state} />
* <Gtk.Button $clicked={remove} />
* </Gtk.Box>
* </Gtk.Window>
* )
* })
* ```
*/
export function createRoot<T>(fn: (dispose: () => void) => T) {
const scope = new Scope(null)
return scope.run(() => fn(() => scope.dispose()))
}

View File

@@ -0,0 +1,588 @@
import GObject from "gi://GObject"
import Gio from "gi://Gio"
import GLib from "gi://GLib"
import { type Pascalify, camelify, kebabify } from "../util.js"
import type { DeepInfer, RecursiveInfer } from "../variant.js"
type SubscribeCallback = () => void
type DisposeFunction = () => void
type SubscribeFunction = (callback: SubscribeCallback) => DisposeFunction
export type Accessed<T> = T extends Accessor<infer V> ? V : never
const empty = Symbol("empty computed value")
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export class Accessor<T = unknown> extends Function {
static $gtype = GObject.TYPE_JSOBJECT as unknown as GObject.GType<Accessor>
#get: () => T
#subscribe: SubscribeFunction
constructor(get: () => T, subscribe?: SubscribeFunction) {
super("return arguments.callee._call.apply(arguments.callee, arguments)")
this.#subscribe = subscribe ?? (() => () => void 0)
this.#get = get
}
/**
* Subscribe for value changes.
* @param callback The function to run when the current value changes.
* @returns Unsubscribe function.
*/
subscribe(callback: SubscribeCallback): DisposeFunction {
return this.#subscribe(callback)
}
/**
* @returns The current value.
*/
get(): T {
return this.#get()
}
/**
* Create a new `Accessor` that applies a transformation on its value.
* @param transform The transformation to apply. Should be a pure function.
*/
as<R = T>(transform: (value: T) => R): Accessor<R> {
return new Accessor(() => transform(this.#get()), this.#subscribe)
}
protected _call<R = T>(transform: (value: T) => R): Accessor<R> {
let value: typeof empty | R = empty
let unsub: DisposeFunction
const subscribers = new Set<SubscribeCallback>()
const subscribe: SubscribeFunction = (callback) => {
if (subscribers.size === 0) {
unsub = this.subscribe(() => {
const newValue = transform(this.get())
if (value !== newValue) {
value = newValue
Array.from(subscribers).forEach((cb) => cb())
}
})
}
subscribers.add(callback)
return () => {
subscribers.delete(callback)
if (subscribers.size === 0) {
value = empty
unsub()
}
}
}
const get = (): R => {
return value !== empty ? value : transform(this.get())
}
return new Accessor(get, subscribe)
}
toString(): string {
return `Accessor<${this.get()}>`
}
[Symbol.toPrimitive]() {
console.warn("Accessor implicitly converted to a primitive value.")
return this.toString()
}
}
export interface Accessor<T> {
/**
* Create a computed `Accessor` that caches its transformed value.
* @param transform The transformation to apply. Should be a pure function.
* see {@link createComputed} and {@link createComputedProducer}
*/
<R = T>(transform: (value: T) => R): Accessor<R>
}
export type Setter<T> = {
(value: T): void
(value: (prev: T) => T): void
}
export type State<T> = [Accessor<T>, Setter<T>]
/**
* Create a writable signal.
*
* @param init The intial value of the signal
* @returns An `Accessor` and a setter function
*/
export function createState<T>(init: T): State<T> {
let currentValue = init
const subscribers = new Set<SubscribeCallback>()
const subscribe: SubscribeFunction = (callback) => {
subscribers.add(callback)
return () => subscribers.delete(callback)
}
const set = (newValue: unknown) => {
const value: T = typeof newValue === "function" ? newValue(currentValue) : newValue
if (currentValue !== value) {
currentValue = value
// running callbacks might mutate subscribers
Array.from(subscribers).forEach((cb) => cb())
}
}
return [new Accessor(() => currentValue, subscribe), set as Setter<T>]
}
function createComputedProducer<T>(fn: (track: <V>(signal: Accessor<V>) => V) => T): Accessor<T> {
let value: typeof empty | T = empty
let prevDeps = new Map<Accessor, DisposeFunction>()
const subscribers = new Set<SubscribeCallback>()
const cache = new Map<Accessor, unknown>()
const effect = () => {
const deps = new Set<Accessor>()
const newValue = fn((v) => {
deps.add(v)
return (cache.get(v) as any) || v.get()
})
const didChange = value !== newValue
value = newValue
const newDeps = new Map<Accessor, DisposeFunction>()
for (const [dep, unsub] of prevDeps) {
if (!deps.has(dep)) {
unsub()
} else {
newDeps.set(dep, unsub)
}
}
for (const dep of deps) {
if (!newDeps.has(dep)) {
const dispose = dep.subscribe(() => {
const value = dep.get()
if (cache.get(dep) !== value) {
cache.set(dep, value)
effect()
}
})
newDeps.set(dep, dispose)
}
}
prevDeps = newDeps
if (didChange) {
Array.from(subscribers).forEach((cb) => cb())
}
}
const subscribe: SubscribeFunction = (callback) => {
if (subscribers.size === 0) {
effect()
}
subscribers.add(callback)
return () => {
subscribers.delete(callback)
if (subscribers.size === 0) {
value = empty
for (const [, unsub] of prevDeps) {
unsub()
}
}
}
}
const get = (): T => {
return value !== empty ? value : fn((v) => v.get())
}
return new Accessor(get, subscribe)
}
function createComputedArgs<
const Deps extends Array<Accessor<any>>,
Args extends { [K in keyof Deps]: Accessed<Deps[K]> },
V = Args,
>(deps: Deps, transform?: (...args: Args) => V): Accessor<V> {
let dispose: Array<DisposeFunction>
let value: typeof empty | V = empty
const subscribers = new Set<SubscribeCallback>()
const cache = new Array<unknown>(deps.length)
const compute = (): V => {
const args = deps.map((dep, i) => {
if (!cache[i]) {
cache[i] = dep.get()
}
return cache[i]
})
return transform ? transform(...(args as Args)) : (args as V)
}
const subscribe: SubscribeFunction = (callback) => {
if (subscribers.size === 0) {
dispose = deps.map((dep, i) =>
dep.subscribe(() => {
const newDepValue = dep.get()
if (cache[i] !== newDepValue) {
cache[i] = newDepValue
const newValue = compute()
if (value !== newValue) {
value = newValue
Array.from(subscribers).forEach((cb) => cb())
}
}
}),
)
}
subscribers.add(callback)
return () => {
subscribers.delete(callback)
if (subscribers.size === 0) {
value = empty
dispose.map((cb) => cb())
dispose.length = 0
cache.length = 0
}
}
}
const get = (): V => {
return value !== empty ? value : compute()
}
return new Accessor(get, subscribe)
}
/**
* Create an `Accessor` from a producer function that tracks its dependencies.
*
* ```ts Example
* let a: Accessor<number>
* let b: Accessor<number>
* const c: Accessor<number> = createComputed((get) => get(a) + get(b))
* ```
*
* @experimental
* @param producer The producer function which let's you track dependencies
* @returns The computed `Accessor`.
*/
export function createComputed<T>(
producer: (track: <V>(signal: Accessor<V>) => V) => T,
): Accessor<T>
/**
* Create an `Accessor` which is computed from a list of given `Accessor`s.
*
* ```ts Example
* let a: Accessor<number>
* let b: Accessor<string>
* const c: Accessor<[number, string]> = createComputed([a, b])
* const d: Accessor<string> = createComputed([a, b], (a: number, b: string) => `${a} ${b}`)
* ```
*
* @param deps List of `Accessors`.
* @param transform An optional transform function.
* @returns The computed `Accessor`.
*/
export function createComputed<
const Deps extends Array<Accessor<any>>,
Args extends { [K in keyof Deps]: Accessed<Deps[K]> },
T = Args,
>(deps: Deps, transform?: (...args: Args) => T): Accessor<T>
export function createComputed(
...args:
| [producer: (track: <V>(signal: Accessor<V>) => V) => unknown]
| [deps: Array<Accessor>, transform?: (...args: unknown[]) => unknown]
) {
const [depsOrProducer, transform] = args
if (typeof depsOrProducer === "function") {
return createComputedProducer(depsOrProducer)
} else {
return createComputedArgs(depsOrProducer, transform)
}
}
/**
* Create an `Accessor` on a `GObject.Object`'s `property`.
*
* @param object The `GObject.Object` to create the `Accessor` on.
* @param property One of its registered properties.
*/
export function createBinding<T extends GObject.Object, P extends keyof T>(
object: T,
property: Extract<P, string>,
): Accessor<T[P]>
// TODO: support nested bindings
// export function createBinding<
// T extends GObject.Object,
// P1 extends keyof T,
// P2 extends keyof NonNullable<T[P1]>,
// >(
// object: T,
// property1: Extract<P1, string>,
// property2: Extract<P2, string>,
// ): Accessor<NonNullable<T[P1]>[P2]>
/**
* Create an `Accessor` on a `Gio.Settings`'s `key`.
* Values are recursively unpacked.
*
* @deprecated prefer using {@link createSettings}.
* @param object The `Gio.Settings` to create the `Accessor` on.
* @param key The settings key
*/
export function createBinding<T>(settings: Gio.Settings, key: string): Accessor<T>
export function createBinding<T>(object: GObject.Object | Gio.Settings, key: string): Accessor<T> {
const prop = kebabify(key) as keyof typeof object
const subscribe: SubscribeFunction = (callback) => {
const sig = object instanceof Gio.Settings ? "changed" : "notify"
const id = object.connect(`${sig}::${prop}`, () => callback())
return () => object.disconnect(id)
}
const get = (): T => {
if (object instanceof Gio.Settings) {
return object.get_value(key).recursiveUnpack() as T
}
if (object instanceof GObject.Object) {
const getter = `get_${prop.replaceAll("-", "_")}` as keyof typeof object
if (getter in object && typeof object[getter] === "function") {
return (object[getter] as () => unknown)() as T
}
if (prop in object) return object[prop] as T
if (key in object) return object[key as keyof typeof object] as T
}
throw Error(`cannot get property "${key}" on "${object}"`)
}
return new Accessor(get, subscribe)
}
type ConnectionHandler<
O extends GObject.Object,
S extends keyof O["$signals"],
T,
> = O["$signals"][S] extends (...args: any[]) => infer R
? void extends R
? (...args: [...Parameters<O["$signals"][S]>, currentValue: T]) => T
: never
: never
/**
* Create an `Accessor` which sets up a list of `GObject.Object` signal connections.
*
* ```ts Example
* const value: Accessor<string> = createConnection(
* "initial value",
* [obj1, "sig-name", (...args) => "str"],
* [obj2, "sig-name", (...args) => "str"]
* )
* ```
*
* @param init The initial value
* @param signals A list of `GObject.Object`, signal name and callback pairs to connect.
*/
export function createConnection<
T,
O1 extends GObject.Object,
S1 extends keyof O1["$signals"],
O2 extends GObject.Object,
S2 extends keyof O2["$signals"],
O3 extends GObject.Object,
S3 extends keyof O3["$signals"],
O4 extends GObject.Object,
S4 extends keyof O4["$signals"],
O5 extends GObject.Object,
S5 extends keyof O5["$signals"],
O6 extends GObject.Object,
S6 extends keyof O6["$signals"],
O7 extends GObject.Object,
S7 extends keyof O7["$signals"],
O8 extends GObject.Object,
S8 extends keyof O8["$signals"],
O9 extends GObject.Object,
S9 extends keyof O9["$signals"],
>(
init: T,
h1: [O1, S1, ConnectionHandler<O1, S1, T>],
h2?: [O2, S2, ConnectionHandler<O2, S2, T>],
h3?: [O3, S3, ConnectionHandler<O3, S3, T>],
h4?: [O4, S4, ConnectionHandler<O4, S4, T>],
h5?: [O5, S5, ConnectionHandler<O5, S5, T>],
h6?: [O6, S6, ConnectionHandler<O6, S6, T>],
h7?: [O7, S7, ConnectionHandler<O7, S7, T>],
h8?: [O8, S8, ConnectionHandler<O8, S8, T>],
h9?: [O9, S9, ConnectionHandler<O9, S9, T>],
) {
let value = init
let dispose: Array<DisposeFunction>
const subscribers = new Set<SubscribeCallback>()
const signals = [h1, h2, h3, h4, h5, h6, h7, h8, h9].filter((h) => h !== undefined)
const subscribe: SubscribeFunction = (callback) => {
if (subscribers.size === 0) {
dispose = signals.map(([object, signal, callback]) => {
const id = GObject.Object.prototype.connect.call(
object,
signal as string,
(_, ...args) => {
const newValue = callback(...args, value)
if (value !== newValue) {
value = newValue
Array.from(subscribers).forEach((cb) => cb())
}
},
)
return () => GObject.Object.prototype.disconnect.call(object, id)
})
}
subscribers.add(callback)
return () => {
subscribers.delete(callback)
if (subscribers.size === 0) {
dispose.map((cb) => cb())
dispose.length = 0
}
}
}
return new Accessor(() => value, subscribe)
}
/**
* Create a signal from a provier function.
* The provider is called when the first subscriber appears and the returned dispose
* function from the provider will be called when the number of subscribers drop to zero.
*
* Example:
*
* ```ts
* const value = createExternal(0, (set) => {
* const interval = setInterval(() => set((v) => v + 1))
* return () => clearInterval(interval)
* })
* ```
*
* @param init The initial value
* @param producer The producer function which should return a cleanup function
*/
export function createExternal<T>(
init: T,
producer: (set: Setter<T>) => DisposeFunction,
): Accessor<T> {
let currentValue = init
let dispose: DisposeFunction
const subscribers = new Set<SubscribeCallback>()
const subscribe: SubscribeFunction = (callback) => {
if (subscribers.size === 0) {
dispose = producer((v: unknown) => {
const newValue: T = typeof v === "function" ? v(currentValue) : v
if (newValue !== currentValue) {
currentValue = newValue
Array.from(subscribers).forEach((cb) => cb())
}
})
}
subscribers.add(callback)
return () => {
subscribers.delete(callback)
if (subscribers.size === 0) {
dispose()
}
}
}
return new Accessor(() => currentValue, subscribe)
}
/** @experimental */
type Settings<T extends Record<string, string>> = {
[K in keyof T as Uncapitalize<Pascalify<K>>]: Accessor<RecursiveInfer<T[K]>>
} & {
[K in keyof T as `set${Pascalify<K>}`]: Setter<DeepInfer<T[K]>>
}
/**
* @experimental
*
* Wrap a {@link Gio.Settings} into a collection of setters and accessors.
*
* Example:
*
* ```ts
* const s = createSettings(settings, {
* "complex-key": "a{sa{ss}}",
* "simple-key": "s",
* })
*
* s.complexKey.subscribe(() => {
* print(s.complexKey.get())
* })
*
* s.setComplexKey((prev) => ({
* ...prev,
* key: { nested: "" },
* }))
* ```
*/
export function createSettings<const T extends Record<string, string>>(
settings: Gio.Settings,
keys: T,
): Settings<T> {
return Object.fromEntries(
Object.entries(keys).flatMap(([key, type]) => [
[
camelify(key),
new Accessor(
() => settings.get_value(key).recursiveUnpack(),
(callback) => {
const id = settings.connect(`changed::${key}`, callback)
return () => settings.disconnect(id)
},
),
],
[
`set${key[0].toUpperCase() + camelify(key).slice(1)}`,
(v: unknown) => {
settings.set_value(
key,
new GLib.Variant(
type,
typeof v === "function" ? v(settings.get_value(key).deepUnpack()) : v,
),
)
},
],
]),
)
}

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<!-- esbuild aliasing:
--alias:gnim=resource:///gnim/index.js
--alias:gnim/fetch=resource:///gnim/fetch.js
--alias:gnim/dbus=resource:///gnim/dbus.js
--alias:gnim/gobject=resource:///gnim/gobject.js
--alias:gnim/gtk4/jsx-runtime=resource:///gnim/gtk4/jsx-runtime.js
--alias:gnim/gtk3/jsx-runtime=resource:///gnim/gtk3/jsx-runtime.js
--alias:gnim/gnome/jsx-runtime=resource:///gnim/gnome/jsx-runtime.js
-->
<!-- env.ts
declare module "resource:///gnim/index.js" {
export * from "gnim"
}
declare module "resource:///gnim/dbus.js" {
export * from "gnim/dbus"
}
declare module "resource:///gnim/gobject.js" {
export * from "gnim/gobject"
}
declare module "resource:///gnim/fetch.js" {
export * from "gnim/fetch"
}
declare module "resource:///gnim/gnome/jsx-runtime.js" {
export * from "gnim/gnome/jsx-runtime"
}
declare module "resource:///gnim/gtk4/jsx-runtime.js" {
export * from "gnim/gtk4/jsx-runtime"
}
declare module "resource:///gnim/gtk3/jsx-runtime.js" {
export * from "gnim/gtk3/jsx-runtime"
}
-->
<gresource prefix="/gnim">
<file>dbus.js</file>
<file>fetch.js</file>
<file>gobject.js</file>
<file>util.js</file>
<file>index.js</file>
<file>gnome/jsx-runtime.js</file>
<file>gtk3/jsx-runtime.js</file>
<file>gtk4/jsx-runtime.js</file>
<file>jsx/env.js</file>
<file>jsx/For.js</file>
<file>jsx/Fragment.js</file>
<file>jsx/jsx.js</file>
<file>jsx/scope.js</file>
<file>jsx/state.js</file>
<file>jsx/This.js</file>
<file>jsx/With.js</file>
</gresource>
</gresources>

View File

@@ -0,0 +1,90 @@
import type GObject from "gi://GObject"
export function kebabify(str: string) {
return str
.replace(/([a-z])([A-Z])/g, "$1-$2")
.replaceAll("_", "-")
.toLowerCase()
}
export function snakeify(str: string) {
return str
.replace(/([a-z])([A-Z])/g, "$1-$2")
.replaceAll("-", "_")
.toLowerCase()
}
export function camelify(str: string) {
return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
}
export type Pascalify<S> = S extends `${infer Head}${"-" | "_"}${infer Tail}`
? `${Capitalize<Head>}${Pascalify<Tail>}`
: S extends string
? Capitalize<S>
: never
export type XmlNode = {
name: string
attributes?: Record<string, string>
children?: Array<XmlNode>
}
export function xml({ name, attributes, children }: XmlNode) {
let builder = `<${name}`
const attrs = Object.entries(attributes ?? [])
if (attrs.length > 0) {
for (const [key, value] of attrs) {
builder += ` ${key}="${value}"`
}
}
if (children && children.length > 0) {
builder += ">"
for (const node of children) {
builder += xml(node)
}
builder += `</${name}>`
} else {
builder += " />"
}
return builder
}
// Bindings work over properties in kebab-case because thats the convention of gobject
// however in js its either snake_case or camelCase
// also on DBus interfaces its PascalCase by convention
// so as a workaround we use get_property_name and only use the property field as a fallback
export function definePropertyGetter<T extends object>(object: T, prop: Extract<keyof T, string>) {
Object.defineProperty(object, `get_${kebabify(prop).replaceAll("-", "_")}`, {
configurable: false,
enumerable: true,
value: () => object[prop],
})
}
// attempt setting a property of GObject.Object
export function set(obj: GObject.Object, prop: string, value: any) {
const key = snakeify(prop)
const getter = `get_${key}` as keyof typeof obj
const setter = `set_${key}` as keyof typeof obj
let current: unknown
if (getter in obj && typeof obj[getter] === "function") {
current = (obj[getter] as () => unknown)()
} else {
current = obj[prop as keyof typeof obj]
}
if (current !== value) {
if (setter in obj && typeof obj[setter] === "function") {
;(obj[setter] as (v: any) => void)(value)
} else {
Object.assign(obj, { [prop]: value })
}
}
}

View File

@@ -0,0 +1,348 @@
// See: https://github.com/gjsify/ts-for-gir/issues/286
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-empty-object-type */
import type GLib from "gi://GLib"
type Variant<S extends string = any> = GLib.Variant<S>
// prettier-ignore
type CreateIndexType<Key extends string, Value> =
Key extends `s` | `o` | `g` ? { [key: string]: Value } :
Key extends `n` | `q` | `t` | `d` | `u` | `i` | `x` | `y` ? { [key: number]: Value } : never;
type VariantTypeError<T extends string> = { error: true } & T
/**
* Handles the {kv} of a{kv} where k is a basic type and v is any possible variant type string.
*/
// prettier-ignore
type $ParseDeepVariantDict<State extends string, Memo extends Record<string, any> = {}> =
string extends State
? VariantTypeError<"$ParseDeepVariantDict: 'string' is not a supported type.">
// Hitting the first '}' indicates the dictionary type is complete
: State extends `}${infer State}`
? [Memo, State]
// This separates the key (basic type) from the rest of the remaining expression.
: State extends `${infer Key}${''}${infer State}`
? $ParseDeepVariantValue<State> extends [infer Value, `${infer State}`]
? State extends `}${infer State}`
? [CreateIndexType<Key, Value>, State]
: VariantTypeError<`$ParseDeepVariantDict encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseDeepVariantValue returned unexpected value for: ${State}`>
: VariantTypeError<`$ParseDeepVariantDict encountered an invalid variant string: ${State} (2)`>;
/**
* Handles parsing values within a tuple (e.g. (vvv)) where v is any possible variant type string.
*/
// prettier-ignore
type $ParseDeepVariantArray<State extends string, Memo extends any[] = []> =
string extends State
? VariantTypeError<"$ParseDeepVariantArray: 'string' is not a supported type.">
: State extends `)${infer State}`
? [Memo, State]
: $ParseDeepVariantValue<State> extends [infer Value, `${infer State}`]
? State extends `${infer _NextValue})${infer _NextState}`
? $ParseDeepVariantArray<State, [...Memo, Value]>
: State extends `)${infer State}`
? [[...Memo, Value], State]
: VariantTypeError<`1: $ParseDeepVariantArray encountered an invalid variant string: ${State}`>
: VariantTypeError<`2: $ParseDeepVariantValue returned unexpected value for: ${State}`>;
/**
* Handles parsing {kv} without an 'a' prefix (key-value pair) where k is a basic type
* and v is any possible variant type string.
*/
// prettier-ignore
type $ParseDeepVariantKeyValue<State extends string, Memo extends any[] = []> =
string extends State
? VariantTypeError<"$ParseDeepVariantKeyValue: 'string' is not a supported type.">
: State extends `}${infer State}`
? [Memo, State]
: State extends `${infer Key}${''}${infer State}`
? $ParseDeepVariantValue<State> extends [infer Value, `${infer State}`]
? State extends `}${infer State}`
? [[...Memo, $ParseVariant<Key>, Value], State]
: VariantTypeError<`$ParseDeepVariantKeyValue encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseDeepVariantKeyValue returned unexpected value for: ${State}`>
: VariantTypeError<`$ParseDeepVariantKeyValue encountered an invalid variant string: ${State} (2)`>;
/**
* Handles parsing any variant 'value' or base unit.
*
* - ay - Array of bytes (Uint8Array)
* - a* - Array of type *
* - a{k*} - Dictionary
* - {k*} - KeyValue
* - (**) - tuple
* - s | o | g - string types
* - n | q | t | d | u | i | x | y - number types
* - b - boolean type
* - v - unknown Variant type
* - h | ? - unknown types
*/
// prettier-ignore
type $ParseDeepVariantValue<State extends string> =
string extends State
? unknown
: State extends `${`s` | `o` | `g`}${infer State}`
? [string, State]
: State extends `${`n` | `q` | `t` | `d` | `u` | `i` | `x` | `y`}${infer State}`
? [number, State]
: State extends `b${infer State}`
? [boolean, State]
: State extends `v${infer State}`
? [Variant, State]
: State extends `${'h' | '?'}${infer State}`
? [unknown, State]
: State extends `(${infer State}`
? $ParseDeepVariantArray<State>
: State extends `a{${infer State}`
? $ParseDeepVariantDict<State>
: State extends `{${infer State}`
? $ParseDeepVariantKeyValue<State>
: State extends `ay${infer State}` ?
[Uint8Array, State]
: State extends `m${infer State}`
? $ParseDeepVariantValue<State> extends [infer Value, `${infer State}`]
? [Value | null, State]
: VariantTypeError<`$ParseDeepVariantValue encountered an invalid variant string: ${State} (3)`>
: State extends `a${infer State}` ?
$ParseDeepVariantValue<State> extends [infer Value, `${infer State}`] ?
[Value[], State]
: VariantTypeError<`$ParseDeepVariantValue encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseDeepVariantValue encountered an invalid variant string: ${State} (2)`>;
// prettier-ignore
type $ParseDeepVariant<T extends string> =
$ParseDeepVariantValue<T> extends infer Result
? Result extends [infer Value, string]
? Value
: Result extends VariantTypeError<any>
? Result
: VariantTypeError<"$ParseDeepVariantValue returned unexpected Result">
: VariantTypeError<"$ParseDeepVariantValue returned uninferrable Result">;
// prettier-ignore
type $ParseRecursiveVariantDict<State extends string, Memo extends Record<string, any> = {}> =
string extends State
? VariantTypeError<"$ParseRecursiveVariantDict: 'string' is not a supported type.">
: State extends `}${infer State}`
? [Memo, State]
: State extends `${infer Key}${''}${infer State}`
? $ParseRecursiveVariantValue<State> extends [infer Value, `${infer State}`]
? State extends `}${infer State}`
? [CreateIndexType<Key, Value>, State]
: VariantTypeError<`$ParseRecursiveVariantDict encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseRecursiveVariantValue returned unexpected value for: ${State}`>
: VariantTypeError<`$ParseRecursiveVariantDict encountered an invalid variant string: ${State} (2)`>;
// prettier-ignore
type $ParseRecursiveVariantArray<State extends string, Memo extends any[] = []> =
string extends State
? VariantTypeError<"$ParseRecursiveVariantArray: 'string' is not a supported type.">
: State extends `)${infer State}`
? [Memo, State]
: $ParseRecursiveVariantValue<State> extends [infer Value, `${infer State}`]
? State extends `${infer _NextValue})${infer _NextState}`
? $ParseRecursiveVariantArray<State, [...Memo, Value]>
: State extends `)${infer State}`
? [[...Memo, Value], State]
: VariantTypeError<`$ParseRecursiveVariantArray encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseRecursiveVariantValue returned unexpected value for: ${State} (2)`>;
// prettier-ignore
type $ParseRecursiveVariantKeyValue<State extends string, Memo extends any[] = []> =
string extends State
? VariantTypeError<"$ParseRecursiveVariantKeyValue: 'string' is not a supported type.">
: State extends `}${infer State}`
? [Memo, State]
: State extends `${infer Key}${''}${infer State}`
? $ParseRecursiveVariantValue<State> extends [infer Value, `${infer State}`]
? State extends `}${infer State}`
? [[...Memo, Key, Value], State]
: VariantTypeError<`$ParseRecursiveVariantKeyValue encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseRecursiveVariantKeyValue returned unexpected value for: ${State}`>
: VariantTypeError<`$ParseRecursiveVariantKeyValue encountered an invalid variant string: ${State} (2)`>;
// prettier-ignore
type $ParseRecursiveVariantValue<State extends string> =
string extends State
? unknown
: State extends `${`s` | `o` | `g`}${infer State}`
? [string, State]
: State extends `${`n` | `q` | `t` | `d` | `u` | `i` | `x` | `y`}${infer State}`
? [number, State]
: State extends `b${infer State}`
? [boolean, State]
: State extends `v${infer State}`
? [unknown, State]
: State extends `${'h' | '?'}${infer State}`
? [unknown, State]
: State extends `(${infer State}`
? $ParseRecursiveVariantArray<State>
: State extends `a{${infer State}`
? $ParseRecursiveVariantDict<State>
: State extends `{${infer State}`
? $ParseRecursiveVariantKeyValue<State>
: State extends `ay${infer State}` ?
[Uint8Array, State]
: State extends `m${infer State}`
? $ParseRecursiveVariantValue<State> extends [infer Value, `${infer State}`]
? [Value | null, State]
: VariantTypeError<`$ParseRecursiveVariantValue encountered an invalid variant string: ${State} (3)`>
: State extends `a${infer State}` ?
$ParseRecursiveVariantValue<State> extends [infer Value, `${infer State}`] ?
[Value[], State]
: VariantTypeError<`$ParseRecursiveVariantValue encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseRecursiveVariantValue encountered an invalid variant string: ${State} (2)`>;
// prettier-ignore
type $ParseRecursiveVariant<T extends string> =
$ParseRecursiveVariantValue<T> extends infer Result
? Result extends [infer Value, string]
? Value
: Result extends VariantTypeError<any>
? Result
: never
: never;
// prettier-ignore
type $ParseVariantDict<State extends string, Memo extends Record<string, any> = {}> =
string extends State
? VariantTypeError<"$ParseVariantDict: 'string' is not a supported type.">
: State extends `}${infer State}`
? [Memo, State]
: State extends `${infer Key}${''}${infer State}`
? $ParseVariantValue<State> extends [infer Value, `${infer State}`]
? State extends `}${infer State}`
? [CreateIndexType<Key, Variant<Value extends string ? Value : any>>, State]
: VariantTypeError<`$ParseVariantDict encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseVariantValue returned unexpected value for: ${State}`>
: VariantTypeError<`$ParseVariantDict encountered an invalid variant string: ${State} (2)`>;
// prettier-ignore
type $ParseVariantArray<State extends string, Memo extends any[] = []> =
string extends State
? VariantTypeError<"$ParseVariantArray: 'string' is not a supported type.">
: State extends `)${infer State}`
? [Memo, State]
: $ParseVariantValue<State> extends [infer Value, `${infer State}`]
? State extends `${infer _NextValue})${infer _NextState}`
? $ParseVariantArray<State, [...Memo, Variant<Value extends string ? Value : any>]>
: State extends `)${infer State}`
? [[...Memo, Variant<Value extends string ? Value : any>], State]
: VariantTypeError<`$ParseVariantArray encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseVariantValue returned unexpected value for: ${State} (2)`>;
// prettier-ignore
type $ParseVariantKeyValue<State extends string, Memo extends any[] = []> =
string extends State
? VariantTypeError<"$ParseVariantKeyValue: 'string' is not a supported type.">
: State extends `}${infer State}`
? [Memo, State]
: State extends `${infer Key}${''}${infer State}`
? $ParseVariantValue<State> extends [infer Value, `${infer State}`]
? State extends `}${infer State}`
? [[...Memo, Variant<Key>, Variant<Value extends string ? Value: any>], State]
: VariantTypeError<`$ParseVariantKeyValue encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseVariantKeyValue returned unexpected value for: ${State}`>
: VariantTypeError<`$ParseVariantKeyValue encountered an invalid variant string: ${State} (2)`>;
// prettier-ignore
type $ParseShallowRootVariantValue<State extends string> =
string extends State
? unknown
: State extends `${`s` | `o` | `g`}${infer State}`
? [string, State]
: State extends `${`n` | `q` | `t` | `d` | `u` | `i` | `x` | `y`}${infer State}`
? [number, State]
: State extends `b${infer State}`
? [boolean, State]
: State extends `v${infer State}`
? [Variant, State]
: State extends `h${infer State}`
? [unknown, State]
: State extends `?${infer State}`
? [unknown, State]
: State extends `(${infer State}`
? $ParseVariantArray<State>
: State extends `a{${infer State}`
? $ParseVariantDict<State>
: State extends `{${infer State}`
? $ParseVariantKeyValue<State>
: State extends `ay${infer State}` ?
[Uint8Array, State]
: State extends `m${infer State}`
? $ParseVariantValue<State> extends [infer Value, `${infer State}`]
? [Value | null, State]
: VariantTypeError<`$ParseShallowRootVariantValue encountered an invalid variant string: ${State} (2)`>
: State extends `a${infer State}` ?
[Variant<State>[], State]
: VariantTypeError<`$ParseShallowRootVariantValue encountered an invalid variant string: ${State} (1)`>;
// prettier-ignore
type $ParseVariantValue<State extends string> =
string extends State
? unknown
: State extends `s${infer State}`
? ['s', State]
: State extends `o${infer State}`
? ['o', State]
: State extends `g${infer State}`
? ['g', State]
: State extends `n${infer State}`
? ["n", State]
: State extends `q${infer State}`
? ["q", State]
: State extends `t${infer State}`
? ["t", State]
: State extends `d${infer State}`
? ["d", State]
: State extends `u${infer State}`
? ["u", State]
: State extends `i${infer State}`
? ["i", State]
: State extends `x${infer State}`
? ["x", State]
: State extends `y${infer State}`
? ["y", State]
: State extends `b${infer State}`
? ['b', State]
: State extends `v${infer State}`
? ['v', State]
: State extends `h${infer State}`
? ['h', State]
: State extends `?${infer State}`
? ['?', State]
: State extends `(${infer State}`
? $ParseVariantArray<State>
: State extends `a{${infer State}`
? $ParseVariantDict<State>
: State extends `{${infer State}`
? $ParseVariantKeyValue<State>
: State extends `ay${infer State}` ?
[Uint8Array, State]
: State extends `m${infer State}`
? $ParseVariantValue<State> extends [infer Value, `${infer State}`]
? [Value | null, State]
: VariantTypeError<`$ParseVariantValue encountered an invalid variant string: ${State} (3)`>
: State extends `a${infer State}` ?
$ParseVariantValue<State> extends [infer Value, `${infer State}`] ?
[Value[], State]
: VariantTypeError<`$ParseVariantValue encountered an invalid variant string: ${State} (1)`>
: VariantTypeError<`$ParseVariantValue encountered an invalid variant string: ${State} (2)`>;
// prettier-ignore
type $ParseVariant<T extends string> =
$ParseShallowRootVariantValue<T> extends infer Result
? Result extends [infer Value, string]
? Value
: Result extends VariantTypeError<any>
? Result
: never
: never;
export type Infer<S extends string> = $ParseVariant<S>
export type DeepInfer<S extends string> = $ParseDeepVariant<S>
export type RecursiveInfer<S extends string> = $ParseRecursiveVariant<S>

View File

@@ -0,0 +1,79 @@
{
"name": "gnim",
"version": "1.8.2",
"type": "module",
"author": "Aylur",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Aylur/gnim.git"
},
"funding": {
"type": "kofi",
"url": "https://ko-fi.com/aylur"
},
"scripts": {
"build": "./scripts/build.sh",
"lint": "eslint . --fix",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"devDependencies": {
"@eslint/js": "latest",
"@girs/adw-1": "latest",
"@girs/clutter-16": "latest",
"@girs/gtk-3.0": "latest",
"@girs/gtk-4.0": "latest",
"@girs/soup-3.0": "latest",
"@girs/shell-16": "latest",
"@girs/st-16": "latest",
"@girs/gnome-shell": "latest",
"@girs/gjs": "latest",
"esbuild": "latest",
"eslint": "latest",
"typescript": "latest",
"typescript-eslint": "latest",
"vitepress": "latest"
},
"exports": {
".": "./dist/index.ts",
"./dbus": "./dist/dbus.ts",
"./fetch": "./dist/fetch.ts",
"./gobject": "./dist/gobject.ts",
"./resource": "./dist/resource/resource.ts",
"./gnome/jsx-runtime": "./dist/gnome/jsx-runtime.ts",
"./gtk3/jsx-runtime": "./dist/gtk3/jsx-runtime.ts",
"./gtk4/jsx-runtime": "./dist/gtk4/jsx-runtime.ts"
},
"files": [
"dist"
],
"engines": {
"gjs": ">=1.79.0"
},
"keywords": [
"GJS",
"Gnome",
"GTK",
"JSX"
],
"prettier": {
"semi": false,
"tabWidth": 4,
"quoteProps": "consistent",
"trailingComma": "all",
"printWidth": 100,
"experimentalTernaries": false,
"overrides": [
{
"files": "**/*.md",
"options": {
"tabWidth": 2,
"printWidth": 80,
"proseWrap": "always"
}
}
]
}
}

45
home/ags-config/node_modules/.pnpm/lock.yaml generated vendored Normal file
View File

@@ -0,0 +1,45 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
ags:
specifier: link:/usr/share/ags/js
version: link:../../../../usr/share/ags/js
fuse.js:
specifier: ^7.1.0
version: 7.1.0
gnim-utils:
specifier: github:retrozinndev/gnim-utils#1.0.0
version: https://codeload.github.com/retrozinndev/gnim-utils/tar.gz/8bfb7d21817ac91a639c3cc90f1b4f66eb990b1e(gnim@1.8.2)
packages:
fuse.js@7.1.0:
resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==}
engines: {node: '>=10'}
gnim-utils@https://codeload.github.com/retrozinndev/gnim-utils/tar.gz/8bfb7d21817ac91a639c3cc90f1b4f66eb990b1e:
resolution: {tarball: https://codeload.github.com/retrozinndev/gnim-utils/tar.gz/8bfb7d21817ac91a639c3cc90f1b4f66eb990b1e}
version: 1.0.0
peerDependencies:
gnim: ^1.8.2
gnim@1.8.2:
resolution: {integrity: sha512-VwvTLclraJPAhS//SzdlWNOdaA7xKqC7KkJpIq5jq9DKDTXPis5SCrExFxrcjtcujK+I2l4drNvoXp6s1G78NA==}
engines: {gjs: '>=1.79.0'}
snapshots:
fuse.js@7.1.0: {}
gnim-utils@https://codeload.github.com/retrozinndev/gnim-utils/tar.gz/8bfb7d21817ac91a639c3cc90f1b4f66eb990b1e(gnim@1.8.2):
dependencies:
gnim: 1.8.2
gnim@1.8.2: {}

1
home/ags-config/node_modules/.pnpm/node_modules/gnim generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../gnim@1.8.2/node_modules/gnim