use ishou_tokens::{Signal, SignalMode, TearSignals};
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SignalRenderMode {
Emoji,
Glyph,
Label,
}
impl SignalRenderMode {
#[must_use]
pub fn to_ishou(self) -> SignalMode {
match self {
Self::Emoji => SignalMode::Emoji,
Self::Glyph => SignalMode::Glyph,
Self::Label => SignalMode::Label,
}
}
}
impl Default for SignalRenderMode {
fn default() -> Self {
Self::Emoji
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TearSignalKind {
SessionActive,
SessionDetached,
PaneZoomed,
PrefixArmed,
Online,
Offline,
Degraded,
Bell,
FleetMark,
Window,
SplitHorizontal,
SplitVertical,
SyncPanes,
CopyMode,
}
impl TearSignalKind {
#[must_use]
pub fn signal(self) -> Signal {
let s = TearSignals::prescribed();
match self {
Self::SessionActive => s.fleet.session_active,
Self::SessionDetached => s.fleet.session_detached,
Self::PaneZoomed => s.fleet.pane_zoomed,
Self::PrefixArmed => s.fleet.prefix_armed,
Self::Online => s.fleet.online,
Self::Offline => s.fleet.offline,
Self::Degraded => s.fleet.degraded,
Self::Bell => s.fleet.bell,
Self::FleetMark => s.fleet.fleet_mark,
Self::Window => s.window,
Self::SplitHorizontal => s.split_horizontal,
Self::SplitVertical => s.split_vertical,
Self::SyncPanes => s.sync_panes,
Self::CopyMode => s.copy_mode,
}
}
#[must_use]
pub fn render(self, mode: SignalRenderMode) -> &'static str {
self.signal().render_or_fallback(mode.to_ishou())
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SegmentAlignment {
Left,
Center,
Right,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum Segment {
Text { value: String },
SessionName,
WindowName,
PaneCommand,
PaneCwdBasename,
Time { format: String },
Hostname { short: bool },
Shell {
cmd: String,
interval_seconds: u32,
},
If {
cond: String,
then: Box<Segment>,
otherwise: Box<Segment>,
},
Signal {
signal: TearSignalKind,
#[serde(default)]
mode: SignalRenderMode,
},
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct StatusBar {
#[serde(default)]
pub left: Vec<Segment>,
#[serde(default)]
pub center: Vec<Segment>,
#[serde(default)]
pub right: Vec<Segment>,
#[serde(default = "default_interval")]
pub refresh_interval_seconds: u32,
#[serde(default = "default_visible")]
pub visible: bool,
}
fn default_interval() -> u32 {
5
}
fn default_visible() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signal_kinds_resolve_from_the_fleet_atlas() {
let atlas = TearSignals::prescribed();
assert_eq!(
TearSignalKind::SessionActive.signal(),
atlas.fleet.session_active
);
assert_eq!(TearSignalKind::PaneZoomed.signal(), atlas.fleet.pane_zoomed);
assert_eq!(
TearSignalKind::PrefixArmed.signal(),
atlas.fleet.prefix_armed
);
assert_eq!(TearSignalKind::Window.signal(), atlas.window);
assert_eq!(TearSignalKind::SyncPanes.signal(), atlas.sync_panes);
assert_eq!(TearSignalKind::CopyMode.signal(), atlas.copy_mode);
}
#[test]
fn active_session_is_the_fleet_tide_mark() {
assert_eq!(
TearSignalKind::SessionActive.render(SignalRenderMode::Emoji),
"🌊"
);
assert_eq!(
TearSignalKind::SessionActive.render(SignalRenderMode::Glyph),
"≈"
);
}
#[test]
fn glyph_mode_is_single_width_for_every_kind() {
for kind in [
TearSignalKind::SessionActive,
TearSignalKind::SessionDetached,
TearSignalKind::PaneZoomed,
TearSignalKind::PrefixArmed,
TearSignalKind::Online,
TearSignalKind::Offline,
TearSignalKind::Degraded,
TearSignalKind::Bell,
TearSignalKind::FleetMark,
TearSignalKind::Window,
TearSignalKind::SplitHorizontal,
TearSignalKind::SplitVertical,
TearSignalKind::SyncPanes,
TearSignalKind::CopyMode,
] {
let glyph = kind.render(SignalRenderMode::Glyph);
assert_eq!(
glyph.chars().count(),
1,
"{kind:?} glyph must be single-width, got {glyph:?}"
);
}
}
#[test]
fn signal_segment_serde_round_trips_with_default_mode() {
let seg = Segment::Signal {
signal: TearSignalKind::PaneZoomed,
mode: SignalRenderMode::default(),
};
let json = serde_json::to_string(&seg).unwrap();
assert!(json.contains("\"kind\":\"signal\""), "tag: {json}");
assert!(json.contains("\"signal\":\"pane-zoomed\""), "signal: {json}");
let back: Segment = serde_json::from_str(&json).unwrap();
assert_eq!(back, seg);
let no_mode: Segment =
serde_json::from_str(r#"{"kind":"signal","signal":"sync-panes"}"#).unwrap();
assert_eq!(
no_mode,
Segment::Signal {
signal: TearSignalKind::SyncPanes,
mode: SignalRenderMode::Emoji,
}
);
}
}