prepping for release

This commit is contained in:
Penelope Gwen 2026-01-12 23:05:22 -08:00
parent 907daea38e
commit 4bf8b57d5f
18 changed files with 885 additions and 814 deletions

33
Cargo.lock generated
View file

@ -1808,9 +1808,9 @@ dependencies = [
[[package]]
name = "swayipc"
version = "3.0.3"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8c50cb2e98e88b52066a35ef791fffd8f6fa631c3a4983de18ba41f718c736"
checksum = "7fd6ee13016b1ae2fbf741ddb2133d983a3fecdc34686eec6202e2a80612d82f"
dependencies = [
"serde",
"serde_json",
@ -1819,13 +1819,13 @@ dependencies = [
[[package]]
name = "swayipc-types"
version = "1.4.2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4f6205b8f8ea7cd6244d76adce0a0f842525a13c47376feecf04280bda57231"
checksum = "a29165dc67819a052bd67962b6c9de7c5d4461f0e8d224eb0f7cdd26cf05a650"
dependencies = [
"serde",
"serde_json",
"thiserror 1.0.69",
"thiserror 2.0.16",
]
[[package]]
@ -1955,9 +1955,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.23"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
dependencies = [
"serde",
"serde_spanned",
@ -1967,33 +1967,26 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.11"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tracing"
version = "0.1.41"
@ -2530,9 +2523,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.13"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]

View file

@ -20,7 +20,7 @@ serde = "1.0.219"
serde_json = "1.0.142"
sha2 = "0.10.9"
shellexpand = "3.1.1"
swayipc = "3.0.3"
swayipc = "4.0.0"
systemctl = "0.5.0"
tokio = "1.47.1"
walkdir = "2.5.0"

View file

@ -1,6 +1,9 @@
# sway-de-utils
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.
SDU 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.
*<center>Made with no "vibe coding"</center>*
*<center>Fuck you, learn how to have fun</center>*
**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.**
@ -18,7 +21,7 @@ todo
```
## Usage
`sway-profiles-rs --help` for now
`sdu --help` for now
## FAQ
@ -28,45 +31,15 @@ For my uses, yeah
> Is it fast?
Probably, idk man don't judge
Probably, idk girl don't judge
> Is it good?
Probably not
Probably not, if you:
* have opinions
* know what a computer is
## todo
* better failure/error handling
* daemonize?
## sway-profiles parity
* <span style="color:red"></span> sp-clipboard - not implemented, consider using something like `clipvault list | head -n10 | sed -e 's/^\w*\s*//'`
* <span style="color:green"></span> sp-launcher - implemented
* `sdu launch --program [program]`
* <span style="color:green"></span> sp-powermenu - implemented
* `sdu power [shutdown|reboot|suspend|lock|logout]`
* <span style="color:green"></span> sp-profile-icon - implemented
* `sdu profile get (--monitor) icon`
* <span style="color:red"></span> sp-rename - not implemented
* <span style="color:red"></span> sp-shortcuts - not yet implemented, plan to do so as `sdu shortcuts ...`
* <span style="color:red"></span> sp-global-shortcuts - not yet implemented, plan to do so as `sdu shortcuts -g ...`
* <span style="color:green"></span> sp-lock - implemented
* `sdu lock`
* <span style="color:yellow"></span> sp-profile - partially implemented
* `sdu profile get (--monitor) name`
* <span style="color:green"></span> sp-profiles - implemented
* `sdu profile init`
* `sdu profile switch [next|prev]`
* `sdu profile switch to (--name|--index) [name or index]`
* <span style="color:red"></span> sp-screenshot - not implemented, consider using [ferrishot](https://github.com/nik-rev/ferrishot) instead
* <span style="color:red"></span> sp-wallpaper - not planned, use [multibg-wayland](https://github.com/gergo-salyi/multibg-wayland) instead
## Exit Codes (reference)
* 0: success
* 1: generic
* 2: sway ipc/exec failure
* 3: bad configuration
* 4: bad cli args
* daemonize?

View file

@ -0,0 +1,16 @@
# sway-profiles parity
If you used [the original version of this project](https://git.pogmom.me/pogmommy/sway-profiles) (unlikely, it was basically just a bunch of messily packaged scripts) then you might be interested in how sway-desktop-utils provides the information/functionality of sway-profiles. This table was mostly used to prep alpha development for release.
|sway-profiles|status|sway-desktop-utils|
|-|-|-|
|`sp-launcher`|<span style="color:green"> implemented</span>|`sdu launch --program <program>`
|`sp-powermenu`|<span style="color:yellow"> partially implemented</span>|`sdu power [shutdown\|reboot\|suspend\|lock\|logout]` <hr>Currently prints power options in dmenu-compatible-ish list. Could be wrapped in script and passed back to self|
|`sp-rename`|<span style="color:orange"> not planned for initial release</span>|`sdu workspace rename` maybe?|
|`sp-shortcuts`<hr>`sp-global-shortcuts`|<span style="color:green"> implemented</span>|`sdu [profile] shortcuts get [--mode dmenu\|{json}]`<br>`sdu profile shortcuts exec <shortcut_name>`|
|`sp-lock`|<span style="color:green"> implemented</span>|`sdu lock [--force-render-background]`|
|`sp-profile`<hr>`sp-profile-icon`|<span style="color:green"> implemented</span>|`sdu profile get [--monitor] [name\|icon\|json]`<br>`sdu profile shortcuts`|
|`sp-profiles`|<span style="color:green"> implemented</span>|`sdu profile init`<br>`sdu profile switch <next\|prev>`<br>`sdu profile switch to [--name\|--index] <name or index>`|
|`sp-screenshot`|<span style="color:red"> not planned</span>|consider using [ferrishot](https://github.com/nik-rev/ferrishot) instead|
|`sp-clipboard`|<span style="color:red"> not planned</span>|consider using [clipvault](https://github.com/densumesh/clip-vault) instead, something like:<br>`clipvault list \| head -n10 \| sed -e 's/^\w*\s*//'`|
|`sp-wallpaper`|<span style="color:red"> not planned</span>|consider using [multibg-wayland](https://github.com/gergo-salyi/multibg-wayland) instead|

View file

@ -1,7 +1,7 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Serialize,Deserialize,Clone)]
#[derive(Serialize,Deserialize,Clone, Debug)]
pub struct WindowIcon {
pub icon: String,
pub substring: String
@ -31,17 +31,18 @@ pub struct ScriptConf {
pub icon: String,
pub command: String
}
#[derive(Serialize,Deserialize,Clone)]
#[derive(Serialize,Deserialize,Clone,Debug)]
pub struct Config {
// pub title_length: Option<usize>,
pub index_starts_at_1: bool,
pub initial_workspace: usize,
pub preserve_keyboard_order: bool,
// pub initial_workspace: Option<usize>,
pub window_icons: Vec<WindowIcon>,
pub programs: HashMap<String, Programs>,
pub lock: LockConf,
pub profiles: Vec<Profile>,
pub scripts: Vec<ScriptConf>,
}
impl ::std::default::Default for Config {
fn default() -> Self { Self { index_starts_at_1: false, initial_workspace: 1, window_icons: vec![], programs: HashMap::new(), lock: LockConf { wallpaper_path: None, blur: 10.0, scale: 0.75 }, profiles: vec![] } }
fn default() -> Self { Self { preserve_keyboard_order: false, window_icons: vec![], programs: HashMap::new(), lock: LockConf { wallpaper_path: None, blur: 10.0, scale: 0.75 }, profiles: vec![], scripts: vec![] } }
}

217
src/lib/cli.rs Normal file
View file

@ -0,0 +1,217 @@
use clap::{
ArgAction,
Parser,
Subcommand, ValueEnum
};
#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
/// Disable truncation of window titles
#[arg(short = 'T', long = "no-truncate-title", action = ArgAction::SetTrue)]
no_truncate_title: Option<bool>,
/// Enables monitoring for supported event types
#[arg(short = 'm', long = "monitor", action = ArgAction::SetTrue)]
monitor: Option<bool>,
/// Turn debugging information on
#[arg(short, long, action = ArgAction::Count)]
debug: u8,
#[command(subcommand)]
pub(crate) command: Commands,
// command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Prints Information about Sway Environment
#[clap(alias = "s")]
Sway {
#[command(subcommand)]
sway_command: SwayCommand,
},
/// Launch Program with Current Workspace's Profile Configuration
#[clap(alias = "L")]
Launch {
#[arg(short, long)]
program: String
},
/// Set up blurred wallpaper for screen lock, and locks screen
#[clap(alias = "l")]
Lock {
#[arg(short, long, action = ArgAction::SetTrue)]
force_render_background: Option<bool>,
},
/// Profile Management and Information,
#[clap(alias = "p")]
Profile {
#[command(subcommand)]
profile_command: ProfileCommand,
},
/// Lists global/profile shortcuts and optionally executes them
#[clap(alias = "scut")]
Shortcuts {
/// List shortcuts from global shortcut list rather than
// #[arg(short, long, action = ArgAction::SetTrue)]
// global: Option<bool>,
#[command(subcommand)]
shortcut_command: ShortcutCommand,
// /// Execute foud command
// #[arg(short, long, action = ArgAction::SetTrue)]
// execute: bool,
// /// Mode to print shortcut information
// #[command(subcommand)]
// mode: Option<ShortcutMode>
},
/// List or Execute Power/Session Commands
#[clap(alias = "pow")]
Power {
#[command(subcommand)]
power_command: Option<PowerCommand>,
}
}
#[derive(Clone)]
#[derive(Subcommand)]
pub enum ShortcutCommand {
/// Execute command by passed name
#[clap(alias = "exec")]
Execute {
shortcut_name: String
},
/// Mode to print shortcut information
#[clap(alias = "g")]
Get {
#[arg(short,long)]
mode: Option<ShortcutMode>
}
}
#[derive(Clone, Copy, ValueEnum)]
#[derive(Subcommand)]
pub enum ShortcutMode {
/// Print formatted for dmenu/wofi
#[clap(alias = "d")]
Dmenu,
/// Print formatted as json/for eww
#[clap(alias = "j")]
Json
}
#[derive(Subcommand)]
pub enum SwayCommand {
#[clap(alias = "g")]
Get {
/// Enable Monitoring for Sway IPC Events
#[arg(short, long, action = ArgAction::SetTrue)]
monitor: Option<bool>,
/// Get Sway Environment Information
#[command(subcommand)]
get_command: Option<SwayGetCommand>,
},
}
#[derive(Subcommand)]
pub enum SwayGetCommand {
/// Get Workspace Information
#[clap(alias = "ws")]
Workspaces,
/// Get Window Information
#[clap(alias = "n")]
Window,
/// Get Window and Workspace Information in one JSON output
#[clap(alias = "f")]
Full
}
#[derive(Subcommand)]
pub enum PowerCommand {
/// Powers down the system
#[clap(alias = "s")]
Shutdown,
/// Reboots the system
#[clap(alias = "r")]
Reboot,
/// Suspends (idles) the system
#[clap(alias = "i")]
Suspend,
/// Locks the system
#[clap(alias = "l")]
Lock {
#[arg(short, long, action = ArgAction::SetTrue)]
force_render_background: Option<bool>,
},
/// Logs out of (exits) the session
#[clap(alias = "e")]
Logout,
}
#[derive(Subcommand)]
pub enum ProfileCommand {
/// Switch Profiles
#[clap(alias = "s")]
Switch {
#[command(subcommand)]
switch_command: Option<ProfileSwitchCommand>,
},
/// Get Profile Information
#[clap(alias = "g")]
Get {
/// Monitor Profile Information
#[arg(short, long, action = ArgAction::SetTrue)]
monitor: Option<bool>,
#[command(subcommand)]
get_command: Option<ProfileGetCommand>,
},
/// Get profile shortcut and optionall execute them
#[clap(alias = "scut")]
Shortcuts {
#[command(subcommand)]
shortcut_command: ShortcutCommand,
},
/// Initialize Sway-DE-Utils
#[clap(alias = "i")]
Init
}
#[derive(Subcommand)]
pub enum ProfileSwitchCommand {
/// Switch to Next Profile
#[clap(alias = "n")]
Next,
/// Switch to Previous Profile
#[clap(alias = "p")]
Prev,
/// Switch to Profile by Name or Index
#[clap(alias = "t")]
To {
/// Explicitly Specify Profile by Name
#[clap(group = "switch_to")]
#[arg(short, long)]
name: Option<String>,
/// Explicitly Specify Profile by Index
#[clap(group = "switch_to")]
#[arg(short, long)]
index: Option<usize>,
/// Target Profile Name or Index
query: Option<String>
}
}
#[derive(Clone)]
#[derive(Subcommand)]
pub enum ProfileGetCommand {
/// Get Profile Details in JSON Format
#[clap(alias = "j")]
Json,
/// Get Profile Name in Plaintext
#[clap(alias = "n")]
Name,
/// Get Profile Icon in Plaintext
#[clap(alias = "i")]
Icon
}

View file

@ -1,37 +1,32 @@
//use crate::config::Profile;
use crate::{config::{Config,Profile}, profile::{active_profile_index,profile_from_index}, ErrorMessage, ProfileGetCommand};
//use crate::utils::config::c;
use serde_json::json;
use watchexec::Watchexec;
use watchexec_events::Tag;
use miette::{IntoDiagnostic, Result};
use {
crate::{
cli::ProfileGetCommand, config::Profile, profile::{
active_profile_index,
profile_from_index
}
}, miette::{
IntoDiagnostic,
Result
}, serde_json::json, watchexec::Watchexec, watchexec_events::Tag
};
pub fn profile_info(profiles: Vec<Profile>,info: ProfileGetCommand) -> Result<String,ErrorMessage> {
match active_profile_index() {
Ok(i) => {
match profile_from_index(profiles, i) {
Ok(p) => {
match info {
ProfileGetCommand::Json => Ok(json!(p).to_string()),
ProfileGetCommand::Name => Ok(p.name),
ProfileGetCommand::Icon => Ok(p.icon),
}
}
Err(e) => Err(e),
}
},
Err(e) => Err(e),
pub fn profile_info(profiles: Vec<Profile>,info: ProfileGetCommand) -> String {
let active_profile = profile_from_index(profiles, active_profile_index());
match info {
ProfileGetCommand::Json => json!(active_profile).to_string(),
ProfileGetCommand::Name => active_profile.name,
ProfileGetCommand::Icon => active_profile.icon,
//ProfileGetCommand::Shortcuts => json!(active_profile.scripts).to_string(),
}
}
#[tokio::main]
pub async fn watch(config: Config,info: ProfileGetCommand,watch_path: String) -> Result<()> {
let profiles = config.profiles;
pub async fn watch(profiles_config: Vec<Profile>,info: ProfileGetCommand,watch_path: String) -> Result<()> {
let wx = Watchexec::new(move |mut action| {
//yeah, this is... fine
action.events.iter().find(|_event|_event.tags.iter().any(|tag| match tag {
Tag::FileEventKind(watchexec_events::filekind::FileEventKind::Access(watchexec_events::filekind::AccessKind::Close(watchexec_events::filekind::AccessMode::Write))) => {
println!("{}",profile_info(profiles.clone(),info).unwrap_or_default());
println!("{}",profile_info(profiles_config.clone(),info.clone()));
true
},
_ => false

View file

@ -1,6 +1,19 @@
use std::collections::HashMap;
use crate::{config::{Profile, Programs}, profile::active_profile, ErrorMessage};
use log::debug;
use crate::{SDUError, config::{Profile, Programs}, profile::active_profile, sway_ipc::{get_sway_connection, run_sway_command}};
pub fn launch( exec: &String, profiles: Vec<Profile>, programs: HashMap<String, Programs> ) -> Result<(), SDUError> {
debug!("{}",exec);
match profile_launch(profiles, programs, exec) {
Ok(p) => {
debug!("{}",p);
run_sway_command(&mut get_sway_connection(),p)
},
Err(e) => Err(e),
}
}
pub fn append_arguments(mut command: String, args: Vec<String>) -> String {
for a in args {
@ -9,7 +22,7 @@ pub fn append_arguments(mut command: String, args: Vec<String>) -> String {
command
}
pub fn profile_launch(profiles: Vec<Profile>, programs_config: HashMap<String, Programs>, program_arg: &String) -> Result<String,ErrorMessage> {
pub fn profile_launch(profiles: Vec<Profile>, programs_config: HashMap<String, Programs>, program_arg: &String) -> Result<String,SDUError> {
match programs_config.iter().find(|x|x.0.eq(program_arg)) {
Some(p) => {
let mut swaymsg_command = format!("exec {}", p.1.command);
@ -26,7 +39,7 @@ pub fn profile_launch(profiles: Vec<Profile>, programs_config: HashMap<String, P
}
},
None => {
Err(ErrorMessage { message: String::from("No matching program found"), code: 3 })
Err(SDUError { message: String::from("No matching program found") })
},
}
}

View file

@ -1,15 +1,15 @@
use std::{env::consts, fs::{self, DirBuilder, exists, write}, io, ops::Sub, os::unix::fs::DirBuilderExt, path::{Path, PathBuf}, vec};
use std::{env::consts, fs::{self, DirBuilder, write}, io, path::PathBuf, vec};
use log::debug;
use walkdir::WalkDir;
use image::{imageops::GaussianBlurParameters, DynamicImage};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Sha256, Digest};
use swayipc::Connection;
use xdg::BaseDirectories;
use shellexpand::tilde;
use crate::{ErrorMessage, config::{Config, LockConf}, error_handler};
use crate::{DirectoryType, SDUError, config::LockConf, sway_ipc::get_sway_connection};
#[derive(Serialize,Deserialize,Debug)]
pub struct WallpaperCache {
@ -17,6 +17,82 @@ pub struct WallpaperCache {
pub hash: String
}
pub fn lock(lock_config: LockConf,force_bg_render: bool) -> Result<(),SDUError> {
let mut sway_connection = get_sway_connection();
match &lock_config.wallpaper_path {
Some(wallpaper_path) => {
let wallpaper_root_path = PathBuf::from(tilde(&wallpaper_path).to_string());
let wp_cache_dir = get_path(DirectoryType::Cache, "sway-profiles-rs/lock/");
DirBuilder::new().recursive(true).create(&wp_cache_dir).expect("Failed to create cache directory");
let wp_hash_json = wallpaper_check_hash(wp_cache_dir.join("hashes.json"));
debug!("{:?}",wp_hash_json);
let mut wp_hash_array: Vec<WallpaperCache> = vec![];
let sway_visible_workspaces = match sway_connection.get_workspaces() {
Ok(w) => w.iter().filter(|w|w.visible).cloned().collect(),
Err(_) => vec![],
};
for workspace in sway_visible_workspaces {
debug!("operating on workspace {} ({})",workspace.num, workspace.name);
let wp_output_path = wallpaper_root_path.join(&workspace.output);
let output_image_path = wp_output_path.join(format!("{:0>2}.jpg",workspace.num));
let mut wp_sha256 = Sha256::new();
let img_file_data = match fs::File::open(&output_image_path) {
Ok(mut img_file_data) => Ok(io::copy(&mut img_file_data, &mut wp_sha256).unwrap_or_default()),
Err(_) => Err(SDUError { message: format!("Could not open wallpaper image for output {}", workspace.output) }),
};
if img_file_data.is_ok() {
let wp_hash = hex::encode(wp_sha256.finalize());
let wp_image = image::open(output_image_path).expect("Could not open wallpaper file as image");
let output_cache_image_path = wp_cache_dir.join(format!("{}.jpg",workspace.output));
match wp_hash_json.iter().find(|wc|wc.display == workspace.output) {
Some (saved_hash) => {
if !(saved_hash.hash == wp_hash && output_cache_image_path.exists()) || force_bg_render {
generate_image(wp_image,output_cache_image_path, lock_config.clone())
};
},
None => generate_image(wp_image, output_cache_image_path, lock_config.clone())
}
wp_hash_array.push(WallpaperCache { display: workspace.output, hash: wp_hash });
};
};
debug!("{:?}",wp_hash_array);
let new_json = json!(wp_hash_array);
let _ = write(wp_cache_dir.join("hashes.json"), new_json.to_string());
let gtklock_modules_dir = format!("/usr/lib/{}-linux-gnu/gtklock/",consts::ARCH);//.to_owned() + consts::ARCH + "-linux-gnu/gtklock/";
let gktlock_modules_path = PathBuf::from(gtklock_modules_dir);
let mut gtklock_args: Vec<String> = vec![];
for gtklock_module in WalkDir::new(gktlock_modules_path).into_iter().filter(|x|x.as_ref().expect("TODO").file_name().to_str().unwrap_or_default().ends_with("-module.so")) {
let gtklock_module_path = gtklock_module.expect("TODO").path().to_string_lossy().to_string();
debug!("Foud gtklock module: {}", gtklock_module_path);
gtklock_args.push("-m".to_owned());
gtklock_args.push(gtklock_module_path);
}
let mut gtklock_command = "exec gtklock".to_owned();
for gtklock_arg in gtklock_args {
gtklock_command = gtklock_command + " " + &gtklock_arg;
}
debug!("{}", gtklock_command);
match sway_connection.run_command(gtklock_command) {
Ok(_) => Ok(()),
Err(_) => Err(SDUError { message: "Could not lock screen!".to_owned() }),
}
},
None => Err(SDUError { message: "Wallpaper path is not set!".to_owned() }),
}
}
pub fn generate_image(orig_img_data: DynamicImage,gen_image_path: PathBuf, lock_conf: LockConf) {// Result<(), ImageError> {
//let img_data = image::open(image_path).unwrap();
let w = orig_img_data.width() as f32 * lock_conf.scale;
@ -31,60 +107,10 @@ pub fn generate_image(orig_img_data: DynamicImage,gen_image_path: PathBuf, lock_
}
}
pub fn wallpaper_setup() -> Result<(),ErrorMessage> {
todo!()
}
pub fn wallpaper_check_hash(hashes_json_path: PathBuf) -> Vec<WallpaperCache> {
//let mut wallpaper_hashes: Vec<WallpaperCache> = vec![];
//let mut wallpaper_hashes: Vec<WallpaperCache> = if hashes_json_path.exists().then(||{
/*let wallpaper_hashes: Vec<WallpaperCache> = fs::File::open(&hashes_json_path).map(|f| -> Vec<WallpaperCache> {match serde_json::from_reader(f) {
Ok(p) => p,
Err(_) => vec![],
}}).unwrap(); //.or_else(|_|Ok(vec![])).expect("Wallpaper cache vec should be empty if it can't be mapped");
*/
let wallpaper_hashes: Vec<WallpaperCache> = fs::File::open(hashes_json_path).ok()
.and_then(|f| serde_json::from_reader(f).ok()).unwrap_or(vec![]);
todo!();
/* let wallpaper_hashes = match fs::File::open(hashes_json_path).and_then(|f| Ok(serde_json::from_reader(f))) {
Ok(o) => {
let t = o.unwrap();
},
Err(_) => vec![],
};*/
/* let mut wallpaper_hashes = match fs::File::open(hashes_json_path) {
Ok(f) => {
let json = serde_json::from_reader(f).unwrap_or_default();
}
Err(_) => todo!(),
};*/
// let mut wallpaper_hashes: Vec<WallpaperCache> = hashes_json_path.try_exists();
// exists().then_some(||{
todo!();
// }); //.unwrap_or(vec![])
/*if hashes_json_path.exists() {
serde_json::from_reader(fs::File::open(hashes_json_path))?
} else { vec![] };*/ /*{
match fs::File::open(hashes_json_path) {
Ok(hash_file) => serde_json::from_reader(hash_file).unwrap_or(vec![]),
Err(_) => vec![],
}
} else { vec![] };*/
todo!()
}
pub fn wallpaper_hash_write() -> Result<(),ErrorMessage>{
todo!()
}
pub enum DirectoryType {
Cache,
Config
wallpaper_hashes
}
pub fn get_path(dir_type: DirectoryType, suffix: &str) -> PathBuf {
@ -95,120 +121,3 @@ pub fn get_path(dir_type: DirectoryType, suffix: &str) -> PathBuf {
}.expect("HOME could not be found");
base_dir.join(suffix)
}
pub fn lock_screen(config: &Config, mut sway_connection: Connection, force_bg_render: bool) -> Result<Vec<Result<(), swayipc::Error>>, swayipc::Error> {
match &config.lock.wallpaper_path {
Some(wallpaper_path) => {
let wallpaper_root_path = PathBuf::from(tilde(&wallpaper_path).to_string());
let wp_cache_dir = get_path(DirectoryType::Cache, "sway-profiles-rs/lock/");
DirBuilder::new().recursive(true).create(wp_cache_dir).expect("Failed to create cache directory");
let mut wp_hash_json = wallpaper_check_hash(wp_cache_dir.join("hashes.json"));
//let mut wp_hash_array: Vec<WallpaperCache> = vec![];
match sway_connection.get_outputs() {
Ok(outputs) => {
for output in outputs {
match sway_connection.get_workspaces() {
Ok(workspaces) => {
let current_workspace = match output.current_workspace.clone() {
Some(current_workspace_name) => current_workspace_name,
None => todo!(),
};
let output_workspace = match workspaces.iter().find(|x|x.name == current_workspace) {
Some(output_workspace) => output_workspace,
None => todo!(),
};
format!("{:0>2}",output_workspace.num.sub(1)).chars().next().unwrap_or_default();
let output_ws_num = match format!("{:0>2}",output_workspace.num.sub(1)).chars().next() {
Some(output_ws_num) => output_ws_num,
None => todo!(),
};
let image_path = wallpaper_root_path.join(output.name.clone()).join(format!("p_{}.jpg",output_ws_num));
let mut img_file_data = match fs::File::open(&image_path) {
Ok(img_file_data) => img_file_data,
Err(_) => todo!(),
};
let mut sha256 = Sha256::new();
match io::copy(&mut img_file_data, &mut sha256) {
Ok(_) => {
let hash = hex::encode(sha256.finalize());
wp_hash_array.push(WallpaperCache { display: output.name, hash });
let wallpaper_data = match image::open(&image_path) {
Ok(wallpaper_data) => wallpaper_data,
Err(_) => todo!(),
};
let wallpaper_cache_path = match cache_dir.as_ref() {
Some(wallpaper_cache_path) => wallpaper_cache_path.join(format!("sway-profiles-rs/lock/{}.jpg",output.name)),
None => todo!(),
};
match wp_hash_json.iter().find(|x|x.display == output.name) {
Some (saved_hash) => {
if !(saved_hash.hash == hash && wallpaper_cache_path.exists()) || force_bg_render {
generate_image(wallpaper_data,wallpaper_cache_path, config.lock.clone())
};
},
None => generate_image(wallpaper_data, wallpaper_cache_path, config.lock.clone())
}
},
Err(_) => error_handler( crate::ErrorMessage { message: "unable to read image file".to_string(), code: 99 }),
}
},
Err(_) => todo!(),
};
}
},
Err(_) => todo!(),
}
},
None => todo!(),
}
//let wallpaper_root_path = tilde(&config.lock.wallpaper_path).to_string();
//let wallpaper_root_path = Path::new(&wallpaper_root_path);
// for output in sway_connection.get_outputs().unwrap() {
//let sway_workspaces = sway_connection.get_workspaces().unwrap().clone();
//let output_ws_num = format!("{:0>2}",sway_workspaces.iter().find(|x|x.name == output.current_workspace
// .clone().unwrap()).unwrap().num.sub(1)).chars().next().unwrap();
//let image_path = wallpaper_root_path.join(output.name.clone()).join(format!("p_{}.jpg",output_ws_num));
//let mut img_file_data = fs::File::open(&image_path).unwrap();
//let mut sha256 = Sha256::new();
//let _ = io::copy(&mut img_file_data, &mut sha256);
//let hash = format!("{:x}",sha256.finalize());
//wp_hash_array.push(WallpaperCache { display: output.name.clone(), hash: hash.clone() });
// if hashes_json_path.exists() {
//let wallpaper_data = image::open(&image_path).unwrap();
//let wallpaper_cache_path = cache_dir.as_ref().unwrap().join(format!("sway-profiles-rs/lock/{}.jpg",output.name));
/* match wp_hash_json.iter().find(|x|x.display == output.name) {
Some (saved_hash) => {
if !(saved_hash.hash == hash && wallpaper_cache_path.exists()) || force_bg_render {
generate_image(wallpaper_data,wallpaper_cache_path, config.lock.clone())
};
},
None => generate_image(wallpaper_data, wallpaper_cache_path, config.lock.clone())
} */
// }
todo!();
/* let gtklock_modules_dir = format!("/usr/lib/{}-linux-gnu/gtklock/",consts::ARCH);//.to_owned() + consts::ARCH + "-linux-gnu/gtklock/";
let gktlock_modules_path = Path::new(&gtklock_modules_dir);
let mut gtklock_args: Vec<String> = vec![];
for file in WalkDir::new(gktlock_modules_path).into_iter() {//.find(|x|x.as_ref().unwrap().file_name().to_str().unwrap().ends_with("-module.so")) {
if let Some(f) = file.iter().find(|&x|x.path().to_str().unwrap().ends_with("-module.so")) {
gtklock_args.push("-m".to_owned());
gtklock_args.push(f.path().to_str().unwrap().to_string());
}
}
let new_json = json!(wp_hash_array);
let _ = write(hashes_json_path, new_json.to_string());
let mut gtklock_command = "exec gtklock".to_owned();
for a in gtklock_args {
gtklock_command = gtklock_command + " " + &a;
}
println!("{:?}",gtklock_command);
// let _ = sway_connection.run_command(gtklock_command);
sway_connection.run_command(gtklock_command)*/
}

49
src/lib/power.rs Normal file
View file

@ -0,0 +1,49 @@
use {
crate::{
SDUError, cli::PowerCommand, config::LockConf, lock, run_sway_command, sway_ipc::get_sway_connection
}, dialog::DialogBox, log::debug
};
pub fn power(power_command_arg: &Option<PowerCommand>, lock_config: LockConf ) -> Result<(),SDUError> {
match power_command_arg {
Some(power_command) => {
let action_prompt = match power_command {
PowerCommand::Shutdown => vec!["shut down","poweroff.target"],
PowerCommand::Reboot => vec!["restart","reboot.target"],
PowerCommand::Suspend => vec!["suspend","suspend.target"],
PowerCommand::Lock { force_render_background: _ } => vec!["lock"],
PowerCommand::Logout => vec!["log out"],
};
let action = action_prompt.first().unwrap_or(&"not found");
if let Ok(dialog::Choice::Yes) = dialog::Question::new(format!("Are you sure you would like to {}?",action)).title(format!("Confirm {}...",action)).show() {
match power_command {
PowerCommand::Lock { force_render_background } => {
debug!("running lock_screen function");
return lock(lock_config, force_render_background.unwrap_or_default())
},
PowerCommand::Logout => {
debug!("sending exit command to sway");
return run_sway_command(&mut get_sway_connection(), "exit".to_string())
},
_ => {
let target = action_prompt.get(1).unwrap_or(&"not found");
let systemctl = systemctl::SystemCtl::default();
debug!("looking for {}",target);
if let Ok(true) = systemctl.exists(target) {
debug!("found and starting {}",target);
match systemctl.start(target) {
Ok(_) => return Ok(todo!("properly handle this return")),
Err(_) => return Err(SDUError { message: "Could not start systemctl target".to_string() }),
}
}
}
}
};
Ok(())
},
None => {
println!("⏻ Shutdown\n Reboot\n Suspend\n Lock\n Logout");
Ok(())
},
}
}

View file

@ -1,65 +1,154 @@
use std::fs::{self, write};
use {
crate::{
SDUError,
cli::{
ProfileCommand,
ProfileGetCommand,
ProfileSwitchCommand
}, config::{ Config, Profile},
/*error_handler,*/ get,
get_xdg_dirs,
setup_runtime_dir,
shortcuts::shortcuts,
sway_ipc::{get_sway_connection, run_sway_command}
},
log::{debug, error},
serde_json::json,
std::fs::{self, DirBuilder, write},
swayipc::Connection,
xdg::BaseDirectories
};
use log::debug;
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, initial_workspace: usize) -> Result<(),ErrorMessage> { //payload: String) {
let workspace_index_start = (uindex * 10) + initial_workspace;
debug!("{}",workspace_index_start);
todo!();
//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),
};
pub fn profile(profile_command: &ProfileCommand, profiles_config: Vec<Profile>, preserve_keyboard_order: bool) -> Result<(),SDUError> {
// debug!("{:?}",xdg_directories.get_runtime_directory());
let xdg_dirs = get_xdg_dirs();
setup_runtime_dir(xdg_dirs);
match profile_command {
ProfileCommand::Init => {
//let index_start = if config.preserve_keyboard_order { 1 } else { 0 };
//debug!("profile index will start at {}",index_start);
let active_profile_index = active_profile_index();
match switch_by_index(profiles_config, active_profile_index, preserve_keyboard_order) {
Ok(_) => Ok(debug!("successfully initialized sway-desktop-utils")),
Err(e) => Err(e),
}
},
ProfileCommand::Switch { switch_command} => {
match switch_command {
Some(ProfileSwitchCommand::To { index,name,query }) => {
match index {
Some(i) => {
switch_by_index(profiles_config, *i, preserve_keyboard_order)
},
None => {
match name {
Some(n) => {
switch_by_name(profiles_config, n.to_string(), preserve_keyboard_order)
},
None => match query {
Some(q) => match q.parse::<usize>() {
Ok(i) => match switch_by_index(profiles_config.clone(), i, preserve_keyboard_order) {
Ok(o) => Ok(o),
Err(_) => switch_by_name(profiles_config, q.to_string(), preserve_keyboard_order),
},
Err(_) => switch_by_name(profiles_config, q.to_string(), preserve_keyboard_order),
},
None => Err(SDUError { message: "No profile index or name provided.".to_string() }),
},
}
},
}
},
Some(ProfileSwitchCommand::Next) => match next(profiles_config, preserve_keyboard_order) {
Ok(_) => Ok(debug!("Successfully switched to next profile")),
Err(e) => Err(e),
},
Some(ProfileSwitchCommand::Prev) => match previous(profiles_config, preserve_keyboard_order) {
Ok(_) => Ok(debug!("successfully switched to previous profile")),
Err(e) => Err(e),
},
None => {
Ok(for profile in profiles_config {
println!("{} {}", profile.icon, profile.name);
})
},
}
},
ProfileCommand::Get { get_command, monitor } => {
let profile_detail = get_command.clone().unwrap_or(ProfileGetCommand::Json);
println!("{}",get::profile_info(profiles_config.clone(),profile_detail.clone()));
Ok(if monitor.unwrap_or_default() {
let _ = get::watch(profiles_config, profile_detail,get_xdg_dirs().runtime_dir.expect("XDG Runtime dir could not be found").join("sway-desktop-utils/active-profile.json").to_str().expect("could not create str from path").to_string());
})
}
ProfileCommand::Shortcuts { shortcut_command } => {
let active_profile = active_profile(profiles_config).expect("could not determine active profile");
shortcuts(shortcut_command, active_profile.scripts.expect("could not find scripts for profile"))
},
}
match run_sway_command(&mut sway_connection, format!("workspace number {}1:{}",profile_index,profile.icon)) {
}
pub fn initialize(mut sway_connection: Connection, profile: Profile,uindex: usize, preserve_keyboard_order: bool) -> Result<(),SDUError> { //payload: String) {
//let payload = "bindsym $mod+{} workspace number {}{}:{}";
// let profile_initial_workspace: usize = if preserve_keyboard_order { 1 } else { 0 };
for i in 0..10 {
let workspace = (uindex*10) + match i.eq(&0) && !preserve_keyboard_order {
true => 10,
false => i,
};
debug!("key {}: workspace {}",i,workspace);
//debug!("profile index: {}, uindex: {}",profile_index,uindex);
debug!("bindsym $mod+{} workspace number {}:{}",i,workspace,profile.icon);
match run_sway_command(&mut sway_connection, format!("bindsym $mod+{} workspace number {}:{}",i,workspace,profile.icon)) {
Ok(_) => {
},
Err(_) => todo!(),
}
};
let base_directories = BaseDirectories::new();
let active_profile_cache = base_directories.get_runtime_directory().expect("why tf do xdg dirs not exist").join("sway-desktop-utils/active-profile.json");
let active_profile = json!(uindex);
DirBuilder::new().recursive(true).create(active_profile_cache.parent().expect("Unable to determine cache dir")).expect("Failed to create cache directory");
match write(active_profile_cache, active_profile.to_string()) {
Ok(o) => debug!("{:#?}",o),
Err(e) => debug!("{:#?}",e),
};
match run_sway_command(&mut sway_connection, format!("workspace number {}:{}",(uindex*10)+1,profile.icon)) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
pub fn profile_from_index(profiles: Vec<Profile>, index: usize) -> Result<Profile,ErrorMessage>{
match profiles.get(index) {
Some(p) => Ok(p.clone()),
None => Err(ErrorMessage { message: "Profile not found for index".to_string(), code: 3 }),
}
pub fn profile_from_index(profiles: Vec<Profile>, index: usize) -> Profile {
profiles.get(index).expect("Profile not found for index").clone() // {
// Some(p) => Ok(p.clone())
}
pub fn _profile_from_name(config: Config, name: String) -> Result<Profile,ErrorMessage> {
pub fn _profile_from_name(config: Config, name: String) -> Result<Profile,SDUError> {
match config.profiles.iter().find(|x|x.name == name) {
Some(p) => Ok(p.clone()),
None => Err(ErrorMessage { message: format!("Profile not found with name {}",name), code: 3 }),
None => Err(SDUError { message: format!("Profile not found with name {}",name) }),
}
}
pub fn switch_by_index(config: Config,index: usize,sway_connection: Connection, initial_workspace: usize) -> Result<(),ErrorMessage> {
match profile_from_index(config.profiles, index) {
Ok(p) => match initialize(sway_connection, p.clone(), index_string(index), index, initial_workspace) {
Ok(_) => {
println!("successfully switched to profile at index {}",index);
Ok(())
},
Err(e) => Err(e),
pub fn switch_by_index(profiles_config: Vec<Profile>,index: usize, preserve_keyboard_order: bool) -> Result<(),SDUError> {
let profile = profile_from_index(profiles_config, index);
match initialize(get_sway_connection(), profile, index, preserve_keyboard_order) {
Ok(_) => {
println!("successfully switched to profile at index {}",index);
Ok(())
},
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.clone(), index, sway_connection, config.initial_workspace) {
pub fn switch_by_name(profiles_config: Vec<Profile>,name: String,preserve_keyboard_order: bool) -> Result<(),SDUError> {
match index_from_name(profiles_config.clone(), name.clone()) {
Ok(index) => match switch_by_index(profiles_config, index,preserve_keyboard_order) {
Ok(_) => {
println!("Successfully switched to profile with name {}",name);
Ok(())
@ -68,93 +157,93 @@ pub fn switch_by_name(config: Config,name: String,sway_connection: Connection) -
},
Err(e) => Err(e),
}
}
/* match profile_from_name(config, name) {
Ok(p) => match {
pub fn active_profile_index() -> usize {
let base_directories = BaseDirectories::new();
let active_profile_cache_json = base_directories.get_runtime_directory().expect("xdg dirs do not exist?").join("sway-desktop-utils/active-profile.json");
/* match active_profile_cache_json.exists().then(|| fs::File::open(active_profile_cache_json).and_then(|f| Ok(serde_json::from_reader::<fs::File,usize>(f)))) {
Some(a) => todo!(),
None => todo!(),
} */
/* Ok(u) => {
Ok(u)
},
Err(_) => Err(ErrorMessage { message: "could not parse json from active profile cache file".to_string(), code: 3 }),
Err(_) => Err(ErrorMessage { message: "could not open active profile cache file".to_string(), code: 3 }),
} */
if active_profile_cache_json.exists() {
fs::File::open(active_profile_cache_json).ok()
.and_then(|f|serde_json::from_reader::<fs::File,usize>(f).ok())
.expect("could not parse active profile cache file")
//serde_json::from_reader::<fs::File,usize>(cache_json_reader).expect("could not parse json from active profile cache file")
// let cache_json_reader = fs::File::open(active_profile_cache_json).expect("could not open active profile cache file");
// serde_json::from_reader::<fs::File,usize>(cache_json_reader).expect("could not parse json from active profile cache file")
} else {
error!("no active profile cache file");
0
}
/* 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: "could not parse json from active profile cache file".to_string(), code: 3 }),
}
},
Err(_) => Err(ErrorMessage { message: "could not open active profile cache file".to_string(), code: 3 }),
} */
// )
/* {
true => {
},
Err(e) => Err(e),
false => Err(ErrorMessage { message: "no active profile cache file".to_string(), code: 3 }),
} */
}
pub fn active_profile_index() -> 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: "could not parse json from active profile cache file".to_string(), code: 3 }),
}
},
Err(_) => Err(ErrorMessage { message: "could not open active profile cache file".to_string(), code: 3 }),
}
},
false => Err(ErrorMessage { message: "no active profile cache file".to_string(), code: 3 }),
pub fn active_profile(profiles: Vec<Profile>) -> Result<Profile,SDUError> {
match profiles.get(active_profile_index()) {
Some(p) => Ok(p.clone()),
None => Err(SDUError { message: "Could not get profile by index".to_string() }),
}
}
pub fn active_profile(profiles: Vec<Profile>) -> Result<Profile,ErrorMessage> {
//let active_profile_index =
match active_profile_index() {
Ok(i) => match profiles.get(i) {
Some(p) => Ok(p.clone()),
None => Err(ErrorMessage { message: "Could not get profile by index".to_string(), code: 1 }),
pub fn next(profiles_config: Vec<Profile>, preserve_keyboard_order: bool) -> Result<(),SDUError> {
let profile_count = profiles_config.len();
let mut next_profile = active_profile_index() + 1;
if next_profile.ge(&profile_count) {
next_profile = 0
}
match switch_by_index(profiles_config, next_profile, preserve_keyboard_order) {
Ok(_) => {
println!("switched to next profile ({})",next_profile);
Ok(())
},
Err(e) => Err(e),
}
}
pub fn next(config: Config, sway_connection: Connection) -> Result<(),ErrorMessage> {
match active_profile_index() {
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.clone(), next_profile, sway_connection, config.initial_workspace) {
Ok(_) => {
println!("switched to next profile ({})",next_profile);
Ok(())
},
Err(e) => Err(e),
}
pub fn previous(profiles_config: Vec<Profile>, preserve_keyboard_order: bool) -> Result<(),SDUError> {
let profile_index = active_profile_index();
let prev_profile: usize = if profile_index.eq(&0) {
profiles_config.len() - 1
} else {
profile_index - 1
};
match switch_by_index(profiles_config, prev_profile, preserve_keyboard_order) {
Ok(_) => {
println!("switched to prev profile ({})",prev_profile);
Ok(())
},
Err(e) => Err(e),
}
}
pub fn previous(config: Config, sway_connection: Connection) -> Result<(),ErrorMessage> {
match active_profile_index() {
Ok(u) => {
let prev_profile: usize = if u.eq(&0) {
config.profiles.len() - 1
} else {
u - 1
};
match switch_by_index(config.clone(), prev_profile, sway_connection, config.initial_workspace) {
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) {
pub fn index_from_name(profiles_config: Vec<Profile>, name: String) -> Result<usize, SDUError>{
match profiles_config.iter().position(|x|x.name == name) {
Some(i) => Ok(i),
None => Err(ErrorMessage { message: "Index not found for profile?".to_string(), code: 3 }),
None => Err(SDUError { message: "Index not found for profile?".to_string() }),
}
}
pub fn index_string(index: usize) -> String {
index.to_string().trim_matches('0').to_string()
}

37
src/lib/shortcuts.rs Normal file
View file

@ -0,0 +1,37 @@
use log::debug;
use serde_json::json;
use crate::{SDUError, cli::{ShortcutCommand, ShortcutMode}, config::ScriptConf, sway_ipc::{get_sway_connection, run_sway_command}};
pub fn print_shortcuts(mode: &ShortcutMode, shortcuts: Vec<ScriptConf>) -> String {
match mode {
ShortcutMode::Dmenu => {
let mut dmenu_shortcuts: String = String::new();
for shortcut in shortcuts {
dmenu_shortcuts = format!("{}\n{} {}",dmenu_shortcuts,shortcut.icon, shortcut.name)
};
dmenu_shortcuts
},
ShortcutMode::Json => {
json!(shortcuts).to_string()
},
}
}
pub fn exec_shortcut(shortcut_name: String, shortcuts: Vec<ScriptConf>) -> Result<(),SDUError> {
let shortcut = shortcuts.iter().find(|x|x.name.eq(&shortcut_name)).expect("cannot find shortcut with name");
debug!("running [{}]", shortcut.command);
run_sway_command(&mut get_sway_connection(), format!("exec {}",shortcut.command.clone()))
}
pub fn shortcuts(shortcut_command: &ShortcutCommand, shortcuts: Vec<ScriptConf> ) -> Result<(),SDUError> {
match shortcut_command {
ShortcutCommand::Execute { shortcut_name } => exec_shortcut(shortcut_name.to_string(), shortcuts),
ShortcutCommand::Get { mode } => {
let shortcuts_string = print_shortcuts(&mode.unwrap_or(ShortcutMode::Json), shortcuts);
println!("{}", shortcuts_string);
Ok(())
},
}
}

View file

@ -1,43 +1,33 @@
use std::time::Instant;
use swayipc::{Connection, EventType};
use crate::{config::Config, windows::print_window_info, workspaces::print_workspace_info, Cli, 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: e.to_string(), code: 2 }),
}
}
pub fn monitor_events(event_type: Vec<EventType>, cli: Cli, config: Config) {
let sway_connection = Connection::new().unwrap();
for event in sway_connection.subscribe(event_type).unwrap() {
let start = Instant::now();
let e = event.unwrap();
match e {
swayipc::Event::Window(w) => {
print_window_info(w.container, &cli, &config)
},
swayipc::Event::Workspace(_) => {
match cli.command {
crate::Commands::Window { window_command: _ } => {
let mut con = Connection::new().unwrap();
print_window_info(con.get_tree().unwrap().find(|x|x.focused).unwrap(), &cli, &config)
},
crate::Commands::Workspace { workspace_command: _ } => {
let mut con = Connection::new().unwrap();
print_workspace_info(con.get_workspaces().unwrap())
},
_ => todo!(),
}
},
swayipc::Event::Tick(_) => todo!(),
_ => unreachable!(),
use {
crate::{
SDUError, cli::{
SwayCommand,
SwayGetCommand
}, config::WindowIcon, get_window_info, get_workspace_info, sway_ipc::{
self,
get_sway_info
}
log::debug!("time taken: {:?}",start.elapsed());
},
swayipc::EventType
};
pub fn sway( sway_command: &SwayCommand, window_icons: &[WindowIcon] ) -> Result<(),SDUError> {
match sway_command {
SwayCommand::Get { monitor, get_command } => {
let requested_info = get_command.as_ref().unwrap_or(&SwayGetCommand::Full);
let sway_json = match requested_info {
SwayGetCommand::Workspaces => get_workspace_info(),
SwayGetCommand::Window => get_window_info(window_icons),
SwayGetCommand::Full => get_sway_info(window_icons),
};
println!("{}",sway_json);
Ok(if monitor.unwrap_or_default() {
let event_monitors = match requested_info {
SwayGetCommand::Workspaces => vec![EventType::Workspace],
SwayGetCommand::Window | SwayGetCommand::Full => vec![EventType::Window,EventType::Workspace],
};
sway_ipc::monitor_events(event_monitors, requested_info, window_icons);
})
},
}
}

56
src/lib/sway_ipc.rs Normal file
View file

@ -0,0 +1,56 @@
use {
crate::{
SDUError, cli::SwayGetCommand, config::WindowIcon, windows::get_window_info, workspaces::get_workspace_info
}, log::debug, serde_json::{Value, json}, std::time::Instant, swayipc::{
Connection,
EventType
}
};
#[derive(serde::Serialize)]
pub struct SwayInfo {
window_info: Value,
workspace_info: Value,
}
pub fn get_sway_connection() -> Connection {
Connection::new().expect("Unable to connect to Sway IPC")
}
pub fn run_sway_command(sway_connection: &mut Connection,payload: String) -> Result<(),SDUError> {
match sway_connection.run_command(&payload) {
Ok(sway_result) => {
debug!("{:#?}", sway_result);
println!("Command [{}] ran successfully",payload);
Ok(todo!("properly handle this exit"))
},
Err(e) => Err(SDUError{ message: e.to_string() }),
}
}
pub fn get_sway_info(window_icons: &[WindowIcon]) -> Value {
let window_info = get_window_info(window_icons);
let workspace_info = get_workspace_info();
let full_info = SwayInfo{ window_info, workspace_info };
json!(full_info)
}
pub fn monitor_events(event_type: Vec<EventType>, get_command: &SwayGetCommand, window_icons: &[WindowIcon]) {
let sway_connection = get_sway_connection();
for event in sway_connection.subscribe(event_type).expect("unable to subscribe to sway events") {
let start = Instant::now();
let sway_json = match event.expect("unable to parse sway event") {
swayipc::Event::Workspace(_) | swayipc::Event::Window(_) => {
match get_command {
SwayGetCommand::Window => get_window_info(window_icons),
SwayGetCommand::Workspaces => get_workspace_info(),
SwayGetCommand::Full => get_sway_info(window_icons),
}
},
//swayipc::Event::Tick(_) => todo!(),
_ => unreachable!(),
};
println!("{}",sway_json);
log::debug!("time taken: {:?}",start.elapsed());
}
}

View file

@ -1,42 +1,64 @@
use serde_json::json;
use swayipc::{Connection, Node};
use crate::{config::Config, Cli, ErrorMessage};
use {
log::debug,
serde_json::{
Value,
json
},
swayipc::Connection,
crate::config::WindowIcon,
};
/*#[derive(serde::Serialize)]
pub struct WindowContext {
pub window_count: usize,
pub
}*/
#[derive(serde::Serialize)]
pub struct WindowInfo {
pub title: String,
pub window_count: usize
}
pub fn get_window_name(window_node: Node, cli: &Cli, config: &Config) -> Result<String,ErrorMessage> {
match window_node.name {
Some(mut window_title) => {
for pair in config.window_icons.clone() {
if window_title.contains(&pair.substring) {
window_title = format!("{} {}", pair.icon, window_title.replace(&pair.substring,""))
}
}
/* if cli.no_truncate_title.is_some_and(|x|!x) && config.title_length.is_some_and(|x|x.lt(&window_title.chars().count())) {
let trunc_point = window_title.char_indices().map(|(i, _)| i).nth(config.title_length.unwrap()).unwrap_or(window_title.chars().count());
window_title.truncate(trunc_point);
window_title.push('…');
} */
Ok(window_title)
},
None => {
Ok("".to_string())
pub fn get_window_info(window_icons: &[WindowIcon]) -> Value {
let mut sway_connection = Connection::new().expect("Unable to connect to sway ipc socket");
let nodes_tree = sway_connection.get_tree().expect("Unable to parse Sway node tree");
/* RETRIEVING NODE LAYOUT
for output in nodes_tree.nodes.iter() {
println!("|({:#?}) {:#?}", output.node_type, output.name.clone().unwrap());
for workspace in output.nodes.iter() {
println!("||({:#?}) {}", workspace.node_type, workspace.name.clone().unwrap());
println!("|||Children: {:#?}",workspace.iter().filter(|x|(x.node_type.eq(&swayipc::NodeType::Con) || x.node_type.eq(&swayipc::NodeType::FloatingCon)) && x.name.is_some()).count());
println!("|||Focused: {:#?}",workspace.iter().find(|x|x.focused).is_some());
}
}
}
pub fn print_window_info(window_node: Node,cli: &Cli,config: &Config) {
let mut sc = Connection::new().unwrap();
let window_count = sc.get_workspaces().unwrap().iter().find(|x|x.focused).unwrap().focus.len();
let window_title = match window_count.gt(&0) {
true => get_window_name(window_node, cli, config).unwrap(),
false => "".to_string(),
*/
let focused_workspace_raw = nodes_tree.iter()
.find(|x|x.node_type.eq(&swayipc::NodeType::Workspace) && x.iter().any(|x|x.focused));
let window_count = match focused_workspace_raw {
Some(ws) => ws.iter().filter(|x|(x.node_type.eq(&swayipc::NodeType::Con) || x.node_type.eq(&swayipc::NodeType::FloatingCon)) && x.name.is_some()).count(),
None => 0,
};
let window_info = WindowInfo { title: window_title, window_count};
let window_output = json!(window_info);
println!("{}",window_output);
debug!("{:#?}",window_count);
let window_title = if window_count.gt(&0) {
let window_node = nodes_tree.iter().find(|x|x.focused);
debug!("{:#?}",window_node);
match window_node {
Some(n) => {
let mut window_name = n.name.clone().unwrap_or_default();
for pair in window_icons {
if window_name.contains(&pair.substring) {
window_name = format!("{} {}", pair.icon, window_name.replace(&pair.substring, ""))
}
}
Some(window_name)
},
None => None,
}
} else {
None
};
let window_info = WindowInfo { title: window_title.unwrap_or_default(), window_count};
json!(window_info)
// let window_output = json!(window_info);
// window_output.to_string()
}

View file

@ -1,5 +1,10 @@
use serde_json::json;
use swayipc::Workspace;
use {
swayipc::Connection,
serde_json::{
Value,
json,
}
};
#[derive(serde::Serialize)]
pub struct WorkspaceInfo {
@ -9,8 +14,9 @@ pub struct WorkspaceInfo {
pub position: char
}
pub fn print_workspace_info(workspaces: Vec<Workspace>) {
let current_ws = workspaces.iter().find(|&x| x.focused).unwrap().num;
pub fn get_workspace_info() -> Value {
let workspaces = Connection::new().expect("unable to connect to sway IPC").get_workspaces().expect("Unable to parse sway workspaces");
let current_ws = workspaces.iter().find(|&x| x.focused).expect("unable to find focused workspace").num;
let mut workspaces_info: Vec<WorkspaceInfo> = vec![];
for workspace in workspaces {
let mut ws_position = 'c';
@ -23,6 +29,7 @@ pub fn print_workspace_info(workspaces: Vec<Workspace>) {
workspaces_info.push(workspace_info);
}
workspaces_info.sort_by(|a, b| a.num.cmp(&b.num));
let workspace_output = json!(workspaces_info);
println!("{}",workspace_output)
json!(workspaces_info)
// let workspace_output = json!(workspaces_info);
// workspace_output.to_string()
}

View file

@ -1,397 +1,101 @@
#![warn(unused_crate_dependencies)]
use std::{any::Any, process::exit, time::Instant};
use log::{info,debug,error,trace};
use clap::{ArgAction, Parser, Subcommand};
use swayipc::{Connection, EventType, Fallible};
use systemctl;
use dialog::{DialogBox, backends::Backend};
#![allow(clippy::restriction)]
mod config;
#[path = "lib/windows.rs"]
mod windows;
use windows::print_window_info;
#[path = "lib/workspaces.rs"]
mod workspaces;
use workspaces::print_workspace_info;
#[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/sway_ipc.rs"]
mod sway_ipc;
#[path = "lib/profile.rs"]
mod profile;
#[path = "lib/get.rs"]
mod get;
#[path = "lib/sway.rs"]
mod sway;
#[path = "lib/power.rs"]
mod power;
#[path = "lib/cli.rs"]
mod cli;
#[path = "lib/shortcuts.rs"]
mod shortcuts;
use xdg::BaseDirectories;
use crate::profile::switch_by_index;
#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
/// Disable truncation of window titles
#[arg(short = 'T', long = "no-truncate-title", action = ArgAction::SetTrue)]
no_truncate_title: Option<bool>,
/// Enables monitoring for supported event types
#[arg(short = 'm', long = "monitor", action = ArgAction::SetTrue)]
monitor: Option<bool>,
/// Turn debugging information on
#[arg(short, long, action = ArgAction::Count)]
debug: u8,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Prints the Currently Active Window Title
Window {
#[command(subcommand)]
window_command: WindowCommand,
use {
crate::{
cli::{
Cli,
Commands
},
config::Config,
power::power,
shortcuts::shortcuts,
sway::sway
},
/// Prints the Currently Active Workspace layout
Workspace {
#[command(subcommand)]
workspace_command: WorkspaceCommand,
},
/// Launch Program with Current Workspace's Profile Configuration
Launch {
#[arg(short, long)]
program: String
},
/// Set up blurred wallpaper for screen lock, and load gtklock modules
Lock {
#[arg(short, long, action = ArgAction::SetTrue)]
force_render_background: Option<bool>,
},
/// Profile WIP,
Profile {
#[command(subcommand)]
profile_command: ProfileCommand,
},
/// Shortcuts TODO
Shortcuts {
#[arg(short, long, action = ArgAction::SetTrue)]
global: Option<bool>,
},
Power {
#[command(subcommand)]
power_command: PowerCommand,
}
}
clap::Parser,
launch::launch,
lock::lock,
log::{debug, error},
profile::profile,
std::process::exit,
sway_ipc::run_sway_command,
windows::get_window_info,
workspaces::get_workspace_info,
xdg::BaseDirectories
};
#[derive(Subcommand)]
enum WorkspaceCommand {
/// todo
#[clap(alias = "g")]
Get,
#[clap(alias = "r")]
Rename
}
#[derive(Subcommand)]
enum WindowCommand {
/// todo
#[clap(alias = "g")]
Get
}
#[derive(Subcommand)]
enum PowerCommand {
/// power down the system
#[clap(alias = "s")]
Shutdown,
/// Reboot the system
#[clap(alias = "r")]
Reboot,
/// Suspend (idle) the system
#[clap(alias = "i")]
Suspend,
/// Lock the system
#[clap(alias = "l")]
Lock {
#[arg(short, long, action = ArgAction::SetTrue)]
force_render_background: Option<bool>,
},
/// Log out of (exit) the session
#[clap(alias = "e")]
Logout,
}
#[derive(Subcommand)]
enum ProfileCommand {
/// switch profiles
#[clap(alias = "s")]
Switch {
#[command(subcommand)]
switch_command: Option<ProfileSwitchCommand>,
},
/// 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(Clone, Copy)]
#[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
pub enum DirectoryType {
Cache,
Config
}
#[derive(Debug)]
pub struct ErrorMessage {
message: String,
code: i32
pub struct SDUError {
message: String
}
pub fn error_handler(error: ErrorMessage) {
/*pub fn error_handler(error: ErrorMessage) {
debug!("ERROR: {}",error.message);
exit(error.code)
}
}*/
pub fn setup_runtime_dir(xdg_directories: BaseDirectories) {
match xdg_directories.create_runtime_directory("sway-profiles-rs") {
Ok(_) => debug!("success"),
Err(_) => debug!("failed"),
match xdg_directories.create_runtime_directory("sway-desktop-utils") {
Ok(_) => debug!("created runtime directories"),
Err(_) => debug!("failed to create runtime directories"),
}
}
fn main() -> Fallible<()> {
env_logger::init();
// env_logger::
let xdg_directories = BaseDirectories::new();
let cli = Cli::parse();
let config = confy::load("sway-profiles-rs", "config").unwrap();
pub fn get_xdg_dirs() -> BaseDirectories {
BaseDirectories::new()
}
let mut sway_connection = Connection::new()?;
match &cli.command {
Commands::Window { window_command } => {
match window_command {
WindowCommand::Get => {
//let start = Instant::now();
match sway_connection.get_tree() {
Ok(t) => {
match t.iter().find(|&x | x.focused) {
Some(f) => print_window_info(f.clone(), &cli, &config),
None => error_handler(ErrorMessage { message: "Could not find focused window".to_string(), code: 99 }),
}
},
Err(_) => error_handler(ErrorMessage { message: "Could not unwrap sway tree".to_string(), code: 99}),
}
//print_window_info(sway_connection.get_tree().unwrap().iter().find(|&x | x.focused).unwrap().clone(), &cli, &config);
//debug!("time taken: {:?}",start.elapsed());
if cli.monitor.unwrap() {
sway::monitor_events(vec![EventType::Window,EventType::Workspace], cli, config);
}
},
}
}
Commands::Workspace { workspace_command } => {
match workspace_command {
WorkspaceCommand::Get => {
print_workspace_info(sway_connection.get_workspaces().unwrap());
if cli.monitor.unwrap() {
sway::monitor_events(vec![EventType::Workspace], cli, config);
}
},
WorkspaceCommand::Rename => todo!(),
}
}
Commands::Launch { program } => {
trace!("{:?}",config.programs);
match profile_launch(config.profiles,config.programs, program) {
Ok(p) => {
trace!("{}",p);
match run_sway_command(&mut sway_connection,p) {
Ok(_) => debug!("launch successful"),
Err(e) => error_handler(e),
}
},
Err(e) => error_handler(e),
}
},
Commands::Lock { force_render_background } => {
match lock_screen(&config, sway_connection, force_render_background.unwrap_or(false)) {
Ok(o) => debug!("Screen locked successfully{:?}",o.first().unwrap()),
Err(e) => error!("{:?}",e),
};
},
Commands::Profile { profile_command} => {
debug!("{:?}",xdg_directories.get_runtime_directory());
setup_runtime_dir(xdg_directories.clone());
match profile_command {
ProfileCommand::Init => {
let index_start = if config.index_starts_at_1 { 1 } else { 0 };
let initial_workspace = config.initial_workspace;
debug!("profile index will start at {}",index_start);
let active_profile_index = match profile::active_profile_index() {
Ok(i) => i,
Err(_) => 0,
};
// todo!();
// match profile::profile_from_index(config.profiles.clone(), 0) {
// Ok(_) => {
match switch_by_index(config, active_profile_index, sway_connection, initial_workspace) {
Ok(_) => info!("successfully initialized sway-profiles-rs"),
Err(e) => error_handler(e),
}
// },
// Err(e) => error_handler(e),
// }
},
ProfileCommand::Switch { switch_command} => {
let initial_workspace = config.initial_workspace;
match switch_command {
Some(ProfileSwitchCommand::To { index,name,query }) => {
match index {
Some(i) => {
match profile::switch_by_index(config, *i, sway_connection, initial_workspace) {
Ok(_) => info!("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, initial_workspace) {
Ok(_) => (),
Err(_) => match profile::switch_by_name(config, q.to_string(), Connection::new().unwrap()) {
Ok(_) => (),
Err(_) => error_handler(ErrorMessage { message: format!("Could not find profile with index or name: {}",q), code: 4 }),
},
},
Err(_) => match profile::switch_by_name(config, q.to_string(), sway_connection) {
Ok(_) => (),
Err(_) => error_handler(ErrorMessage { message: format!("Could not find profile with index or name: {}",q), code: 4 }),
},
},
None => error_handler(ErrorMessage { message: "No profile index or name provided.".to_string(), code: 4 }),
},
}
},
}
},
Some(ProfileSwitchCommand::Next) => match profile::next(config, sway_connection) {
Ok(_) => info!("Successfully switched to next profile"),
Err(e) => error_handler(e),
},
Some(ProfileSwitchCommand::Prev) => match profile::previous(config, sway_connection) {
Ok(_) => info!("successfully switched to previous profile"),
Err(e) => error_handler(e),
},
None => {
for profile in config.profiles {
println!("{} {}", profile.icon, profile.name);
}
},
}
},
ProfileCommand::Get { get_command, monitor } => {
match Some(get_command) {
Some(&g) => {
println!("{}",get::profile_info(config.profiles.clone(),g).unwrap());
if monitor.unwrap() {
let _ = get::watch(config, g,xdg_directories.runtime_dir.unwrap().join("sway-profiles-rs/active-profile.json").to_str().unwrap().to_string());
}
},
None => error_handler(ErrorMessage { message: "No matching Profile Detail".to_string(), code: 4 }),
}
}
}
},
Commands::Shortcuts { global: _ } => {
todo!()
},
Commands::Power { power_command } => {
let action_prompt = match power_command {
PowerCommand::Shutdown => vec!["shut down","poweroff.target"],
PowerCommand::Reboot => vec!["restart","reboot.target"],
PowerCommand::Suspend => vec!["suspend","suspend.target"],
PowerCommand::Lock { force_render_background: _ } => vec!["lock"],
PowerCommand::Logout => vec!["log out"],
};
let action = action_prompt.iter().nth(0).unwrap_or(&"not found");
if let Ok(dialog::Choice::Yes) = dialog::Question::new(format!("Are you sure you would like to {}?",action)).title(format!("Confirm {}...",action)).show() {
match power_command {
PowerCommand::Lock { force_render_background } => {
debug!("running lock_screen function");
let _ = lock_screen(&config, sway_connection, force_render_background.unwrap_or(false));
},
PowerCommand::Logout => {
debug!("sending exit command to sway");
let _ = run_sway_command(&mut sway_connection, "exit".to_string());
},
_ => {
let target = action_prompt.iter().nth(1).unwrap_or(&"not found");
let systemctl = systemctl::SystemCtl::default();
debug!("looking for {}",target);
if let Ok(true) = systemctl.exists(target) {
debug!("found and starting {}",target);
let _ = systemctl.start(&target);
}
}
};
}
}
fn main() -> Result<(),()> {
env_logger::init();
// let xdg_directories = BaseDirectories::new();
let cli = Cli::parse();
let config = if confy::get_configuration_file_path("sway-desktop-utils", "config").expect("Unable to determine config file location").exists() {
confy::load("sway-desktop-utils", "config").unwrap_or_default()
} else {
let _ = confy::store("sway-desktop-utils", "config", Config::default());
Config::default()
};
let sdu_result = match &cli.command {
Commands::Sway { sway_command } => sway(sway_command, &config.window_icons),
Commands::Launch { program } => launch(program, config.profiles, config.programs),
Commands::Lock { force_render_background } => lock(config.lock, force_render_background.unwrap_or_default()),
Commands::Profile { profile_command} => profile(profile_command, config.profiles, config.preserve_keyboard_order),
Commands::Shortcuts { shortcut_command } => shortcuts(shortcut_command, config.scripts),
Commands::Power { power_command } => power(power_command, config.lock)
};
match sdu_result {
Ok(_) => debug!("Command ran successfully"),
Err(e) => error!("SDU Encountered an error: {}", e.message),
}
exit(0);
}

0
src/utils.rs Normal file
View file