readme update, add untracked files

This commit is contained in:
Penelope Gwen 2025-09-19 23:50:21 -07:00
parent 3cabb696c1
commit da30455563
6 changed files with 441 additions and 48 deletions

View file

@ -2,6 +2,8 @@
This is a tool heavily targeting my own personal use-cases. This is a tool heavily targeting my own personal use-cases.
It is a collection of utility commands which primarily serve to manage a multiple-workspace setup in Sway. It is a collection of utility commands which primarily serve to manage a multiple-workspace setup in Sway.
**This software is unfinished and in early development. It is not ready for use, and its behavior is subject to change drastically between now and release.**
## Building ## Building
### Universal: ### Universal:
@ -31,3 +33,15 @@ Probably, idk man don't judge
> Is it good? > Is it good?
Probably not Probably not
## todo
* better failure/error handling
## Exit Codes (reference)
* 0: success
* 1: generic
* 2: sway ipc/exec failure
* 3: bad configuration
* 4: bad cli args

39
src/lib/get.rs Normal file
View file

@ -0,0 +1,39 @@
use crate::{config::Config, profile::{active_profile,profile_from_index}, ErrorMessage, ProfileGetCommand};
use serde_json::json;
pub fn print(config: Config,info: &ProfileGetCommand) -> Result<String,ErrorMessage> {
match active_profile() {
Ok(i) => {
match profile_from_index(config, i) {
Ok(p) => {
match info {
ProfileGetCommand::Json => Ok(json!(p).to_string()),
ProfileGetCommand::Name => Ok(p.name),
ProfileGetCommand::Icon => Ok(p.icon),
}
/* Ok(if monitor {
let xdg_directories = BaseDirectories::new();
let config = notify::Config::default().with_compare_contents(true).with_compare_contents(true).with_poll_interval(Duration::from_secs_f32(0.5));
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = PollWatcher::new(tx, config).unwrap();
watcher.watch(xdg_directories.runtime_dir.unwrap().join("sway-profiles-rs/active-profile.json").as_path(),RecursiveMode::Recursive);
for _res in rx {
match info {
ProfileGetCommand::Json => println!("{}",json!(p).to_string()),
ProfileGetCommand::Name => println!("{}",p.name),
ProfileGetCommand::Icon => println!("{}",p.icon),
};
match res {
Ok(event) => println!("changed: {:?}", event),
Err(e) => println!("watch error: {:?}", e),
};
}
})*/
}
Err(e) => Err(e),
}
},
Err(e) => Err(e),
}
}

19
src/lib/launch.rs Normal file
View file

@ -0,0 +1,19 @@
use std::collections::HashMap;
use crate::{config::Programs, ErrorMessage};
pub fn profile_launch(programs_config: HashMap<String, Programs>, program_arg: &String) -> Result<String,ErrorMessage> {
match programs_config.iter().find(|x|x.0.eq(program_arg)) {
Some(p) => {
println!("{:?}",p);
let mut swaymsg_command = format!("exec {}", p.1.command);
for a in &p.1.arguments {
swaymsg_command = format!("{} {}",swaymsg_command,a);
}
Ok(swaymsg_command)
},
None => {
Err(ErrorMessage { message: Some(String::from("no matching program found")), code: Some(3) })
},
}
}

145
src/lib/profile.rs Normal file
View file

