Skip to content

Commit 7f715e7

Browse files
Merge pull request #10 from bettercoderthanyou/bettercoderthanyou/drag-reorder-widgets
feat: drag-and-drop widget reordering
2 parents 6ea8b6e + 03f7457 commit 7f715e7

File tree

3 files changed

+203
-8
lines changed

3 files changed

+203
-8
lines changed

Barik/Config/ConfigManager.swift

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ final class ConfigManager: ObservableObject {
1111
private var fileWatchSource: DispatchSourceFileSystemObject?
1212
private var fileDescriptor: CInt = -1
1313
private var configFilePath: String?
14+
private var suppressNextReload = false
1415

1516
private init() {
1617
loadOrCreateConfigIfNeeded()
@@ -123,6 +124,10 @@ final class ConfigManager: ObservableObject {
123124
guard let self = self, let path = self.configFilePath else {
124125
return
125126
}
127+
if self.suppressNextReload {
128+
self.suppressNextReload = false
129+
return
130+
}
126131
self.parseConfigFile(at: path)
127132
}
128133
fileWatchSource?.setCancelHandler { [weak self] in
@@ -258,6 +263,84 @@ final class ConfigManager: ObservableObject {
258263
}
259264
}
260265

266+
func updateDisplayedWidgets(_ items: [TomlWidgetItem]) {
267+
guard let path = configFilePath else {
268+
print("Config file path is not set")
269+
return
270+
}
271+
do {
272+
let currentText = try String(contentsOfFile: path, encoding: .utf8)
273+
let updatedText = replaceDisplayedArray(in: currentText, with: items)
274+
suppressNextReload = true
275+
try updatedText.write(toFile: path, atomically: true, encoding: .utf8)
276+
DispatchQueue.main.async {
277+
self.parseConfigFile(at: path)
278+
}
279+
} catch {
280+
suppressNextReload = false
281+
print("Error updating displayed widgets:", error)
282+
}
283+
}
284+
285+
private func replaceDisplayedArray(in original: String, with items: [TomlWidgetItem]) -> String {
286+
let lines = original.components(separatedBy: "\n")
287+
var inWidgetsSection = false
288+
var arrayStartLine: Int?
289+
var arrayEndLine: Int?
290+
var bracketDepth = 0
291+
var foundStart = false
292+
293+
for (lineIndex, line) in lines.enumerated() {
294+
let trimmed = line.trimmingCharacters(in: .whitespaces)
295+
296+
if trimmed.hasPrefix("[") && trimmed.hasSuffix("]") {
297+
inWidgetsSection = (trimmed == "[widgets]")
298+
if foundStart && !inWidgetsSection {
299+
break
300+
}
301+
continue
302+
}
303+
304+
if inWidgetsSection && !foundStart {
305+
if trimmed.hasPrefix("displayed") && trimmed.contains("=") {
306+
arrayStartLine = lineIndex
307+
foundStart = true
308+
for char in trimmed {
309+
if char == Character("[") { bracketDepth += 1 }
310+
if char == Character("]") { bracketDepth -= 1 }
311+
}
312+
if bracketDepth == 0 {
313+
arrayEndLine = lineIndex
314+
break
315+
}
316+
}
317+
} else if foundStart && arrayEndLine == nil {
318+
for char in trimmed {
319+
if char == Character("[") { bracketDepth += 1 }
320+
if char == Character("]") { bracketDepth -= 1 }
321+
}
322+
if bracketDepth == 0 {
323+
arrayEndLine = lineIndex
324+
break
325+
}
326+
}
327+
}
328+
329+
guard let start = arrayStartLine, let end = arrayEndLine else {
330+
return original
331+
}
332+
333+
let newArrayLines = "displayed = " + items.toTomlDisplayedArray()
334+
335+
var newLines = Array(lines[0..<start])
336+
newLines.append(newArrayLines)
337+
if end + 1 < lines.count {
338+
newLines.append(contentsOf: lines[(end + 1)...])
339+
}
340+
341+
return newLines.joined(separator: "\n")
342+
}
343+
261344
func globalWidgetConfig(for widgetId: String) -> ConfigData {
262345
config.rootToml.widgets.config(for: widgetId) ?? [:]
263346
}

Barik/Config/ConfigModels.swift

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,19 @@ struct WidgetsSection: Decodable {
113113
}
114114
}
115115

116-
struct TomlWidgetItem: Decodable {
116+
struct TomlWidgetItem: Decodable, Equatable, Hashable {
117+
let instanceID: UUID
117118
let id: String
118119
let inlineParams: ConfigData
119120

120121
init(id: String, inlineParams: ConfigData) {
122+
self.instanceID = UUID()
121123
self.id = id
122124
self.inlineParams = inlineParams
123125
}
124126

125127
init(from decoder: Decoder) throws {
128+
self.instanceID = UUID()
126129
let container = try decoder.singleValueContainer()
127130

128131
if let strValue = try? container.decode(String.self) {
@@ -148,9 +151,34 @@ struct TomlWidgetItem: Decodable {
148151
self.id = widgetId
149152
self.inlineParams = params
150153
}
154+
155+
static func == (lhs: TomlWidgetItem, rhs: TomlWidgetItem) -> Bool {
156+
lhs.instanceID == rhs.instanceID
157+
}
158+
159+
func hash(into hasher: inout Hasher) {
160+
hasher.combine(instanceID)
161+
}
162+
163+
func toTomlString() -> String {
164+
if inlineParams.isEmpty {
165+
return "\"\(id)\""
166+
}
167+
let paramsString = inlineParams.map { key, value in
168+
"\(key) = \(value.toTomlValueString())"
169+
}.joined(separator: ", ")
170+
return "{ \"\(id)\" = { \(paramsString) } }"
171+
}
151172
}
152173

153-
enum TOMLValue: Decodable {
174+
extension Array where Element == TomlWidgetItem {
175+
func toTomlDisplayedArray() -> String {
176+
let items = self.map { " \($0.toTomlString())" }.joined(separator: ",\n")
177+
return "[\n\(items)\n]"
178+
}
179+
}
180+
181+
enum TOMLValue: Decodable, Equatable, Hashable {
154182
case string(String)
155183
case bool(Bool)
156184
case int(Int)
@@ -216,6 +244,22 @@ extension TOMLValue {
216244
if case let .dictionary(dict) = self { return dict }
217245
return nil
218246
}
247+
248+
func toTomlValueString() -> String {
249+
switch self {
250+
case .string(let s): return "\"\(s)\""
251+
case .bool(let b): return b ? "true" : "false"
252+
case .int(let i): return "\(i)"
253+
case .double(let d): return "\(d)"
254+
case .array(let arr):
255+
let inner = arr.map { $0.toTomlValueString() }.joined(separator: ", ")
256+
return "[\(inner)]"
257+
case .dictionary(let dict):
258+
let inner = dict.map { "\($0.key) = \($0.value.toTomlValueString())" }.joined(separator: ", ")
259+
return "{ \(inner) }"
260+
case .null: return "\"\""
261+
}
262+
}
219263
}
220264

221265
struct YabaiConfig: Decodable {

Barik/Views/MenuBarView.swift

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import SwiftUI
2+
import UniformTypeIdentifiers
23

34
struct MenuBarView: View {
45
@ObservedObject var configManager = ConfigManager.shared
6+
@State private var widgetItems: [TomlWidgetItem] = []
7+
@State private var draggingItem: TomlWidgetItem?
8+
9+
private var displayedFingerprint: String {
10+
configManager.config.rootToml.widgets.displayed
11+
.map { $0.id }
12+
.joined(separator: "|")
13+
}
514

615
var body: some View {
716
let theme: ColorScheme? =
@@ -14,17 +23,28 @@ struct MenuBarView: View {
1423
.none
1524
}
1625

17-
let items = configManager.config.rootToml.widgets.displayed
18-
1926
HStack(spacing: 0) {
2027
HStack(spacing: configManager.config.experimental.foreground.spacing) {
21-
ForEach(0..<items.count, id: \.self) { index in
22-
let item = items[index]
28+
ForEach(widgetItems, id: \.instanceID) { item in
2329
buildView(for: item)
30+
.opacity(draggingItem?.instanceID == item.instanceID ? 0.5 : 1.0)
31+
.onDrag {
32+
draggingItem = item
33+
return NSItemProvider(object: item.instanceID.uuidString as NSString)
34+
}
35+
.onDrop(
36+
of: [UTType.text],
37+
delegate: WidgetDropDelegate(
38+
item: item,
39+
items: $widgetItems,
40+
draggingItem: $draggingItem,
41+
onReorderComplete: persistWidgetOrder
42+
)
43+
)
2444
}
2545
}
2646

27-
if !items.contains(where: { $0.id == "system-banner" }) {
47+
if !widgetItems.contains(where: { $0.id == "system-banner" }) {
2848
SystemBannerWidget(withLeftPadding: true)
2949
}
3050
}
@@ -34,6 +54,18 @@ struct MenuBarView: View {
3454
.padding(.horizontal, configManager.config.experimental.foreground.horizontalPadding)
3555
.background(.black.opacity(0.001))
3656
.preferredColorScheme(theme)
57+
.onAppear {
58+
widgetItems = configManager.config.rootToml.widgets.displayed
59+
}
60+
.onChange(of: displayedFingerprint) {
61+
if draggingItem == nil {
62+
widgetItems = configManager.config.rootToml.widgets.displayed
63+
}
64+
}
65+
}
66+
67+
private func persistWidgetOrder() {
68+
configManager.updateDisplayedWidgets(widgetItems)
3769
}
3870

3971
@ViewBuilder
@@ -54,7 +86,7 @@ struct MenuBarView: View {
5486
case "default.time":
5587
TimeWidget(configProvider: config)
5688
.environmentObject(config)
57-
89+
5890
case "default.nowplaying", "default.spotify":
5991
NowPlayingWidget()
6092
.environmentObject(config)
@@ -84,3 +116,39 @@ struct MenuBarView: View {
84116
}
85117
}
86118
}
119+
120+
struct WidgetDropDelegate: DropDelegate {
121+
let item: TomlWidgetItem
122+
@Binding var items: [TomlWidgetItem]
123+
@Binding var draggingItem: TomlWidgetItem?
124+
let onReorderComplete: () -> Void
125+
126+
func performDrop(info: DropInfo) -> Bool {
127+
draggingItem = nil
128+
onReorderComplete()
129+
return true
130+
}
131+
132+
func dropEntered(info: DropInfo) {
133+
guard let dragging = draggingItem,
134+
dragging.instanceID != item.instanceID,
135+
let fromIndex = items.firstIndex(where: { $0.instanceID == dragging.instanceID }),
136+
let toIndex = items.firstIndex(where: { $0.instanceID == item.instanceID })
137+
else { return }
138+
139+
withAnimation(.smooth(duration: 0.2)) {
140+
items.move(
141+
fromOffsets: IndexSet(integer: fromIndex),
142+
toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex
143+
)
144+
}
145+
}
146+
147+
func dropUpdated(info: DropInfo) -> DropProposal? {
148+
DropProposal(operation: .move)
149+
}
150+
151+
func validateDrop(info: DropInfo) -> Bool {
152+
draggingItem != nil
153+
}
154+
}

0 commit comments

Comments
 (0)