feat: make AGS colorshell configuration fully declarative

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

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

View File

@@ -0,0 +1,13 @@
import { Gtk } from "ags/gtk4";
import { Windows } from "../../../windows";
import { createBinding } from "ags";
import { tr } from "../../../i18n/intl";
export const Apps = () =>
<Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((openWindows) =>
`apps ${Object.hasOwn(openWindows, "apps-window") ? "open" : ""}`
)} iconName={"applications-other-symbolic"} halign={Gtk.Align.CENTER}
hexpand tooltipText={tr("apps")} onClicked={() =>
Windows.getDefault().open("apps-window")}
/>;

View File

@@ -0,0 +1,16 @@
import { Gtk } from "ags/gtk4";
import { Windows } from "../../../windows";
import { createBinding } from "ags";
import { time } from "../../../modules/utils";
import { generalConfig } from "../../../config";
export const Clock = () =>
<Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((wins) =>
`clock ${wins.includes("center-window") ? "open" : ""}`)}
onClicked={() => Windows.getDefault().toggle("center-window")}
label={time((dt) => dt.format(
generalConfig.getProperty("clock.date_format", "string"))
?? "An error occurred"
)}
/>;

View File

@@ -0,0 +1,42 @@
import { CompositorHyprland } from "../../../modules/compositors/hyprland";
import { Gtk } from "ags/gtk4";
import { createBinding, With } from "ags";
import { variableToBoolean } from "../../../modules/utils";
import { getAppIcon, getSymbolicIcon } from "../../../modules/apps";
import Pango from "gi://Pango?version=1.0";
const hyprland = new CompositorHyprland;
export const FocusedClient = () => {
const focusedClient = createBinding(hyprland, "focusedClient");
return <Gtk.Box class={"focused-client"} visible={variableToBoolean(focusedClient)}>
<With value={focusedClient}>
{(focusedClient) => focusedClient?.class && <Gtk.Box>
<Gtk.Image iconName={createBinding(focusedClient, "class").as((clss) =>
getSymbolicIcon(clss) ?? getAppIcon(clss) ??
getAppIcon(focusedClient.initialClass) ??
"application-x-executable-symbolic"
)} vexpand
/>
<Gtk.Box valign={Gtk.Align.CENTER} class={"text-content"}
orientation={Gtk.Orientation.VERTICAL}>
<Gtk.Label class={"class"} xalign={0} maxWidthChars={55}
ellipsize={Pango.EllipsizeMode.END}
label={createBinding(focusedClient, "class")}
tooltipText={createBinding(focusedClient, "class")}
/>
<Gtk.Label class={"title"} xalign={0} maxWidthChars={50}
ellipsize={Pango.EllipsizeMode.END}
label={createBinding(focusedClient, "title")}
tooltipText={createBinding(focusedClient, "title")}
/>
</Gtk.Box>
</Gtk.Box>}
</With>
</Gtk.Box>;
}

View File

