Skip to content

Commit

Permalink
New menu-bar-style configuration to add extra menu functionality ('te…
Browse files Browse the repository at this point in the history
…xt' monospaced default, 'squares' images indicator and 'i3' squares + not active workspaces with windows in them)
  • Loading branch information
mobile-ar committed Jan 29, 2025
1 parent dbd39fd commit 5a510a5
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 13 deletions.
109 changes: 100 additions & 9 deletions Sources/AppBundle/MenuBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,34 @@ public func menuBar(viewModel: TrayMenuModel) -> some Scene {
}.keyboardShortcut("Q", modifiers: .command)
} label: {
if viewModel.isEnabled {
MonospacedText(viewModel.trayText)
switch config.menuBarStyle {
case .text:
MenuBarLabel(viewModel.trayText)
case .squares:
MenuBarLabel(viewModel.trayText, trayItems: viewModel.trayItems)
case .i3:
MenuBarLabel(viewModel.trayText, trayItems: viewModel.trayItems, workspaces: viewModel.workspaces)
}
} else {
MonospacedText("⏸️")
MenuBarLabel("⏸️")
}
}
}

struct MonospacedText: View {
struct MenuBarLabel: View {
@Environment(\.colorScheme) var colorScheme: ColorScheme
var text: String
init(_ text: String) { self.text = text }
var trayItems: [TrayItem]?
var workspaces: [WorkspaceViewModel]?

init(_ text: String, trayItems: [TrayItem]? = nil, workspaces: [WorkspaceViewModel]? = nil) {
self.text = text
self.trayItems = trayItems
self.workspaces = workspaces
}

var body: some View {
let renderer = ImageRenderer(
content: Text(text)
.font(.system(.largeTitle, design: .monospaced))
.foregroundStyle(colorScheme == .light ? Color.black : Color.white)
)
let renderer = ImageRenderer(content: menuBarContent)
if let cgImage = renderer.cgImage {
// Using scale: 1 results in a blurry image for unknown reasons
Image(cgImage, scale: 2, label: Text(text))
Expand All @@ -78,6 +88,87 @@ struct MonospacedText: View {
Text(text)
}
}

@ViewBuilder
var menuBarContent: some View {
let color = colorScheme == .light ? Color.black : Color.white
if let trayItems {
HStack(spacing: 4) {
ForEach(trayItems, id: \.name) { item in
if item.name.containsEmoji() {
Text(item.name)
.font(.system(.largeTitle, design: .monospaced))
.foregroundStyle(color)
.bold()
} else {
Image(systemName: item.systemImageName)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundStyle(color)
if item.type == .mode {
Text(":")
.font(.system(.largeTitle, design: .monospaced))
.foregroundStyle(color)
.bold()
}
}
}
if workspaces != nil {
let otherWorkspaces = Workspace.all.filter { workspace in
!workspace.isEffectivelyEmpty && !trayItems.contains(where: { item in item.name == workspace.name })
}
if !otherWorkspaces.isEmpty {
Group {
Text("|")
.font(.system(.largeTitle, design: .monospaced))
.foregroundStyle(color)
.bold()
.padding(.bottom, 2)
ForEach(otherWorkspaces, id: \.name) { item in
if item.name.containsEmoji() {
Text(item.name)
.font(.system(.largeTitle, design: .monospaced))
.foregroundStyle(color)
.bold()
} else {
Image(systemName: "\(item.name.lowercased()).square")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundStyle(color)
}
}
}
.opacity(0.6)
}
}
}
.frame(height: 40)
} else {
Text(text)
.font(.system(.largeTitle, design: .monospaced))
.foregroundStyle(colorScheme == .light ? Color.black : Color.white)
}
}
}

enum MenuBarStyle: String {
case text = "text"
case squares = "squares"
case i3 = "i3"
}

extension String {
func parseMenuBarStyle() -> MenuBarStyle? {
if let parsed = MenuBarStyle(rawValue: self) {
return parsed
} else {
return nil
}
}

func containsEmoji() -> Bool {
unicodeScalars.contains { $0.properties.isEmoji && $0.properties.isEmojiPresentation }
}
}

func getTextEditorToOpenConfig() -> URL {
Expand Down
40 changes: 36 additions & 4 deletions Sources/AppBundle/TrayMenuModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class TrayMenuModel: ObservableObject {
private init() {}

@Published var trayText: String = ""
@Published var trayItems: [TrayItem] = []
/// Is "layouting" enabled
@Published var isEnabled: Bool = true
@Published var workspaces: [WorkspaceViewModel] = []
Expand All @@ -15,20 +16,51 @@ public class TrayMenuModel: ObservableObject {
func updateTrayText() {
let sortedMonitors = sortedMonitors
let focus = focus
TrayMenuModel.shared.trayText = (activeMode?.takeIf { $0 != mainModeId }?.first?.lets { "[\($0.uppercased())] " } ?? "") +
sortedMonitors
var items: [TrayItem] = []
TrayMenuModel.shared.trayText = (
activeMode?.takeIf { $0 != mainModeId }?.first?.lets {
items.append(TrayItem(type: .mode ,name: String($0), isActive: true))
return "[\($0.uppercased())] " } ?? ""
) +
sortedMonitors
.map {
($0.activeWorkspace == focus.workspace && sortedMonitors.count > 1 ? "*" : "") + $0.activeWorkspace.name
items.append(TrayItem(type: .monitor, name: $0.activeWorkspace.name, isActive: $0.activeWorkspace == focus.workspace && sortedMonitors.count > 1))
return ($0.activeWorkspace == focus.workspace && sortedMonitors.count > 1 ? "*" : "") + $0.activeWorkspace.name
}
.joined(separator: "")
TrayMenuModel.shared.workspaces = Workspace.all.map {
let monitor = $0.isVisible || !$0.isEffectivelyEmpty ? " - \($0.workspaceMonitor.name)" : ""
return WorkspaceViewModel(name: $0.name, suffix: monitor, isFocused: focus.workspace == $0)
}
TrayMenuModel.shared.trayItems = items
}

struct WorkspaceViewModel {
struct WorkspaceViewModel: Hashable {
let name: String
let suffix: String
let isFocused: Bool
}

enum TrayItemType: String {
case mode
case monitor
}

struct TrayItem: Hashable {
let type: TrayItemType
let name: String
let isActive: Bool
var systemImageName: String {
let lowercasedName = name.lowercased()
switch type {
case .mode:
return "\(lowercasedName).circle"
case .monitor:
if isActive {
return "\(lowercasedName).square.fill"
} else {
return "\(lowercasedName).square"
}
}
}
}
1 change: 1 addition & 0 deletions Sources/AppBundle/config/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ struct Config: Copyable {
var _indentForNestedContainersWithTheSameOrientation: Void = ()
var enableNormalizationFlattenContainers: Bool = true
var _nonEmptyWorkspacesRootContainersLayoutOnStartup: Void = ()
var menuBarStyle: MenuBarStyle = .text
var defaultRootContainerLayout: Layout = .tiles
var defaultRootContainerOrientation: DefaultContainerOrientation = .auto
var startAtLogin: Bool = false
Expand Down
7 changes: 7 additions & 0 deletions Sources/AppBundle/config/parseConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ private let configParser: [String: any ParserProtocol<Config>] = [
"enable-normalization-flatten-containers": Parser(\.enableNormalizationFlattenContainers, parseBool),
"enable-normalization-opposite-orientation-for-nested-containers": Parser(\.enableNormalizationOppositeOrientationForNestedContainers, parseBool),

"menu-bar-style": Parser(\.menuBarStyle, parseMenuBarStyle),

"default-root-container-layout": Parser(\.defaultRootContainerLayout, parseLayout),
"default-root-container-orientation": Parser(\.defaultRootContainerOrientation, parseDefaultContainerOrientation),

Expand Down Expand Up @@ -275,6 +277,11 @@ private func parseLayout(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace
.flatMap { $0.parseLayout().orFailure(.semantic(backtrace, "Can't parse layout '\($0)'")) }
}

private func parseMenuBarStyle(_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<MenuBarStyle> {
parseString(raw, backtrace)
.flatMap { $0.parseMenuBarStyle().orFailure(.semantic(backtrace, "Can't parse menu bar style '\($0)'")) }
}

private func skipParsing<T>(_ value: T) -> (_ raw: TOMLValueConvertible, _ backtrace: TomlBacktrace) -> ParsedToml<T> {
{ _, _ in .success(value) }
}
Expand Down
8 changes: 8 additions & 0 deletions docs/config-examples/default-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ accordion-padding = 30
# Possible values: tiles|accordion
default-root-container-layout = 'tiles'

# Possible values: text|squares|i3
# - text: displays menu bar as monospaced text (only active workspaces for each monitor).
# - squares: displays menu bar as square with the workspace name inside. Filled square is the
# active monitor, bordered square for other monitors (displays only active workspaces
# for each monitor).
# - i3: same as squares + shows all other workspaces that contains windows in it, like i3.
menu-bar-style = 'text'

# Possible values: horizontal|vertical|auto
# 'auto' means: wide monitor (anything wider than high) gets horizontal orientation,
# tall monitor (anything higher than wide) gets vertical orientation
Expand Down

0 comments on commit 5a510a5

Please sign in to comment.