diff --git a/resources/styles/_control-center.scss b/resources/styles/_control-center.scss index efae207..2a76c28 100644 --- a/resources/styles/_control-center.scss +++ b/resources/styles/_control-center.scss @@ -199,78 +199,67 @@ .tiles-container { @include mixins.reset-props; - & > flowbox { - & > flowboxchild .tile { - $radius: 18px; + & .tile { + $radius: 18px; + $padding: 4px; - &:not(.toggled) > .toggle-button, - &:not(.toggled) > button.more { - background: colors.$bg-primary; + background: rgba(colors.$bg-primary, .5); + border-radius: $radius; + padding: $padding; + min-height: 40px; + + & .icon { + transition: 120ms ease-in; + border-radius: calc($radius - $padding); + padding: 8px 12px; + margin-right: 6px; + background: color.scale($color: colors.$bg-primary, $lightness: 10%); + + & image { + -gtk-icon-size: 18px; } - &:not(.toggled) > .toggle-button:hover, - &:not(.toggled) > button.more:hover { - background: color.scale($color: colors.$bg-primary, $lightness: 10%); + &:hover { + background: color.scale($color: colors.$bg-primary, $lightness: 15%); } - &.toggled .toggle-button:hover, - &.toggled button.more:hover { + &:active { + border-radius: calc($radius - $padding - 2px); + } + } + + & .content { + & .title { + font-weight: 600; + font-size: 15.1px; + } + + & .description { + font-size: 12px; + color: colors.$fg-disabled; + font-weight: 400; + } + } + + & .arrow { + -gtk-icon-size: 12px; + color: rgba(colors.$fg-disabled, .4); + } + + &:hover { + background: color.scale($color: colors.$bg-primary, $lightness: 5%); + } + + &.enabled .icon { + background: colors.$bg-secondary; + + &:hover { background: colors.$bg-tertiary; } + } - &.toggled > .toggle-button, - &.toggled > button.more { - background: colors.$bg-secondary; - } - - &.has-more > .toggle-button, - &.has-more > button.toggle-button:active { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - & > button.toggle-button { - border-radius: $radius; - - &:active { - border-radius: calc($radius - 4px); - } - - & .content { - padding: 8px; - - & > .icon { - margin-right: 6px; - } - - & > .text { - & > .title { - font-weight: 600; - font-size: 15.1px; - } - - & > .description { - font-size: 12px; - color: colors.$fg-disabled; - font-weight: 400; - } - } - } - } - - & > button.more { - border-top-right-radius: $radius; - border-bottom-right-radius: $radius; - - &:active { - border-top-right-radius: calc($radius - 4px); - border-bottom-right-radius: calc($radius - 4px); - } - - & label { - font-size: 16px; - } - } + &:active { + border-radius: calc($radius - 2px); } } } diff --git a/src/modules/arg-handler.ts b/src/modules/arg-handler.ts index 90b73a5..ad214b3 100644 --- a/src/modules/arg-handler.ts +++ b/src/modules/arg-handler.ts @@ -10,6 +10,7 @@ import { generalConfig, Shell } from "../app"; import AstalIO from "gi://AstalIO"; import AstalMpris from "gi://AstalMpris"; +import { Gtk } from "ags/gtk4"; export type RemoteCaller = { @@ -21,30 +22,33 @@ let wsTimeout: AstalIO.Time|undefined; const help = `Manage Astal Windows and do more stuff. From retrozinndev's colorshell, \ made using GTK4, AGS, Gnim and Astal libraries by Aylur. - Window Management: - open [window]: opens the specified window. - close [window]: closes all instances of specified window. - toggle [window]: toggle-open/close the specified window. - windows: list shell windows and their respective status. - reload: quit this instance and start a new one. - reopen: restart all open-windows. - quit: exit the main instance of the shell. +Window Management: + open [window]: opens the specified window. + close [window]: closes all instances of specified window. + toggle [window]: toggle-open/close the specified window. + windows: list shell windows and their respective status. + reload: quit this instance and start a new one. + reopen: restart all open-windows. + quit: exit the main instance of the shell. - Audio Controls: - volume: speaker and microphone volume controller, see "volume help". +Audio Controls: + volume: speaker and microphone volume controller, see "volume help". - Media Controls: - media: manage colorshell's active player, see "media help". - - Other options: - runner [initial_text]: open the application runner, optionally add an initial search. - peek-workspace-num [millis]: peek the workspace numbers on bar window. - v, version: display current colorshell version. - h, help: shows this help message. +Media Controls: + media: manage colorshell's active player, see "media help". +${DEVEL ? ` +Development Tools: + dev: tools to help debugging colorshell +` : ""} +Other options: + runner [initial_text]: open the application runner, optionally add an initial search. + peek-workspace-num [millis]: peek the workspace numbers on bar window. + v, version: display current colorshell version. + h, help: shows this help message. - 2025 (c) retrozinndev's colorshell, licensed under the MIT License. - https://github.com/retrozinndev/colorshell - `.split('\n').map(l => l.replace(/^ {8}/, "")).join('\n'); +2025 (c) retrozinndev's colorshell, licensed under the MIT License. +https://github.com/retrozinndev/colorshell +`.trim(); export function handleArguments(cmd: RemoteCaller, args: Array): number { switch(args[0]) { @@ -59,6 +63,9 @@ export function handleArguments(cmd: RemoteCaller, args: Array): number }${DEVEL ? " (devel)" : ""}\nhttps://github.com/retrozinndev/colorshell`); return 0; + case "dev": + return handleDevArgs(cmd, args); + case "open": case "close": case "toggle": @@ -115,6 +122,28 @@ export function handleArguments(cmd: RemoteCaller, args: Array): number return 1; } +function handleDevArgs(cmd: RemoteCaller, args: Array): number { + if(/h|help/.test(args[1])) { + cmd.print_literal(` +Debugging tools for colorshell. + +Options: + inspector: open GTK's visual debugger +`.trim()); + return 0; + } + + switch(args[1]) { + case "inspector": + cmd.print_literal("Opening inspector..."); + Gtk.Window.set_interactive_debugging(true); + return 0; + } + + cmd.printerr_literal("Error: command not found! try checking `dev help`"); + return 1; +} + function handleMediaArgs(cmd: RemoteCaller, args: Array): number { if(/h|help/.test(args[1])) { const mediaHelp = ` diff --git a/src/widget/control-center/tiles/Bluetooth.tsx b/src/widget/control-center/tiles/Bluetooth.tsx index 0d49cda..9802033 100644 --- a/src/widget/control-center/tiles/Bluetooth.tsx +++ b/src/widget/control-center/tiles/Bluetooth.tsx @@ -7,16 +7,17 @@ import { createBinding, createComputed } from "ags"; export const TileBluetooth = () => { - const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0]; - return connected && connectedDev ? connectedDev.get_alias() : "" - })} onToggledOn={() => AstalBluetooth.get_default().adapter?.set_powered(true)} - onToggledOff={() => AstalBluetooth.get_default().adapter?.set_powered(false)} - onClickMore={() => TilesPages?.toggle(BluetoothPage)} - enableOnClickMore={true} iconSize={16} - toggleState={createBinding(AstalBluetooth.get_default(), "isPowered")} - icon={createComputed([ + createBinding(AstalBluetooth.get_default(), "adapter").as(Boolean) + } description={createBinding(AstalBluetooth.get_default(), "isConnected").as((connected) => { + const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0]; + return connected && connectedDev ? connectedDev.get_alias() : "" + })} + onEnabled={() => AstalBluetooth.get_default().adapter?.set_powered(true)} + onDisabled={() => AstalBluetooth.get_default().adapter?.set_powered(false)} + onClicked={() => TilesPages?.toggle(BluetoothPage)} + enableOnClicked hasArrow + state={createBinding(AstalBluetooth.get_default(), "isPowered")} + icon={createComputed([ createBinding(AstalBluetooth.get_default(), "isPowered"), createBinding(AstalBluetooth.get_default(), "isConnected") ], @@ -24,5 +25,6 @@ export const TileBluetooth = () => powered ? ( isConnected ? "bluetooth-active-symbolic" : "bluetooth-symbolic" - ) : "bluetooth-disabled-symbolic")} + ) : "bluetooth-disabled-symbolic") + } />; diff --git a/src/widget/control-center/tiles/DoNotDisturb.tsx b/src/widget/control-center/tiles/DoNotDisturb.tsx index 02568ec..8985d36 100644 --- a/src/widget/control-center/tiles/DoNotDisturb.tsx +++ b/src/widget/control-center/tiles/DoNotDisturb.tsx @@ -7,9 +7,8 @@ export const TileDND = () => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled"))} - onToggledOff={() => Notifications.getDefault().getNotifd().dontDisturb = false} - onToggledOn={() => Notifications.getDefault().getNotifd().dontDisturb = true} + onDisabled={() => Notifications.getDefault().getNotifd().dontDisturb = false} + onEnabled={() => Notifications.getDefault().getNotifd().dontDisturb = true} icon={"minus-circle-filled-symbolic"} - iconSize={16} - toggleState={Notifications.getDefault().getNotifd().dontDisturb} + state={Notifications.getDefault().getNotifd().dontDisturb} />; diff --git a/src/widget/control-center/tiles/Network.tsx b/src/widget/control-center/tiles/Network.tsx index c2eee33..5dcbd22 100644 --- a/src/widget/control-center/tiles/Network.tsx +++ b/src/widget/control-center/tiles/Network.tsx @@ -29,11 +29,11 @@ export const TileNetwork = () => return tr("connecting") + "..."; } })() - )} onToggledOn={() => wifi.set_enabled(true)} - onToggledOff={() => wifi.set_enabled(false)} - onClickMore={() => TilesPages?.toggle(PageNetwork)} + )} onEnabled={() => wifi.set_enabled(true)} + onDisabled={() => wifi.set_enabled(false)} + hasArrow onClicked={() => TilesPages?.toggle(PageNetwork)} icon={"network-wireless-signal-excellent-symbolic"} - toggleState={createBinding(wifi, "enabled")} + state={createBinding(wifi, "enabled")} /> } else if(primary === AstalNetwork.Primary.WIRED) { @@ -48,9 +48,9 @@ export const TileNetwork = () => return tr("connecting") + "..."; } })} - onToggledOn={() => execAsync("nmcli n on")} - onToggledOff={() => execAsync("nmcli n off")} - onClickMore={() => TilesPages?.toggle(PageNetwork)} + hasArrow onEnabled={() => execAsync("nmcli n on")} + onDisabled={() => execAsync("nmcli n off")} + onClicked={() => TilesPages?.toggle(PageNetwork)} icon={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => { switch(internet) { case AstalNetwork.Internet.CONNECTED: @@ -61,8 +61,7 @@ export const TileNetwork = () => return "network-wired-no-route-symbolic"; })} - iconSize={16} - toggleState={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => + state={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED )} @@ -72,12 +71,11 @@ export const TileNetwork = () => return execAsync("nmcli n on")} - onToggledOff={() => execAsync("nmcli n off")} - onClickMore={() => TilesPages?.toggle(PageNetwork)} + onEnabled={() => execAsync("nmcli n on")} + onDisabled={() => execAsync("nmcli n off")} + hasArrow onClicked={() => TilesPages?.toggle(PageNetwork)} icon={"network-wired-disconnected-symbolic"} - iconSize={16} - toggleState={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => + state={createBinding(wired, "internet").as((internet: AstalNetwork.Internet) => internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)} /> }} diff --git a/src/widget/control-center/tiles/NightLight.tsx b/src/widget/control-center/tiles/NightLight.tsx index d63ad45..04852e4 100644 --- a/src/widget/control-center/tiles/NightLight.tsx +++ b/src/widget/control-center/tiles/NightLight.tsx @@ -16,10 +16,10 @@ export const TileNightLight = () => tr("control_center.tiles.night_light.default_desc") : `${temp}K`} ${ gamma < NightLight.getDefault().maxGamma ? `(${gamma}%)` : ""}` )} - visible={isInstalled("hyprsunset")} - onToggledOff={() => NightLight.getDefault().identity = true} - onToggledOn={() => NightLight.getDefault().identity = false} - enableOnClickMore={true} - onClickMore={() => TilesPages?.toggle(PageNightLight)} - toggleState={createBinding(NightLight.getDefault(), "identity").as(identity => !identity)} + hasArrow visible={isInstalled("hyprsunset")} + onDisabled={() => NightLight.getDefault().identity = true} + onEnabled={() => NightLight.getDefault().identity = false} + enableOnClicked + onClicked={() => TilesPages?.toggle(PageNightLight)} + state={createBinding(NightLight.getDefault(), "identity").as(identity => !identity)} /> diff --git a/src/widget/control-center/tiles/Recording.tsx b/src/widget/control-center/tiles/Recording.tsx index b488741..6d3f660 100644 --- a/src/widget/control-center/tiles/Recording.tsx +++ b/src/widget/control-center/tiles/Recording.tsx @@ -24,8 +24,7 @@ export const TileRecording = () => })} icon={"media-record-symbolic"} visible={isInstalled("wf-recorder")} - onToggledOff={() => Recording.getDefault().stopRecording()} - onToggledOn={() => Recording.getDefault().startRecording()} - toggleState={createBinding(Recording.getDefault(), "recording")} - iconSize={16} + onDisabled={() => Recording.getDefault().stopRecording()} + onEnabled={() => Recording.getDefault().startRecording()} + state={createBinding(Recording.getDefault(), "recording")} />; diff --git a/src/widget/control-center/tiles/Tile.tsx b/src/widget/control-center/tiles/Tile.tsx index 20bd100..ac793f3 100644 --- a/src/widget/control-center/tiles/Tile.tsx +++ b/src/widget/control-center/tiles/Tile.tsx @@ -1,30 +1,13 @@ import { Gtk } from "ags/gtk4"; -import { tr } from "../../../i18n/intl"; -import { Accessor, createBinding, createComputed, createState, getScope, onCleanup } from "ags"; +import { createBinding } from "ags"; import { omitObjectKeys, variableToBoolean } from "../../../modules/utils"; -import GObject, { property, register, signal } from "ags/gobject"; - +import { property, register, signal } from "ags/gobject"; import Pango from "gi://Pango?version=1.0"; -export { Tile, TileProps }; +export { Tile }; -type TileProps = { - class?: string | Accessor; - icon?: string | Accessor; - visible?: boolean | Accessor; - iconSize?: number | Accessor; - title: string | Accessor; - description?: string | Accessor; - toggleState?: boolean | Accessor; - enableOnClickMore?: boolean | Accessor; - onUnmap?: () => void; - onToggledOn: () => void; - onToggledOff: () => void; - onClickMore?: () => void; -}; -/* TODO: finish the tile class @register({ GTypeName: "Tile" }) class Tile extends Gtk.Box { @signal(Boolean) toggled(_state: boolean) {} @@ -42,6 +25,8 @@ class Tile extends Gtk.Box { public enableOnClicked: boolean = true; @property(Boolean) public state: boolean = false; + @property(Boolean) + public hasArrow: boolean = false; declare $signals: Gtk.Box.SignalSignatures & { "toggled": (_state: boolean) => void; @@ -53,25 +38,29 @@ class Tile extends Gtk.Box { public enable(): void { if(this.state) return; + this.state = true; + !this.has_css_class("enabled") && + this.add_css_class("enabled"); this.emit("toggled", true); this.emit("enabled"); - this.state = true; } public disable(): void { if(!this.state) return; + this.state = false; + this.remove_css_class("enabled"); this.emit("toggled", false); this.emit("disabled"); - this.state = false; } - constructor(props: Omit & { + constructor(props: Partial> & { icon: string; title: string; description?: string; state?: boolean; enableOnClicked?: boolean; + hasArrow?: boolean; }) { super(omitObjectKeys(props, [ "icon", @@ -80,10 +69,15 @@ class Tile extends Gtk.Box { "state", "enableOnClicked" ])); - + + this.add_css_class("tile"); + this.icon = props.icon; this.title = props.title; + if(props.hasArrow != null) + this.hasArrow = props.hasArrow; + if(props.description != null) this.description = props.description; @@ -93,32 +87,52 @@ class Tile extends Gtk.Box { if(props.enableOnClicked != null) this.enableOnClicked = props.enableOnClicked; - const connections = new Map(); - const gestureClick = Gtk.GestureClick.new(); - - this.add_controller(gestureClick); - - connections.set(gestureClick, gestureClick.connect("released", () => { - this.emit("clicked"); - if(this.enableOnClicked && !this.state) - this.enable(); - return true; - })); + if(this.state) + this.add_css_class("enabled"); // fix no highlight with state = true on construct this.prepend( - - + + + { + this.state ? this.disable() : this.enable(); + }} /> as Gtk.Box ); this.append( - - - + + + + + + { + this.emit("clicked"); + if(this.enableOnClicked && !this.state) + this.enable(); + + return true; + }} /> as Gtk.Box ); - getScope()?.onCleanup(() => connections.forEach((id, obj) => obj.disconnect(id))); + if(this.hasArrow) + this.append( + + { + this.emit("clicked"); + if(this.enableOnClicked && !this.state) + this.enable(); + + return true; + }} /> + as Gtk.Image + ); } emit( @@ -135,81 +149,3 @@ class Tile extends Gtk.Box { return super.connect(signal, callback); } } -*/ - -function Tile(props: TileProps): Gtk.Widget { - const subs: Array<() => void> = []; - const [toggled, setToggled] = createState(((props.toggleState instanceof Accessor) ? - props.toggleState.get() - : props.toggleState) ?? false); - - - (props.toggleState instanceof Accessor) && subs.push( - props.toggleState.subscribe(() => - setToggled((props.toggleState as Accessor).get() ?? false)) - ); - - onCleanup(() => subs.forEach(s => s())); - - return - `tile ${clss} ${isToggled ? "toggled" : ""} ${ - props.onClickMore ? "has-more" : "" - }` - ) - : toggled.as(isToggled => - `tile ${props.class ? props.class : ""} ${isToggled ? "toggled" : ""} ${ - props.onClickMore ? "has-more" : "" - }` - ) - }> - { - if(toggled.get()) { - setToggled(false); - props.onToggledOff?.(); - return; - } - - setToggled(true); - props.onToggledOn?.(); - }}> - - - {props.icon && `font-size: ${size}px;`) - : (props.iconSize ? - `font-size: ${props.iconSize ?? 16}px;` - : undefined) - } />} - - - - - - {props.description && str ?? "") - : (props.description ?? "") - } halign={Gtk.Align.START} - />} - - - - - - { - ((props.enableOnClickMore instanceof Accessor) ? - props.enableOnClickMore.get() - : props.enableOnClickMore) && props.onToggledOn?.(); - - props.onClickMore?.(); - }} tooltipText={tr("control_center.tiles.more")} /> - as Gtk.Widget; -}