mirror of
https://github.com/zoriya/astal.git
synced 2025-12-05 21:56:11 +00:00
init 0.1.0
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
build/
|
||||
result
|
||||
.cache/
|
||||
test.sh
|
||||
tmp/
|
||||
219
README.md
Normal file
219
README.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# libastal
|
||||
|
||||
> [!WARNING]
|
||||
> WIP: everything is subject to change
|
||||
|
||||
The main goal of this project is to further abstract gtk bindings in higher level
|
||||
languages with custom state management mechanisms namely in javascript (gjs, node),
|
||||
lua (lua-lgi) and python (pygobject)
|
||||
|
||||
libastal, which is the library written in Vala, comes with
|
||||
a few widgets built on top of gtk3 and
|
||||
tools to execute external binaries and store their output
|
||||
it also comes with a builtin cli client
|
||||
|
||||
## Developing
|
||||
|
||||
first install libastal or enter nix shell
|
||||
|
||||
```bash
|
||||
bash meson-install.sh # non nix
|
||||
nix develop .#astal # nix
|
||||
```
|
||||
|
||||
python and lua should be stright forward, have a look at sample.py and sample.lua
|
||||
|
||||
for javascript do
|
||||
|
||||
```bash
|
||||
cd js
|
||||
npm i
|
||||
npm run types
|
||||
npm run build -- --watch
|
||||
```
|
||||
|
||||
## Gtk abstractions
|
||||
|
||||
`Variable` and `Binding` objects and a function that turns widget constructors
|
||||
into ones that can take `Binding` objects as parameters are added ontop of gtk bindings
|
||||
this mechanism takes care of all state management one would need
|
||||
|
||||
This works the same in js/lua/python, its just demonstrated in js
|
||||
|
||||
```javascript
|
||||
// this example will work with Variable<string>
|
||||
// but it can take any type of value
|
||||
const v = Variable("value")
|
||||
.poll(1000, "some-executable on $PATH")
|
||||
.poll(1000, ["some-executable", "with", "args"])
|
||||
.poll(1000, () => "some-function")
|
||||
.watch("some-executable")
|
||||
.watch(["some-executable", "with", "args"])
|
||||
.observe(someGObject, "signal", (...args) => "some output")
|
||||
.observe([[gobj1, "signal"], [gobj2, "signal"]], (...args) => "some output")
|
||||
.onError(console.error) // when the script fails
|
||||
.onDropped(() => "clean-up") // cleanup resources if needed on drop() or GC
|
||||
|
||||
Button({
|
||||
label: bind(v),
|
||||
label: bind(v).as(v => "transformed"),
|
||||
label: v(t => "transformed"), // shorthand for the above
|
||||
|
||||
// in ags we have Service.bind("prop")
|
||||
// here we will do this, since gobject implementations
|
||||
// will come from Vala code and not js
|
||||
label: bind(anyGObject, "one-of-its-prop").as(prop => "transformed"),
|
||||
|
||||
// event handlers
|
||||
on_signalname(self, ...args) { print(self, args) },
|
||||
|
||||
// setup prop is still here, but should be rarely needed
|
||||
setup(self) {
|
||||
self.hook(v, (self) => print(self))
|
||||
self.hook(gobject, "signal", (self) => print(self))
|
||||
}
|
||||
})
|
||||
|
||||
// some additional Variable and Binding methods
|
||||
v.stop_poll()
|
||||
v.start_poll()
|
||||
|
||||
v.stop_watch()
|
||||
v.start_watch()
|
||||
|
||||
v.get()
|
||||
v.set("new-value")
|
||||
|
||||
const unsub = v.subscribe(value => console.log(value))
|
||||
unsub() // to unsubscribe
|
||||
|
||||
const b = bind(v)
|
||||
b.get()
|
||||
// note that its value cannot be set through a Binding
|
||||
// if you want to, you are doing something wrong
|
||||
|
||||
// same subscribe mechanism
|
||||
const unsub = b.subscribe(value => console.log(value))
|
||||
unsub()
|
||||
|
||||
const derived = Variable.derive([v, b], (vval, bval) => {
|
||||
return "can take a list of Variable | Binding"
|
||||
})
|
||||
|
||||
v.drop() // dispose when no longer needed
|
||||
|
||||
// handle cli client
|
||||
App.start({
|
||||
instanceName: "my-instance",
|
||||
responseHandler(msg, response) {
|
||||
console.log("message from cli", msg)
|
||||
response("hi")
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
after `App.start` is called, it will open a socket, which can be used
|
||||
with the cli client that comes with libastal
|
||||
|
||||
```bash
|
||||
astal --instance-name my-instance "message was sent from cli"
|
||||
```
|
||||
|
||||
## Lower level languages
|
||||
|
||||
As said before, the main goal is to make js/lua/python dx better, but libastal
|
||||
can be used in **any** language that has bindings for glib/gtk.
|
||||
`Binding` is not implemented in Vala, but in each language, because
|
||||
they are language specific, and it doesn't make much sense for lower
|
||||
level languages as they usually don't have a way to declaratively build
|
||||
layouts. Subclassed widgets and `Variable` can still be used, but they will
|
||||
need to be hooked **imperatively**. For languages like rust/go/c++
|
||||
you will mostly benefit from the other libraries (called `Service` in ags).
|
||||
I can also recommend using [blueprint](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/)
|
||||
which lets you define layouts declaratively and hook functionality in your
|
||||
preferred language.
|
||||
|
||||
I am open to add support for any other language if it makes sense,
|
||||
but if using blueprint makes more sense, I would rather maintain
|
||||
templates and examples instead to get started with development.
|
||||
|
||||
## Goals
|
||||
|
||||
- libastal
|
||||
- Variables
|
||||
- [x] poll (interval, string)
|
||||
- [x] pollv (interval, string[])
|
||||
- [x] pollfn (interval, closure)
|
||||
- [x] watch (string)
|
||||
- [x] watchv (string[])
|
||||
- [ ] ?observe (object, signal, closure)
|
||||
- Time
|
||||
- [x] interval
|
||||
- [x] timeout
|
||||
- [x] idle
|
||||
- [x] now signal
|
||||
- Process
|
||||
- [x] exec: string, error as Error
|
||||
- [x] execAsync: proc, stdout, stderr signal
|
||||
- [x] subprocess: proc, stdout, stderr signal
|
||||
- app instance with a socket: Application
|
||||
- [x] gtk settings as props
|
||||
- [x] window getters
|
||||
- [x] include cli client
|
||||
- few additional widgets
|
||||
- [x] window widget with gtk-layer-shell
|
||||
- [x] box with children prop
|
||||
- [ ] button with abstract signals for button-event
|
||||
- [ ] ?custom calendar like gtk4
|
||||
- [x] centerbox
|
||||
- [ ] circularprogress
|
||||
- [ ] eventbox
|
||||
- [ ] icon
|
||||
- [ ] overlay
|
||||
- [ ] scrollable/viewport
|
||||
- [ ] slider
|
||||
- [ ] stack, shown, children setter
|
||||
- widgets with no additional behaviour only for the sake of it
|
||||
- [ ] ?drawingarea
|
||||
- [ ] ?entry
|
||||
- [ ] ?fixed
|
||||
- [ ] ?flowbox
|
||||
- [ ] ?label
|
||||
- [ ] ?levelbar
|
||||
- [ ] ?revealer
|
||||
- [ ] ?switch
|
||||
- widget prop setters
|
||||
- [x] css
|
||||
- [x] class-names
|
||||
- [x] cursor
|
||||
- [ ] click-through
|
||||
|
||||
- language bindings
|
||||
- Binding for Variable and any GObject `bind(gobject, property).as(transform)`
|
||||
- .hook() for widgets
|
||||
- setup prop for widgets
|
||||
- constructor overrides to take bindings
|
||||
- override default `visible` for widgets to true
|
||||
- wrap Variable in native object to make sure no GValue crashes
|
||||
- Variable.observe for signals
|
||||
- Variable.derive that takes either Variables or Bindings
|
||||
|
||||
## Help needed
|
||||
|
||||
- packaging
|
||||
- I am not familiar with python or lua ecosystem at all how should they be distributed?
|
||||
- node-gtk promise issue
|
||||
- python types
|
||||
- I don't know much python, and quite honestly I hate python
|
||||
|
||||
## TODO
|
||||
|
||||
- docs
|
||||
- consider moving each language into separate repo
|
||||
- I want to keep these at one place until I'm sure all of them works as expected
|
||||
- support jsx
|
||||
- make sure conventions like the casing of names are followed for each language
|
||||
- I constantly switched between 4 languages they might be off
|
||||
|
||||
- port services from ags into Vala
|
||||
- and add more
|
||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1715266358,
|
||||
"narHash": "sha256-doPgfj+7FFe9rfzWo1siAV2mVCasW+Bh8I1cToAXEE4=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f1010e0469db743d14519a1efd37e23f8513d714",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
58
flake.nix
Normal file
58
flake.nix
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version);
|
||||
system = "x86_64-linux";
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
wrapGAppsHook
|
||||
gobject-introspection
|
||||
meson
|
||||
pkg-config
|
||||
ninja
|
||||
vala
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
glib
|
||||
gtk3
|
||||
gtk-layer-shell
|
||||
];
|
||||
in {
|
||||
packages.${system} = rec {
|
||||
default = astal;
|
||||
astal = pkgs.stdenv.mkDerivation {
|
||||
inherit nativeBuildInputs buildInputs;
|
||||
pname = "astal";
|
||||
version = version;
|
||||
src = ./.;
|
||||
outputs = ["out" "dev"];
|
||||
};
|
||||
};
|
||||
|
||||
devShells.${system} = let
|
||||
inputs = with pkgs; buildInputs ++ [
|
||||
(lua.withPackages(ps: [ps.lgi]))
|
||||
(python3.withPackages(ps: [ps.pygobject3]))
|
||||
gjs
|
||||
deno
|
||||
nodejs
|
||||
];
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
inherit nativeBuildInputs;
|
||||
buildInputs = inputs;
|
||||
};
|
||||
astal = pkgs.mkShell {
|
||||
inherit nativeBuildInputs;
|
||||
buildInputs = inputs ++ [
|
||||
self.packages.${system}.astal
|
||||
pkgs.playerctl # FIXME: just for demo
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
128
js/.eslintrc.yml
Normal file
128
js/.eslintrc.yml
Normal file
@@ -0,0 +1,128 @@
|
||||
env:
|
||||
es2021: true
|
||||
extends:
|
||||
- "eslint:recommended"
|
||||
- "plugin:@typescript-eslint/recommended"
|
||||
parser: "@typescript-eslint/parser"
|
||||
parserOptions:
|
||||
ecmaVersion: 2022
|
||||
sourceType: "module"
|
||||
project: "./tsconfig.json"
|
||||
warnOnUnsupportedTypeScriptVersion: false
|
||||
root: true
|
||||
ignorePatterns:
|
||||
- example/
|
||||
- types/
|
||||
- gi-types/
|
||||
- _build/
|
||||
- build/
|
||||
- result/
|
||||
plugins:
|
||||
- "@typescript-eslint"
|
||||
rules:
|
||||
"@typescript-eslint/ban-ts-comment": error
|
||||
"@typescript-eslint/no-non-null-assertion": off
|
||||
"@typescript-eslint/no-explicit-any": off
|
||||
"@typescript-eslint/no-unused-vars":
|
||||
- error
|
||||
- varsIgnorePattern: (^unused|_$)
|
||||
argsIgnorePattern: ^(unused|_)
|
||||
"@typescript-eslint/no-empty-interface": off
|
||||
"@typescript-eslint/no-namespace": off
|
||||
"@typescript-eslint/prefer-namespace-keyword": off
|
||||
"@typescript-eslint/ban-types": off
|
||||
|
||||
arrow-parens:
|
||||
- error
|
||||
- as-needed
|
||||
comma-dangle:
|
||||
- error
|
||||
- always-multiline
|
||||
comma-spacing:
|
||||
- error
|
||||
- before: false
|
||||
after: true
|
||||
comma-style:
|
||||
- error
|
||||
- last
|
||||
curly:
|
||||
- error
|
||||
- multi-or-nest
|
||||
- consistent
|
||||
dot-location:
|
||||
- error
|
||||
- property
|
||||
eol-last:
|
||||
- error
|
||||
indent:
|
||||
- error
|
||||
- 4
|
||||
- SwitchCase: 1
|
||||
keyword-spacing:
|
||||
- error
|
||||
- before: true
|
||||
lines-between-class-members:
|
||||
- error
|
||||
- always
|
||||
- exceptAfterSingleLine: true
|
||||
padded-blocks:
|
||||
- error
|
||||
- never
|
||||
- allowSingleLineBlocks: false
|
||||
prefer-const:
|
||||
- error
|
||||
quotes:
|
||||
- error
|
||||
- double
|
||||
- avoidEscape: true
|
||||
semi:
|
||||
- error
|
||||
- never
|
||||
nonblock-statement-body-position:
|
||||
- error
|
||||
- below
|
||||
no-trailing-spaces:
|
||||
- error
|
||||
no-useless-escape:
|
||||
- off
|
||||
max-len:
|
||||
- error
|
||||
- code: 100
|
||||
|
||||
func-call-spacing:
|
||||
- error
|
||||
array-bracket-spacing:
|
||||
- error
|
||||
space-before-function-paren:
|
||||
- error
|
||||
- anonymous: never
|
||||
named: never
|
||||
asyncArrow: ignore
|
||||
space-before-blocks:
|
||||
- error
|
||||
key-spacing:
|
||||
- error
|
||||
object-curly-spacing:
|
||||
- error
|
||||
- always
|
||||
|
||||
globals:
|
||||
pkg: readonly
|
||||
ARGV: readonly
|
||||
Debugger: readonly
|
||||
GIRepositoryGType: readonly
|
||||
globalThis: readonly
|
||||
imports: readonly
|
||||
Intl: readonly
|
||||
log: readonly
|
||||
logError: readonly
|
||||
print: readonly
|
||||
printerr: readonly
|
||||
window: readonly
|
||||
TextEncoder: readonly
|
||||
TextDecoder: readonly
|
||||
console: readonly
|
||||
setTimeout: readonly
|
||||
setInterval: readonly
|
||||
clearTimeout: readonly
|
||||
clearInterval: readonly
|
||||
4
js/.gitignore
vendored
Normal file
4
js/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
result
|
||||
@girs/
|
||||
dist/
|
||||
7
js/.ts-for-girrc.js
Normal file
7
js/.ts-for-girrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
"verbose": true,
|
||||
"environments": ["gjs", "node"],
|
||||
"outdir": "@girs",
|
||||
"package": true,
|
||||
"generateAlias": true,
|
||||
}
|
||||
51
js/gjs/application.ts
Normal file
51
js/gjs/application.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import Astal from "gi://Astal"
|
||||
import GObject from "gi://GObject"
|
||||
import Gio from "gi://Gio"
|
||||
import { RequestHandler, Config, runJS } from "../src/application.js"
|
||||
|
||||
// @ts-expect-error missing types
|
||||
// https://github.com/gjsify/ts-for-gir/issues/164
|
||||
import { setConsoleLogDomain } from "console"
|
||||
import { exit, programArgs } from "system"
|
||||
|
||||
class AstalJS extends Astal.Application {
|
||||
static { GObject.registerClass(this) }
|
||||
|
||||
eval = runJS
|
||||
requestHandler?: RequestHandler
|
||||
|
||||
vfunc_response(msg: string, conn: Gio.SocketConnection): void {
|
||||
if (typeof this.requestHandler === "function") {
|
||||
this.requestHandler(msg, response => {
|
||||
Astal.write_sock(conn, response, (_, res) =>
|
||||
Astal.write_sock_finish(res),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
super.vfunc_response(msg, conn)
|
||||
}
|
||||
}
|
||||
|
||||
start({ requestHandler, css, hold, ...cfg }: Config = {}, callback?: (args: string[]) => void) {
|
||||
Object.assign(this, cfg)
|
||||
setConsoleLogDomain(this.instanceName)
|
||||
|
||||
this.requestHandler = requestHandler
|
||||
this.connect("activate", () => callback?.(programArgs))
|
||||
if (!this.acquire_socket()) {
|
||||
print(`Astal instance "${this.instanceName}" already running`)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
if (css)
|
||||
this.apply_css(css, false)
|
||||
|
||||
hold ??= true
|
||||
if (hold)
|
||||
this.hold()
|
||||
|
||||
this.runAsync([])
|
||||
}
|
||||
}
|
||||
|
||||
export const App = new AstalJS
|
||||
37
js/gjs/astal.ts
Normal file
37
js/gjs/astal.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import Astal from "gi://Astal"
|
||||
import Time from "../src/time.js"
|
||||
import Process from "../src/process.js"
|
||||
import * as variable from "../src/variable.js"
|
||||
|
||||
const { interval, timeout, idle } = Time(Astal.Time)
|
||||
const { subprocess, exec, execAsync } = Process({
|
||||
defaultOut: print,
|
||||
defaultErr: console.error,
|
||||
exec: Astal.Process.exec,
|
||||
execv: Astal.Process.execv,
|
||||
execAsync: Astal.Process.exec_async,
|
||||
execAsyncv: Astal.Process.exec_asyncv,
|
||||
subprocess: Astal.Process.subprocess,
|
||||
subprocessv: Astal.Process.subprocessv,
|
||||
})
|
||||
|
||||
variable.config.defaultErrHandler = print
|
||||
variable.config.execAsync = execAsync
|
||||
variable.config.subprocess = subprocess
|
||||
variable.config.interval = interval
|
||||
variable.config.VariableBase = Astal.VariableBase
|
||||
Object.freeze(variable.config)
|
||||
|
||||
export { subprocess, exec, execAsync }
|
||||
export { interval, timeout, idle }
|
||||
export { bind } from "../src/binding.js"
|
||||
export { Variable } from "../src/variable.js"
|
||||
export * as Widget from "./widgets.js"
|
||||
export { App } from "./application.js"
|
||||
|
||||
// for convinience
|
||||
export { default as GLib } from "gi://GLib?version=2.0"
|
||||
export { default as Gtk } from "gi://Gtk?version=3.0"
|
||||
export { default as Gio } from "gi://Gio?version=2.0"
|
||||
export { default as GObject } from "gi://GObject?version=2.0"
|
||||
export { default as Astal } from "gi://Astal?version=0.1"
|
||||
60
js/gjs/widgets.ts
Normal file
60
js/gjs/widgets.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable max-len */
|
||||
import Gtk from "gi://Gtk"
|
||||
import Astal from "gi://Astal"
|
||||
import { kebabify } from "../src/binding.js"
|
||||
import proxy, { type ConstructProps, type Widget } from "../src/astalify.js"
|
||||
|
||||
const proxify = proxy(Gtk,
|
||||
prop => `set_${kebabify(prop).replaceAll("-", "_")}`,
|
||||
{
|
||||
cssGetter: Astal.widget_get_css,
|
||||
cssSetter: Astal.widget_set_css,
|
||||
classGetter: Astal.widget_get_class_names,
|
||||
classSetter: Astal.widget_set_class_names,
|
||||
cursorGetter: Astal.widget_get_cursor,
|
||||
cursorSetter: Astal.widget_set_cursor,
|
||||
})
|
||||
|
||||
export function astalify<
|
||||
C extends typeof Gtk.Widget,
|
||||
P extends Record<string, any>,
|
||||
N extends string = "Widget",
|
||||
>(klass: C) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type Astal<N> = Omit<C, "new"> & {
|
||||
new(props?: P, ...children: Gtk.Widget[]): Widget<C>
|
||||
(props?: P, ...children: Gtk.Widget[]): Widget<C>
|
||||
}
|
||||
|
||||
return proxify(klass) as unknown as Astal<N>
|
||||
}
|
||||
|
||||
// Label
|
||||
export const Label = astalify<typeof Gtk.Label, LabelProps, "Label">(Gtk.Label)
|
||||
export type LabelProps = ConstructProps<typeof Gtk.Label, Gtk.Label.ConstructorProperties>
|
||||
|
||||
// Icon
|
||||
export const Icon = astalify<typeof Astal.Icon, IconProps, "Icon">(Astal.Icon)
|
||||
export type IconProps = ConstructProps<typeof Astal.Icon, Astal.Icon.ConstructorProperties>
|
||||
|
||||
// Button
|
||||
export const Button = astalify<typeof Astal.Button, ButtonProps, "Button">(Astal.Button)
|
||||
export type ButtonProps = ConstructProps<typeof Astal.Button, Astal.Button.ConstructorProperties, {
|
||||
onClicked: (self: Widget<typeof Astal.Button>) => void
|
||||
}>
|
||||
|
||||
// Window
|
||||
export const Window = astalify<typeof Astal.Window, WindowProps, "Window">(Astal.Window)
|
||||
export type WindowProps = ConstructProps<typeof Astal.Window, Astal.Window.ConstructorProperties>
|
||||
|
||||
// Box
|
||||
export const Box = astalify<typeof Astal.Box, BoxProps, "Box">(Astal.Box)
|
||||
export type BoxProps = ConstructProps<typeof Astal.Box, Astal.Box.ConstructorProperties>
|
||||
|
||||
// CenterBox
|
||||
export const CenterBox = astalify<typeof Astal.CenterBox, CenterBoxProps, "CenterBox">(Astal.CenterBox)
|
||||
export type CenterBoxProps = ConstructProps<typeof Astal.CenterBox, Astal.CenterBox.ConstructorProperties>
|
||||
|
||||
// EventBox
|
||||
export const EventBox = astalify<typeof Astal.EventBox, EventBoxProps, "EventBox">(Astal.EventBox)
|
||||
export type EventBoxProps = ConstructProps<typeof Astal.EventBox, Astal.EventBox.ConstructorProperties>
|
||||
51
js/node/application.ts
Normal file
51
js/node/application.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import gi from "node-gtk"
|
||||
import { RequestHandler, Config, runJS } from "../src/application.js"
|
||||
const Astal = gi.require("Astal", "0.1")
|
||||
|
||||
class AstalJS extends Astal.Application {
|
||||
static GTypeName = "AstalJS"
|
||||
static { gi.registerClass(this) }
|
||||
|
||||
eval = runJS
|
||||
requestHandler?: RequestHandler
|
||||
|
||||
vfunc_response(msg: string, conn: any): void {
|
||||
if (typeof this.requestHandler === "function") {
|
||||
this.requestHandler(msg, response => {
|
||||
Astal.writeSock(conn, response, (_, res) =>
|
||||
Astal.writeSockFinish(res),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// @ts-expect-error missing type
|
||||
super.vfunc_response(msg, conn)
|
||||
}
|
||||
}
|
||||
|
||||
start(
|
||||
{ requestHandler, css, ...cfg }: Omit<Config, "hold"> = {},
|
||||
callback?: (args: string[]) => any,
|
||||
) {
|
||||
Object.assign(this, cfg)
|
||||
|
||||
this.requestHandler = requestHandler
|
||||
this.on("activate", () => {
|
||||
callback?.(process.argv)
|
||||
})
|
||||
|
||||
if (!this.acquireSocket()) {
|
||||
console.error(`Astal instance "${this.instanceName}" already running`)
|
||||
process.exit()
|
||||
}
|
||||
|
||||
if (css)
|
||||
this.applyCss(css, false)
|
||||
|
||||
// FIXME: promises never resolve
|
||||
// https://github.com/romgrk/node-gtk/issues/121
|
||||
// https://gitlab.gnome.org/GNOME/gjs/-/issues/468
|
||||
App.run([])
|
||||
}
|
||||
}
|
||||
|
||||
export const App = new AstalJS
|
||||
38
js/node/astal.ts
Normal file
38
js/node/astal.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import gi from "node-gtk"
|
||||
import Time from "../src/time.js"
|
||||
import Process from "../src/process.js"
|
||||
import * as variable from "../src/variable.js"
|
||||
const Astal = gi.require("Astal", "0.1")
|
||||
|
||||
const { interval, timeout, idle } = Time(Astal.Time)
|
||||
const { subprocess, exec, execAsync } = Process({
|
||||
defaultOut: console.log,
|
||||
defaultErr: console.error,
|
||||
exec: Astal.Process.exec,
|
||||
execv: Astal.Process.execv,
|
||||
execAsync: Astal.Process.execAsync,
|
||||
execAsyncv: Astal.Process.execAsyncv,
|
||||
subprocess: Astal.Process.subprocess,
|
||||
subprocessv: Astal.Process.subprocessv,
|
||||
})
|
||||
|
||||
variable.config.defaultErrHandler = console.log
|
||||
variable.config.execAsync = execAsync
|
||||
variable.config.subprocess = subprocess
|
||||
variable.config.interval = interval
|
||||
variable.config.VariableBase = Astal.VariableBase
|
||||
Object.freeze(variable.config)
|
||||
|
||||
export { subprocess, exec, execAsync }
|
||||
export { interval, timeout, idle }
|
||||
export { bind } from "../src/binding.js"
|
||||
export { Variable } from "../src/variable.js"
|
||||
export * as Widget from "./widgets.js"
|
||||
export { App } from "./application.js"
|
||||
|
||||
// for convinience
|
||||
export const GLib = gi.require("GLib", "2.0")
|
||||
export const Gtk = gi.require("Gtk", "3.0")
|
||||
export const Gio = gi.require("Gio", "2.0")
|
||||
export const GObject = gi.require("GObject", "2.0")
|
||||
export { Astal, gi }
|
||||
63
js/node/widgets.ts
Normal file
63
js/node/widgets.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/* eslint-disable max-len */
|
||||
import gi from "node-gtk"
|
||||
import proxy, { type ConstructProps, type Widget } from "../src/astalify.js"
|
||||
import type GtkT from "@girs/node-gtk-3.0/node-gtk-3.0"
|
||||
import type AstalT from "@girs/node-astal-0.1/node-astal-0.1"
|
||||
|
||||
const Astal = gi.require("Astal", "0.1")
|
||||
const Gtk = gi.require("Gtk", "3.0")
|
||||
|
||||
const proxify = proxy(Gtk,
|
||||
prop => `set${prop.charAt(0).toUpperCase() + prop.slice(1)}`,
|
||||
{
|
||||
cssGetter: Astal.widgetGetCss,
|
||||
cssSetter: Astal.widgetSetCss,
|
||||
classGetter: Astal.widgetGetClassNames,
|
||||
classSetter: Astal.widgetSetClassNames,
|
||||
cursorGetter: Astal.widgetGetCursor,
|
||||
cursorSetter: Astal.widgetSetCursor,
|
||||
})
|
||||
|
||||
export function astalify<
|
||||
C extends typeof Gtk.Widget,
|
||||
P extends Record<string, any>,
|
||||
N extends string = "Widget",
|
||||
>(klass: C) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type Astal<N> = Omit<C, "new"> & {
|
||||
new(props: P, ...children: GtkT.Widget[]): Widget<C>
|
||||
(props: P, ...children: GtkT.Widget[]): Widget<C>
|
||||
}
|
||||
|
||||
return proxify(klass) as unknown as Astal<N>
|
||||
}
|
||||
|
||||
// Label
|
||||
export const Label = astalify<typeof Gtk.Label, LabelProps, "Label">(Gtk.Label)
|
||||
export type LabelProps = ConstructProps<typeof Gtk.Label, GtkT.Label.ConstructorProperties>
|
||||
|
||||
// Icon
|
||||
export const Icon = astalify<typeof Astal.Icon, IconProps, "Icon">(Astal.Icon)
|
||||
export type IconProps = ConstructProps<typeof Astal.Icon, AstalT.Icon.ConstructorProperties>
|
||||
|
||||
// Button
|
||||
export const Button = astalify<typeof Astal.Button, ButtonProps, "Button">(Astal.Button)
|
||||
export type ButtonProps = ConstructProps<typeof Astal.Button, AstalT.Button.ConstructorProperties, {
|
||||
onClicked: (self: Widget<typeof Astal.Button>) => void
|
||||
}>
|
||||
|
||||
// Window
|
||||
export const Window = astalify<typeof Astal.Window, WindowProps, "Window">(Astal.Window)
|
||||
export type WindowProps = ConstructProps<typeof Astal.Window, AstalT.Window.ConstructorProperties>
|
||||
|
||||
// Box
|
||||
export const Box = astalify<typeof Astal.Box, BoxProps, "Box">(Astal.Box)
|
||||
export type BoxProps = ConstructProps<typeof Astal.Box, AstalT.Box.ConstructorProperties>
|
||||
|
||||
// CenterBox
|
||||
export const CenterBox = astalify<typeof Astal.CenterBox, CenterBoxProps, "CenterBox">(Astal.CenterBox)
|
||||
export type CenterBoxProps = ConstructProps<typeof Astal.CenterBox, AstalT.CenterBox.ConstructorProperties>
|
||||
|
||||
// EventBox
|
||||
export const EventBox = astalify<typeof Astal.EventBox, EventBoxProps, "EventBox">(Astal.EventBox)
|
||||
export type EventBoxProps = ConstructProps<typeof Astal.EventBox, AstalT.EventBox.ConstructorProperties>
|
||||
4038
js/package-lock.json
generated
Normal file
4038
js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
js/package.json
Normal file
20
js/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"node-gtk": "^0.14.0",
|
||||
"typescript": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ts-for-gir/cli": "^3.3.0",
|
||||
"@types/node": "^20.12.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||
"@typescript-eslint/parser": "^5.33.0",
|
||||
"esbuild": "^0.20.2",
|
||||
"eslint": "^8.42.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"build": "tsc",
|
||||
"types": "ts-for-gir generate"
|
||||
}
|
||||
}
|
||||
43
js/sample.gjs.js
Executable file
43
js/sample.gjs.js
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env -S gjs -m
|
||||
import { Variable, App, Widget, Astal, bind } from "../js/dist/gjs/astal.js"
|
||||
import Playerctl from "gi://Playerctl"
|
||||
|
||||
// state
|
||||
const player = Playerctl.Player.new("spotify")
|
||||
const date = Variable("").poll(1000, "date")
|
||||
const title = Variable(player.get_title()).observe(player, "metadata", () => player.get_title())
|
||||
|
||||
// ui
|
||||
function Bar(monitor) {
|
||||
return Widget.Window(
|
||||
{
|
||||
monitor,
|
||||
application: App,
|
||||
exclusivity: Astal.Exclusivity.EXCLUSIVE,
|
||||
anchor: Astal.WindowAnchor.BOTTOM |
|
||||
Astal.WindowAnchor.LEFT |
|
||||
Astal.WindowAnchor.RIGHT
|
||||
},
|
||||
Widget.CenterBox({
|
||||
startWidget: Widget.Label({
|
||||
label: date(l => `Current date: ${l}`),
|
||||
}),
|
||||
endWidget: Widget.Label({
|
||||
label: bind(title).as(t => `Title: ${t}`)
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// main
|
||||
App.start({
|
||||
requestHandler(msg, res) {
|
||||
switch (msg) {
|
||||
case "inspector": return res(App.inspector())
|
||||
case "quit": return res(App.quit())
|
||||
default: return App.eval(msg).then(res).catch(console.error)
|
||||
}
|
||||
}
|
||||
}, () => {
|
||||
Bar(0)
|
||||
})
|
||||
49
js/sample.node.js
Executable file
49
js/sample.node.js
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
import { Variable, App, Widget, Astal, bind, gi } from "../js/dist/node/astal.js"
|
||||
const Playerctl = gi.require("Playerctl", "2.0")
|
||||
|
||||
// state
|
||||
const player = Playerctl.Player.new("spotify")
|
||||
const title = Variable(player.getTitle()).observe(player, "metadata", () => player.getTitle())
|
||||
const date = Variable("")
|
||||
// FIXME: doesn't work because promises don't resolve
|
||||
// .poll(1000, "date")
|
||||
// FIXME: don't know why but this doesn't work either
|
||||
// .watch("bash -c 'while true; do date; sleep 1; done'")
|
||||
// this does
|
||||
.poll(1000, Date)
|
||||
|
||||
// ui
|
||||
function Bar(monitor) {
|
||||
return Widget.Window(
|
||||
{
|
||||
monitor,
|
||||
application: App,
|
||||
exclusivity: Astal.Exclusivity.EXCLUSIVE,
|
||||
anchor: Astal.WindowAnchor.BOTTOM |
|
||||
Astal.WindowAnchor.LEFT |
|
||||
Astal.WindowAnchor.RIGHT
|
||||
},
|
||||
Widget.CenterBox({
|
||||
startWidget: Widget.Label({
|
||||
label: date(l => `Current date: ${l}`),
|
||||
}),
|
||||
endWidget: Widget.Label({
|
||||
label: bind(title).as(t => `Title: ${t}`)
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// main
|
||||
App.start({
|
||||
requestHandler(msg, res) {
|
||||
switch (msg) {
|
||||
case "inspector": return res(App.inspector())
|
||||
case "quit": return res(App.quit())
|
||||
default: return App.eval(msg).then(res).catch(console.error)
|
||||
}
|
||||
}
|
||||
}, () => {
|
||||
Bar(0)
|
||||
})
|
||||
28
js/src/application.ts
Normal file
28
js/src/application.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export type RequestHandler = {
|
||||
(request: string, res: (response: string) => void): void
|
||||
}
|
||||
|
||||
export type Config = Partial<{
|
||||
instanceName: string
|
||||
gtkTheme: string
|
||||
iconTheme: string
|
||||
cursorTheme: string
|
||||
css: string
|
||||
requestHandler: RequestHandler
|
||||
hold: boolean
|
||||
}>
|
||||
|
||||
export function runJS(body: string): Promise<any> {
|
||||
return new Promise((res, rej) => {
|
||||
try {
|
||||
const fn = Function(`return (async function() {
|
||||
${body.includes(";") ? body : `return ${body};`}
|
||||
})`)
|
||||
fn()()
|
||||
.then(res)
|
||||
.catch(rej)
|
||||
} catch (error) {
|
||||
rej(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
213
js/src/astalify.ts
Normal file
213
js/src/astalify.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import Binding, { kebabify, type Connectable, type Subscribable } from "./binding.js"
|
||||
|
||||
export type Widget<C extends { new(...args: any): any }> = InstanceType<C> & {
|
||||
className: string
|
||||
css: string
|
||||
cursor: Cursor
|
||||
hook(
|
||||
object: Connectable,
|
||||
signal: string,
|
||||
callback: (self: Widget<C>, ...args: any[]) => void,
|
||||
): Widget<C>
|
||||
hook(
|
||||
object: Subscribable,
|
||||
callback: (self: Widget<C>, ...args: any[]) => void,
|
||||
): Widget<C>
|
||||
}
|
||||
|
||||
export default function <G extends { Bin: any, Container: any, Widget: any }>(
|
||||
Gtk: G,
|
||||
setter: (prop: string) => `set${string}`,
|
||||
Astal: {
|
||||
cssSetter: (w: any, css: string) => void,
|
||||
cssGetter: (w: any) => string | null,
|
||||
classSetter: (w: any, name: string[]) => void,
|
||||
classGetter: (w: any) => string[],
|
||||
cursorSetter: (w: any, cursor: string) => void,
|
||||
cursorGetter: (w: any) => string | null,
|
||||
},
|
||||
) {
|
||||
function hook(
|
||||
self: any,
|
||||
object: Connectable | Subscribable,
|
||||
signalOrCallback: string | ((self: any, ...args: any[]) => void),
|
||||
callback?: (self: any, ...args: any[]) => void,
|
||||
) {
|
||||
if (typeof object.connect === "function" && callback) {
|
||||
const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => {
|
||||
callback(self, ...args)
|
||||
})
|
||||
self.connect("destroy", () => {
|
||||
(object.disconnect as Connectable["disconnect"])(id)
|
||||
})
|
||||
}
|
||||
|
||||
else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") {
|
||||
const unsub = object.subscribe((...args: unknown[]) => {
|
||||
signalOrCallback(self, ...args)
|
||||
})
|
||||
self.connect("destroy", unsub)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
function setChild(parent: any, child: any) {
|
||||
if (parent instanceof Gtk.Bin) {
|
||||
if (parent.get_child())
|
||||
parent.remove(parent.get_child()!)
|
||||
}
|
||||
if (parent instanceof Gtk.Container)
|
||||
parent.add(child)
|
||||
}
|
||||
|
||||
function ctor(self: any, config: any, ...children: any[]) {
|
||||
const { setup, child, ...props } = config
|
||||
props.visible ??= true
|
||||
|
||||
const bindings = Object.keys(props).reduce((acc: any, prop) => {
|
||||
if (props[prop] instanceof Binding) {
|
||||
const bind = [prop, props[prop]]
|
||||
prop === "child"
|
||||
? setChild(self, props[prop].get())
|
||||
: self[setter(prop)](props[prop].get())
|
||||
|
||||
delete props[prop]
|
||||
return [...acc, bind]
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const onHandlers = Object.keys(props).reduce((acc: any, key) => {
|
||||
if (key.startsWith("on")) {
|
||||
const sig = kebabify(key).split("-").slice(1).join("-")
|
||||
const handler = [sig, props[key]]
|
||||
delete props[key]
|
||||
return [...acc, handler]
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
Object.assign(self, props)
|
||||
Object.assign(self, {
|
||||
hook(obj: any, sig: any, callback: any) {
|
||||
return hook(self, obj, sig, callback)
|
||||
},
|
||||
})
|
||||
|
||||
if (child instanceof Binding) {
|
||||
setChild(self, child.get())
|
||||
self.connect("destroy", child.subscribe(v => {
|
||||
setChild(self, v)
|
||||
}))
|
||||
} else if (self instanceof Gtk.Container && child instanceof Gtk.Widget) {
|
||||
self.add(child)
|
||||
}
|
||||
|
||||
for (const [signal, callback] of onHandlers)
|
||||
self.connect(signal, callback)
|
||||
|
||||
if (self instanceof Gtk.Container && children) {
|
||||
for (const child of children)
|
||||
self.add(child)
|
||||
}
|
||||
|
||||
for (const [prop, bind] of bindings) {
|
||||
self.connect("destroy", bind.subscribe((v: any) => {
|
||||
self[`${setter(prop)}`](v)
|
||||
}))
|
||||
}
|
||||
|
||||
setup?.(self)
|
||||
return self
|
||||
}
|
||||
|
||||
return function proxify<
|
||||
C extends { new(...args: any[]): any },
|
||||
>(klass: C) {
|
||||
Object.defineProperty(klass.prototype, "className", {
|
||||
get() { return Astal.classGetter(this).join(" ") },
|
||||
set(v) { Astal.classSetter(this, v.split(/\s+/)) },
|
||||
})
|
||||
|
||||
Object.defineProperty(klass.prototype, "css", {
|
||||
get() { return Astal.cssGetter(this) },
|
||||
set(v) { Astal.cssSetter(this, v) },
|
||||
})
|
||||
|
||||
Object.defineProperty(klass.prototype, "cursor", {
|
||||
get() { return Astal.cursorGetter(this) },
|
||||
set(v) { Astal.cursorSetter(this, v) },
|
||||
})
|
||||
|
||||
const proxy = new Proxy(klass, {
|
||||
construct(_, [conf, ...children]) {
|
||||
const self = new klass
|
||||
return ctor(self, conf, ...children)
|
||||
},
|
||||
apply(_t, _a, [conf, ...children]) {
|
||||
const self = new klass
|
||||
return ctor(self, conf, ...children)
|
||||
},
|
||||
})
|
||||
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type BindableProps<T> = {
|
||||
[K in keyof T]: Binding<NonNullable<T[K]>> | T[K];
|
||||
}
|
||||
|
||||
export type ConstructProps<
|
||||
Self extends { new(...args: any[]): any },
|
||||
Props = unknown,
|
||||
Signals = unknown
|
||||
> = {
|
||||
[Key in `on${string}`]: (self: Widget<Self>) => unknown
|
||||
} & Partial<Signals> & BindableProps<Props & {
|
||||
className?: string
|
||||
css?: string
|
||||
cursor?: string
|
||||
}> & {
|
||||
onDestroy?: (self: Widget<Self>) => unknown
|
||||
onDraw?: (self: Widget<Self>) => unknown
|
||||
setup?: (self: Widget<Self>) => void
|
||||
}
|
||||
|
||||
type Cursor =
|
||||
| "default"
|
||||
| "help"
|
||||
| "pointer"
|
||||
| "context-menu"
|
||||
| "progress"
|
||||
| "wait"
|
||||
| "cell"
|
||||
| "crosshair"
|
||||
| "text"
|
||||
| "vertical-text"
|
||||
| "alias"
|
||||
| "copy"
|
||||
| "no-drop"
|
||||
| "move"
|
||||
| "not-allowed"
|
||||
| "grab"
|
||||
| "grabbing"
|
||||
| "all-scroll"
|
||||
| "col-resize"
|
||||
| "row-resize"
|
||||
| "n-resize"
|
||||
| "e-resize"
|
||||
| "s-resize"
|
||||
| "w-resize"
|
||||
| "ne-resize"
|
||||
| "nw-resize"
|
||||
| "sw-resize"
|
||||
| "se-resize"
|
||||
| "ew-resize"
|
||||
| "ns-resize"
|
||||
| "nesw-resize"
|
||||
| "nwse-resize"
|
||||
| "zoom-in"
|
||||
| "zoom-out"
|
||||
78
js/src/binding.ts
Normal file
78
js/src/binding.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
export const kebabify = (str: string) => str
|
||||
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
||||
.replaceAll("_", "-")
|
||||
.toLowerCase()
|
||||
|
||||
export interface Subscribable<T = unknown> {
|
||||
subscribe(callback: () => void): () => void
|
||||
get(): T
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface Connectable {
|
||||
connect(signal: string, callback: (...args: any[]) => unknown): number
|
||||
disconnect(id: number): void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export default class Binding<Value> {
|
||||
private emitter: Subscribable<Value> | Connectable
|
||||
private prop?: string
|
||||
private transformFn = (v: any) => v
|
||||
|
||||
static bind<
|
||||
T extends Connectable,
|
||||
P extends keyof T,
|
||||
>(object: T, property: P): Binding<T[P]>
|
||||
|
||||
static bind<T>(object: Subscribable<T>): Binding<T>
|
||||
|
||||
static bind(emitter: Connectable | Subscribable, prop?: string) {
|
||||
return new Binding(emitter, prop)
|
||||
}
|
||||
|
||||
private constructor(emitter: Connectable | Subscribable<Value>, prop?: string) {
|
||||
this.emitter = emitter
|
||||
this.prop = prop && kebabify(prop)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Binding<${this.emitter}${this.prop ? `, "${this.prop}"` : ""}>`
|
||||
}
|
||||
|
||||
as<T>(fn: (v: Value) => T): Binding<T> {
|
||||
const bind = new Binding(this.emitter, this.prop)
|
||||
bind.transformFn = (v: Value) => fn(this.transformFn(v))
|
||||
return bind as unknown as Binding<T>
|
||||
}
|
||||
|
||||
get(): Value {
|
||||
if (typeof this.emitter.get === "function")
|
||||
return this.transformFn(this.emitter.get())
|
||||
|
||||
if (typeof this.prop === "string")
|
||||
return this.transformFn(this.emitter[this.prop])
|
||||
|
||||
throw Error("can not get value of binding")
|
||||
}
|
||||
|
||||
subscribe(callback: (value: Value) => void): () => void {
|
||||
if (typeof this.emitter.subscribe === "function") {
|
||||
return this.emitter.subscribe(() => {
|
||||
callback(this.get())
|
||||
})
|
||||
}
|
||||
else if (typeof this.emitter.connect === "function") {
|
||||
const signal = `notify::${this.prop}`
|
||||
const id = this.emitter.connect(signal, () => {
|
||||
callback(this.get())
|
||||
})
|
||||
return () => {
|
||||
(this.emitter.disconnect as Connectable["disconnect"])(id)
|
||||
}
|
||||
}
|
||||
throw Error(`${this.emitter} is not bindable`)
|
||||
}
|
||||
}
|
||||
|
||||
export const { bind } = Binding
|
||||
83
js/src/process.ts
Normal file
83
js/src/process.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
type Proc = {
|
||||
connect(sig: "stdout" | "stderr", fn: (_: any, out: string) => void): number
|
||||
}
|
||||
|
||||
type Config<P extends Proc> = {
|
||||
defaultOut(stdout: string): void
|
||||
defaultErr(stdout: string): void
|
||||
subprocess(cmd: string): P
|
||||
subprocessv(cmd: string[]): P
|
||||
exec(cmd: string): string | null
|
||||
execv(cmd: string[]): string | null
|
||||
execAsync(cmd: string): P
|
||||
execAsyncv(cmd: string[]): P
|
||||
}
|
||||
|
||||
type Args<Out = void, Err = void> = {
|
||||
cmd: string | string[],
|
||||
out?: (stdout: string) => Out,
|
||||
err?: (stderr: string) => Err,
|
||||
}
|
||||
|
||||
export default function <P extends Proc>(config: Config<P>) {
|
||||
function args<O, E>(argsOrCmd: Args | string | string[], onOut: O, onErr: E) {
|
||||
const params = Array.isArray(argsOrCmd) || typeof argsOrCmd === "string"
|
||||
return {
|
||||
cmd: params ? argsOrCmd : argsOrCmd.cmd,
|
||||
err: params ? onErr : argsOrCmd.err || onErr,
|
||||
out: params ? onOut : argsOrCmd.out || onOut,
|
||||
}
|
||||
}
|
||||
|
||||
function subprocess(args: Args): P
|
||||
function subprocess(
|
||||
cmd: string | string[],
|
||||
onOut?: (stdout: string) => void,
|
||||
onErr?: (stderr: string) => void,
|
||||
): P
|
||||
function subprocess(
|
||||
argsOrCmd: Args | string | string[],
|
||||
onOut: (stdout: string) => void = config.defaultOut,
|
||||
onErr: (stderr: string) => void = config.defaultErr,
|
||||
) {
|
||||
const { cmd, err, out } = args(argsOrCmd, onOut, onErr)
|
||||
const proc = Array.isArray(cmd)
|
||||
? config.subprocessv(cmd)
|
||||
: config.subprocess(cmd)
|
||||
|
||||
proc.connect("stdout", (_, stdout: string) => out(stdout))
|
||||
proc.connect("stderr", (_, stderr: string) => err(stderr))
|
||||
return proc
|
||||
}
|
||||
|
||||
function exec<Out = string, Err = string>(
|
||||
args: Args<Out, Err>
|
||||
): Out | Err
|
||||
function exec<Out = string, Err = string>(
|
||||
cmd: string | string[],
|
||||
onOut?: (stdout: string) => Out,
|
||||
onErr?: (stderr: string) => Err,
|
||||
): Out | Err
|
||||
function exec<Out = string, Err = string>(
|
||||
argsOrCmd: Args<Out, Err> | string | string[],
|
||||
onOut: (stdout: string) => Out = out => out as Out,
|
||||
onErr: (stderr: string) => Err = out => out as Err,
|
||||
): Out | Err {
|
||||
const { cmd, err, out } = args(argsOrCmd, onOut, onErr)
|
||||
return Array.isArray(cmd)
|
||||
? out(config.execv(cmd)!) as Out
|
||||
: err(config.exec(cmd)!) as Err
|
||||
}
|
||||
|
||||
function execAsync(cmd: string | string[]): Promise<string> {
|
||||
const proc = Array.isArray(cmd)
|
||||
? config.execAsyncv(cmd)
|
||||
: config.execAsync(cmd)
|
||||
return new Promise((resolve, reject) => {
|
||||
proc.connect("stdout", (_, out: string) => resolve(out))
|
||||
proc.connect("stderr", (_, err: string) => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
return { subprocess, exec, execAsync }
|
||||
}
|
||||
27
js/src/time.ts
Normal file
27
js/src/time.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
interface Time {
|
||||
connect(sig: "now", fn: () => void): number
|
||||
cancel(): void
|
||||
}
|
||||
|
||||
export default function Time<T extends Time>(Time: {
|
||||
interval(interval: number, closure: any): T
|
||||
timeout(timeout: number, closure: any): T
|
||||
idle(closure: any): T
|
||||
}) {
|
||||
function interval(interval: number, callback: () => void) {
|
||||
const t = Time.interval(interval, null)
|
||||
t.connect("now", callback)
|
||||
return t
|
||||
}
|
||||
function timeout(timeout: number, callback: () => void) {
|
||||
const t = Time.timeout(timeout, null)
|
||||
t.connect("now", callback)
|
||||
return t
|
||||
}
|
||||
function idle(callback: () => void) {
|
||||
const t = Time.idle(null)
|
||||
t.connect("now", callback)
|
||||
return t
|
||||
}
|
||||
return { interval, timeout, idle }
|
||||
}
|
||||
250
js/src/variable.ts
Normal file
250
js/src/variable.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import Binding, { type Connectable } from "./binding.js"
|
||||
|
||||
type VariableBase = {
|
||||
emit(sig: "error" | "changed" | "dropped", ...args: any[]): void
|
||||
connect(sig: "error" | "changed" | "dropped", fn: (...args: any[]) => void): number
|
||||
disconnect(id: number): void
|
||||
runDispose?(): void // node, deno
|
||||
run_dispose?(): void // gjs
|
||||
}
|
||||
|
||||
type VariableBaseCtor = {
|
||||
new(): VariableBase
|
||||
}
|
||||
|
||||
type Time = any
|
||||
type Process = any
|
||||
|
||||
type Config = {
|
||||
defaultErrHandler(err: any): void
|
||||
VariableBase: VariableBaseCtor
|
||||
interval(n: number, fn: () => void): Time
|
||||
execAsync(cmd: string | string[]): Promise<string>
|
||||
subprocess(args: {
|
||||
cmd: string | string[],
|
||||
out: (s: string) => void,
|
||||
err: (s: string) => void
|
||||
}): Process
|
||||
}
|
||||
|
||||
// @ts-expect-error missing values
|
||||
export const config: Config = {}
|
||||
|
||||
class VariableWrapper<T> extends Function {
|
||||
private variable!: VariableBase
|
||||
private errHandler? = config.defaultErrHandler
|
||||
|
||||
private _value: T
|
||||
private _poll?: Time
|
||||
private _watch?: Process
|
||||
|
||||
private pollInterval = 1000
|
||||
private pollExec?: string[] | string
|
||||
private pollTransform?: (stdout: string, prev: T) => T
|
||||
private pollFn?: (prev: T) => T | Promise<T>
|
||||
|
||||
private watchTransform?: (stdout: string, prev: T) => T
|
||||
private watchExec?: string[] | string
|
||||
|
||||
constructor(init: T) {
|
||||
super()
|
||||
this._value = init
|
||||
this.variable = new config.VariableBase
|
||||
this.variable.connect("dropped", () => {
|
||||
this.stopWatch()
|
||||
this.stopPoll()
|
||||
})
|
||||
this.variable.connect("error", (_, err) => this.errHandler?.(err))
|
||||
return new Proxy(this, {
|
||||
apply: (target, _, args) => target._call(args[0]),
|
||||
})
|
||||
}
|
||||
|
||||
private _call<R = T>(transform?: (value: T) => R): Binding<R> {
|
||||
const b = Binding.bind(this)
|
||||
return transform ? b.as(transform) : b as unknown as Binding<R>
|
||||
}
|
||||
|
||||
toString() {
|
||||
return String(`Variable<${this.get()}>`)
|
||||
}
|
||||
|
||||
get(): T { return this._value }
|
||||
set(value: T) {
|
||||
if (value !== this._value) {
|
||||
this._value = value
|
||||
this.variable.emit("changed")
|
||||
}
|
||||
}
|
||||
|
||||
startPoll() {
|
||||
if (this._poll)
|
||||
return
|
||||
|
||||
if (this.pollFn) {
|
||||
this._poll = config.interval(this.pollInterval, () => {
|
||||
const v = this.pollFn!(this.get())
|
||||
if (v instanceof Promise) {
|
||||
v.then(v => this.set(v))
|
||||
.catch(err => this.variable.emit("error", err))
|
||||
} else {
|
||||
this.set(v)
|
||||
}
|
||||
})
|
||||
} else if (this.pollExec) {
|
||||
this._poll = config.interval(this.pollInterval, () => {
|
||||
config.execAsync(this.pollExec!)
|
||||
.then(v => this.set(this.pollTransform!(v, this.get())))
|
||||
.catch(err => this.variable.emit("error", err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
startWatch() {
|
||||
if (this._watch)
|
||||
return
|
||||
|
||||
this._watch = config.subprocess({
|
||||
cmd: this.watchExec!,
|
||||
out: out => this.set(this.watchTransform!(out, this.get())),
|
||||
err: err => this.variable.emit("error", err),
|
||||
})
|
||||
}
|
||||
|
||||
stopPoll() {
|
||||
this._poll?.cancel()
|
||||
delete this._poll
|
||||
}
|
||||
|
||||
stopWatch() {
|
||||
this._watch?.kill()
|
||||
delete this._watch
|
||||
}
|
||||
|
||||
isPolling() { return !!this._poll }
|
||||
isWatching() { return !!this._watch }
|
||||
|
||||
drop() {
|
||||
this.variable.emit("dropped")
|
||||
this.variable.runDispose?.()
|
||||
this.variable.run_dispose?.()
|
||||
}
|
||||
|
||||
onDropped(callback: () => void) {
|
||||
this.variable.connect("dropped", callback)
|
||||
return this as unknown as Variable<T>
|
||||
}
|
||||
|
||||
onError(callback: (err: string) => void) {
|
||||
delete this.errHandler
|
||||
this.variable.connect("error", (_, err) => callback(err))
|
||||
return this as unknown as Variable<T>
|
||||
}
|
||||
|
||||
subscribe(callback: (value: T) => void) {
|
||||
const id = this.variable.connect("changed", () => {
|
||||
callback(this.get())
|
||||
})
|
||||
return () => this.variable.disconnect(id)
|
||||
}
|
||||
|
||||
poll(
|
||||
interval: number,
|
||||
exec: string | string[],
|
||||
transform?: (stdout: string, prev: T) => T
|
||||
): Variable<T>
|
||||
|
||||
poll(
|
||||
interval: number,
|
||||
callback: (prev: T) => T | Promise<T>
|
||||
): Variable<T>
|
||||
|
||||
poll(
|
||||
interval: number,
|
||||
exec: string | string[] | ((prev: T) => T | Promise<T>),
|
||||
transform: (stdout: string, prev: T) => T = out => out as T,
|
||||
) {
|
||||
this.stopPoll()
|
||||
this.pollInterval = interval
|
||||
this.pollTransform = transform
|
||||
if (typeof exec === "function") {
|
||||
this.pollFn = exec
|
||||
delete this.pollExec
|
||||
} else {
|
||||
this.pollExec = exec
|
||||
delete this.pollFn
|
||||
}
|
||||
this.startPoll()
|
||||
return this as unknown as Variable<T>
|
||||
}
|
||||
|
||||
watch(
|
||||
exec: string | string[],
|
||||
transform: (stdout: string, prev: T) => T = out => out as T,
|
||||
) {
|
||||
this.stopWatch()
|
||||
this.watchExec = exec
|
||||
this.watchTransform = transform
|
||||
this.startWatch()
|
||||
return this as unknown as Variable<T>
|
||||
}
|
||||
|
||||
observe(
|
||||
objs: Array<[obj: Connectable, signal: string]>,
|
||||
callback: (...args: any[]) => T): Variable<T>
|
||||
|
||||
observe(
|
||||
obj: Connectable,
|
||||
signal: string,
|
||||
callback: (...args: any[]) => T): Variable<T>
|
||||
|
||||
observe(
|
||||
objs: Connectable | Array<[obj: Connectable, signal: string]>,
|
||||
sigOrFn: string | ((...args: any[]) => T),
|
||||
callback?: (...args: any[]) => T,
|
||||
) {
|
||||
const f = typeof sigOrFn === "function" ? sigOrFn : callback ?? (() => this.get())
|
||||
const set = (_: Connectable, ...args: any[]) => this.set(f(...args))
|
||||
|
||||
if (Array.isArray(objs)) {
|
||||
for (const obj of objs) {
|
||||
const [o, s] = obj
|
||||
o.connect(s, set)
|
||||
}
|
||||
} else {
|
||||
if (typeof sigOrFn === "string")
|
||||
objs.connect(sigOrFn, set)
|
||||
}
|
||||
|
||||
return this as unknown as Variable<T>
|
||||
}
|
||||
|
||||
static derive<V,
|
||||
const Deps extends Array<Variable<any> | Binding<any>>,
|
||||
Args extends {
|
||||
[K in keyof Deps]: Deps[K] extends Variable<infer T>
|
||||
? T : Deps[K] extends Binding<infer T> ? T : never
|
||||
},
|
||||
>(deps: Deps, fn: (...args: Args) => V) {
|
||||
const update = () => fn(...deps.map(d => d.get()) as Args)
|
||||
const derived = new Variable(update())
|
||||
const unsubs = deps.map(dep => dep.subscribe(() => derived.set(update())))
|
||||
derived.onDropped(() => unsubs.map(unsub => unsub()))
|
||||
return derived
|
||||
}
|
||||
}
|
||||
|
||||
export interface Variable<T> extends Omit<VariableWrapper<T>, "bind"> {
|
||||
<R>(transform: (value: T) => R): Binding<R>
|
||||
(): Binding<T>
|
||||
}
|
||||
|
||||
export const Variable = new Proxy(VariableWrapper as any, {
|
||||
apply: (_t, _a, args) => new VariableWrapper(args[0]),
|
||||
}) as {
|
||||
derive: typeof VariableWrapper["derive"]
|
||||
<T>(init: T): Variable<T>
|
||||
new <T>(init: T): Variable<T>
|
||||
}
|
||||
|
||||
export default Variable
|
||||
23
js/tsconfig.json
Normal file
23
js/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"lib": [
|
||||
"ES2022"
|
||||
],
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": ".",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"./@girs",
|
||||
"./src",
|
||||
"./gjs",
|
||||
"./node"
|
||||
]
|
||||
}
|
||||
76
lua/astal/application.lua
Normal file
76
lua/astal/application.lua
Normal file
@@ -0,0 +1,76 @@
|
||||
local lgi = require("lgi")
|
||||
local Astal = lgi.require("Astal", "0.1")
|
||||
|
||||
local AstalLua = Astal.Application:derive("AstalLua")
|
||||
local request_handler
|
||||
|
||||
function AstalLua:do_request(msg, conn)
|
||||
if type(request_handler) == "function" then
|
||||
request_handler(msg, function(request)
|
||||
Astal.write_sock(conn, request, function(_, res)
|
||||
Astal.write_sock_finish(res)
|
||||
end)
|
||||
end)
|
||||
else
|
||||
Astal.Application.do_request(self, msg, conn)
|
||||
end
|
||||
end
|
||||
|
||||
local app = AstalLua()
|
||||
|
||||
---@class StartConfig
|
||||
---@field instance_name? string
|
||||
---@field gtk_theme? string
|
||||
---@field icon_theme? string
|
||||
---@field cursor_theme? string
|
||||
---@field css? string
|
||||
---@field hold? boolean
|
||||
---@field request_handler? fun(msg: string, response: fun(res: string))
|
||||
|
||||
---@param config StartConfig | nil
|
||||
---@param callback function | nil
|
||||
function Astal.Application:start(config, callback)
|
||||
if config == nil then
|
||||
config = {}
|
||||
end
|
||||
|
||||
if config.hold == nil then
|
||||
config.hold = true
|
||||
end
|
||||
|
||||
request_handler = config.request_handler
|
||||
|
||||
if config.css then
|
||||
self:apply_css(config.css)
|
||||
end
|
||||
if config.instance_name then
|
||||
self.instance_name = config.instance_name
|
||||
end
|
||||
if config.gtk_theme then
|
||||
self.gtk_theme = config.gtk_theme
|
||||
end
|
||||
if config.icon_theme then
|
||||
self.icon_theme = config.icon_theme
|
||||
end
|
||||
if config.cursor_theme then
|
||||
self.cursor_theme = config.cursor_theme
|
||||
end
|
||||
|
||||
app.on_activate = function()
|
||||
if config.hold then
|
||||
self:hold()
|
||||
end
|
||||
if type(callback) == "function" then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
if not app:acquire_socket() then
|
||||
print('Astal instance "' .. app.instance_name .. '" is already running')
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
self:run(nil)
|
||||
end
|
||||
|
||||
return app
|
||||
65
lua/astal/binding.lua
Normal file
65
lua/astal/binding.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
local lgi = require("lgi")
|
||||
local GObject = lgi.require("GObject", "2.0")
|
||||
|
||||
---@class Binding
|
||||
---@field emitter object
|
||||
---@field property? string
|
||||
---@field transformFn function
|
||||
local Binding = {}
|
||||
|
||||
---@param emitter object
|
||||
---@param property? string
|
||||
---@return Binding
|
||||
function Binding.new(emitter, property)
|
||||
return setmetatable({
|
||||
emitter = emitter,
|
||||
property = property,
|
||||
transformFn = function(v)
|
||||
return v
|
||||
end,
|
||||
}, Binding)
|
||||
end
|
||||
|
||||
function Binding:__tostring()
|
||||
local str = "Binding<" .. tostring(self:get())
|
||||
if self.property ~= nil then
|
||||
str = str .. ", " .. self.property
|
||||
end
|
||||
return str .. ">"
|
||||
end
|
||||
|
||||
function Binding:get()
|
||||
if type(self.emitter.get) == "function" then
|
||||
return self.transformFn(self.emitter:get())
|
||||
end
|
||||
return self.transformFn(self.emitter[self.property])
|
||||
end
|
||||
|
||||
---@param transform fun(value: any): any
|
||||
---@return Binding
|
||||
function Binding:as(transform)
|
||||
local b = Binding.new(self.emitter, self.property)
|
||||
b.transformFn = function(v)
|
||||
return transform(self.transformFn(v))
|
||||
end
|
||||
return b
|
||||
end
|
||||
|
||||
---@param callback fun(value: any)
|
||||
---@return function
|
||||
function Binding:subscribe(callback)
|
||||
if type(self.emitter.subscribe) == "function" then
|
||||
return self.emitter:subscribe(function()
|
||||
callback(self:get())
|
||||
end)
|
||||
end
|
||||
local id = self.emitter.on_notify:connect(function()
|
||||
callback(self:get())
|
||||
end, self.property, false)
|
||||
return function()
|
||||
GObject.signal_handler_disconnect(self.emitter, id)
|
||||
end
|
||||
end
|
||||
|
||||
Binding.__index = Binding
|
||||
return Binding
|
||||
30
lua/astal/init.lua
Normal file
30
lua/astal/init.lua
Normal file
@@ -0,0 +1,30 @@
|
||||
local lgi = require("lgi")
|
||||
local Astal = lgi.require("Astal", "0.1")
|
||||
local Gtk = lgi.require("Gtk", "3.0")
|
||||
local GObject = lgi.require("GObject", "2.0")
|
||||
local Widget = require("astal.widget")
|
||||
local Variable = require("astal.variable")
|
||||
local Binding = require("astal.binding")
|
||||
local App = require("astal.application")
|
||||
local Process = require("astal.process")
|
||||
local Time = require("astal.time")
|
||||
|
||||
return {
|
||||
App = App,
|
||||
Variable = Variable,
|
||||
Widget = Widget,
|
||||
bind = Binding.new,
|
||||
interval = Time.interval,
|
||||
timeout = Time.timeout,
|
||||
idle = Time.idle,
|
||||
subprocess = Process.subprocess,
|
||||
exec = Process.exec,
|
||||
exec_async = Process.exec_async,
|
||||
|
||||
Astal = Astal,
|
||||
Gtk = Gtk,
|
||||
GObject = GObject,
|
||||
GLib = lgi.require("GLib", "2.0"),
|
||||
Gio = lgi.require("Gio", "2.0"),
|
||||
require = lgi.require,
|
||||
}
|
||||
93
lua/astal/process.lua
Normal file
93
lua/astal/process.lua
Normal file
@@ -0,0 +1,93 @@
|
||||
local lgi = require("lgi")
|
||||
local Astal = lgi.require("Astal", "0.1")
|
||||
|
||||
local M = {}
|
||||
|
||||
local defualt_proc_args = function(on_stdout, on_stderr)
|
||||
if on_stdout == nil then
|
||||
on_stdout = function(out)
|
||||
io.stdout:write(tostring(out) .. "\n")
|
||||
return tostring(out)
|
||||
end
|
||||
end
|
||||
|
||||
if on_stderr == nil then
|
||||
on_stderr = function(err)
|
||||
io.stderr:write(tostring(err) .. "\n")
|
||||
return tostring(err)
|
||||
end
|
||||
end
|
||||
|
||||
return on_stdout, on_stderr
|
||||
end
|
||||
|
||||
---@param commandline string | string[]
|
||||
---@param on_stdout? fun(out: string): nil
|
||||
---@param on_stderr? fun(err: string): nil
|
||||
---@return { kill: function } | nil proc
|
||||
function M.subprocess(commandline, on_stdout, on_stderr)
|
||||
local out, err = defualt_proc_args(on_stdout, on_stderr)
|
||||
local proc, fail
|
||||
if type(commandline) == "table" then
|
||||
proc, fail = Astal.Process.subprocessv(commandline)
|
||||
else
|
||||
proc, fail = Astal.Process.subprocess(commandline)
|
||||
end
|
||||
if fail ~= nil then
|
||||
err(fail)
|
||||
return nil
|
||||
end
|
||||
proc.on_stdout = function(_, str)
|
||||
out(str)
|
||||
end
|
||||
proc.on_stderr = function(_, str)
|
||||
err(str)
|
||||
end
|
||||
return proc
|
||||
end
|
||||
|
||||
---@generic T
|
||||
---@param commandline string | string[]
|
||||
---@param on_stdout? fun(out: string): T
|
||||
---@param on_stderr? fun(err: string): T
|
||||
---@return T
|
||||
function M.exec(commandline, on_stdout, on_stderr)
|
||||
local out, err = defualt_proc_args(on_stdout, on_stderr)
|
||||
local stdout, stderr
|
||||
if type(commandline) == "table" then
|
||||
stdout, stderr = Astal.Process.execv(commandline)
|
||||
else
|
||||
stdout, stderr = Astal.Process.exec(commandline)
|
||||
end
|
||||
if stderr then
|
||||
return err(stderr)
|
||||
end
|
||||
return out(stdout)
|
||||
end
|
||||
|
||||
---@param commandline string | string[]
|
||||
---@param on_stdout? fun(out: string): nil
|
||||
---@param on_stderr? fun(err: string): nil
|
||||
---@return { kill: function } | nil proc
|
||||
function M.exec_async(commandline, on_stdout, on_stderr)
|
||||
local out, err = defualt_proc_args(on_stdout, on_stderr)
|
||||
local proc, fail
|
||||
if type(commandline) == "table" then
|
||||
proc, fail = Astal.Process.exec_asyncv(commandline)
|
||||
else
|
||||
proc, fail = Astal.Process.exec_async(commandline)
|
||||
end
|
||||
if fail ~= nil then
|
||||
err(fail)
|
||||
return nil
|
||||
end
|
||||
proc.on_stdout = function(_, str)
|
||||
out(str)
|
||||
end
|
||||
proc.on_stderr = function(_, str)
|
||||
err(str)
|
||||
end
|
||||
return proc
|
||||
end
|
||||
|
||||
return M
|
||||
27
lua/astal/time.lua
Normal file
27
lua/astal/time.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
local lgi = require("lgi")
|
||||
local Astal = lgi.require("Astal", "0.1")
|
||||
local GObject = lgi.require("GObject", "2.0")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param interval number
|
||||
---@param fn function
|
||||
---@return { cancel: function, on_now: function }
|
||||
function M.interval(interval, fn)
|
||||
return Astal.Time.interval(interval, GObject.Closure(fn))
|
||||
end
|
||||
|
||||
---@param timeout number
|
||||
---@param fn function
|
||||
---@return { cancel: function, on_now: function }
|
||||
function M.timeout(timeout, fn)
|
||||
return Astal.Time.timeout(timeout, GObject.Closure(fn))
|
||||
end
|
||||
|
||||
---@param fn function
|
||||
---@return { cancel: function, on_now: function }
|
||||
function M.idle(fn)
|
||||
return Astal.Time.idle(GObject.Closure(fn))
|
||||
end
|
||||
|
||||
return M
|
||||
270
lua/astal/variable.lua
Normal file
270
lua/astal/variable.lua
Normal file
@@ -0,0 +1,270 @@
|
||||
local lgi = require("lgi")
|
||||
local Astal = lgi.require("Astal", "0.1")
|
||||
local GObject = lgi.require("GObject", "2.0")
|
||||
local Binding = require("astal.binding")
|
||||
local Time = require("astal.time")
|
||||
local Process = require("astal.process")
|
||||
|
||||
---@class Variable
|
||||
---@field private variable object
|
||||
---@field private err_handler? function
|
||||
---@field private _value any
|
||||
---@field private _poll? object
|
||||
---@field private _watch? object
|
||||
---@field private poll_interval number
|
||||
---@field private poll_exec? string[] | string
|
||||
---@field private poll_transform? fun(next: any, prev: any): any
|
||||
---@field private poll_fn? function
|
||||
---@field private watch_transform? fun(next: any, prev: any): any
|
||||
---@field private watch_exec? string[] | string
|
||||
local Variable = {}
|
||||
Variable.__index = Variable
|
||||
|
||||
---@param value any
|
||||
---@return Variable
|
||||
function Variable.new(value)
|
||||
local v = Astal.VariableBase()
|
||||
local variable = setmetatable({
|
||||
variable = v,
|
||||
_value = value,
|
||||
}, Variable)
|
||||
v.on_dropped = function()
|
||||
variable:stop_watch()
|
||||
variable:stop_watch()
|
||||
end
|
||||
v.on_error = function(_, err)
|
||||
if variable.err_handler then
|
||||
variable.err_handler(err)
|
||||
end
|
||||
end
|
||||
return variable
|
||||
end
|
||||
|
||||
---@param transform function
|
||||
---@return Binding
|
||||
function Variable:__call(transform)
|
||||
if transform == nil then
|
||||
transform = function(v)
|
||||
return v
|
||||
end
|
||||
return Binding.new(self)
|
||||
end
|
||||
return Binding.new(self):as(transform)
|
||||
end
|
||||
|
||||
function Variable:__tostring()
|
||||
return "Variable<" .. tostring(self:get()) .. ">"
|
||||
end
|
||||
|
||||
function Variable:get()
|
||||
return self._value or nil
|
||||
end
|
||||
|
||||
function Variable:set(value)
|
||||
if value ~= self:get() then
|
||||
self._value = value
|
||||
self.variable:emit_changed()
|
||||
end
|
||||
end
|
||||
|
||||
function Variable:start_poll()
|
||||
if self._poll ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
if self.poll_fn then
|
||||
self._poll = Time.interval(self.poll_interval, function()
|
||||
self:set(self.poll_fn(self:get()))
|
||||
end)
|
||||
elseif self.poll_exec then
|
||||
self._poll = Time.interval(self.poll_interval, function()
|
||||
Process.exec_async(self.poll_exec, function(out)
|
||||
self:set(self.poll_transform(out, self:get()))
|
||||
end, function(err)
|
||||
self.variable.emit_error(err)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function Variable:start_watch()
|
||||
if self._watch then
|
||||
return
|
||||
end
|
||||
|
||||
self._watch = Process.subprocess(self.watch_exec, function(out)
|
||||
self:set(self.watch_transform(out, self:get()))
|
||||
end, function(err)
|
||||
self.variable.emit_error(err)
|
||||
end)
|
||||
end
|
||||
|
||||
function Variable:stop_poll()
|
||||
if self._poll then
|
||||
self._poll.cancel()
|
||||
end
|
||||
self._poll = nil
|
||||
end
|
||||
|
||||
function Variable:stop_watch()
|
||||
if self._watch then
|
||||
self._watch.kill()
|
||||
end
|
||||
self._watch = nil
|
||||
end
|
||||
|
||||
function Variable:is_polling()
|
||||
return self._poll ~= nil
|
||||
end
|
||||
|
||||
function Variable:is_watching()
|
||||
return self._watch ~= nil
|
||||
end
|
||||
|
||||
function Variable:drop()
|
||||
self.variable.emit_dropped()
|
||||
self.variable.run_dispose()
|
||||
end
|
||||
|
||||
---@param callback function
|
||||
---@return Variable
|
||||
function Variable:on_dropped(callback)
|
||||
self.variable.on_dropped = callback
|
||||
return self
|
||||
end
|
||||
|
||||
---@param callback function
|
||||
---@return Variable
|
||||
function Variable:on_error(callback)
|
||||
self.err_handler = nil
|
||||
self.variable.on_eror = function(_, err)
|
||||
callback(err)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
---@param callback fun(value: any)
|
||||
---@return function
|
||||
function Variable:subscribe(callback)
|
||||
local id = self.variable.on_changed:connect(function()
|
||||
callback(self:get())
|
||||
end)
|
||||
return function()
|
||||
GObject.signal_handler_disconnect(self.variable, id)
|
||||
end
|
||||
end
|
||||
|
||||
---@param interval number
|
||||
---@param exec string | string[] | function
|
||||
---@param transform? fun(next: any, prev: any): any
|
||||
function Variable:poll(interval, exec, transform)
|
||||
if transform == nil then
|
||||
transform = function(next)
|
||||
return next
|
||||
end
|
||||
end
|
||||
self:stop_poll()
|
||||
self.poll_interval = interval
|
||||
self.poll_transform = transform
|
||||
|
||||
if type(exec) == "function" then
|
||||
self.poll_fn = exec
|
||||
self.poll_exec = nil
|
||||
else
|
||||
self.poll_exec = exec
|
||||
self.poll_fn = nil
|
||||
end
|
||||
self:start_poll()
|
||||
return self
|
||||
end
|
||||
|
||||
---@param exec string | string[]
|
||||
---@param transform? fun(next: any, prev: any): any
|
||||
function Variable:watch(exec, transform)
|
||||
if transform == nil then
|
||||
transform = function(next)
|
||||
return next
|
||||
end
|
||||
end
|
||||
self:stop_poll()
|
||||
self.watch_exec = exec
|
||||
self.watch_transform = transform
|
||||
self:start_watch()
|
||||
return self
|
||||
end
|
||||
|
||||
---@param object object | table[]
|
||||
---@param sigOrFn string | fun(...): any
|
||||
---@param callback fun(...): any
|
||||
---@return Variable
|
||||
function Variable:observe(object, sigOrFn, callback)
|
||||
local f
|
||||
if type(sigOrFn) == "function" then
|
||||
f = sigOrFn
|
||||
elseif type(callback) == "function" then
|
||||
f = callback
|
||||
else
|
||||
f = function()
|
||||
return self:get()
|
||||
end
|
||||
end
|
||||
local set = function(_, ...)
|
||||
self:set(f(...))
|
||||
end
|
||||
|
||||
if type(sigOrFn) == "string" then
|
||||
object["on_" .. sigOrFn]:connect(set)
|
||||
else
|
||||
for _, obj in ipairs(object) do
|
||||
obj[1]["on_" .. obj[2]]:connect(set)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
---@param deps Variable | (Binding | Variable)[]
|
||||
---@param transform fun(...): any
|
||||
---@return Variable
|
||||
function Variable.derive(deps, transform)
|
||||
if getmetatable(deps) == Variable then
|
||||
local var = Variable.new(transform(deps:get()))
|
||||
deps:subscribe(function(v)
|
||||
var:set(transform(v))
|
||||
end)
|
||||
return var
|
||||
end
|
||||
|
||||
for i, var in ipairs(deps) do
|
||||
if getmetatable(var) == Variable then
|
||||
deps[i] = Binding.new(var)
|
||||
end
|
||||
end
|
||||
|
||||
local update = function()
|
||||
local params = {}
|
||||
for _, binding in ipairs(deps) do
|
||||
table.insert(params, binding:get())
|
||||
end
|
||||
return transform(table.unpack(params))
|
||||
end
|
||||
|
||||
local var = Variable.new(update())
|
||||
|
||||
local unsubs = {}
|
||||
for _, b in ipairs(deps) do
|
||||
table.insert(unsubs, b:subscribe(update))
|
||||
end
|
||||
|
||||
var.variable.on_dropped = function()
|
||||
for _, unsub in ipairs(unsubs) do
|
||||
var:set(unsub())
|
||||
end
|
||||
end
|
||||
return var
|
||||
end
|
||||
|
||||
return setmetatable(Variable, {
|
||||
__call = function(_, v)
|
||||
return Variable.new(v)
|
||||
end,
|
||||
})
|
||||
123
lua/astal/widget.lua
Normal file
123
lua/astal/widget.lua
Normal file
@@ -0,0 +1,123 @@
|
||||
local lgi = require("lgi")
|
||||
local Astal = lgi.require("Astal", "0.1")
|
||||
local Gtk = lgi.require("Gtk", "3.0")
|
||||
local GObject = lgi.require("GObject", "2.0")
|
||||
local Binding = require("astal.binding")
|
||||
|
||||
local function filter(tbl, fn)
|
||||
local copy = {}
|
||||
for key, value in pairs(tbl) do
|
||||
if fn(value, key) then
|
||||
copy[key] = value
|
||||
end
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
Gtk.Widget._attribute.css = {
|
||||
get = Astal.widget_get_css,
|
||||
set = Astal.widget_set_css,
|
||||
}
|
||||
|
||||
Gtk.Widget._attribute.class_name = {
|
||||
get = function(self)
|
||||
local result = ""
|
||||
local strings = Astal.widget_set_class_names(self)
|
||||
for i, str in ipairs(strings) do
|
||||
result = result .. str
|
||||
if i < #strings then
|
||||
result = result .. " "
|
||||
end
|
||||
end
|
||||
return result
|
||||
end,
|
||||
set = function(self, class_name)
|
||||
local names = {}
|
||||
for word in class_name:gmatch("%S+") do
|
||||
table.insert(names, word)
|
||||
end
|
||||
Astal.widget_set_class_names(self, names)
|
||||
end,
|
||||
}
|
||||
|
||||
Gtk.Widget._attribute.cursor = {
|
||||
get = Astal.widget_get_cursor,
|
||||
set = Astal.widget_set_cursor,
|
||||
}
|
||||
|
||||
Astal.Box._attribute.children = {
|
||||
get = Astal.Box.get_children,
|
||||
set = Astal.Box.set_children,
|
||||
}
|
||||
|
||||
local function astalify(ctor)
|
||||
function ctor:hook(object, signalOrCallback, callback)
|
||||
if type(object.subscribe) == "function" then
|
||||
local unsub = object.subscribe(function(...)
|
||||
signalOrCallback(self, ...)
|
||||
end)
|
||||
self.on_destroy = unsub
|
||||
return
|
||||
end
|
||||
local id = object["on_" .. signalOrCallback](function(_, ...)
|
||||
callback(self, ...)
|
||||
end)
|
||||
self.on_destroy = function()
|
||||
GObject.signal_handler_disconnect(object, id)
|
||||
end
|
||||
end
|
||||
|
||||
return function(tbl)
|
||||
local bindings = {}
|
||||
local setup = tbl.setup
|
||||
|
||||
local visible
|
||||
if type(tbl.visible) == "boolean" then
|
||||
visible = tbl.visible
|
||||
else
|
||||
visible = true
|
||||
end
|
||||
|
||||
local props = filter(tbl, function(_, key)
|
||||
return key ~= "visible" and key ~= "setup"
|
||||
end)
|
||||
|
||||
for prop, value in pairs(props) do
|
||||
if getmetatable(value) == Binding then
|
||||
bindings[prop] = value
|
||||
props[prop] = value:get()
|
||||
end
|
||||
end
|
||||
|
||||
local widget = ctor(props)
|
||||
|
||||
for prop, binding in pairs(bindings) do
|
||||
widget.on_destroy = binding:subscribe(function(v)
|
||||
widget[prop] = v
|
||||
end)
|
||||
end
|
||||
|
||||
widget.visible = visible
|
||||
if type(setup) == "function" then
|
||||
setup(widget)
|
||||
end
|
||||
return widget
|
||||
end
|
||||
end
|
||||
|
||||
local Widget = {
|
||||
astalify = astalify,
|
||||
Box = astalify(Astal.Box),
|
||||
Button = astalify(Astal.Button),
|
||||
CenterBox = astalify(Astal.CenterBox),
|
||||
Label = astalify(Gtk.Label),
|
||||
Icon = astalify(Astal.Icon),
|
||||
Window = astalify(Astal.Window),
|
||||
EventBox = astalify(Astal.EventBox),
|
||||
}
|
||||
|
||||
return setmetatable(Widget, {
|
||||
__call = function(_, ctor)
|
||||
return astalify(ctor)
|
||||
end,
|
||||
})
|
||||
81
lua/sample.lua
Executable file
81
lua/sample.lua
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env lua
|
||||
-- imports
|
||||
local astal = require("astal.init")
|
||||
local Widget, Variable, App, bind = astal.Widget, astal.Variable, astal.App, astal.bind
|
||||
|
||||
-- state
|
||||
local player = astal.require("Playerctl").Player.new("spotify")
|
||||
|
||||
local title = Variable(player:get_title()):observe(player, "metadata", function()
|
||||
return player:get_title()
|
||||
end)
|
||||
|
||||
local rnd = Variable(1):poll(1000, function()
|
||||
return math.random(1, 10)
|
||||
end)
|
||||
|
||||
-- ui
|
||||
local Bar = function(monitor)
|
||||
return Widget.Window({
|
||||
application = App,
|
||||
id = "bar",
|
||||
name = "bar",
|
||||
monitor = monitor,
|
||||
anchor = astal.Astal.WindowAnchor.BOTTOM
|
||||
+ astal.Astal.WindowAnchor.LEFT
|
||||
+ astal.Astal.WindowAnchor.RIGHT,
|
||||
exclusivity = "EXCLUSIVE",
|
||||
|
||||
Widget.CenterBox({
|
||||
class_name = "bar",
|
||||
start_widget = Widget.Label({
|
||||
valign = "CENTER",
|
||||
label = "Welcome to Astal.lua",
|
||||
}),
|
||||
center_widget = Widget.Box({
|
||||
children = bind(rnd):as(function(n)
|
||||
local children = {}
|
||||
for i = 1, n, 1 do
|
||||
table.insert(
|
||||
children,
|
||||
Widget.Button({
|
||||
label = tostring(i),
|
||||
on_clicked = function()
|
||||
print(i)
|
||||
end,
|
||||
})
|
||||
)
|
||||
end
|
||||
return children
|
||||
end),
|
||||
}),
|
||||
end_widget = Widget.Label({
|
||||
valign = "CENTER",
|
||||
label = bind(title),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
-- css
|
||||
local css = [[
|
||||
.bar button {
|
||||
color: blue;
|
||||
}
|
||||
]]
|
||||
|
||||
-- main
|
||||
App:start({
|
||||
request_handler = function(msg, res)
|
||||
if msg == "quit" then
|
||||
os.exit(0)
|
||||
end
|
||||
if msg == "inspector" then
|
||||
res(App:inspector())
|
||||
end
|
||||
res("hi")
|
||||
end,
|
||||
css = css,
|
||||
}, function()
|
||||
Bar(0)
|
||||
end)
|
||||
3
lua/stylua.toml
Normal file
3
lua/stylua.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
indent_type = "Spaces"
|
||||
indent_width = 4
|
||||
column_width = 100
|
||||
16
meson-install.sh
Normal file
16
meson-install.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
meson setup \
|
||||
--prefix /usr \
|
||||
--libexecdir lib \
|
||||
--sbindir bin \
|
||||
--buildtype plain \
|
||||
--auto-features enabled \
|
||||
--wrap-mode nodownload \
|
||||
-D b_lto=false \
|
||||
-D b_pie=true \
|
||||
-D python.bytecompile=1 \
|
||||
--wipe \
|
||||
build
|
||||
|
||||
meson install -C build
|
||||
17
meson.build
Normal file
17
meson.build
Normal file
@@ -0,0 +1,17 @@
|
||||
project(
|
||||
'astal',
|
||||
'vala',
|
||||
'c',
|
||||
version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(),
|
||||
meson_version: '>= 0.62.0',
|
||||
default_options: [
|
||||
'warning_level=2',
|
||||
'werror=false',
|
||||
'c_std=gnu11',
|
||||
],
|
||||
)
|
||||
|
||||
# math
|
||||
add_project_arguments(['-X', '-lm'], language: 'vala')
|
||||
|
||||
subdir('src')
|
||||
2
meson_options.txt
Normal file
2
meson_options.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
option('typelib', type: 'boolean', value: true, description: 'Needed files for runtime bindings')
|
||||
option('cli_client', type: 'boolean', value: true, description: 'Minimal cli client for Astal applications')
|
||||
1
python/.gitignore
vendored
Normal file
1
python/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
16
python/astal/__init__.py
Normal file
16
python/astal/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("Astal", "0.1")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("GLib", "2.0")
|
||||
gi.require_version("Gio", "2.0")
|
||||
gi.require_version("GObject", "2.0")
|
||||
from gi.repository import Astal, Gtk, GLib, Gio, GObject
|
||||
from .application import App
|
||||
from .variable import Variable
|
||||
from .binding import Binding
|
||||
from . import widget as Widget
|
||||
|
||||
bind = Binding
|
||||
|
||||
__all__ = ["App", "Variable", "Widget" "bind", "Astal", "Gtk", "GLib", "Gio", "GObject"]
|
||||
62
python/astal/application.py
Normal file
62
python/astal/application.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from collections.abc import Callable
|
||||
from gi.repository import Astal, Gio
|
||||
|
||||
RequestHandler = Callable[[str, Callable[[str], None]], None]
|
||||
|
||||
|
||||
class _Application(Astal.Application):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.request_handler: RequestHandler | None = None
|
||||
|
||||
def do_response(self, msg: str, conn: Gio.SocketConnection) -> None:
|
||||
if self.request_handler:
|
||||
self.request_handler(
|
||||
msg,
|
||||
lambda response: Astal.write_sock(
|
||||
conn,
|
||||
response,
|
||||
lambda _, res: Astal.write_sock_finish(res),
|
||||
),
|
||||
)
|
||||
else:
|
||||
super().do_response(msg, conn)
|
||||
|
||||
def start(
|
||||
self,
|
||||
instance_name: str | None = None,
|
||||
gtk_theme: str | None = None,
|
||||
icon_theme: str | None = None,
|
||||
cursor_theme: str | None = None,
|
||||
css: str | None = None,
|
||||
hold: bool | None = True,
|
||||
request_handler: RequestHandler | None = None,
|
||||
callback: Callable | None = None,
|
||||
) -> None:
|
||||
if request_handler:
|
||||
self.request_handler = request_handler
|
||||
if hold:
|
||||
self.hold()
|
||||
if instance_name:
|
||||
self.instance_name = instance_name
|
||||
if gtk_theme:
|
||||
self.gtk_theme = gtk_theme
|
||||
if icon_theme:
|
||||
self.icon_theme = icon_theme
|
||||
if cursor_theme:
|
||||
self.cursor_theme = icon_theme
|
||||
if css:
|
||||
self.apply_css(css, False)
|
||||
if not self.acquire_socket():
|
||||
print(f"Astal instance {self.instance_name} already running")
|
||||
return
|
||||
|
||||
def on_activate(app):
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
self.connect("activate", on_activate)
|
||||
self.run()
|
||||
|
||||
|
||||
App = _Application()
|
||||
33
python/astal/binding.py
Normal file
33
python/astal/binding.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import re
|
||||
|
||||
|
||||
def kebabify(string):
|
||||
return re.sub(r"([a-z])([A-Z])", r"\1-\2", string).replace("_", "-").lower()
|
||||
|
||||
|
||||
class Binding:
|
||||
def __init__(self, emitter, prop=None):
|
||||
self.emitter = emitter
|
||||
self.prop = kebabify(prop) if prop else None
|
||||
self.transform_fn = lambda v: v
|
||||
|
||||
def __str__(self):
|
||||
return f"Binding<{self.emitter}{', ' + self.prop if self.prop else ''}>"
|
||||
|
||||
def as_(self, fn):
|
||||
bind = Binding(self.emitter, self.prop)
|
||||
bind.transform_fn = lambda v: fn(self.transform_fn(v))
|
||||
return bind
|
||||
|
||||
def get(self):
|
||||
if hasattr(self.emitter, "get") and callable(self.emitter.get):
|
||||
return self.transform_fn(self.emitter.get())
|
||||
|
||||
return self.transform_fn(self.emitter[f"get_{self.prop}"]())
|
||||
|
||||
def subscribe(self, callback):
|
||||
if hasattr(self.emitter, "subscribe") and callable(self.emitter.subscribe):
|
||||
return self.emitter.subscribe(lambda _: callback(self.get()))
|
||||
|
||||
i = self.emitter.connect(f"notify::{self.prop}", lambda: callback(self.get()))
|
||||
return lambda: self.emitter.disconnect(i)
|
||||
100
python/astal/variable.py
Normal file
100
python/astal/variable.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from gi.repository import Astal
|
||||
|
||||
from .binding import Binding
|
||||
|
||||
|
||||
class Variable:
|
||||
def __init__(self, init):
|
||||
v = Astal.Variable.new(init)
|
||||
self._variable = v
|
||||
self._err_handler = print
|
||||
v.connect("error", lambda _, err: self._err_handler(err) if self._err_handler else None)
|
||||
|
||||
def __call__(self, transform=None):
|
||||
if transform:
|
||||
return Binding(self).as_(transform)
|
||||
|
||||
return Binding(self)
|
||||
|
||||
def __str__(self):
|
||||
return f"Variable<{self.get()}>"
|
||||
|
||||
def get(self):
|
||||
return self._variable.get_value()
|
||||
|
||||
def set(self, value):
|
||||
return self._variable.set_value(value)
|
||||
|
||||
def watch(self, cmd):
|
||||
if isinstance(cmd, str):
|
||||
self._variable.watch(cmd)
|
||||
elif isinstance(cmd, list):
|
||||
self._variable.watchv(cmd)
|
||||
return self
|
||||
|
||||
def poll(self, interval, cmd):
|
||||
if isinstance(cmd, str):
|
||||
self._variable.poll(interval, cmd)
|
||||
elif isinstance(cmd, list):
|
||||
self._variable.pollv(interval, cmd)
|
||||
else:
|
||||
self._variable.pollfn(interval, cmd)
|
||||
return self
|
||||
|
||||
def start_watch(self):
|
||||
self._variable.start_watch()
|
||||
|
||||
def start_poll(self):
|
||||
self._variable.start_poll()
|
||||
|
||||
def stop_watch(self):
|
||||
self._variable.stop_watch()
|
||||
|
||||
def stop_poll(self):
|
||||
self._variable.stop_poll()
|
||||
|
||||
def drop(self):
|
||||
self._variable.emit_dropped()
|
||||
self._variable.run_dispose()
|
||||
|
||||
def on_dropped(self, callback):
|
||||
self._variable.connect("dropped", lambda _: callback())
|
||||
return self
|
||||
|
||||
def on_error(self, callback):
|
||||
self._err_handler = None
|
||||
self._variable.connect("error", lambda _, e: callback(e))
|
||||
return self
|
||||
|
||||
def subscribe(self, callback):
|
||||
s = self._variable.connect("changed", lambda _: callback(self.get()))
|
||||
return lambda: self._variable.disconnect(s)
|
||||
|
||||
def observe(self, objs, sigOrFn, callback=None):
|
||||
if callable(sigOrFn):
|
||||
f = sigOrFn
|
||||
elif callable(callback):
|
||||
f = callback
|
||||
else:
|
||||
f = lambda *_: self.get()
|
||||
|
||||
def setter(_, *args):
|
||||
self.set(f(*args))
|
||||
|
||||
if isinstance(objs, list):
|
||||
for obj in objs:
|
||||
obj[0].connect(obj[1], setter)
|
||||
elif isinstance(sigOrFn, str):
|
||||
objs.connect(sigOrFn, setter)
|
||||
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def derive(deps, fn):
|
||||
def update():
|
||||
return fn(*[d.get() for d in deps])
|
||||
|
||||
derived = Variable(update())
|
||||
unsubs = [dep.subscribe(lambda _: derived.set(update())) for dep in deps]
|
||||
derived.on_dropped(lambda: ([unsub() for unsub in unsubs]))
|
||||
return derived
|
||||
65
python/astal/widget.py
Normal file
65
python/astal/widget.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from gi.repository import Astal, Gtk
|
||||
from .binding import Binding
|
||||
|
||||
|
||||
def set_child(self, child):
|
||||
if isinstance(self, Gtk.Bin):
|
||||
self.remove(self.get_child())
|
||||
if isinstance(self, Gtk.Container):
|
||||
self.add(child)
|
||||
|
||||
|
||||
def astalify(ctor):
|
||||
ctor.set_css = Astal.widget_set_css
|
||||
ctor.get_css = Astal.widget_get_css
|
||||
|
||||
ctor.set_class_name = lambda self, names: Astal.widget_set_class_names(self, names.split())
|
||||
ctor.get_class_name = lambda self: " ".join(Astal.widget_set_class_names(self))
|
||||
|
||||
ctor.set_cursor = Astal.widget_set_cursor
|
||||
ctor.get_cursor = Astal.widget_get_cursor
|
||||
|
||||
def widget(**kwargs):
|
||||
args = {}
|
||||
bindings = {}
|
||||
handlers = {}
|
||||
setup = None
|
||||
if not hasattr(kwargs, "visible"):
|
||||
kwargs["visible"] = True
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key == "setup":
|
||||
setup = value
|
||||
if isinstance(value, Binding):
|
||||
bindings[key] = value
|
||||
if key.startswith("on_"):
|
||||
handlers[key] = value
|
||||
else:
|
||||
args[key] = value
|
||||
|
||||
self = ctor(**args)
|
||||
|
||||
for key, value in bindings.items():
|
||||
setter = getattr(self, f"set_{key}")
|
||||
setter(value.get())
|
||||
unsub = value.subscribe(setter)
|
||||
self.connect("destroy", lambda _: unsub())
|
||||
|
||||
for key, value in handlers.items():
|
||||
self.connect(key.replace("on_", ""), value)
|
||||
|
||||
if setup:
|
||||
setup(self)
|
||||
|
||||
return self
|
||||
|
||||
return widget
|
||||
|
||||
|
||||
Window = astalify(Astal.Window)
|
||||
Box = astalify(Astal.Box)
|
||||
Button = astalify(Astal.Button)
|
||||
CenterBox = astalify(Astal.CenterBox)
|
||||
Label = astalify(Gtk.Label)
|
||||
Icon = astalify(Astal.Icon)
|
||||
EventBox = astalify(Astal.EventBox)
|
||||
14
python/pyproject.toml
Normal file
14
python/pyproject.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[tool.poetry]
|
||||
name = "astal.py"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = []
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
gengir = "^1.0.2"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
62
python/ruff.toml
Normal file
62
python/ruff.toml
Normal file
@@ -0,0 +1,62 @@
|
||||
target-version = "py311"
|
||||
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
]
|
||||
|
||||
line-length = 100
|
||||
indent-width = 4
|
||||
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
ignore = ["D", "ANN101", "ERA", "ANN"]
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
[format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
|
||||
# Like Black, respect magic trailing commas.
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
|
||||
# Enable auto-formatting of code examples in docstrings. Markdown,
|
||||
# reStructuredText code/literal blocks and doctests are all supported.
|
||||
docstring-code-format = false
|
||||
|
||||
# Set the line length limit used when formatting code snippets in
|
||||
# docstrings.
|
||||
docstring-code-line-length = "dynamic"
|
||||
31
python/sample.py
Executable file
31
python/sample.py
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import gi
|
||||
|
||||
gi.require_version("Playerctl", "2.0")
|
||||
|
||||
from gi.repository import Playerctl
|
||||
from astal import App, Astal, Variable, Widget, bind
|
||||
|
||||
player = Playerctl.Player.new("spotify")
|
||||
v = Variable(player.get_title()).observe(player, "metadata", lambda *_: player.get_title())
|
||||
|
||||
|
||||
def Bar(monitor):
|
||||
return Widget.Window(
|
||||
anchor=Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT,
|
||||
monitor=monitor,
|
||||
exclusivity=Astal.Exclusivity.EXCLUSIVE,
|
||||
child=Widget.CenterBox(
|
||||
start_widget=Widget.Label(
|
||||
label="Welcome to Astal.py!",
|
||||
),
|
||||
end_widget=Widget.Label(label=v()),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def start():
|
||||
Bar(0)
|
||||
|
||||
|
||||
App.start(callback=start)
|
||||
171
src/astal.vala
Normal file
171
src/astal.vala
Normal file
@@ -0,0 +1,171 @@
|
||||
namespace Astal {
|
||||
public class Application : Gtk.Application {
|
||||
public signal void request (string request);
|
||||
private List<Gtk.CssProvider> css_providers;
|
||||
private SocketService service;
|
||||
private string socket;
|
||||
|
||||
public string instance_name { get; construct set; }
|
||||
|
||||
public List<Gtk.Window> windows {
|
||||
get { return get_windows(); }
|
||||
}
|
||||
|
||||
public Gtk.Settings settings {
|
||||
get { return Gtk.Settings.get_default(); }
|
||||
}
|
||||
|
||||
public Gdk.Screen screen {
|
||||
get { return Gdk.Screen.get_default(); }
|
||||
}
|
||||
|
||||
public string gtk_theme {
|
||||
owned get { return settings.gtk_theme_name; }
|
||||
set { settings.gtk_theme_name = value; }
|
||||
}
|
||||
|
||||
public string icon_theme {
|
||||
owned get { return settings.gtk_icon_theme_name; }
|
||||
set { settings.gtk_icon_theme_name = value; }
|
||||
}
|
||||
|
||||
public string cursor_theme {
|
||||
owned get { return settings.gtk_cursor_theme_name; }
|
||||
set { settings.gtk_cursor_theme_name = value; }
|
||||
}
|
||||
|
||||
public void reset_css() {
|
||||
foreach(var provider in css_providers) {
|
||||
Gtk.StyleContext.remove_provider_for_screen(screen, provider);
|
||||
css_providers.remove_all(provider);
|
||||
}
|
||||
}
|
||||
|
||||
public void inspector() {
|
||||
Gtk.Window.set_interactive_debugging(true);
|
||||
}
|
||||
|
||||
public Gtk.Window get_window(string name) throws WindowError {
|
||||
foreach(var win in windows) {
|
||||
if (win.name == name)
|
||||
return win;
|
||||
}
|
||||
|
||||
throw new WindowError.NO_WINDOW_WITH_NAME(name);
|
||||
}
|
||||
|
||||
public void apply_css(string style, bool reset = false) throws Error {
|
||||
var provider = new Gtk.CssProvider();
|
||||
|
||||
if (reset)
|
||||
reset_css();
|
||||
|
||||
if (FileUtils.test(style, FileTest.EXISTS))
|
||||
provider.load_from_path(style);
|
||||
else
|
||||
provider.load_from_data(style);
|
||||
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER);
|
||||
|
||||
css_providers.append(provider);
|
||||
}
|
||||
|
||||
private async void _socket_request(SocketConnection conn) {
|
||||
string message = yield read_sock(conn);
|
||||
request(message.strip());
|
||||
response(message.strip(), conn);
|
||||
}
|
||||
|
||||
public virtual void response(string msg, SocketConnection conn) {
|
||||
write_sock.begin(conn, "missing response implementation on ".concat(application_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* should be called before `run()`
|
||||
* the return value indicates if instance is already running
|
||||
*/
|
||||
public bool acquire_socket() {
|
||||
socket = GLib.Environment.get_user_runtime_dir().concat(
|
||||
"/",
|
||||
instance_name,
|
||||
".sock");
|
||||
|
||||
if (FileUtils.test(socket, GLib.FileTest.EXISTS)) {
|
||||
info("socket %s exists", socket);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
SocketAddress _;
|
||||
service = new SocketService();
|
||||
service.add_address(
|
||||
new UnixSocketAddress(socket),
|
||||
SocketType.STREAM,
|
||||
SocketProtocol.DEFAULT,
|
||||
null,
|
||||
out _);
|
||||
|
||||
service.incoming.connect((conn) => {
|
||||
_socket_request.begin(conn);
|
||||
return false;
|
||||
});
|
||||
|
||||
info("socket acquired: %s\n", socket);
|
||||
return true;
|
||||
} catch (Error err) {
|
||||
critical("could not acquire socket %s\n", application_id);
|
||||
critical(err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
construct {
|
||||
if (instance_name == null)
|
||||
instance_name = "astal";
|
||||
|
||||
if (application_id == null)
|
||||
application_id = "io.Astal.".concat(instance_name);
|
||||
|
||||
shutdown.connect(() => {
|
||||
if (FileUtils.test(socket, GLib.FileTest.EXISTS)){
|
||||
try {
|
||||
File.new_for_path(socket).delete(null);
|
||||
} catch (Error err) {
|
||||
warning(err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
SourceFunc close = () => { quit(); };
|
||||
Unix.signal_add(1, close, Priority.HIGH);
|
||||
Unix.signal_add(2, close, Priority.HIGH);
|
||||
Unix.signal_add(15, close, Priority.HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
public errordomain WindowError {
|
||||
NO_WINDOW_WITH_NAME
|
||||
}
|
||||
|
||||
public async string read_sock(SocketConnection conn) {
|
||||
try {
|
||||
var stream = new DataInputStream(conn.input_stream);
|
||||
size_t size;
|
||||
return yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, out size);
|
||||
} catch (Error err) {
|
||||
critical(err.message);
|
||||
return err.message;
|
||||
}
|
||||
}
|
||||
|
||||
public async void write_sock(SocketConnection conn, string response) {
|
||||
try {
|
||||
yield conn.output_stream.write_async(
|
||||
response.concat("\x04").data,
|
||||
Priority.DEFAULT);
|
||||
} catch (Error err) {
|
||||
critical(err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/client.vala.in
Normal file
79
src/client.vala.in
Normal file
@@ -0,0 +1,79 @@
|
||||
private static bool version;
|
||||
private static bool help;
|
||||
private static string? instance_name;
|
||||
|
||||
private const GLib.OptionEntry[] options = {
|
||||
{ "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null },
|
||||
{ "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null },
|
||||
{ "instance-name", 'i', OptionFlags.NONE, OptionArg.STRING, ref instance_name, null, null },
|
||||
{ null },
|
||||
};
|
||||
|
||||
async int main(string[] argv) {
|
||||
try {
|
||||
var opts = new OptionContext();
|
||||
opts.add_main_entries(options, null);
|
||||
opts.set_help_enabled(false);
|
||||
opts.set_ignore_unknown_options(false);
|
||||
opts.parse(ref argv);
|
||||
} catch (OptionError err) {
|
||||
printerr (err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (help) {
|
||||
print("Client for the socket of an Astal.Application instance\n\n");
|
||||
print("Usage:\n");
|
||||
print(" %s [flags] message\n\n", argv[0]);
|
||||
print("Flags:\n");
|
||||
print(" -h, --help Print this help and exit\n");
|
||||
print(" -v, --version Print version number and exit\n");
|
||||
print(" -i, --instance-name Instance name of the Astal instance\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (version) {
|
||||
print("@VERSION@");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (instance_name == null)
|
||||
instance_name = "astal";
|
||||
|
||||
var request = "";
|
||||
for (var i = 1; i < argv.length; ++i) {
|
||||
request = request.concat(" ", argv[i]);
|
||||
}
|
||||
|
||||
var client = new SocketClient();
|
||||
var rundir = GLib.Environment.get_user_runtime_dir();
|
||||
var socket = rundir.concat("/", instance_name, ".sock");
|
||||
|
||||
try {
|
||||
var conn = client.connect(new UnixSocketAddress(socket), null);
|
||||
|
||||
try {
|
||||
yield conn.output_stream.write_async(
|
||||
request.concat("\x04").data,
|
||||
Priority.DEFAULT);
|
||||
} catch (Error err) {
|
||||
printerr("could not write to app '%s'", instance_name);
|
||||
}
|
||||
|
||||
var stream = new DataInputStream(conn.input_stream);
|
||||
size_t size;
|
||||
|
||||
try {
|
||||
var res = yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, out size);
|
||||
if (res != null)
|
||||
print("%s", res);
|
||||
} catch (Error err) {
|
||||
printerr(err.message);
|
||||
}
|
||||
} catch (Error err) {
|
||||
printerr("could not connect to app '%s'", instance_name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
85
src/meson.build
Normal file
85
src/meson.build
Normal file
@@ -0,0 +1,85 @@
|
||||
version_split = meson.project_version().split('.')
|
||||
api_version = version_split[0] + '.' + version_split[1]
|
||||
astal_gir = 'Astal-' + api_version + '.gir'
|
||||
astal_typelib = 'Astal-' + api_version + '.typelib'
|
||||
astal_so = 'libastal.so.' + meson.project_version()
|
||||
|
||||
deps = [
|
||||
dependency('glib-2.0'),
|
||||
dependency('gio-unix-2.0'),
|
||||
dependency('gobject-2.0'),
|
||||
dependency('gio-2.0'),
|
||||
dependency('gtk+-3.0'),
|
||||
dependency('gdk-pixbuf-2.0'),
|
||||
dependency('gtk-layer-shell-0'),
|
||||
]
|
||||
|
||||
sources = files(
|
||||
'widget/box.vala',
|
||||
'widget/button.vala',
|
||||
'widget/centerbox.vala',
|
||||
'widget/eventbox.vala',
|
||||
'widget/icon.vala',
|
||||
# 'widget/circularprogress.vala', # TODO: math lib -X -lm
|
||||
'widget/widget.vala',
|
||||
'widget/window.vala',
|
||||
'astal.vala',
|
||||
'process.vala',
|
||||
'time.vala',
|
||||
'variable.vala',
|
||||
)
|
||||
|
||||
libastal = library(
|
||||
meson.project_name(),
|
||||
sources,
|
||||
dependencies: deps,
|
||||
vala_header: meson.project_name() + '.h',
|
||||
vala_vapi: meson.project_name() + '.vapi',
|
||||
vala_gir: astal_gir,
|
||||
version: meson.project_version(),
|
||||
install: true,
|
||||
install_dir: [true, true, true, true],
|
||||
)
|
||||
|
||||
import('pkgconfig').generate(
|
||||
description: 'libastal',
|
||||
libraries: libastal,
|
||||
name: meson.project_name(),
|
||||
filebase: meson.project_name() + '-' + api_version,
|
||||
version: meson.project_version(),
|
||||
subdirs: meson.project_name(),
|
||||
requires: 'gio-2.0',
|
||||
install_dir: get_option('libdir') / 'pkgconfig',
|
||||
)
|
||||
|
||||
if get_option('typelib')
|
||||
custom_target(
|
||||
astal_typelib,
|
||||
command: [
|
||||
find_program('g-ir-compiler'),
|
||||
'--output', '@OUTPUT@',
|
||||
'--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@',
|
||||
meson.current_build_dir() / astal_gir,
|
||||
],
|
||||
input: libastal,
|
||||
output: astal_typelib,
|
||||
depends: libastal,
|
||||
install: true,
|
||||
install_dir: get_option('libdir') / 'girepository-1.0',
|
||||
)
|
||||
endif
|
||||
|
||||
if get_option('cli_client')
|
||||
executable(
|
||||
meson.project_name(),
|
||||
configure_file(
|
||||
input: 'client.vala.in',
|
||||
output: 'client.vala',
|
||||
configuration: {
|
||||
'VERSION': meson.project_version(),
|
||||
},
|
||||
),
|
||||
dependencies: deps,
|
||||
install: true,
|
||||
)
|
||||
endif
|
||||
126
src/process.vala
Normal file
126
src/process.vala
Normal file
@@ -0,0 +1,126 @@
|
||||
namespace Astal {
|
||||
public class Process : Object {
|
||||
private void read_stream(DataInputStream stream, bool err) {
|
||||
stream.read_line_utf8_async.begin(Priority.DEFAULT, null, (_, res) => {
|
||||
try {
|
||||
var output = stream.read_line_utf8_async.end(res);
|
||||
if (output != null) {
|
||||
if (err)
|
||||
stdout(output.strip());
|
||||
else
|
||||
stderr(output.strip());
|
||||
|
||||
read_stream(stream, err);
|
||||
}
|
||||
} catch (Error err) {
|
||||
printerr("%s\n", err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private DataInputStream out_stream;
|
||||
private DataInputStream err_stream;
|
||||
private DataOutputStream in_stream;
|
||||
private Subprocess process;
|
||||
public string[] argv { construct; get; }
|
||||
|
||||
public signal void stdout (string out);
|
||||
public signal void stderr (string err);
|
||||
|
||||
public void kill() {
|
||||
process.force_exit();
|
||||
}
|
||||
|
||||
public void write(string in) throws Error {
|
||||
in_stream.put_string(in);
|
||||
}
|
||||
|
||||
public void write_async(string in) {
|
||||
in_stream.write_all_async.begin(
|
||||
in.data,
|
||||
Priority.DEFAULT, null, (_, res) => {
|
||||
try {
|
||||
in_stream.write_all_async.end(res, null);
|
||||
} catch (Error err) {
|
||||
printerr("%s\n", err.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public Process.subprocessv(string[] cmd) throws Error {
|
||||
Object(argv: cmd);
|
||||
process = new Subprocess.newv(cmd,
|
||||
SubprocessFlags.STDIN_PIPE |
|
||||
SubprocessFlags.STDERR_PIPE |
|
||||
SubprocessFlags.STDOUT_PIPE
|
||||
);
|
||||
out_stream = new DataInputStream(process.get_stdout_pipe());
|
||||
err_stream = new DataInputStream(process.get_stderr_pipe());
|
||||
in_stream = new DataOutputStream(process.get_stdin_pipe());
|
||||
read_stream(out_stream, true);
|
||||
read_stream(err_stream, false);
|
||||
}
|
||||
|
||||
public static Process subprocess(string cmd) throws Error {
|
||||
string[] argv;
|
||||
Shell.parse_argv(cmd, out argv);
|
||||
return new Process.subprocessv(argv);
|
||||
}
|
||||
|
||||
public static string execv(string[] cmd) throws Error {
|
||||
var process = new Subprocess.newv(
|
||||
cmd,
|
||||
SubprocessFlags.STDERR_PIPE |
|
||||
SubprocessFlags.STDOUT_PIPE
|
||||
);
|
||||
|
||||
string err_str, out_str;
|
||||
process.communicate_utf8(null, null, out out_str, out err_str);
|
||||
var success = process.get_successful();
|
||||
process.dispose();
|
||||
if (success)
|
||||
return out_str.strip();
|
||||
else
|
||||
throw new ProcessError.FAILED(err_str.strip());
|
||||
}
|
||||
|
||||
public static string exec(string cmd) throws Error {
|
||||
string[] argv;
|
||||
Shell.parse_argv(cmd, out argv);
|
||||
return Process.execv(argv);
|
||||
}
|
||||
|
||||
public Process.exec_asyncv(string[] cmd) throws Error {
|
||||
Object(argv: cmd);
|
||||
process = new Subprocess.newv(cmd,
|
||||
SubprocessFlags.STDERR_PIPE |
|
||||
SubprocessFlags.STDOUT_PIPE
|
||||
);
|
||||
|
||||
process.communicate_utf8_async.begin(null, null, (_, res) => {
|
||||
string err_str, out_str;
|
||||
try {
|
||||
process.communicate_utf8_async.end(res, out out_str, out err_str);
|
||||
if (process.get_successful())
|
||||
stdout(out_str.strip());
|
||||
else
|
||||
stderr(err_str.strip());
|
||||
} catch (Error err) {
|
||||
printerr("%s\n", err.message);
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static Process exec_async(string cmd) throws Error {
|
||||
string[] argv;
|
||||
Shell.parse_argv(cmd, out argv);
|
||||
return new Process.exec_asyncv(argv);
|
||||
}
|
||||
}
|
||||
errordomain ProcessError {
|
||||
FAILED
|
||||
}
|
||||
}
|
||||
68
src/time.vala
Normal file
68
src/time.vala
Normal file
@@ -0,0 +1,68 @@
|
||||
namespace Astal {
|
||||
public class Time : Object {
|
||||
public signal void now ();
|
||||
public signal void cancelled ();
|
||||
private Cancellable cancellable;
|
||||
private uint timeout_id;
|
||||
|
||||
construct {
|
||||
cancellable = new Cancellable();
|
||||
cancellable.cancelled.connect(() => {
|
||||
Source.remove(timeout_id);
|
||||
cancelled();
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
|
||||
private void connect_closure(Closure? closure) {
|
||||
if (closure == null)
|
||||
return;
|
||||
|
||||
now.connect(() => {
|
||||
Value ret = Value(Type.POINTER); // void
|
||||
closure.invoke(ref ret, {});
|
||||
});
|
||||
}
|
||||
|
||||
public Time.interval_prio(uint interval, int prio = Priority.DEFAULT, Closure? fn) {
|
||||
connect_closure(fn);
|
||||
Idle.add_once(() => now());
|
||||
timeout_id = Timeout.add(interval, () => {
|
||||
now();
|
||||
return Source.CONTINUE;
|
||||
}, prio);
|
||||
}
|
||||
|
||||
public Time.timeout_prio(uint timeout, int prio = Priority.DEFAULT, Closure? fn) {
|
||||
connect_closure(fn);
|
||||
timeout_id = Timeout.add(timeout, () => {
|
||||
now();
|
||||
return Source.REMOVE;
|
||||
}, prio);
|
||||
}
|
||||
|
||||
public Time.idle_prio(int prio = Priority.DEFAULT_IDLE, Closure? fn) {
|
||||
connect_closure(fn);
|
||||
timeout_id = Idle.add(() => {
|
||||
now();
|
||||
return Source.REMOVE;
|
||||
}, prio);
|
||||
}
|
||||
|
||||
public static Time interval(uint interval, Closure? fn) {
|
||||
return new Time.interval_prio(interval, Priority.DEFAULT, fn);
|
||||
}
|
||||
|
||||
public static Time timeout(uint timeout, Closure? fn) {
|
||||
return new Time.timeout_prio(timeout, Priority.DEFAULT, fn);
|
||||
}
|
||||
|
||||
public static Time idle(Closure? fn) {
|
||||
return new Time.idle_prio(Priority.DEFAULT_IDLE, fn);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
cancellable.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
190
src/variable.vala
Normal file
190
src/variable.vala
Normal file
@@ -0,0 +1,190 @@
|
||||
namespace Astal {
|
||||
public class VariableBase : Object {
|
||||
public signal void changed ();
|
||||
public signal void dropped ();
|
||||
public signal void error (string err);
|
||||
|
||||
// lua-lgi crashes when using its emitting mechanism
|
||||
public void emit_changed() { changed(); }
|
||||
public void emit_dropped() { dropped(); }
|
||||
public void emit_error(string err) { this.error(err); }
|
||||
|
||||
~VariableBase() {
|
||||
dropped();
|
||||
}
|
||||
}
|
||||
|
||||
public class Variable : VariableBase {
|
||||
public Value value { owned get; set; }
|
||||
|
||||
private uint poll_id = 0;
|
||||
private Process? watch_proc;
|
||||
|
||||
private uint poll_interval { get; set; default = 1000; }
|
||||
private string[] poll_exec { get; set; }
|
||||
private Closure? poll_transform { get; set; }
|
||||
private Closure? poll_fn { get; set; }
|
||||
|
||||
private Closure? watch_transform { get; set; }
|
||||
private string[] watch_exec { get; set; }
|
||||
|
||||
public Variable(Value init) {
|
||||
Object(value: init);
|
||||
}
|
||||
|
||||
public Variable poll(
|
||||
uint interval,
|
||||
string exec,
|
||||
Closure? transform
|
||||
) throws Error {
|
||||
string[] argv;
|
||||
Shell.parse_argv(exec, out argv);
|
||||
return pollv(interval, argv, transform);
|
||||
}
|
||||
|
||||
public Variable pollv(
|
||||
uint interval,
|
||||
string[] execv,
|
||||
Closure? transform
|
||||
) throws Error {
|
||||
if (is_polling())
|
||||
stop_poll();
|
||||
|
||||
poll_interval = interval;
|
||||
poll_exec = execv;
|
||||
poll_transform = transform;
|
||||
poll_fn = null;
|
||||
start_poll();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Variable pollfn(
|
||||
uint interval,
|
||||
Closure fn
|
||||
) throws Error {
|
||||
if (is_polling())
|
||||
stop_poll();
|
||||
|
||||
poll_interval = interval;
|
||||
poll_fn = fn;
|
||||
poll_exec = null;
|
||||
start_poll();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Variable watch(
|
||||
string exec,
|
||||
Closure? transform
|
||||
) throws Error {
|
||||
string[] argv;
|
||||
Shell.parse_argv(exec, out argv);
|
||||
return watchv(argv, transform);
|
||||
}
|
||||
|
||||
public Variable watchv(
|
||||
string[] execv,
|
||||
Closure? transform
|
||||
) throws Error {
|
||||
if (is_watching())
|
||||
stop_watch();
|
||||
|
||||
watch_exec = execv;
|
||||
watch_transform = transform;
|
||||
start_watch();
|
||||
return this;
|
||||
}
|
||||
|
||||
construct {
|
||||
notify["value"].connect(() => changed());
|
||||
dropped.connect(() => {
|
||||
if (is_polling())
|
||||
stop_poll();
|
||||
|
||||
if (is_watching())
|
||||
stop_watch();
|
||||
});
|
||||
}
|
||||
|
||||
private void set_closure(string val, Closure? transform) {
|
||||
if (transform != null) {
|
||||
var str = Value(typeof(string));
|
||||
str.set_string(val);
|
||||
|
||||
var ret_val = Value(this.value.type());
|
||||
transform.invoke(ref ret_val, { str, this.value });
|
||||
this.value = ret_val;
|
||||
}
|
||||
else {
|
||||
if (this.value.type() == Type.STRING && this.value.get_string() == val)
|
||||
return;
|
||||
|
||||
var str = Value(typeof(string));
|
||||
str.set_string(val);
|
||||
this.value = str;
|
||||
}
|
||||
}
|
||||
|
||||
private void set_fn() {
|
||||
var ret_val = Value(this.value.type());
|
||||
poll_fn.invoke(ref ret_val, { this.value });
|
||||
this.value = ret_val;
|
||||
}
|
||||
|
||||
public void start_poll() throws Error {
|
||||
return_if_fail(poll_id == 0);
|
||||
|
||||
if (poll_fn != null) {
|
||||
set_fn();
|
||||
poll_id = Timeout.add(poll_interval, () => {
|
||||
set_fn();
|
||||
return Source.CONTINUE;
|
||||
}, Priority.DEFAULT);
|
||||
}
|
||||
if (poll_exec != null) {
|
||||
var proc = new Process.exec_asyncv(poll_exec);
|
||||
proc.stdout.connect((str) => set_closure(str, poll_transform));
|
||||
proc.stderr.connect((str) => this.error(str));
|
||||
poll_id = Timeout.add(poll_interval, () => {
|
||||
try {
|
||||
proc = new Process.exec_asyncv(poll_exec);
|
||||
proc.stdout.connect((str) => set_closure(str, poll_transform));
|
||||
proc.stderr.connect((str) => this.error(str));
|
||||
return Source.CONTINUE;
|
||||
} catch (Error err) {
|
||||
printerr("%s\n", err.message);
|
||||
poll_id = 0;
|
||||
return Source.REMOVE;
|
||||
}
|
||||
}, Priority.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
public void start_watch() throws Error {
|
||||
return_if_fail(watch_proc == null);
|
||||
return_if_fail(watch_exec != null);
|
||||
|
||||
watch_proc = new Process.subprocessv(watch_exec);
|
||||
watch_proc.stdout.connect((str) => set_closure(str, watch_transform));
|
||||
watch_proc.stderr.connect((str) => this.error(str));
|
||||
}
|
||||
|
||||
public void stop_poll() {
|
||||
return_if_fail(poll_id != 0);
|
||||
Source.remove(poll_id);
|
||||
poll_id = 0;
|
||||
}
|
||||
|
||||
public void stop_watch() {
|
||||
return_if_fail(watch_proc != null);
|
||||
watch_proc.kill();
|
||||
watch_proc = null;
|
||||
}
|
||||
|
||||
public bool is_polling() { return poll_id > 0; }
|
||||
public bool is_watching() { return watch_proc != null; }
|
||||
|
||||
~Variable() {
|
||||
dropped();
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/widget/box.vala
Normal file
60
src/widget/box.vala
Normal file
@@ -0,0 +1,60 @@
|
||||
namespace Astal {
|
||||
public class Box : Gtk.Box {
|
||||
public bool vertical {
|
||||
get { return orientation == Gtk.Orientation.VERTICAL; }
|
||||
set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; }
|
||||
}
|
||||
|
||||
public List<weak Gtk.Widget> children {
|
||||
set { _set_children(value); }
|
||||
owned get { return get_children(); }
|
||||
}
|
||||
|
||||
public new Gtk.Widget child {
|
||||
owned get { return _get_child(); }
|
||||
set { _set_child(value); }
|
||||
}
|
||||
|
||||
construct {
|
||||
notify["orientation"].connect(() => {
|
||||
notify_property("vertical");
|
||||
});
|
||||
}
|
||||
|
||||
private void _set_child(Gtk.Widget child) {
|
||||
var list = new List<weak Gtk.Widget>();
|
||||
list.append(child);
|
||||
_set_children(list);
|
||||
}
|
||||
|
||||
private Gtk.Widget? _get_child() {
|
||||
foreach(var child in get_children())
|
||||
return child;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void _set_children(List<weak Gtk.Widget> arr) {
|
||||
foreach(var child in get_children())
|
||||
remove(child);
|
||||
|
||||
foreach(var child in arr)
|
||||
add(child);
|
||||
}
|
||||
|
||||
public Box(bool vertical, List<weak Gtk.Widget> children) {
|
||||
this.vertical = vertical;
|
||||
_set_children(children);
|
||||
}
|
||||
|
||||
public Box.newh(List<weak Gtk.Widget> children) {
|
||||
this.vertical = false;
|
||||
_set_children(children);
|
||||
}
|
||||
|
||||
public Box.newv(List<weak Gtk.Widget> children) {
|
||||
this.vertical = true;
|
||||
_set_children(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/widget/button.vala
Normal file
35
src/widget/button.vala
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace Astal {
|
||||
public class Button : Gtk.Button {
|
||||
public signal void hover (Gdk.EventCrossing event);
|
||||
public signal void hover_lost (Gdk.EventCrossing event);
|
||||
public signal void click (Gdk.EventButton event);
|
||||
public signal void click_release (Gdk.EventButton event);
|
||||
|
||||
construct {
|
||||
add_events(Gdk.EventMask.SCROLL_MASK);
|
||||
add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK);
|
||||
|
||||
enter_notify_event.connect((self, event) => {
|
||||
if (event.window == self.get_window() &&
|
||||
event.detail != Gdk.NotifyType.INFERIOR) {
|
||||
hover(event);
|
||||
}
|
||||
});
|
||||
|
||||
leave_notify_event.connect((self, event) => {
|
||||
if (event.window == self.get_window() &&
|
||||
event.detail != Gdk.NotifyType.INFERIOR) {
|
||||
hover_lost(event);
|
||||
}
|
||||
});
|
||||
|
||||
button_press_event.connect((event) => {
|
||||
click(event);
|
||||
});
|
||||
|
||||
button_release_event.connect((event) => {
|
||||
click_release(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/widget/centerbox.vala
Normal file
35
src/widget/centerbox.vala
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace Astal {
|
||||
public class CenterBox : Gtk.Box {
|
||||
public bool vertical {
|
||||
get { return orientation == Gtk.Orientation.VERTICAL; }
|
||||
set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; }
|
||||
}
|
||||
|
||||
private Gtk.Widget _start_widget;
|
||||
public Gtk.Widget start_widget {
|
||||
get { return _start_widget; }
|
||||
set {
|
||||
if (_start_widget != null)
|
||||
remove(_start_widget);
|
||||
|
||||
pack_start(value, true, true, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private Gtk.Widget _end_widget;
|
||||
public Gtk.Widget end_widget {
|
||||
get { return _end_widget; }
|
||||
set {
|
||||
if (_end_widget != null)
|
||||
remove(_end_widget);
|
||||
|
||||
pack_end(value, true, true, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public Gtk.Widget center_widget {
|
||||
get { return get_center_widget(); }
|
||||
set { set_center_widget(value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
174
src/widget/circularprogress.vala
Normal file
174
src/widget/circularprogress.vala
Normal file
@@ -0,0 +1,174 @@
|
||||
namespace Astal {
|
||||
public class CircularProgress : Gtk.Bin {
|
||||
public new Gtk.Widget child { get; set; }
|
||||
public double start_at { get; set; }
|
||||
public double end_at { get; set; }
|
||||
public double value { get; set; }
|
||||
public bool inverted { get; set; }
|
||||
public bool rounded { get; set; }
|
||||
|
||||
construct {
|
||||
notify["start-at"].connect(queue_draw);
|
||||
notify["end-at"].connect(queue_draw);
|
||||
notify["value"].connect(queue_draw);
|
||||
notify["inverted"].connect(queue_draw);
|
||||
notify["rounded"].connect(queue_draw);
|
||||
notify["child"].connect(queue_draw);
|
||||
}
|
||||
|
||||
static construct {
|
||||
set_css_name("circular-progress");
|
||||
}
|
||||
|
||||
|
||||
public new void get_preferred_height(out int minh, out int nath) {
|
||||
var val = get_style_context().get_property("min-height", Gtk.StateFlags.NORMAL);
|
||||
if (val.get_int() <= 0) {
|
||||
minh = 40;
|
||||
nath = 40;
|
||||
}
|
||||
|
||||
minh = val.get_int();
|
||||
nath = val.get_int();
|
||||
}
|
||||
|
||||
public new void get_preferred_width(out int minw, out int natw) {
|
||||
var val = get_style_context().get_property("min-width", Gtk.StateFlags.NORMAL);
|
||||
if (val.get_int() <= 0) {
|
||||
minw = 40;
|
||||
natw = 40;
|
||||
}
|
||||
|
||||
minw = val.get_int();
|
||||
natw = val.get_int();
|
||||
}
|
||||
|
||||
private double _to_radian(double percentage) {
|
||||
percentage = Math.floor(percentage * 100);
|
||||
return (percentage / 100) * (2 * Math.PI);
|
||||
}
|
||||
|
||||
private bool _is_full_circle(double start, double end, double epsilon = 1e-10) {
|
||||
// Ensure that start and end are between 0 and 1
|
||||
start = (start % 1 + 1) % 1;
|
||||
end = (end % 1 + 1) % 1;
|
||||
|
||||
// Check if the difference between start and end is close to 1
|
||||
return Math.fabs(start - end) <= epsilon;
|
||||
}
|
||||
|
||||
private double _map_arc_value_to_range(double start, double end, double value) {
|
||||
// Ensure that start and end are between 0 and 1
|
||||
start = (start % 1 + 1) % 1;
|
||||
end = (end % 1 + 1) % 1;
|
||||
|
||||
// Calculate the length of the arc
|
||||
var arcLength = end - start;
|
||||
if (arcLength < 0)
|
||||
arcLength += 1; // Adjust for circular representation
|
||||
|
||||
// Calculate the position on the arc based on the percentage value
|
||||
var position = start + (arcLength * value);
|
||||
|
||||
// Ensure the position is between 0 and 1
|
||||
position = (position % 1 + 1) % 1;
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
private double _min(double[] arr) {
|
||||
double min = arr[0];
|
||||
foreach(var i in arr)
|
||||
if (min > i) min = i;
|
||||
return min;
|
||||
}
|
||||
|
||||
private double _max(double[] arr) {
|
||||
double max = arr[0];
|
||||
foreach(var i in arr)
|
||||
if (max < i) max = i;
|
||||
return max;
|
||||
}
|
||||
|
||||
public new bool draw(Cairo.Context cr) {
|
||||
Gtk.Allocation allocation;
|
||||
get_allocation(out allocation);
|
||||
|
||||
var styles = get_style_context();
|
||||
var width = allocation.width;
|
||||
var height = allocation.height;
|
||||
var thickness = styles.get_property("font-size", Gtk.StateFlags.NORMAL).get_double();
|
||||
var margin = styles.get_margin(Gtk.StateFlags.NORMAL);
|
||||
var fg = styles.get_color(Gtk.StateFlags.NORMAL);
|
||||
var bg = styles.get_background_color(Gtk.StateFlags.NORMAL);
|
||||
|
||||
var bg_stroke = thickness + _min({margin.bottom, margin.top, margin.left, margin.right});
|
||||
var fg_stroke = thickness;
|
||||
var radius = _min({width, height}) / 2.0 - _max({bg_stroke, fg_stroke}) / 2.0;
|
||||
var center_x = width / 2;
|
||||
var center_y = height / 2;
|
||||
|
||||
var start_background = _to_radian(this.start_at);
|
||||
var end_background = _to_radian(this.end_at);
|
||||
var ranged_value = this.value + this.start_at;
|
||||
|
||||
var is_circle = _is_full_circle(this.start_at, this.end_at);
|
||||
|
||||
if (is_circle) {
|
||||
// Redefine endDraw in radius to create an accurate full circle
|
||||
end_background = start_background + 2 * Math.PI;
|
||||
} else {
|
||||
// Range the value for the arc shape
|
||||
ranged_value = _map_arc_value_to_range(
|
||||
this.start_at,
|
||||
this.end_at,
|
||||
this.value
|
||||
);
|
||||
}
|
||||
|
||||
var to = _to_radian(ranged_value);
|
||||
double start_progress, end_progress;
|
||||
|
||||
if (this.inverted) {
|
||||
start_progress = (2 * Math.PI - to) - start_background;
|
||||
end_progress = (2 * Math.PI - start_background) - start_background;
|
||||
} else {
|
||||
start_progress = start_background;
|
||||
end_progress = to;
|
||||
}
|
||||
|
||||
// Draw background
|
||||
cr.set_source_rgba(bg.red, bg.green, bg.blue, bg.alpha);
|
||||
cr.arc(center_x, center_y, radius, start_background, end_background);
|
||||
|
||||
cr.set_line_width(bg_stroke);
|
||||
cr.stroke();
|
||||
|
||||
// Draw progress
|
||||
cr.set_source_rgba(fg.red, fg.green, fg.blue, fg.alpha);
|
||||
cr.arc(center_x, center_y, radius, start_progress, end_progress);
|
||||
cr.set_line_width(fg_stroke);
|
||||
cr.stroke();
|
||||
|
||||
// Draw rounded ends
|
||||
if (this.rounded) {
|
||||
var start_x = center_x + Math.cos(start_background);
|
||||
var start_y = center_y + Math.cos(start_background);
|
||||
var end_x = center_x + Math.cos(to) * radius;
|
||||
var end_y = center_y + Math.cos(to) * radius;
|
||||
cr.set_line_width(0);
|
||||
cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01);
|
||||
cr.fill();
|
||||
cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01);
|
||||
cr.fill();
|
||||
}
|
||||
|
||||
if (this.child != null) {
|
||||
this.child.size_allocate(allocation);
|
||||
this.propagate_draw(this.child, cr);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/widget/eventbox.vala
Normal file
39
src/widget/eventbox.vala
Normal file
@@ -0,0 +1,39 @@
|
||||
namespace Astal {
|
||||
public class EventBox : Gtk.EventBox {
|
||||
public signal void hover (Gdk.EventCrossing event);
|
||||
public signal void hover_lost (Gdk.EventCrossing event);
|
||||
public signal void click (Gdk.EventButton event);
|
||||
public signal void click_release (Gdk.EventButton event);
|
||||
|
||||
construct {
|
||||
add_events(Gdk.EventMask.SCROLL_MASK);
|
||||
add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK);
|
||||
|
||||
enter_notify_event.connect((self, event) => {
|
||||
if (event.window == self.get_window() &&
|
||||
event.detail != Gdk.NotifyType.INFERIOR) {
|
||||
this.set_state_flags(Gtk.StateFlags.PRELIGHT, false);
|
||||
hover(event);
|
||||
}
|
||||
});
|
||||
|
||||
leave_notify_event.connect((self, event) => {
|
||||
if (event.window == self.get_window() &&
|
||||
event.detail != Gdk.NotifyType.INFERIOR) {
|
||||
this.unset_state_flags(Gtk.StateFlags.PRELIGHT);
|
||||
hover_lost(event);
|
||||
}
|
||||
});
|
||||
|
||||
button_press_event.connect((event) => {
|
||||
// TODO: abstract event for easier use
|
||||
click(event);
|
||||
});
|
||||
|
||||
button_release_event.connect((event) => {
|
||||
// TODO: abstract event for easier use
|
||||
click_release(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/widget/icon.vala
Normal file
87
src/widget/icon.vala
Normal file
@@ -0,0 +1,87 @@
|
||||
namespace Astal {
|
||||
public Gtk.IconInfo? lookup_icon(string icon) {
|
||||
var theme = Gtk.IconTheme.get_default();
|
||||
return theme.lookup_icon(icon, 16, Gtk.IconLookupFlags.USE_BUILTIN);
|
||||
}
|
||||
|
||||
public class Icon : Gtk.Image {
|
||||
private IconType type = IconType.NAMED;
|
||||
private double size { get; set; default = 14; }
|
||||
|
||||
public new Gdk.Pixbuf pixbuf { get; set; }
|
||||
public string icon { get; set; default = ""; }
|
||||
|
||||
private void display_icon() {
|
||||
switch(type) {
|
||||
case IconType.NAMED:
|
||||
icon_name = icon;
|
||||
pixel_size = (int)size;
|
||||
break;
|
||||
case IconType.FILE:
|
||||
try {
|
||||
var pb = new Gdk.Pixbuf.from_file_at_size(
|
||||
icon,
|
||||
(int)size * scale_factor,
|
||||
(int)size * scale_factor
|
||||
);
|
||||
var cs = Gdk.cairo_surface_create_from_pixbuf(pb, 0, this.get_window());
|
||||
set_from_surface(cs);
|
||||
} catch (Error err) {
|
||||
printerr(err.message);
|
||||
}
|
||||
break;
|
||||
case IconType.PIXBUF:
|
||||
var pb_scaled = pixbuf.scale_simple(
|
||||
(int)size * scale_factor,
|
||||
(int)size * scale_factor,
|
||||
Gdk.InterpType.BILINEAR
|
||||
);
|
||||
if (pb_scaled != null) {
|
||||
var cs = Gdk.cairo_surface_create_from_pixbuf(pb_scaled, 0, this.get_window());
|
||||
set_from_surface(cs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
construct {
|
||||
notify["icon"].connect(() => {
|
||||
if(FileUtils.test(icon, GLib.FileTest.EXISTS))
|
||||
type = IconType.FILE;
|
||||
else if (lookup_icon(icon) != null)
|
||||
type = IconType.NAMED;
|
||||
else {
|
||||
type = IconType.NAMED;
|
||||
warning("cannot assign %s as icon, "+
|
||||
"it is not a file nor a named icon", icon);
|
||||
}
|
||||
display_icon();
|
||||
});
|
||||
|
||||
notify["pixbuf"].connect(() => {
|
||||
type = IconType.PIXBUF;
|
||||
display_icon();
|
||||
});
|
||||
|
||||
size_allocate.connect(() => {
|
||||
size = get_style_context()
|
||||
.get_property("font-size", Gtk.StateFlags.NORMAL).get_double();
|
||||
|
||||
display_icon();
|
||||
});
|
||||
|
||||
get_style_context().changed.connect(() => {
|
||||
size = get_style_context()
|
||||
.get_property("font-size", Gtk.StateFlags.NORMAL).get_double();
|
||||
|
||||
display_icon();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private enum IconType {
|
||||
NAMED,
|
||||
FILE,
|
||||
PIXBUF,
|
||||
}
|
||||
}
|
||||
132
src/widget/widget.vala
Normal file
132
src/widget/widget.vala
Normal file
@@ -0,0 +1,132 @@
|
||||
namespace Astal {
|
||||
private class Css {
|
||||
private static HashTable<Gtk.Widget, Gtk.CssProvider> _providers;
|
||||
public static HashTable<Gtk.Widget, Gtk.CssProvider> providers {
|
||||
get {
|
||||
if (_providers == null) {
|
||||
_providers = new HashTable<Gtk.Widget, Gtk.CssProvider>(
|
||||
(w) => (uint)w,
|
||||
(a, b) => a == b);
|
||||
}
|
||||
|
||||
return _providers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void remove_provider(Gtk.Widget widget) {
|
||||
var providers = Css.providers;
|
||||
|
||||
if (providers.contains(widget)) {
|
||||
var p = providers.get(widget);
|
||||
widget.get_style_context().remove_provider(p);
|
||||
providers.remove(widget);
|
||||
p.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void widget_set_css(Gtk.Widget widget, string css) {
|
||||
var providers = Css.providers;
|
||||
|
||||
if (providers.contains(widget)) {
|
||||
remove_provider(widget);
|
||||
} else {
|
||||
widget.destroy.connect(() => {
|
||||
remove_provider(widget);
|
||||
});
|
||||
}
|
||||
|
||||
var style = !css.contains("{") || !css.contains("}")
|
||||
? "* { ".concat(css, "}") : css;
|
||||
|
||||
var p = new Gtk.CssProvider();
|
||||
widget.get_style_context()
|
||||
.add_provider(p, Gtk.STYLE_PROVIDER_PRIORITY_USER);
|
||||
|
||||
try {
|
||||
p.load_from_data(style, style.length);
|
||||
providers.set(widget, p);
|
||||
} catch (Error err) {
|
||||
warning(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
public string widget_get_css(Gtk.Widget widget) {
|
||||
var providers = Css.providers;
|
||||
|
||||
if (providers.contains(widget))
|
||||
return providers.get(widget).to_string();
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public void widget_set_class_names(Gtk.Widget widget, string[] class_names) {
|
||||
foreach (var name in widget_get_class_names(widget))
|
||||
widget_toggle_class_name(widget, name, false);
|
||||
|
||||
foreach (var name in class_names)
|
||||
widget_toggle_class_name(widget, name, true);
|
||||
}
|
||||
|
||||
public List<weak string> widget_get_class_names(Gtk.Widget widget) {
|
||||
return widget.get_style_context().list_classes();
|
||||
}
|
||||
|
||||
public void widget_toggle_class_name(Gtk.Widget widget,
|
||||
string class_name,
|
||||
bool condition) {
|
||||
var c = widget.get_style_context();
|
||||
if (condition)
|
||||
c.add_class(class_name);
|
||||
else
|
||||
c.remove_class(class_name);
|
||||
}
|
||||
|
||||
private class Cursor {
|
||||
private static HashTable<Gtk.Widget, string> _cursors;
|
||||
public static HashTable<Gtk.Widget, string> cursors {
|
||||
get {
|
||||
if (_cursors == null) {
|
||||
_cursors = new HashTable<Gtk.Widget, string>(
|
||||
(w) => (uint)w,
|
||||
(a, b) => a == b);
|
||||
}
|
||||
return _cursors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void widget_setup_cursor(Gtk.Widget widget) {
|
||||
widget.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK);
|
||||
widget.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK);
|
||||
widget.enter_notify_event.connect(() => {
|
||||
widget.get_window().set_cursor(
|
||||
new Gdk.Cursor.from_name(
|
||||
Gdk.Display.get_default(),
|
||||
Cursor.cursors.get(widget)));
|
||||
return false;
|
||||
});
|
||||
widget.leave_notify_event.connect(() => {
|
||||
widget.get_window().set_cursor(
|
||||
new Gdk.Cursor.from_name(
|
||||
Gdk.Display.get_default(),
|
||||
"default"));
|
||||
return false;
|
||||
});
|
||||
widget.destroy.connect(() => {
|
||||
if (Cursor.cursors.contains(widget))
|
||||
Cursor.cursors.remove(widget);
|
||||
});
|
||||
}
|
||||
|
||||
public void widget_set_cursor(Gtk.Widget widget, string cursor) {
|
||||
if (!Cursor.cursors.contains(widget))
|
||||
widget_setup_cursor(widget);
|
||||
|
||||
Cursor.cursors.set(widget, cursor);
|
||||
}
|
||||
|
||||
public string widget_get_cursor(Gtk.Widget widget) {
|
||||
return Cursor.cursors.get(widget);
|
||||
}
|
||||
}
|
||||
136
src/widget/window.vala
Normal file
136
src/widget/window.vala
Normal file
@@ -0,0 +1,136 @@
|
||||
using GtkLayerShell;
|
||||
|
||||
namespace Astal {
|
||||
public enum WindowAnchor {
|
||||
NONE = 0,
|
||||
TOP = 1,
|
||||
RIGHT = 2,
|
||||
LEFT = 4,
|
||||
BOTTOM = 8,
|
||||
}
|
||||
|
||||
public enum Exclusivity {
|
||||
NORMAL,
|
||||
EXCLUSIVE,
|
||||
IGNORE,
|
||||
}
|
||||
|
||||
public enum Layer {
|
||||
TOP = GtkLayerShell.Layer.TOP,
|
||||
OVERLAY = GtkLayerShell.Layer.OVERLAY,
|
||||
BOTTOM = GtkLayerShell.Layer.BOTTOM,
|
||||
BACKGROUND = GtkLayerShell.Layer.BACKGROUND,
|
||||
}
|
||||
|
||||
public enum Keymode {
|
||||
NONE = GtkLayerShell.KeyboardMode.NONE,
|
||||
ON_DEMAND = GtkLayerShell.KeyboardMode.ON_DEMAND,
|
||||
EXCLUSIVE = GtkLayerShell.KeyboardMode.EXCLUSIVE,
|
||||
}
|
||||
|
||||
public class Window : Gtk.Window {
|
||||
construct {
|
||||
height_request = 1;
|
||||
width_request = 1;
|
||||
init_for_window(this);
|
||||
set_namespace(this, name);
|
||||
notify["name"].connect(() => set_namespace(this, name));
|
||||
}
|
||||
|
||||
public int anchor {
|
||||
set {
|
||||
set_anchor(this, Edge.TOP, WindowAnchor.TOP in value);
|
||||
set_anchor(this, Edge.BOTTOM, WindowAnchor.BOTTOM in value);
|
||||
set_anchor(this, Edge.LEFT, WindowAnchor.LEFT in value);
|
||||
set_anchor(this, Edge.RIGHT, WindowAnchor.RIGHT in value);
|
||||
}
|
||||
get {
|
||||
var a = WindowAnchor.NONE;
|
||||
if (get_anchor(this, Edge.TOP))
|
||||
a = a | WindowAnchor.TOP;
|
||||
|
||||
if (get_anchor(this, Edge.RIGHT))
|
||||
a = a | WindowAnchor.RIGHT;
|
||||
|
||||
if (get_anchor(this, Edge.LEFT))
|
||||
a = a | WindowAnchor.LEFT;
|
||||
|
||||
if (get_anchor(this, Edge.BOTTOM))
|
||||
a = a | WindowAnchor.BOTTOM;
|
||||
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
public Exclusivity exclusivity {
|
||||
set {
|
||||
switch (value) {
|
||||
case Exclusivity.NORMAL:
|
||||
set_exclusive_zone(this, 0);
|
||||
break;
|
||||
case Exclusivity.EXCLUSIVE:
|
||||
auto_exclusive_zone_enable (this);
|
||||
break;
|
||||
case Exclusivity.IGNORE:
|
||||
set_exclusive_zone(this, -1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
get {
|
||||
if (auto_exclusive_zone_is_enabled (this))
|
||||
return Exclusivity.EXCLUSIVE;
|
||||
|
||||
if (get_exclusive_zone(this) == -1)
|
||||
return Exclusivity.IGNORE;
|
||||
|
||||
return Exclusivity.NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
public Layer layer {
|
||||
get { return (Layer)get_layer(this); }
|
||||
set { set_layer(this, (GtkLayerShell.Layer)value); }
|
||||
}
|
||||
|
||||
public Keymode keymode {
|
||||
set { set_keyboard_mode(this, (GtkLayerShell.KeyboardMode)value); }
|
||||
get { return (Keymode)get_keyboard_mode(this); }
|
||||
}
|
||||
|
||||
public Gdk.Monitor gdkmonitor {
|
||||
set { set_monitor (this, value); }
|
||||
get { return get_monitor(this); }
|
||||
}
|
||||
|
||||
/**
|
||||
* CAUTION: the id might not be the same mapped by the compositor
|
||||
* to reset and let the compositor map it pass a negative number
|
||||
*/
|
||||
public int monitor {
|
||||
set {
|
||||
if (value < 0)
|
||||
set_monitor(this, (Gdk.Monitor)null);
|
||||
|
||||
var m = Gdk.Display.get_default().get_monitor(value);
|
||||
set_monitor(this, m);
|
||||
}
|
||||
get {
|
||||
var m = get_monitor(this);
|
||||
var d = Gdk.Display.get_default();
|
||||
for (var i = 0; i < d.get_n_monitors(); ++i) {
|
||||
if (m == d.get_monitor(i))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CAUTION: the id might not be the same mapped by the compositor
|
||||
*/
|
||||
public uint get_num_monitors() {
|
||||
return Gdk.Display.get_default().get_n_monitors();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user