@@ -0,0 +1,130 @@
import { createBinding, With } from "ags";
import { Gtk } from "ags/gtk4";
import { Separator } from "../../../widget/Separator";
import { Windows } from "../../../windows";
import { Clipboard } from "../../../modules/clipboard";
import { getPlayerIconFromBusName, secureBaseBinding, variableToBoolean } from "../../../modules/utils";
import { tr } from "../../../i18n/intl";
import { default as Player } from "../../../modules/media";
import AstalMpris from "gi://AstalMpris";
import Pango from "gi://Pango?version=1.0";
export const Media = () =>
<Gtk.Box class={"media"} visible={createBinding(Player.getDefault(), "player").as(p => p.available)}>
<Gtk.EventControllerScroll $={(self) => {
self.set_flags(Gtk.EventControllerScrollFlags.VERTICAL)
}} onScroll={(_, __, dy) => {
if(AstalMpris.get_default().players.length === 1 &&
Player.getDefault().player.busName === AstalMpris.get_default().players[0].busName)
return true;
const players = AstalMpris.get_default().players;
for(let i = 0; i < players.length; i++) {
const pl = players[i];
if(pl.busName !== Player.getDefault().player.busName)
continue;
if(dy > 0 && players[i-1]) {
Player.getDefault().player = players[i-1];
break;
}
if(dy < 0 && players[i+1]) {
Player.getDefault().player = players[i+1];
break;
}
}
return true;
}}
/>
<Gtk.GestureClick onReleased={() => Windows.getDefault().toggle("center-window")} />
<Gtk.EventControllerMotion onEnter={(self) => {
const revealer = self.get_widget()!.get_last_child() as Gtk.Revealer;
revealer.set_reveal_child(true);
}} onLeave={(self) => {
const revealer = self.get_widget()!.get_last_child() as Gtk.Revealer;
revealer.set_reveal_child(false);
}}
/>
<Gtk.Box spacing={4} visible={createBinding(Player.getDefault(), "player")
.as(p => p.available)}>
<With value={createBinding(Player.getDefault(), "player")
.as(p => p.available)}>
{(available: boolean) => available && <Gtk.Box>
<Gtk.Image class={"player-icon"} iconName={
secureBaseBinding<AstalMpris.Player>(createBinding(
Player.getDefault(), "player"
), "busName", "org.MediaPlayer2.folder-music-symbolic").as(
getPlayerIconFromBusName
)}
/>
<Gtk.Label class={"title"} label={secureBaseBinding<AstalMpris.Player>(createBinding(
Player.getDefault(), "player"
), "title", "").as(title => title ?? tr("media.no_title"))}
maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END}
/>
<Separator orientation={Gtk.Orientation.HORIZONTAL} size={1} margin={5}
alpha={.3} spacing={6} />
<Gtk.Label class={"artist"} label={secureBaseBinding<AstalMpris.Player>(createBinding(
Player.getDefault(), "player"
), "artist", "").as(artist => artist ?? tr("media.no_artist"))}
maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END}
/>
</Gtk.Box>}
</With>
</Gtk.Box>
<Gtk.Revealer transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT} transitionDuration={260}
revealChild={false}>
<With value={createBinding(Player.getDefault(), "player")
.as(p => p.available)}>
{(available: boolean) => available && <Gtk.Box class={"buttons"} spacing={4}>
<Gtk.Box class={"extra button-row"}>
<Gtk.Button class={"link"} iconName={"edit-paste-symbolic"}
visible={variableToBoolean(Player.accessMediaUrl(Player.getDefault().player))}
tooltipText={tr("copy_to_clipboard")} onClicked={() => {
const url = Player.getMediaUrl(Player.getDefault().player);
url && Clipboard.getDefault().copyAsync(url);
}}
/>
</Gtk.Box>
<Gtk.Box class={"media-controls button-row"}>
<Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"}
tooltipText={tr("media.previous")} onClicked={() =>
Player.getDefault().player.canGoPrevious &&
Player.getDefault().player.previous()
}
/>
<Gtk.Button class={"play-pause"} iconName={secureBaseBinding<AstalMpris.Player>(
createBinding(Player.getDefault(), "player"),
"playbackStatus",
AstalMpris.PlaybackStatus.PAUSED
).as(status => status === AstalMpris.PlaybackStatus.PAUSED ?
"media-playback-start-symbolic"
: "media-playback-pause-symbolic"
)}
tooltipText={secureBaseBinding<AstalMpris.Player>(
createBinding(Player.getDefault(), "player"),
"playbackStatus",
AstalMpris.PlaybackStatus.PAUSED
).as(status => status === AstalMpris.PlaybackStatus.PAUSED ?
tr("media.play") : tr("media.pause")
)} onClicked={() => Player.getDefault().player.play_pause()}
/>
<Gtk.Button class={"next"} iconName={"media-skip-forward-symbolic"}
tooltipText={tr("media.next")} onClicked={() => Player.getDefault().player.canGoNext &&
Player.getDefault().player.next()}
/>
</Gtk.Box>
</Gtk.Box>}
</With>
</Gtk.Revealer>
</Gtk.Box> as Gtk.Box;

View File