@ -0,0 +1,145 @@
use std::fs::{self, write};
use serde_json::json;
use swayipc::Connection;
use xdg::BaseDirectories;
use crate::{config::{Config, Profile}, sway::run_sway_command, ErrorMessage};
pub fn initialize(mut sway_connection: Connection, profile: Profile,profile_index: String,uindex: usize) -> Result<(),ErrorMessage> { //payload: String) {
//todo: preserve keyboard layout, where 0 is last in workspace index
for i in 0..10 {
let _ = match run_sway_command(&mut sway_connection, format!("bindsym $mod+{} workspace number {}{}:{}",i,profile_index,i,profile.icon)) {
Ok(_) => {
let base_directories = BaseDirectories::new();
let active_profile_cache = base_directories.get_runtime_directory().unwrap().join("sway-profiles-rs/active-profile.json");
let active_profile = json!(uindex);
let _ = write(active_profile_cache, active_profile.to_string());
Ok(())
},
Err(e) => Err(e),
};
}
match run_sway_command(&mut sway_connection, format!("workspace number {}1:{}",profile_index,profile.icon)) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
pub fn profile_from_index(config: Config, index: usize) -> Result<Profile,ErrorMessage>{
match config.profiles.get(index) {
Some(p) => Ok(p.clone()),
None => Err(ErrorMessage { message: Some("Profile not found for index".to_string()), code: Some(3) }),
}
}
pub fn _profile_from_name(config: Config, name: String) -> Result<Profile,ErrorMessage> {
match config.profiles.iter().find(|x|x.name == name) {
Some(p) => Ok(p.clone()),
None => Err(ErrorMessage { message: Some(format!("Profile not found with name {}",name)), code: Some(3) }),
}
}
pub fn switch_by_index(config: Config,index: usize,sway_connection: Connection) -> Result<(),ErrorMessage> {
match profile_from_index(config, index) {
Ok(p) => match initialize(sway_connection, p.clone(), index_string(index), index) {
Ok(_) => {
println!("successfully switched to profile at index {}",index);
Ok(())
},
Err(e) => Err(e),
},
Err(e) => Err(e),
}
}
pub fn switch_by_name(config: Config,name: String,sway_connection: Connection) -> Result<(),ErrorMessage> {
match index_from_name(config.clone(), name.clone()) {
Ok(index) => match switch_by_index(config, index, sway_connection) {
Ok(_) => {
println!("Successfully switched to profile with name {}",name);
Ok(())
},
Err(e) => Err(e),
},
Err(e) => Err(e),
}
/* match profile_from_name(config, name) {
Ok(p) => match {
},
Err(e) => Err(e),
} */
}
pub fn active_profile() -> Result<usize,ErrorMessage> {
let base_directories = BaseDirectories::new();
let active_profile_cache_json = base_directories.get_runtime_directory().unwrap().join("sway-profiles-rs/active-profile.json");
match active_profile_cache_json.exists() {
true => {
match fs::File::open(active_profile_cache_json) {
Ok(f) => {
match serde_json::from_reader::<fs::File,usize>(f) {
Ok(u) => {
Ok(u)
},
Err(_) => Err(ErrorMessage { message: Some("could not parse json from active profile cache file".to_string()), code: Some(3) }),
}
},
Err(_) => Err(ErrorMessage { message: Some("could not open active profile cache file".to_string()), code: Some(3) }),
}
},
false => Err(ErrorMessage { message: Some("no active profile cache file".to_string()), code: Some(3) }),
}
}
pub fn next(config: Config, sway_connection: Connection) -> Result<(),ErrorMessage> {
match active_profile() {
Ok(u) => {
let profile_count = config.profiles.len();
let mut next_profile = u + 1;
if next_profile.ge(&profile_count) {
next_profile = 0
}
match switch_by_index(config, next_profile, sway_connection) {
Ok(_) => {
println!("switched to next profile ({})",next_profile);
Ok(())
},
Err(e) => Err(e),
}
},
Err(e) => Err(e),
}
}
pub fn previous(config: Config, sway_connection: Connection) -> Result<(),ErrorMessage> {
match active_profile() {
Ok(u) => {
let prev_profile: usize = if u.eq(&0) {
config.profiles.len() - 1
} else {
u - 1
};
match switch_by_index(config, prev_profile, sway_connection) {
Ok(_) => {
println!("switched to prev profile ({})",prev_profile);
Ok(())
},
Err(e) => Err(e),
}
},
Err(e) => Err(e),
}
}
pub fn index_from_name(config: Config, name: String) -> Result<usize, ErrorMessage>{
match config.profiles.iter().position(|x|x.name == name) {
Some(i) => Ok(i),
None => Err(ErrorMessage { message: Some(String::from("Index not found for profile?")), code: Some(3) }),
}
}
pub fn index_string(index: usize) -> String {
index.to_string().trim_matches('0').to_string()
}

13
src/lib/sway.rs Normal file
View file

@ -0,0 +1,13 @@
use swayipc::Connection;
use crate::ErrorMessage;
pub fn run_sway_command(sway_connection: &mut Connection,payload: String) -> Result<(),ErrorMessage> {
match sway_connection.run_command(&payload) {
Ok(_) => {
println!("Command [{}] ran successfully",payload);
Ok(())
},
Err(e) => Err(ErrorMessage{ message: Some(e.to_string()), code: Some(2) }),
}
}

View file

@ -1,7 +1,10 @@
use std::process::exit; #![deny(unused_crate_dependencies)]
use clap::{Parser,Subcommand,ArgAction}; use std::{process::exit, time::Duration};
use clap::{ArgAction, Parser, Subcommand};
use swayipc::{Connection, Event, EventType, Fallible}; use swayipc::{Connection, Event, EventType, Fallible};
use notify::{PollWatcher, RecursiveMode, Watcher};
mod config; mod config;
#[path = "lib/windows.rs"] #[path = "lib/windows.rs"]
@ -13,8 +16,21 @@ use workspaces::print_workspace_array;
#[path = "lib/lock.rs"] #[path = "lib/lock.rs"]
mod lock; mod lock;
use lock::lock_screen; use lock::lock_screen;
#[path = "lib/launch.rs"]
mod launch;
use launch::profile_launch;
#[path = "lib/sway.rs"]
mod sway;
use sway::run_sway_command;
#[path = "lib/profile.rs"]
mod profile;
#[path = "lib/get.rs"]
mod get;
use config::Config; use config::Config;
use xdg::BaseDirectories;
use crate::profile::switch_by_index;
#[derive(Parser)] #[derive(Parser)]
@ -28,10 +44,6 @@ pub struct Cli {
#[arg(short = 'm', long = "monitor", action = ArgAction::SetTrue)] #[arg(short = 'm', long = "monitor", action = ArgAction::SetTrue)]
monitor: Option<bool>, monitor: Option<bool>,
// /// Sets a custom config file
// #[arg(short, long, value_name = "FILE")]
// config: Option<PathBuf>,
/// Turn debugging information on /// Turn debugging information on
#[arg(short, long, action = ArgAction::Count)] #[arg(short, long, action = ArgAction::Count)]
debug: u8, debug: u8,
@ -56,32 +68,97 @@ enum Commands {
#[arg(short, long, action = ArgAction::SetTrue)] #[arg(short, long, action = ArgAction::SetTrue)]
force_render_background: Option<bool>, force_render_background: Option<bool>,
}, },
//Rename, /// Profile WIP,
Profile, Profile {
#[command(subcommand)]
profile_command: ProfileCommand,
},
/// Shortcuts TODO
Shortcuts { Shortcuts {
#[arg(short, long, action = ArgAction::SetTrue)] #[arg(short, long, action = ArgAction::SetTrue)]
global: Option<bool>, global: Option<bool>,
}, }
// Monitor {
// /// monitor a sway activity type
// #[arg(short, long)]
// #[command(subcommand)]
// monitor_type: MonitorTypes,
// },
} }
#[derive(Subcommand)] #[derive(Subcommand)]
enum MonitorTypes { enum ProfileCommand {
/// Monitors workspace changes /// switch profiles
Workspaces, #[clap(alias = "s")]
/// Monitor window changes Switch {
Windows, #[command(subcommand)]
/// Monitor active profile switch_command: Option<ProfileSwitchCommand>,
Profile
},
/// get profile info
#[clap(alias = "g")]
Get {
/// monitor profile information
#[arg(short, long, action = ArgAction::SetTrue)]
monitor: Option<bool>,
#[command(subcommand)]
get_command: ProfileGetCommand,
},
/// Initialize sway-profiles-rs
#[clap(alias = "i")]
Init
}
#[derive(Subcommand)]
enum ProfileSwitchCommand {
/// switch to next profile
#[clap(alias = "n")]
Next,
/// switch to previous profile
#[clap(alias = "p")]
Prev,
/// switch to profile by name or id
#[clap(alias = "t")]
To {
#[clap(group = "switch_to")]
#[arg(short, long)]
name: Option<String>,
#[clap(group = "switch_to")]
#[arg(short, long)]
index: Option<usize>,
query: Option<String>
}
}
#[derive(Subcommand)]
enum ProfileGetCommand {
/// get profile json
#[clap(alias = "j")]
Json,
/// get profile name
#[clap(alias = "n")]
Name,
/// get profile icon
#[clap(alias = "i")]
Icon
}
#[derive(Debug)]
pub struct ErrorMessage {
message: Option<String>,
code: Option<i32>
}
pub fn error_handler(error: ErrorMessage) {
println!("ERROR: {}",error.message.unwrap_or_default());
exit(error.code.unwrap_or(1))
}
pub fn setup_runtime_dir(xdg_directories: BaseDirectories) {
match xdg_directories.create_runtime_directory("sway-profiles-rs") {
Ok(_) => println!("success"),
Err(_) => println!("failed"),
}
} }
fn main() -> Fallible<()> { fn main() -> Fallible<()> {
//let xdg_dirs = BaseDirectories::with_prefix("sway-profiles-rs"); //let xdg_dirs = BaseDirectories::with_prefix("sway-profiles-rs");
let xdg_directories = BaseDirectories::new();
let cli = Cli::parse(); let cli = Cli::parse();
// let config = config::parse_config(); // let config = config::parse_config();
let config = confy::load("sway-profiles-rs", "config").unwrap(); let config = confy::load("sway-profiles-rs", "config").unwrap();
@ -91,24 +168,24 @@ fn main() -> Fallible<()> {
Commands::Windows => { Commands::Windows => {
print_window_title(sway_connection.get_tree().unwrap().iter().find(|&x | x.focused).unwrap().clone(), &cli, &config); print_window_title(sway_connection.get_tree().unwrap().iter().find(|&x | x.focused).unwrap().clone(), &cli, &config);
if cli.monitor.unwrap() { if cli.monitor.unwrap() {
monitor_events(EventType::Window, cli, config); monitor_sway_events(EventType::Window, cli, config);
} }
} }
Commands::Workspaces => { Commands::Workspaces => {
print_workspace_array(sway_connection.get_workspaces().unwrap()); print_workspace_array(sway_connection.get_workspaces().unwrap());
if cli.monitor.unwrap() { if cli.monitor.unwrap() {
monitor_events(EventType::Workspace, cli, config); monitor_sway_events(EventType::Workspace, cli, config);
} }
} }
Commands::Launch { program } => { Commands::Launch { program } => {
if let Some(launch_program) = config.programs.iter().find(|x|x.0 == program) { match profile_launch(config.programs, program) {
println!("found: {:#?} {:?}",launch_program.1.command, launch_program.1.arguments); Ok(p) => {
let mut swaymsg_command = "exec ".to_owned() + &launch_program.1.command; match run_sway_command(&mut sway_connection,p) {
for a in &launch_program.1.arguments { Ok(_) => todo!(),
swaymsg_command = swaymsg_command + " " + a; Err(e) => error_handler(e),
} }
println!("{:#?}",swaymsg_command); },
let _ = sway_connection.run_command(swaymsg_command); Err(e) => error_handler(e),
} }
}, },
Commands::Lock { force_render_background } => { Commands::Lock { force_render_background } => {
@ -117,33 +194,119 @@ fn main() -> Fallible<()> {
Err(e) => println!("{:?}",e), Err(e) => println!("{:?}",e),
}; };
}, },
//Commands::Rename => todo!(), Commands::Profile { profile_command} => {
Commands::Profile => { println!("{:?}",xdg_directories.get_runtime_directory());
println!("{:?}", config.profiles.len()); setup_runtime_dir(xdg_directories.clone());
for p in config.profiles { match profile_command {
println!("{:?} {:?}",p.0, p.1.name ) ProfileCommand::Init => {
match profile::profile_from_index(config.clone(), 0) {
Ok(_) => {
match switch_by_index(config, 0, sway_connection) {
Ok(_) => todo!(),
Err(e) => error_handler(e),
}
},
Err(e) => error_handler(e),
}
},
ProfileCommand::Switch { switch_command} => {
match switch_command {
Some(ProfileSwitchCommand::To { index,name,query }) => {
match index {
Some(i) => {
match profile::switch_by_index(config, *i, sway_connection) {
Ok(_) => println!("successfully switched to profile at index {}",i),
Err(e) => error_handler(e),
};
},
None => {
match name {
Some(n) => {
match profile::switch_by_name(config, n.to_string(), sway_connection) {
Ok(_) => (),
Err(e) => error_handler(e),
}
},
None => match query {
Some(q) => match q.parse::<usize>() {
Ok(i) => match profile::switch_by_index(config.clone(), i, sway_connection) {
Ok(_) => (),
Err(_) => match profile::switch_by_name(config, q.to_string(), Connection::new().unwrap()) {
Ok(_) => (),
Err(_) => error_handler(ErrorMessage { message: Some(format!("Could not find profile with index or name: {}",q)), code: Some(4) }),
},
},
Err(_) => match profile::switch_by_name(config, q.to_string(), sway_connection) {
Ok(_) => (),
Err(_) => error_handler(ErrorMessage { message: Some(format!("Could not find profile with index or name: {}",q)), code: Some(4) }),
},
},
None => error_handler(ErrorMessage { message: Some("No profile index or name provided.".to_string()), code: Some(4) }),
},
}
},
}
},
Some(ProfileSwitchCommand::Next) => match profile::next(config, sway_connection) {
Ok(p) => {
println!("{:?}",p)
},
Err(e) => error_handler(e),
},
Some(ProfileSwitchCommand::Prev) => match profile::previous(config, sway_connection) {
Ok(p) => {
println!("{:?}",p)
},
Err(e) => error_handler(e),
},
None => {
// Would like for this to eventually present a menu (using wofi/worf?) which can be used to select the profile from a list
todo!();
/* match run_sway_command(&mut sway_connection, payload) {
Ok(_) => todo!(),
Err(_) => todo!(),
} */
},
}
},
ProfileCommand::Get { get_command, monitor } => {
match Some(get_command) {
Some(g) => {
println!("{}",get::print(config.clone(),g).unwrap());
if monitor.unwrap() {
let notify_config = notify::Config::default().with_compare_contents(true).with_poll_interval(Duration::from_millis(500));
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = PollWatcher::new(tx, notify_config).unwrap();
match watcher.watch(xdg_directories.runtime_dir.unwrap().join("sway-profiles-rs/active-profile.json").as_path(),RecursiveMode::Recursive) {
Ok(_) => {
for _res in rx {
println!("{}",get::print(config.clone(),g).unwrap());
}
},
Err(_) => error_handler(ErrorMessage { message: Some("Watcher failed".to_string()), code: Some(1) }),
};
}
},
None => error_handler(ErrorMessage { message: Some("No matching Profile Detail".to_string()), code: Some(4) }),
}
}
} }
}, },
Commands::Shortcuts { global: _ } => todo!(), Commands::Shortcuts { global: _ } => {
todo!()
},
} }
exit(0); exit(0);
} }
pub fn monitor_events(event_type: EventType, cli: Cli, config: Config) { pub fn monitor_sway_events(event_type: EventType, cli: Cli, config: Config) {
/* let subs = [
// Valid EventTypes: Workspace, Output, Input, Tick, Shutdown, Mode, Window, BarStateUpdate, BarConfigUpdate, Binding
//EventType::Workspace,
//EventType::Tick,
//EventType::Window,
event_type
]; */
let sway_connection = Connection::new().unwrap(); let sway_connection = Connection::new().unwrap();
for event in sway_connection.subscribe([event_type]).unwrap() { for event in sway_connection.subscribe([event_type]).unwrap() {
let e = event.unwrap(); let e = event.unwrap();
match e { match e {
Event::Window(w) => { Event::Window(w) => {
print_window_title(w.container, &cli, &config); print_window_title(w.container, &cli, &config);
}, },
Event::Workspace(_) => { Event::Workspace(_) => {
print_workspace_array(self::Connection::get_workspaces(&mut self::Connection::new().unwrap()).unwrap()); print_workspace_array(self::Connection::get_workspaces(&mut self::Connection::new().unwrap()).unwrap());
}, },