use std::cmp::Ordering;
use std::fmt;
use std::str;
use crate::Error;
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
enum SuffixKind {
ReleaseCandidate(u16),
Final,
Patch(u8),
}
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ReleaseSuffix(SuffixKind);
impl ReleaseSuffix {
pub const RELEASE_CANDIDATE: Self = Self(SuffixKind::ReleaseCandidate(1));
pub const FINAL: Self = Self(SuffixKind::Final);
const PATCH_A: Self = Self(SuffixKind::Patch(b'a'));
#[must_use]
pub const fn release_candidate(number: u16) -> Option<Self> {
if number == 0 {
None
} else {
Some(Self(SuffixKind::ReleaseCandidate(number)))
}
}
#[must_use]
pub fn patch(letter: char) -> Option<Self> {
if !letter.is_ascii_lowercase() {
return None;
}
u8::try_from(letter as u32)
.ok()
.map(|letter| Self(SuffixKind::Patch(letter)))
}
#[must_use]
pub fn patch_letter(self) -> Option<char> {
match self.0 {
SuffixKind::Patch(letter) => Some(char::from(letter)),
SuffixKind::ReleaseCandidate(_) | SuffixKind::Final => None,
}
}
#[must_use]
pub const fn release_candidate_number(self) -> Option<u16> {
match self.0 {
SuffixKind::ReleaseCandidate(number) => Some(number),
SuffixKind::Final | SuffixKind::Patch(_) => None,
}
}
#[must_use]
pub const fn is_final(self) -> bool {
matches!(self.0, SuffixKind::Final)
}
#[must_use]
pub const fn is_release_candidate(self) -> bool {
matches!(self.0, SuffixKind::ReleaseCandidate(_))
}
}
impl fmt::Debug for ReleaseSuffix {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
SuffixKind::ReleaseCandidate(number) => formatter
.debug_tuple("ReleaseCandidate")
.field(&number)
.finish(),
SuffixKind::Final => formatter.write_str("Final"),
SuffixKind::Patch(letter) => formatter
.debug_tuple("Patch")
.field(&char::from(letter))
.finish(),
}
}
}
impl fmt::Display for ReleaseSuffix {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
SuffixKind::ReleaseCandidate(1) => formatter.write_str("-rc"),
SuffixKind::ReleaseCandidate(number) => write!(formatter, "-rc{number}"),
SuffixKind::Final => Ok(()),
SuffixKind::Patch(letter) => write!(formatter, "{}", char::from(letter)),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ReleaseVersion {
major: u16,
minor: u16,
suffix: ReleaseSuffix,
}
impl ReleaseVersion {
#[must_use]
pub const fn new(major: u16, minor: u16, suffix: ReleaseSuffix) -> Self {
Self {
major,
minor,
suffix,
}
}
#[must_use]
pub const fn major(self) -> u16 {
self.major
}
#[must_use]
pub const fn minor(self) -> u16 {
self.minor
}
#[must_use]
pub const fn suffix(self) -> ReleaseSuffix {
self.suffix
}
}
impl fmt::Display for ReleaseVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}.{}{}", self.major, self.minor, self.suffix)
}
}
#[derive(Clone, Debug)]
pub struct TmuxVersion {
raw: Box<str>,
release: Option<ReleaseVersion>,
}
impl TmuxVersion {
pub const MIN_SUPPORTED: ReleaseVersion = ReleaseVersion::new(3, 2, ReleaseSuffix::PATCH_A);
pub fn parse_output(output: &[u8]) -> Result<Self, Error> {
let raw = parse_output_frame(output)
.ok_or_else(|| Error::from_invalid_version_output(output.len()))?;
let release = match raw.as_bytes().first() {
Some(first) if first.is_ascii_digit() => Some(
parse_release(raw)
.ok_or_else(|| Error::from_invalid_version_output(output.len()))?,
),
Some(_) if is_development_identifier(raw) => None,
_ => return Err(Error::from_invalid_version_output(output.len())),
};
Ok(Self {
raw: raw.into(),
release,
})
}
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub const fn release(&self) -> Option<&ReleaseVersion> {
self.release.as_ref()
}
#[must_use]
pub const fn is_development(&self) -> bool {
self.release.is_none()
}
pub(crate) fn behavior_release(&self) -> Option<ReleaseVersion> {
self.release.or_else(|| parse_next_release(&self.raw))
}
#[must_use]
pub fn meets(&self, required: &ReleaseVersion) -> bool {
match self.release {
Some(release) => release >= *required,
None if *required > Self::MIN_SUPPORTED => false,
None if self.raw.as_ref() == "master" => true,
None => parse_next_release(&self.raw).is_some_and(|release| release >= *required),
}
}
pub fn ensure_supported(&self) -> Result<(), Error> {
if self.meets(&Self::MIN_SUPPORTED) {
return Ok(());
}
Err(Error::unsupported_tmux_version(
self.clone(),
Self::MIN_SUPPORTED,
))
}
}
impl fmt::Display for TmuxVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.raw)
}
}
impl PartialEq for TmuxVersion {
fn eq(&self, other: &Self) -> bool {
match (self.release, other.release) {
(Some(left), Some(right)) => left == right,
(None, None) => self.raw == other.raw,
(Some(_), None) | (None, Some(_)) => false,
}
}
}
impl Eq for TmuxVersion {}
impl PartialOrd for TmuxVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self.release, other.release) {
(Some(left), Some(right)) => Some(left.cmp(&right)),
(None, None) if self.raw == other.raw => Some(Ordering::Equal),
_ => None,
}
}
}
fn parse_release(raw: &str) -> Option<ReleaseVersion> {
let (major, remainder) = raw.split_once('.')?;
let minor_end = remainder
.find(|character: char| !character.is_ascii_digit())
.unwrap_or(remainder.len());
let (minor, suffix) = remainder.split_at(minor_end);
let major = parse_u16_component(major)?;
let minor = parse_u16_component(minor)?;
let suffix = parse_release_suffix(suffix)?;
Some(ReleaseVersion::new(major, minor, suffix))
}
fn is_development_identifier(raw: &str) -> bool {
raw == "master" || parse_next_release(raw).is_some()
}
fn parse_next_release(raw: &str) -> Option<ReleaseVersion> {
let (major, minor) = raw.strip_prefix("next-")?.split_once('.')?;
Some(ReleaseVersion::new(
parse_u16_component(major)?,
parse_u16_component(minor)?,
ReleaseSuffix::FINAL,
))
}
fn parse_release_suffix(suffix: &str) -> Option<ReleaseSuffix> {
if suffix.is_empty() {
return Some(ReleaseSuffix::FINAL);
}
if suffix == "-rc" {
return Some(ReleaseSuffix::RELEASE_CANDIDATE);
}
if let Some(number) = suffix.strip_prefix("-rc") {
return ReleaseSuffix::release_candidate(parse_u16_component(number)?);
}
match suffix.as_bytes() {
[letter] if letter.is_ascii_lowercase() => Some(ReleaseSuffix(SuffixKind::Patch(*letter))),
_ => None,
}
}
fn parse_u16_component(component: &str) -> Option<u16> {
if component.is_empty()
|| (component.len() > 1 && component.starts_with('0'))
|| !component.bytes().all(|byte| byte.is_ascii_digit())
{
return None;
}
component.parse().ok()
}
fn parse_output_frame(output: &[u8]) -> Option<&str> {
let line = output.strip_suffix(b"\n")?;
let separator = line.iter().position(|byte| *byte == b' ')?;
let program = line.get(..separator)?;
let version = line.get(separator + 1..)?;
if program.is_empty()
|| version.is_empty()
|| program
.iter()
.any(|byte| byte.is_ascii_whitespace() || byte.is_ascii_control())
|| version
.iter()
.any(|byte| byte.is_ascii_whitespace() || byte.is_ascii_control())
{
return None;
}
str::from_utf8(version).ok()
}
pub mod since {
use super::{ReleaseSuffix, ReleaseVersion};
pub const SERVER_ACCESS: ReleaseVersion = ReleaseVersion::new(3, 3, ReleaseSuffix::FINAL);
pub const PROMPT_HISTORY: ReleaseVersion = ReleaseVersion::new(3, 3, ReleaseSuffix::FINAL);
pub const CAPTURE_LINE_FLAGS: ReleaseVersion = ReleaseVersion::new(3, 7, ReleaseSuffix::FINAL);
pub const CONTROL_PANE_OFF: ReleaseVersion = ReleaseVersion::new(3, 7, ReleaseSuffix::FINAL);
}