@@ -0,0 +1,218 @@
import { Gtk } from "ags/gtk4";
import { Wireplumber } from "../../../modules/volume";
import { Battery } from "../../../modules/battery";
import { Notifications } from "../../../modules/notifications";
import { Windows } from "../../../windows";
import { Recording } from "../../../modules/recording";
import { Accessor, createBinding, createComputed, With } from "ags";
import { variableToBoolean } from "../../../modules/utils";
import { Bluetooth } from "../../../modules/bluetooth";
import GObject from "ags/gobject";
import AstalBluetooth from "gi://AstalBluetooth";
import AstalNetwork from "gi://AstalNetwork";
import AstalWp from "gi://AstalWp";
export const Status = () =>
(
<Gtk.Button
class={createBinding(Windows.getDefault(), "openWindows").as((openWins) =>
openWins.includes("control-center") ? "open status" : "status"
)}
onClicked={() => { console.log("Status clicked!"); Windows.getDefault().toggle("control-center"); }}
>
<Gtk.Box>
<Gtk.Box class={"volume-indicators"} spacing={5}>
<BatteryStatus
visible={Battery.getDefault().bindHasBattery()}
class="battery"
icon={Battery.getDefault().bindIcon()}
percentage={Battery.getDefault().bindPercentage()}
></BatteryStatus>
<VolumeStatus
class="sink"
endpoint={Wireplumber.getDefault().getDefaultSink()}
icon={createBinding(
Wireplumber.getDefault().getDefaultSink(),
"volumeIcon"
).as((icon) =>
!Wireplumber.getDefault().isMutedSink() &&
Wireplumber.getDefault().getSinkVolume() > 0
? icon
: "audio-volume-muted-symbolic"
)}
/>
<VolumeStatus
class="source"
endpoint={Wireplumber.getDefault().getDefaultSource()}
icon={createBinding(
Wireplumber.getDefault().getDefaultSource(),
"volumeIcon"
).as((icon) =>
!Wireplumber.getDefault().isMutedSource() &&
Wireplumber.getDefault().getSourceVolume() > 0
? icon
: "microphone-sensitivity-muted-symbolic"
)}
/>
</Gtk.Box>
<Gtk.Revealer
revealChild={createBinding(Recording.getDefault(), "recording")}
transitionDuration={500}
transitionType={Gtk.RevealerTransitionType.SLIDE_LEFT}
>
<Gtk.Box>
<Gtk.Image
class={"recording state"}
iconName={"media-record-symbolic"}
css={"margin-right: 6px;"}
/>
<Gtk.Label
class={"rec-time"}
label={createBinding(Recording.getDefault(), "recordingTime")}
/>
</Gtk.Box>
</Gtk.Revealer>
<StatusIcons />
</Gtk.Box>
</Gtk.Button>
) as Gtk.Button;
function VolumeStatus(props: {
class?: string;
endpoint: AstalWp.Endpoint;
icon?: string | Accessor<string>;
}) {
return (
<Gtk.Box
spacing={2}
class={props.class}
$={(self) => {
const conns: Map<GObject.Object, number> = new Map();
const controllerScroll = Gtk.EventControllerScroll.new(
Gtk.EventControllerScrollFlags.VERTICAL |
Gtk.EventControllerScrollFlags.KINETIC
);
conns.set(
controllerScroll,
controllerScroll.connect("scroll", (_, _dx, dy) => {
console.log`Scrolled! dx: ${_dx}; dy: ${dy}`;
dy > 0
? Wireplumber.getDefault().decreaseEndpointVolume(
props.endpoint,
5
)
: Wireplumber.getDefault().increaseEndpointVolume(
props.endpoint,
5
);
return true;
})
);
conns.set(
self,
self.connect("destroy", () =>
conns.forEach((id, obj) => obj.disconnect(id))
)
);
}}
>
{props.icon && <Gtk.Image iconName={props.icon} />}
<Gtk.Label
class={"volume"}
label={createBinding(props.endpoint, "volume").as(
(vol) => `${Math.floor(vol * 100)}%`
)}
/>
</Gtk.Box>
) as Gtk.Box;
}
function BatteryStatus(props: {
visible?: Accessor<boolean>;
class?: string;
percentage?: Accessor<string>;
icon?: string | Accessor<string>;
}) {
return (
<Gtk.Box visible={props.visible} spacing={2} class={props.class}>
{props.icon && <Gtk.Image iconName={props.icon} />}
<Gtk.Label class={"level"} label={props.percentage} />
</Gtk.Box>
) as Gtk.Box;
}
function StatusIcons() {
return (
<Gtk.Box class={"status-icons"} spacing={8}>
<Gtk.Image
iconName={createComputed(
[
createBinding(AstalBluetooth.get_default(), "isPowered"),
createBinding(AstalBluetooth.get_default(), "isConnected"),
],
(powered, connected) => {
return powered
? connected
? "bluetooth-active-symbolic"
: "bluetooth-symbolic"
: "bluetooth-disabled-symbolic";
}
)}
class={"bluetooth state"}
visible={createBinding(Bluetooth.getDefault(), "adapter").as(Boolean)}
/>
<Gtk.Box
visible={createBinding(AstalNetwork.get_default(), "primary").as(
(primary) => primary !== AstalNetwork.Primary.UNKNOWN
)}
>
<With value={createBinding(AstalNetwork.get_default(), "primary")}>
{(primary: AstalNetwork.Primary) => {
let device: AstalNetwork.Wifi | AstalNetwork.Wired;
switch (primary) {
case AstalNetwork.Primary.WIRED:
device = AstalNetwork.get_default().wired;
break;
case AstalNetwork.Primary.WIFI:
device = AstalNetwork.get_default().wifi;
break;
default:
return <Gtk.Image iconName={"network-no-route-symbolic"} />;
}
return <Gtk.Image iconName={createBinding(device, "iconName")} />;
}}
</With>
</Gtk.Box>
<Gtk.Box>
<Gtk.Image
class={"bell state"}
iconName={createBinding(
Notifications.getDefault().getNotifd(),
"dontDisturb"
).as((dnd) =>
dnd
? "minus-circle-filled-symbolic"
: "preferences-system-notifications-symbolic"
)}
/>
<Gtk.Image
iconName={"circle-filled-symbolic"}
class={"notification-count"}
visible={variableToBoolean(
createBinding(Notifications.getDefault(), "history")
)}
/>
</Gtk.Box>
</Gtk.Box>
);
}

