Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to this project are documented in this file.

## Unreleased

- Added Omarchy 4.0+ ("Quattro") Quickshell bar support:
- Capability-based detection (`omarchy::shell_available()`) instead of parsing `omarchy-version`.
- `theme-manager bar list|use|reset|defaults|position|transparent`, wrapping `omarchy bar ...`.
- `print-config` reports `OMARCHY_SHELL_AVAILABLE`.
- Browse TUI: on 4.0+, the Waybar and Walker tabs are replaced by a single Bar tab (both are superseded by the Quickshell bar/launcher); 3.x installs are unaffected.
- Added Omarchy 4.0+ Quickshell plugin management:
- `theme-manager plugin list|enable|disable`, plus passthrough for `add|update|remove|rescan|clone|edit|validate` straight to `omarchy-plugin` with interactive stdio.
- Browse TUI: a Plugins tab (4.0+ only) listing every discovered shell plugin (services, overlays, panels, bar widgets, ...) with live enabled state; Enter toggles enable/disable immediately, independent of the theme-apply pipeline.
- Added `theme-manager bar plugin add|move|remove|set|replace ...`, a passthrough to `omarchy-bar-plugin` (with interactive stdio) for editing the bar's widget layout (`shell.json`'s `bar.layout.{left,center,right}`).
- Fixed test isolation: `setup_env()` now shadows the host's real `omarchy` unified CLI with a stub that exits 127, so running the test suite on an actual Omarchy machine no longer lets the live `omarchy` binary silently intercept subcommands meant to exercise the legacy-script fallback (was causing `set_generates_templates_from_colors` and `bg_next_runs_command` to fail).

## 0.3.6

- Added Omarchy 3.7 unlock theme support:
Expand Down
1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ clap = { version = "4.5.4", features = ["derive"] }
crossterm = "0.28.1"
ratatui = "0.28.1"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.120"
toml = "0.8.14"
walkdir = "2.5.0"
which = "6.0.1"
Expand Down
178 changes: 178 additions & 0 deletions rust/src/bar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use anyhow::{anyhow, Result};
use serde::Deserialize;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};

use crate::omarchy;

#[derive(Debug, Clone)]
pub struct BarOption {
pub id: String,
pub name: String,
pub description: String,
}

#[derive(Debug, Deserialize)]
struct CatalogEntry {
id: String,
#[serde(default)]
name: String,
#[serde(default)]
description: String,
#[serde(default)]
kinds: Vec<String>,
#[serde(rename = "barPath")]
bar_path: Option<String>,
}

#[derive(Debug, Deserialize, Default)]
struct ShellConfig {
bar: Option<BarSection>,
}

#[derive(Debug, Deserialize, Default)]
struct BarSection {
id: Option<String>,
}

fn require_shell() -> Result<()> {
if omarchy::shell_available() {
return Ok(());
}
Err(anyhow!(
"bar management requires Omarchy 4.0+ (the Quickshell-based omarchy-shell); \
omarchy-shell/omarchy-bar not found in PATH"
))
}

fn shell_config_path() -> Result<PathBuf> {
let home = env::var("HOME").map_err(|_| anyhow!("HOME is not set"))?;
Ok(PathBuf::from(home).join(".config/omarchy/shell.json"))
}

/// The active bar's plugin id, or `None` when the built-in `omarchy.bar` is active.
pub fn current_bar_id() -> Result<Option<String>> {
let path = shell_config_path()?;
if !path.is_file() {
return Ok(None);
}
let raw = fs::read_to_string(&path)?;
if raw.trim().is_empty() {
return Ok(None);
}
let parsed: ShellConfig = serde_json::from_str(&raw)
.map_err(|err| anyhow!("failed to parse {}: {err}", path.to_string_lossy()))?;
Ok(parsed.bar.and_then(|bar| bar.id))
}

/// Every installed bar-kind plugin (full alternate bar layouts), built-in and
/// user-installed, sourced from `omarchy-plugin-catalog` (manifest walking,
/// works even when the shell isn't running).
pub fn list_bar_options() -> Result<Vec<BarOption>> {
require_shell()?;
let output = Command::new("omarchy-plugin-catalog")
.output()
.map_err(|err| anyhow!("failed to run omarchy-plugin-catalog: {err}"))?;
if !output.status.success() {
return Err(anyhow!(
"omarchy-plugin-catalog exited with {}",
output.status
));
}
let raw = String::from_utf8_lossy(&output.stdout);
let entries: Vec<CatalogEntry> = serde_json::from_str(&raw)
.map_err(|err| anyhow!("failed to parse omarchy-plugin-catalog output: {err}"))?;

let mut options: Vec<BarOption> = entries
.into_iter()
.filter(|entry| entry.kinds.iter().any(|kind| kind == "bar") && entry.bar_path.is_some())
.map(|entry| BarOption {
id: entry.id.clone(),
name: if entry.name.is_empty() {
entry.id
} else {
entry.name
},
description: entry.description,
})
.collect();

options.sort_by(|a, b| {
// Keep the built-in bar pinned first, like Waybar/Walker tabs pin
// `Omarchy-Default`.
let a_builtin = a.id == "omarchy.bar";
let b_builtin = b.id == "omarchy.bar";
b_builtin.cmp(&a_builtin).then_with(|| a.name.cmp(&b.name))
});

Ok(options)
}

pub fn cmd_bar_list() -> Result<()> {
let current = current_bar_id()?;
for option in list_bar_options()? {
let is_current = match &current {
Some(id) => id == &option.id,
None => option.id == "omarchy.bar",
};
let marker = if is_current { "*" } else { " " };
println!("{marker} {} ({})", option.name, option.id);
}
Ok(())
}

