You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

382 lines
10 KiB

// Copyright (C) 2021 Tassilo Horn <tsdh@gnu.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//! Convenience data structures built from the IPC structs.
use crate::config as cfg;
use crate::ipc;
use crate::ipc::NodeMethods;
use crate::util;
use std::cmp;
use std::collections::HashMap;
use std::fmt;
use swayipc::reply as r;
pub trait DisplayFormat {
fn format_for_display(&self, config: &cfg::Config) -> String;
}
#[derive(Debug)]
pub struct Window<'a> {
node: &'a r::Node,
workspace: &'a r::Node,
extra_props: Option<ipc::ExtraProps>,
}
impl Window<'_> {
pub fn get_id(&self) -> i64 {
self.node.id
}
pub fn get_app_name(&self) -> &str {
if let Some(app_id) = &self.node.app_id {
app_id
} else if let Some(wp_class) = self
.node
.window_properties
.as_ref()
.and_then(|wp| wp.class.as_ref())
{
wp_class
} else {
"<Unknown>"
}
}
pub fn get_title(&self) -> &str {
self.node.name.as_ref().unwrap()
}
pub fn is_urgent(&self) -> bool {
self.node.urgent
}
pub fn is_focused(&self) -> bool {
self.node.focused
}
}
impl PartialEq for Window<'_> {
fn eq(&self, other: &Self) -> bool {
self.get_id() == other.get_id()
}
}
impl Eq for Window<'_> {}
impl Ord for Window<'_> {
fn cmp(&self, other: &Self) -> cmp::Ordering {
if self == other {
cmp::Ordering::Equal
} else if self.is_urgent() && !other.is_urgent()
|| !self.is_focused() && other.is_focused()
{
cmp::Ordering::Less
} else if !self.is_urgent() && other.is_urgent()
|| self.is_focused() && !other.is_focused()
{
std::cmp::Ordering::Greater
} else {
let lru_a =
self.extra_props.as_ref().map_or(0, |wp| wp.last_focus_time);
let lru_b = other
.extra_props
.as_ref()
.map_or(0, |wp| wp.last_focus_time);
lru_a.cmp(&lru_b).reverse()
}
}
}
impl PartialOrd for Window<'_> {
fn partial_cmp(&self, other: &Window) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<'a> fmt::Display for Window<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"“{}” — {} on workspace {} (id: {}, urgent: {})",
self.get_title(),
self.get_app_name(),
self.workspace.name.as_ref().unwrap(),
self.get_id(),
self.node.urgent
)
}
}
impl<'a> DisplayFormat for Window<'a> {
fn format_for_display(&self, cfg: &cfg::Config) -> String {
let default_format = cfg::Format::default();
let fmt = cfg
.format
.as_ref()
.and_then(|f| f.window_format.as_ref())
.unwrap_or_else(|| default_format.window_format.as_ref().unwrap());
let urgency_start = cfg
.format
.as_ref()
.and_then(|f| f.urgency_start.as_ref())
.unwrap_or_else(|| default_format.urgency_start.as_ref().unwrap());
let urgency_end = cfg
.format
.as_ref()
.and_then(|f| f.urgency_end.as_ref())
.unwrap_or_else(|| default_format.urgency_end.as_ref().unwrap());
fmt.replace("{id}", format!("{}", self.get_id()).as_str())
.replace(
"{urgency_start}",
if self.is_urgent() {
urgency_start.as_str()
} else {
""
},
)
.replace(
"{urgency_end}",
if self.is_urgent() {
urgency_end.as_str()
} else {
""
},
)
.replace("{app_name}", self.get_app_name())
.replace(
"{workspace_name}",
self.workspace.name.as_ref().unwrap().as_str(),
)
.replace("{title}", self.get_title())
}
}
fn build_windows<'a>(
root: &'a r::Node,
include_scratchpad_windows: bool,
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
) -> Vec<Window<'a>> {
let mut v = vec![];
for workspace in root.workspaces() {
if !include_scratchpad_windows && workspace.is_scratchpad() {
continue;
}
for n in workspace.windows() {
v.push(Window {
node: &n,
extra_props: extra_props.and_then(|m| m.get(&n.id).cloned()),
workspace: &workspace,
})
}
}
v
}
fn build_workspaces<'a>(
root: &'a r::Node,
include_scratchpad: bool,
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
) -> Vec<Workspace<'a>> {
let mut v = vec![];
for workspace in root.workspaces() {
if !include_scratchpad && workspace.is_scratchpad() {
continue;
}
let mut wins: Vec<Window> = workspace
.windows()
.iter()
.map(|w| Window {
node: &w,
extra_props: extra_props.and_then(|m| m.get(&w.id).cloned()),
workspace: &workspace,
})
.collect();
wins.sort();
v.push(Workspace {
node: &workspace,
extra_props: extra_props
.and_then(|m| m.get(&workspace.id).cloned()),
windows: wins,
})
}
v.sort();
v
}
/// Gets all application windows of the tree.
pub fn get_windows<'a>(
root: &'a r::Node,
include_scratchpad_windows: bool,
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
) -> Vec<Window<'a>> {
let extra_props_given = extra_props.is_some();
let mut wins = build_windows(root, include_scratchpad_windows, extra_props);
if extra_props_given {
wins.sort();
}
wins
}
/// Gets all workspaces of the tree.
pub fn get_workspaces<'a>(
root: &'a r::Node,
include_scratchpad: bool,
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
) -> Vec<Workspace<'a>> {
let mut workspaces =
build_workspaces(root, include_scratchpad, extra_props);
workspaces.rotate_left(1);
workspaces
}
pub fn select_window<'a>(
prompt: &str,
windows: &'a [Window],
) -> Option<&'a Window<'a>> {
util::select_from_choices(prompt, windows)
}
pub fn select_workspace<'a>(
prompt: &str,
workspaces: &'a [Workspace],
) -> Option<&'a Workspace<'a>> {
util::select_from_choices(prompt, workspaces)
}
pub enum WsOrWin<'a> {
Ws { ws: &'a Workspace<'a> },
Win { win: &'a Window<'a> },
}
impl<'a> fmt::Display for WsOrWin<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
WsOrWin::Ws { ws } => ws.fmt(f),
WsOrWin::Win { win } => match f.write_str("\t") {
Ok(()) => win.fmt(f),
Err(e) => Err(e),
},
}
}
}
impl DisplayFormat for WsOrWin<'_> {
fn format_for_display(&self, cfg: &cfg::Config) -> String {
match self {
WsOrWin::Ws { ws } => ws.format_for_display(cfg),
WsOrWin::Win { win } => {
"\t".to_owned() + &win.format_for_display(cfg)
}
}
}
}
impl WsOrWin<'_> {
pub fn from_workspaces<'a>(
workspaces: &'a [Workspace],
) -> Vec<WsOrWin<'a>> {
let mut v = vec![];
for ws in workspaces {
v.push(WsOrWin::Ws { ws });
for win in &ws.windows {
v.push(WsOrWin::Win { win: &win });
}
}
v
}
}
pub fn select_workspace_or_window<'a>(
prompt: &'a str,
ws_or_wins: &'a [WsOrWin<'a>],
) -> Option<&'a WsOrWin<'a>> {
util::select_from_choices(prompt, ws_or_wins)
}
pub struct Workspace<'a> {
node: &'a r::Node,
extra_props: Option<ipc::ExtraProps>,
pub windows: Vec<Window<'a>>,
}
impl Workspace<'_> {
pub fn get_name(&self) -> &str {
self.node.name.as_ref().unwrap()
}
pub fn get_id(&self) -> i64 {
self.node.id
}
pub fn is_scratchpad(&self) -> bool {
self.node.is_scratchpad()
}
}
impl PartialEq for Workspace<'_> {
fn eq(&self, other: &Self) -> bool {
self.get_id() == other.get_id()
}
}
impl Eq for Workspace<'_> {}
impl Ord for Workspace<'_> {
fn cmp(&self, other: &Self) -> cmp::Ordering {
if self == other {
cmp::Ordering::Equal
} else {
let lru_a =
self.extra_props.as_ref().map_or(0, |wp| wp.last_focus_time);
let lru_b = other
.extra_props
.as_ref()
.map_or(0, |wp| wp.last_focus_time);
lru_a.cmp(&lru_b).reverse()
}
}
}
impl PartialOrd for Workspace<'_> {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<'a> fmt::Display for Workspace<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "“Workspace {}” (id: {})", self.get_name(), self.get_id())
}
}
impl<'a> DisplayFormat for Workspace<'a> {
fn format_for_display(&self, cfg: &cfg::Config) -> String {
let default_format = cfg::Format::default();
let fmt = cfg
.format
.as_ref()
.and_then(|f| f.workspace_format.as_ref())
.unwrap_or_else(|| {
default_format.workspace_format.as_ref().unwrap()
});
fmt.replace("{id}", format!("{}", self.get_id()).as_str())
.replace("{name}", self.get_name())
}
}