View File

@@ -0,0 +1,60 @@
import { createBinding, createComputed, For, With } from "ags";
import { Gdk, Gtk } from "ags/gtk4";
import { variableToBoolean } from "../../../modules/utils";
import GObject from "gi://GObject?version=2.0";
import AstalTray from "gi://AstalTray"
import Gio from "gi://Gio?version=2.0";
const astalTray = AstalTray.get_default();
export const Tray = () => {
const items = createBinding(astalTray, "items").as(items => items.filter(item => item?.gicon));
return <Gtk.Box class={"tray"} visible={variableToBoolean(items)} spacing={10}>
<For each={items}>
{(item: AstalTray.TrayItem) => <Gtk.Box class={"item"}>
<With value={createComputed([
createBinding(item, "actionGroup"),
createBinding(item, "menuModel")
])}>
{([actionGroup, menuModel]: [Gio.ActionGroup, Gio.MenuModel]) => {
const popover = Gtk.PopoverMenu.new_from_model(menuModel);
popover.insert_action_group("dbusmenu", actionGroup);
popover.hasArrow = false;
return <Gtk.Box class={"item"} tooltipMarkup={
createBinding(item, "tooltipMarkup")
} tooltipText={
createBinding(item, "tooltipText")
} $={(self) => {
const conns: Map<GObject.Object, number> = new Map();
const gestureClick = Gtk.GestureClick.new();
gestureClick.set_button(0);
self.add_controller(gestureClick);
// Set popover parent to this box
popover.set_parent(self);
conns.set(gestureClick, gestureClick.connect("released", (gesture, _, x, y) => {
if(gesture.get_current_button() === Gdk.BUTTON_PRIMARY) {
item.activate(x, y);
return;
}
if(gesture.get_current_button() === Gdk.BUTTON_SECONDARY) {
item.about_to_show();
popover.popup();
}
}))
}}>
<Gtk.Image gicon={createBinding(item, "gicon")} pixelSize={16} />
</Gtk.Box>;
}}
</With>
</Gtk.Box>}
</For>
</Gtk.Box>
}

View File