pub fn cmd_bar_use(id: &str, quiet: bool) -> Result<()> {
require_shell()?;
omarchy::run_omarchy_required("bar", "use", &[id], quiet)
}

pub fn cmd_bar_reset(quiet: bool) -> Result<()> {
require_shell()?;
omarchy::run_omarchy_required("bar", "reset", &[], quiet)
}

pub fn cmd_bar_defaults(quiet: bool) -> Result<()> {
require_shell()?;
omarchy::run_omarchy_required("bar", "defaults", &[], quiet)
}

pub fn cmd_bar_position(position: &str, quiet: bool) -> Result<()> {
require_shell()?;
if !matches!(position, "top" | "bottom" | "left" | "right") {
return Err(anyhow!(
"position must be one of top, bottom, left, right (got '{position}')"
));
}
omarchy::run_omarchy_required("bar", "position", &[position], quiet)
}

pub fn cmd_bar_transparent(value: &str, quiet: bool) -> Result<()> {
require_shell()?;
if !matches!(value, "true" | "false") {
return Err(anyhow!(
"transparent must be 'true' or 'false' (got '{value}')"
));
}
omarchy::run_omarchy_required("bar", "transparent", &[value], quiet)
}

/// Passthrough for widget-layout editing (add/move/remove/set/replace):
/// these accept a mix of positional args and `--section`/`--index`/
/// `--before`/`--after`/`--json` flags that aren't worth re-modeling in
/// clap, so exec `omarchy-bar-plugin` directly with inherited stdio.
pub fn cmd_bar_plugin_passthrough(args: &[String]) -> Result<()> {
require_shell()?;
let status = Command::new("omarchy-bar-plugin")
.args(args)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map_err(|err| anyhow!("failed to run omarchy-bar-plugin: {err}"))?;
if !status.success() {
return Err(anyhow!("omarchy-bar-plugin exited with {status}"));
}
Ok(())
}
101 changes: 101 additions & 0 deletions rust/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub enum Command {
Hyprlock(HyprlockArgs),
Unlock(UnlockArgs),
Starship(StarshipArgs),
Bar(BarArgs),
Plugin(PluginArgs),
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -182,3 +184,102 @@ pub struct StarshipArgs {
#[arg(short = 'q', long = "quiet")]
pub quiet: bool,
}

#[derive(Parser, Debug)]
#[command(
about = "Manage the Omarchy 4.0+ Quickshell bar (requires omarchy-shell)"
)]
pub struct BarArgs {
#[command(subcommand)]
pub command: BarCommand,
}

#[derive(Subcommand, Debug)]
pub enum BarCommand {
/// List installed bar options (built-in and third-party)
List,
/// Switch the active bar to the given plugin id
Use(BarUseArgs),
/// Return to the built-in Omarchy bar
Reset(BarQuietArgs),
/// Restore the default bar layout and service widgets
Defaults(BarQuietArgs),
/// Set the bar position
Position(BarPositionArgs),
/// Set bar transparency
Transparent(BarTransparentArgs),
/// Add, move, remove, set, or replace bar widgets in the layout
/// (passed straight through to `omarchy-bar-plugin` with interactive stdio)
Plugin(BarPluginArgs),
}

#[derive(Parser, Debug)]
pub struct BarPluginArgs {
#[command(subcommand)]
pub command: BarPluginCommand,
}

#[derive(Subcommand, Debug)]
pub enum BarPluginCommand {
/// Anything (add/move/remove/set/replace) is passed straight through
/// to `omarchy-bar-plugin` with interactive stdio
#[command(external_subcommand)]
Other(Vec<String>),
}

#[derive(Parser, Debug)]
pub struct BarUseArgs {
pub id: String,
#[arg(short = 'q', long = "quiet")]
pub quiet: bool,
}

#[derive(Parser, Debug)]
pub struct BarQuietArgs {
#[arg(short = 'q', long = "quiet")]
pub quiet: bool,
}

#[derive(Parser, Debug)]
pub struct BarPositionArgs {
#[arg(value_name = "top|bottom|left|right")]
pub position: String,
#[arg(short = 'q', long = "quiet")]
pub quiet: bool,
}

#[derive(Parser, Debug)]
pub struct BarTransparentArgs {
#[arg(value_name = "true|false")]
pub value: String,
#[arg(short = 'q', long = "quiet")]
pub quiet: bool,
}

#[derive(Parser, Debug)]
#[command(about = "Manage Omarchy 4.0+ Quickshell plugins (requires omarchy-shell)")]
pub struct PluginArgs {
#[command(subcommand)]
pub command: PluginCommand,
}

#[derive(Subcommand, Debug)]
pub enum PluginCommand {
/// List discovered shell plugins
List,
/// Enable a plugin by id
Enable(PluginIdArgs),
/// Disable a plugin by id
Disable(PluginIdArgs),
/// Anything else (add/update/remove/rescan/clone/edit/validate) is passed
/// straight through to `omarchy-plugin` with interactive stdio
#[command(external_subcommand)]
Other(Vec<String>),
}

#[derive(Parser, Debug)]
pub struct PluginIdArgs {
pub id: String,
#[arg(short = 'q', long = "quiet")]
pub quiet: bool,
}
8 changes: 8 additions & 0 deletions rust/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,14 @@ pub fn print_config(config: &ResolvedConfig) {
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default()
);
println!(
"OMARCHY_SHELL_AVAILABLE={}",
if crate::omarchy::shell_available() {
"1"
} else {
""
}
);
println!("WAYBAR_DIR={}", config.waybar_dir.to_string_lossy());
println!(
"WAYBAR_THEMES_DIR={}",
Expand Down
Loading