summaryrefslogtreecommitdiff
path: root/widget/bar/Bar.tsx
blob: 749270869ecdca201154e67c62b331ffafa16281 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import { App, Astal, Gtk, Gdk } from "astal/gtk4"
import { bind, exec, GLib, Variable } from "astal"
import AstalTray from "gi://AstalTray?version=0.1";
import AstalWp from "gi://AstalWp?version=0.1";

type NiriWorkspace = {
  id: number,
  idx: number,
  name: string | null,
  output: string,
  is_active: boolean,
  is_focused: boolean,
  active_window_id: number | null,
};

function getWorkspaces(): NiriWorkspace[] {
  // NOTE: this works only in non-systemd environment on NixOS
  // TODO: try to use Niri socket if it is documented
  return JSON.parse(exec("niri msg -j workspaces"));
}

function getWorkspacesByOutput(output: string): NiriWorkspace[] {
  return getWorkspaces().filter(workspace => workspace.output == output).sort((a, b) => a.idx - b.idx);
}

function focusWorkspace(idx: number) {
  // NOTE: this works only in non-systemd environment on NixOS
  // TODO: try to use Niri socket if it is documented
  exec(`niri msg action focus-workspace ${idx}`);
}

type WorkspaceButtonArguments = {
  idx: number,
  isActive: boolean,
  isFocused: boolean,
};

function WorkspaceButton(args: WorkspaceButtonArguments) {
  const classes: string[] = [];
  args.isActive && classes.push("active");
  args.isFocused && classes.push("focused");
  return <button cssClasses={classes} onClicked={() => focusWorkspace(args.idx)} />
}

type WorkspacesArguments = {
  connector: string,
};

function Workspaces(args: WorkspacesArguments) {
  // NOTE: it is pretty inefficient and not so much responsive
  // TODO: it would be better to use Niri socket in the future
  const workspaces: Variable<NiriWorkspace[]> = Variable(getWorkspacesByOutput(args.connector))
    .poll(1000, () => getWorkspacesByOutput(args.connector));

  // BUG: on workspace change there's no CSS transition
  // I guess it is due to rerender here
  return <box cssClasses={["Workspaces"]}>
    {workspaces(v => v.map(workspace => <WorkspaceButton idx={workspace.idx} isActive={workspace.is_active} isFocused={workspace.is_focused} />))}
  </box>
}


function AudioVolume() {
  const wireplumber = AstalWp.get_default()!;
  const speaker = wireplumber.audio.get_default_speaker()!;

  return <box cssClasses={["AudioVolume"]}>
    <image iconName={bind(speaker, "volumeIcon")} />
    {/* {bind(speaker, "volume")} */}
    <slider
      hexpand
      onScroll={(_self, dx, dy) => speaker.volume += (dx + dy) * -0.05}
      // BUG: this doesn't work due to value being updated immediately with dragging
      // so that new value is never reached (slider "freezes")
      // onChangeValue={({ value }) => new_volume = value}
      // onKeyReleased={() => speaker.volume = new_volume}
      value={bind(speaker, "volume")}
    />
  </box>;
}


function Tray() {
  // BUG: personally I have one fantom icon being along other tray icons
  // For now I don't have any ideas why this is happening
  // TODO: rewrite this using more elements, as this is really restricted design
  const tray = AstalTray.get_default();

  return <box cssClasses={["Tray"]}>
    {bind(tray, "items").as(items =>
      items.map(item =>
        <menubutton
          setup={self => self.insert_action_group("dbusmenu", item.actionGroup)}
          tooltipText={bind(item, "tooltipMarkup")}
        >
          <image gicon={bind(item, "gicon")} />
          {Gtk.PopoverMenu.new_from_model(item.menuModel)}
        </menubutton>
      )
    )}
  </box>
}


type TimeArguments = {
  format: string,
};

function Time(args: TimeArguments) {
  const time = Variable<GLib.DateTime>(GLib.DateTime.new_now_local()).poll(1000, () => GLib.DateTime.new_now_local())

  return <box>
    {time(v => v.format(args.format))}
  </box>
}


export default function Bar(gdkmonitor: Gdk.Monitor) {
  const { TOP, LEFT, RIGHT } = Astal.WindowAnchor

  return <window
    visible
    name={"Bar"}
    cssClasses={["Bar"]}
    gdkmonitor={gdkmonitor}
    exclusivity={Astal.Exclusivity.EXCLUSIVE}
    anchor={TOP | LEFT | RIGHT}
    application={App}>
    <centerbox cssClasses={["bar-container"]}>
      <box halign={Gtk.Align.START}>
        <Workspaces connector={gdkmonitor.get_connector()!} />
      </box>

      <box halign={Gtk.Align.CENTER}>
      </box>

      <box halign={Gtk.Align.END}>
        <AudioVolume />
        <Tray />
        <Time format="%I:%M:%S %p %Z" />
      </box>
    </centerbox>
  </window>
}