Skip to content

Commit aa899aa

Browse files
committed
feat(bar): Begin implementing Sway IPC connection
1 parent c8e0120 commit aa899aa

File tree

5 files changed

+296
-4
lines changed

5 files changed

+296
-4
lines changed

packages/memex_bar/lib/bar.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:memex_bar/main.dart';
22
import 'package:memex_bar/modules/datetime.dart';
3+
import 'package:memex_bar/modules/sway.dart';
34
import 'modules/wifi.dart';
45
import 'package:memex_ui/memex_ui.dart';
56

@@ -10,10 +11,7 @@ class Bar extends ReactiveWidget {
1011
crossAxisAlignment: CrossAxisAlignment.center,
1112
children: [
1213
const SizedBox(width: 16),
13-
Text(
14-
"Window Title",
15-
style: MemexTypography.body.copyWith(fontWeight: FontWeight.bold),
16-
),
14+
const SwayModule(),
1715
const Spacer(),
1816
WifiModule(),
1917
const SizedBox(width: 24),
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
import 'dart:typed_data';
4+
import 'package:flutter/services.dart';
5+
import 'package:memex_ui/memex_ui.dart';
6+
7+
enum MessageType {
8+
RUN_COMMAND,
9+
GET_WORKSPACES,
10+
SUBSCRIBE,
11+
GET_OUTPUTS,
12+
GET_TREE,
13+
GET_MARKS,
14+
GET_BAR_CONFIG,
15+
GET_VERSION,
16+
GET_BINDING_MODES,
17+
GET_CONFIG,
18+
SEND_TICK,
19+
SYNC,
20+
GET_BINDING_STATE,
21+
GET_INPUTS,
22+
GET_SEATS,
23+
}
24+
25+
class Rect {
26+
final int x;
27+
final int y;
28+
final int width;
29+
final int height;
30+
31+
Rect({
32+
required this.x,
33+
required this.y,
34+
required this.width,
35+
required this.height,
36+
});
37+
38+
static Rect fromJson(Map<String, dynamic> json) {
39+
return Rect(
40+
x: json['x'],
41+
y: json['y'],
42+
width: json['width'],
43+
height: json['height'],
44+
);
45+
}
46+
}
47+
48+
class Workspace {
49+
final int id;
50+
final String type;
51+
final String orientation;
52+
final int? percent;
53+
final bool urgent;
54+
final List<String> marks;
55+
final String layout;
56+
final String border;
57+
final int currentBorderWidth;
58+
final Rect rect;
59+
final Rect decoRect;
60+
final Rect windowRect;
61+
final Rect geometry;
62+
final String name;
63+
final int? window;
64+
final List<int> nodes;
65+
final List<int> floatingNodes;
66+
final List<int> focus;
67+
final int fullscreenMode;
68+
final bool sticky;
69+
final int num;
70+
final String output;
71+
final String representation;
72+
final bool focused;
73+
final bool visible;
74+
75+
Workspace({
76+
required this.id,
77+
required this.type,
78+
required this.orientation,
79+
required this.percent,
80+
required this.urgent,
81+
required this.marks,
82+
required this.layout,
83+
required this.border,
84+
required this.currentBorderWidth,
85+
required this.rect,
86+
required this.decoRect,
87+
required this.windowRect,
88+
required this.geometry,
89+
required this.name,
90+
required this.window,
91+
required this.nodes,
92+
required this.floatingNodes,
93+
required this.focus,
94+
required this.fullscreenMode,
95+
required this.sticky,
96+
required this.num,
97+
required this.output,
98+
required this.representation,
99+
required this.focused,
100+
required this.visible,
101+
});
102+
103+
static Workspace fromJson(Map<String, dynamic> json) {
104+
return Workspace(
105+
id: json['id'],
106+
type: json['type'],
107+
orientation: json['orientation'],
108+
percent: json['percent'],
109+
urgent: json['urgent'],
110+
marks: List<String>.from(json['marks']),
111+
layout: json['layout'],
112+
border: json['border'],
113+
currentBorderWidth: json['current_border_width'],
114+
rect: Rect.fromJson(json['rect']),
115+
decoRect: Rect.fromJson(json['deco_rect']),
116+
windowRect: Rect.fromJson(json['window_rect']),
117+
geometry: Rect.fromJson(json['geometry']),
118+
name: json['name'],
119+
window: json['window'],
120+
nodes: List<int>.from(json['nodes']),
121+
floatingNodes: List<int>.from(json['floating_nodes']),
122+
focus: List<int>.from(json['focus']),
123+
fullscreenMode: json['fullscreen_mode'],
124+
sticky: json['sticky'],
125+
num: json['num'],
126+
output: json['output'],
127+
representation: json['representation'],
128+
focused: json['focused'],
129+
visible: json['visible'],
130+
);
131+
}
132+
}
133+
134+
// Map MessageType to int
135+
extension MessageTypeExtension on MessageType {
136+
int get value {
137+
switch (this) {
138+
case MessageType.RUN_COMMAND:
139+
return 0;
140+
case MessageType.GET_WORKSPACES:
141+
return 1;
142+
case MessageType.SUBSCRIBE:
143+
return 2;
144+
case MessageType.GET_OUTPUTS:
145+
return 3;
146+
case MessageType.GET_TREE:
147+
return 4;
148+
case MessageType.GET_MARKS:
149+
return 5;
150+
case MessageType.GET_BAR_CONFIG:
151+
return 6;
152+
case MessageType.GET_VERSION:
153+
return 7;
154+
case MessageType.GET_BINDING_MODES:
155+
return 8;
156+
case MessageType.GET_CONFIG:
157+
return 9;
158+
case MessageType.SEND_TICK:
159+
return 10;
160+
case MessageType.SYNC:
161+
return 11;
162+
case MessageType.GET_BINDING_STATE:
163+
return 12;
164+
case MessageType.GET_INPUTS:
165+
return 100;
166+
case MessageType.GET_SEATS:
167+
return 101;
168+
}
169+
}
170+
}
171+
172+
class SwayIPCConnection {
173+
// [sway-ipc](https://man.archlinux.org/man/sway-ipc.7.en)
174+
Socket? socket;
175+
176+
SwayIPCConnection();
177+
178+
Future<void> open() async {
179+
final host = InternetAddress(
180+
Platform.environment['SWAYSOCK']!,
181+
type: InternetAddressType.unix,
182+
);
183+
socket = await Socket.connect(host, 0);
184+
}
185+
186+
Future<(MessageType type, dynamic payload)> send(
187+
MessageType type, String payload) async {
188+
List<int> header = [];
189+
header.addAll(utf8.encode("i3-ipc")); // IPC Magic
190+
header.addAll(
191+
Uint32List.fromList([payload.length, type.value]).buffer.asUint8List(),
192+
);
193+
header.addAll(utf8.encode(payload));
194+
socket!.add(header);
195+
196+
return await receive();
197+
}
198+
199+
Future<(MessageType type, dynamic payload)> receive() async {
200+
Uint8List raw = (await socket!.take(1).toList()).first;
201+
202+
String magic = utf8.decode(raw.sublist(0, 6));
203+
if (magic != "i3-ipc") {
204+
throw Exception("Invalid magic");
205+
}
206+
Uint32List headerView = Uint32List.view(raw.sublist(6, 14).buffer);
207+
int length = headerView[0];
208+
int type = headerView[1];
209+
210+
final payload = raw.sublist(14);
211+
assert(payload.length == length);
212+
213+
// Parse JSON payload
214+
final json = jsonDecode(utf8.decode(payload));
215+
216+
return (MessageType.values[type], json);
217+
}
218+
219+
void close() {
220+
socket?.close();
221+
}
222+
223+
Future<List<Workspace>> getWorkspaces() async {
224+
final (type, List<dynamic> workspaces) =
225+
await send(MessageType.GET_WORKSPACES, "");
226+
assert(type == MessageType.GET_WORKSPACES);
227+
return workspaces
228+
.map((workspace) => Workspace.fromJson(workspace))
229+
.toList();
230+
}
231+
}
232+
233+
class SwayModule extends StatefulWidget {
234+
const SwayModule({super.key});
235+
236+
@override
237+
SwayModuleState createState() => SwayModuleState();
238+
}
239+
240+
class SwayModuleState extends State<SwayModule> {
241+
SwayIPCConnection connection = SwayIPCConnection();
242+
Prop<List<Workspace>> workspaces = Prop(<Workspace>[]);
243+
244+
Future<void> initConnection() async {
245+
await connection.open();
246+
workspaces.value = await connection.getWorkspaces();
247+
}
248+
249+
@override
250+
void initState() {
251+
super.initState();
252+
initConnection();
253+
}
254+
255+
@override
256+
void dispose() {
257+
connection.close();
258+
super.dispose();
259+
}
260+
261+
@override
262+
Widget build(BuildContext context) {
263+
return ReactiveBuilder(
264+
() => [
265+
Text(
266+
"Window Title",
267+
style: MemexTypography.body.copyWith(fontWeight: FontWeight.bold),
268+
),
269+
...workspaces.value.map((workspace) => Text(workspace.name)
270+
.padding(all: 4)
271+
.highlight(visible: workspace.focused)),
272+
].toRow(),
273+
);
274+
}
275+
}

packages/memex_bar/pubspec.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,14 @@ packages:
562562
url: "https://pub.dev"
563563
source: hosted
564564
version: "3.7.3"
565+
posix:
566+
dependency: "direct main"
567+
description:
568+
name: posix
569+
sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a
570+
url: "https://pub.dev"
571+
source: hosted
572+
version: "6.0.1"
565573
provider:
566574
dependency: transitive
567575
description:

packages/memex_bar/pubspec.lock.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,16 @@
701701
"source": "hosted",
702702
"version": "3.7.3"
703703
},
704+
"posix": {
705+
"dependency": "direct main",
706+
"description": {
707+
"name": "posix",
708+
"sha256": "a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a",
709+
"url": "https://pub.dev"
710+
},
711+
"source": "hosted",
712+
"version": "6.0.1"
713+
},
704714
"provider": {
705715
"dependency": "transitive",
706716
"description": {

packages/memex_bar/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dependencies:
3737
# Use with the CupertinoIcons class for iOS style icons.
3838
cupertino_icons: ^1.0.2
3939
intl: ^0.18.1
40+
posix: ^6.0.1
4041

4142
dev_dependencies:
4243
flutter_test:

0 commit comments

Comments
 (0)