diff --git a/README.md b/README.md index a2ea7bd..75a39bb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ 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. +**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 ### Universal: @@ -30,4 +32,16 @@ Probably, idk man don't judge > Is it good? -Probably not \ No newline at end of file +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 diff --git a/src/lib/get.rs b/src/lib/get.rs new file mode 100644 index 0000000..d0fa366 --- /dev/null +++ b/src/lib/get.rs @@ -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 { + 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), + } +} \ No newline at end of file diff --git a/src/lib/launch.rs b/src/lib/launch.rs new file mode 100644 index 0000000..51505a8 --- /dev/null +++ b/src/lib/launch.rs @@ -0,0 +1,19 @@ +use std::collections::HashMap; + +use crate::{config::Programs, ErrorMessage}; + +pub fn profile_launch(programs_config: HashMap, program_arg: &String) -> Result { + 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) }) + }, + } +} \ No newline at end of file diff --git a/src/lib/profile.rs b/src/lib/profile.rs new file mode 100644 index 0000000..a8d11c4 --- /dev/null +++ b/src/lib/profile.rs @@ -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{ + 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 { + 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 { + 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::(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{ + 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() +} \ No newline at end of file diff --git a/src/lib/sway.rs b/src/lib/sway.rs new file mode 100644 index 0000000..7e850f2 --- /dev/null +++ b/src/lib/sway.rs @@ -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) }), + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ef15d05..ee26914 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 notify::{PollWatcher, RecursiveMode, Watcher}; mod config; #[path = "lib/windows.rs"] @@ -13,8 +16,21 @@ use workspaces::print_workspace_array; #[path = "lib/lock.rs"] mod lock; 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 xdg::BaseDirectories; + +use crate::profile::switch_by_index; #[derive(Parser)] @@ -28,10 +44,6 @@ pub struct Cli { #[arg(short = 'm', long = "monitor", action = ArgAction::SetTrue)] monitor: Option, -// /// Sets a custom config file -// #[arg(short, long, value_name = "FILE")] -// config: Option, - /// Turn debugging information on #[arg(short, long, action = ArgAction::Count)] debug: u8, @@ -56,32 +68,97 @@ enum Commands { #[arg(short, long, action = ArgAction::SetTrue)] force_render_background: Option, }, - //Rename, - Profile, + /// Profile WIP, + Profile { + #[command(subcommand)] + profile_command: ProfileCommand, + }, + /// Shortcuts TODO Shortcuts { #[arg(short, long, action = ArgAction::SetTrue)] global: Option, - }, -// Monitor { -// /// monitor a sway activity type -// #[arg(short, long)] -// #[command(subcommand)] -// monitor_type: MonitorTypes, -// }, + } } #[derive(Subcommand)] -enum MonitorTypes { - /// Monitors workspace changes - Workspaces, - /// Monitor window changes - Windows, - /// Monitor active profile - Profile +enum ProfileCommand { + /// switch profiles + #[clap(alias = "s")] + Switch { + #[command(subcommand)] + switch_command: Option, + + }, + /// get profile info + #[clap(alias = "g")] + Get { + /// monitor profile information + #[arg(short, long, action = ArgAction::SetTrue)] + monitor: Option, + #[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, + #[clap(group = "switch_to")] + #[arg(short, long)] + index: Option, + query: Option + + } +} + +#[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, + code: Option +} + +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<()> { //let xdg_dirs = BaseDirectories::with_prefix("sway-profiles-rs"); + let xdg_directories = BaseDirectories::new(); let cli = Cli::parse(); // let config = config::parse_config(); let config = confy::load("sway-profiles-rs", "config").unwrap(); @@ -91,24 +168,24 @@ fn main() -> Fallible<()> { Commands::Windows => { print_window_title(sway_connection.get_tree().unwrap().iter().find(|&x | x.focused).unwrap().clone(), &cli, &config); if cli.monitor.unwrap() { - monitor_events(EventType::Window, cli, config); + monitor_sway_events(EventType::Window, cli, config); } } Commands::Workspaces => { print_workspace_array(sway_connection.get_workspaces().unwrap()); if cli.monitor.unwrap() { - monitor_events(EventType::Workspace, cli, config); + monitor_sway_events(EventType::Workspace, cli, config); } } Commands::Launch { program } => { - if let Some(launch_program) = config.programs.iter().find(|x|x.0 == program) { - println!("found: {:#?} {:?}",launch_program.1.command, launch_program.1.arguments); - let mut swaymsg_command = "exec ".to_owned() + &launch_program.1.command; - for a in &launch_program.1.arguments { - swaymsg_command = swaymsg_command + " " + a; - } - println!("{:#?}",swaymsg_command); - let _ = sway_connection.run_command(swaymsg_command); + match profile_launch(config.programs, program) { + Ok(p) => { + match run_sway_command(&mut sway_connection,p) { + Ok(_) => todo!(), + Err(e) => error_handler(e), + } + }, + Err(e) => error_handler(e), } }, Commands::Lock { force_render_background } => { @@ -117,33 +194,119 @@ fn main() -> Fallible<()> { Err(e) => println!("{:?}",e), }; }, - //Commands::Rename => todo!(), - Commands::Profile => { - println!("{:?}", config.profiles.len()); - for p in config.profiles { - println!("{:?} {:?}",p.0, p.1.name ) + Commands::Profile { profile_command} => { + println!("{:?}",xdg_directories.get_runtime_directory()); + setup_runtime_dir(xdg_directories.clone()); + match profile_command { + 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::() { + 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); } -pub fn monitor_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 - ]; */ +pub fn monitor_sway_events(event_type: EventType, cli: Cli, config: Config) { let sway_connection = Connection::new().unwrap(); for event in sway_connection.subscribe([event_type]).unwrap() { let e = event.unwrap(); match e { Event::Window(w) => { print_window_title(w.container, &cli, &config); -}, + }, Event::Workspace(_) => { print_workspace_array(self::Connection::get_workspaces(&mut self::Connection::new().unwrap()).unwrap()); },