@@ -0,0 +1,144 @@
import { Gtk } from "ags/gtk4";
import { getAppIcon, getSymbolicIcon } from "../../../modules/apps";
import { Separator } from "../../../widget/Separator";
import { generalConfig } from "../../../config";
import { createBinding, createComputed, createState, For, With } from "ags";
import { variableToBoolean } from "../../../modules/utils";
import AstalHyprland from "gi://AstalHyprland";
const [showNumbers, setShowNumbers] = createState(false);
export const showWorkspaceNumber = (show: boolean) =>
setShowNumbers(show);
export const Workspaces = () => {
const workspaces = createBinding(AstalHyprland.get_default(), "workspaces"),
defaultWorkspaces = workspaces.as(wss =>
wss.filter(ws => ws.id > 0).sort((a, b) => a.id - b.id)),
specialWorkspaces = workspaces.as(wss =>
wss.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id)),
focusedWorkspace = createBinding(AstalHyprland.get_default(), "focusedWorkspace");
return <Gtk.Box class={"workspaces-row"} visible={createComputed([
workspaces.as(wss => wss.length <= 1),
generalConfig.bindProperty("workspaces.hide_if_single", "boolean")
], (hideable, enabled) => enabled && hideable ? false : true
)}>
<Gtk.Box class={"special-workspaces"} spacing={4}>
<For each={specialWorkspaces}>
{(ws: AstalHyprland.Workspace) =>
<Gtk.Button class={"workspace"}
tooltipText={createBinding(ws, "name").as(name => {
name = name.replace(/^special\:/, "");
return name.charAt(0).toUpperCase().concat(name.substring(1, name.length));
})} onClicked={() => AstalHyprland.get_default().dispatch(
"togglespecialworkspace", ws.name.replace(/^special[:]/, "")
)}>
<With value={createBinding(ws, "lastClient")}>
{(lastClient: AstalHyprland.Client|null) => lastClient &&
<Gtk.Image class="last-client" iconName={
createBinding(lastClient, "initialClass").as(initialClass =>
getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ??
"application-x-executable-symbolic")}
/>
}
</With>
</Gtk.Button>
}
</For>
</Gtk.Box>
<Gtk.Revealer transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT}
transitionDuration={220} revealChild={variableToBoolean(specialWorkspaces)}>
<Separator alpha={.2} orientation={Gtk.Orientation.HORIZONTAL}
margin={12} spacing={8} visible={variableToBoolean(specialWorkspaces)}
/>
</Gtk.Revealer>
<Gtk.Box class={"default-workspaces"} spacing={4}>
<Gtk.EventControllerScroll $={(self) => self.set_flags(Gtk.EventControllerScrollFlags.VERTICAL)}
onScroll={(_, __, dy) => {
dy > 0 ?
AstalHyprland.get_default().dispatch("workspace", "e-1")
: AstalHyprland.get_default().dispatch("workspace", "e+1");
return true;
}}
/>
<Gtk.EventControllerMotion onEnter={() => setShowNumbers(true)}
onLeave={() => setShowNumbers(false)}
/>
<For each={defaultWorkspaces}>
{(ws: AstalHyprland.Workspace, i) => {
const showId = createComputed([
generalConfig.bindProperty("workspaces.always_show_id", "boolean").as(Boolean),
generalConfig.bindProperty("workspaces.enable_helper", "boolean").as(Boolean),
showNumbers,
i
], (alwaysShowIds, enableHelper, showIds, i) => {
if(enableHelper && !alwaysShowIds) {
const previousWorkspace = defaultWorkspaces.get()[i-1];
const nextWorkspace = defaultWorkspaces.get()[i+1];
if((defaultWorkspaces.get().filter((_, ii) => ii < i).length > 0 &&
previousWorkspace?.id < (ws.id-1)) ||
(defaultWorkspaces.get().filter((_, ii) => ii > i).length > 0 &&
nextWorkspace?.id > (ws.id+1))
|| (i === 0 && ws.id > 1)) {
return true;
}
}
return alwaysShowIds || showIds;
});
return <Gtk.Button class={createComputed([
createBinding(AstalHyprland.get_default(), "focusedWorkspace"),
showId
], (focusedWs, showWsNumbers) =>
`workspace ${focusedWs.id === ws.id ? "focus" : ""} ${
showWsNumbers ? "show" : ""}`
)} tooltipText={createComputed([
createBinding(ws, "lastClient"),
createBinding(AstalHyprland.get_default(), "focusedWorkspace")
], (lastClient, focusWs) => focusWs.id === ws.id ? "" :
`workspace ${ws.id}${ lastClient ? ` - ${
!lastClient.title.toLowerCase().includes(lastClient.class) ?
`${lastClient.get_class()}: `
: ""
} ${lastClient.title}` : "" }`
)} onClicked={() => focusedWorkspace.get()?.id !== ws.id && ws.focus()}>
<With value={createBinding(ws, "lastClient")}>
{(lastClient: AstalHyprland.Client) =>
<Gtk.Box class={"last-client"} hexpand>
<Gtk.Revealer transitionDuration={280} revealChild={showId}
transitionType={focusedWorkspace.as(
fws => fws.id !== ws.id ?
Gtk.RevealerTransitionType.SLIDE_LEFT
: Gtk.RevealerTransitionType.SLIDE_RIGHT
)}>
<Gtk.Label label={createBinding(ws, "id").as(String)}
class={"id"} hexpand
/>
</Gtk.Revealer>
{lastClient && <Gtk.Image class={"last-client-icon"} iconName={
createBinding(lastClient, "initialClass").as(initialClass =>
getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ??
"application-x-executable-symbolic")}
hexpand vexpand visible={createBinding(AstalHyprland.get_default(), "focusedWorkspace")
.as(fws => fws.id !== ws.id)}
/>}
</Gtk.Box>
}
</With>
</Gtk.Button>
}}
</For>
</Gtk.Box>
</Gtk.Box>
}