Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:58:18.265Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

util.rs

1141 lines · 37.7 KB · rust
1pub mod archive;
2pub mod command;
3pub mod disambiguate;
4pub mod fs;
5pub mod markdown;
6pub mod path_list;
7pub mod paths;
8pub mod process;
9pub mod redact;
10pub mod schemars;
11pub mod serde;
12pub mod shell;
13pub mod shell_builder;
14pub mod shell_env;
15pub mod size;
16#[cfg(any(test, feature = "test-support"))]
17pub mod test;
18pub mod time;
19
20use anyhow::Result;
21use itertools::Either;
22use regex::Regex;
23use std::path::PathBuf;
24use std::sync::LazyLock;
25use std::{
26    borrow::Cow,
27    cmp::{self, Ordering},
28    ops::{Range, RangeInclusive},
29};
30use unicase::UniCase;
31
32pub use gpui_util::*;
33pub use path::PathExt;
34pub use path::normalize_path;
35pub use path::rel_path;
36
37pub use take_until::*;
38#[cfg(any(test, feature = "test-support"))]
39pub use util_macros::{line_endings, path, uri};
40
41pub use self::shell::{
42    get_default_system_shell, get_default_system_shell_preferring_bash, get_system_shell,
43};
44
45#[inline]
46pub const fn is_utf8_char_boundary(u8: u8) -> bool {
47    // This is bit magic equivalent to: b < 128 || b >= 192
48    (u8 as i8) >= -0x40
49}
50
51pub fn truncate(s: &str, max_chars: usize) -> &str {
52    match s.char_indices().nth(max_chars) {
53        None => s,
54        Some((idx, _)) => &s[..idx],
55    }
56}
57
58/// Parses the contents of an `os-release` file (as found at `/etc/os-release`
59/// and described by the systemd spec) into a human-readable string such as
60/// `"ubuntu 24.04"`, combining the `ID` and `VERSION_ID` fields.
61///
62/// Returns `None` if no `ID` field is present. When `VERSION_ID` is absent
63/// (e.g. on rolling releases), only the `ID` is returned.
64pub fn parse_os_release(content: &str) -> Option<String> {
65    let mut id = None;
66    let mut version_id = None;
67    for line in content.lines() {
68        match line.split_once('=') {
69            Some(("ID", value)) => id = Some(value.trim_matches('"')),
70            Some(("VERSION_ID", value)) => version_id = Some(value.trim_matches('"')),
71            _ => {}
72        }
73    }
74    let id = id?;
75    Some(match version_id {
76        Some(version) => format!("{id} {version}"),
77        None => id.to_string(),
78    })
79}
80
81/// Removes characters from the end of the string if its length is greater than `max_chars` and
82/// appends "..." to the string. Returns string unchanged if its length is smaller than max_chars.
83pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {
84    debug_assert!(max_chars >= 5);
85
86    // If the string's byte length is <= max_chars, walking the string can be skipped since the
87    // number of chars is <= the number of bytes.
88    if s.len() <= max_chars {
89        return s.to_string();
90    }
91    let truncation_ix = s.char_indices().map(|(i, _)| i).nth(max_chars);
92    match truncation_ix {
93        Some(index) => s[..index].to_string() + "…",
94        _ => s.to_string(),
95    }
96}
97
98/// Removes characters from the front of the string if its length is greater than `max_chars` and
99/// prepends the string with "...". Returns string unchanged if its length is smaller than max_chars.
100pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String {
101    debug_assert!(max_chars >= 5);
102
103    // If the string's byte length is <= max_chars, walking the string can be skipped since the
104    // number of chars is <= the number of bytes.
105    if s.len() <= max_chars {
106        return s.to_string();
107    }
108    let suffix_char_length = max_chars.saturating_sub(1);
109    let truncation_ix = s
110        .char_indices()
111        .map(|(i, _)| i)
112        .nth_back(suffix_char_length);
113    match truncation_ix {
114        Some(index) if index > 0 => "…".to_string() + &s[index..],
115        _ => s.to_string(),
116    }
117}
118
119/// Takes only `max_lines` from the string and, if there were more than `max_lines-1`, appends a
120/// a newline and "..." to the string, so that `max_lines` are returned.
121/// Returns string unchanged if its length is smaller than max_lines.
122pub fn truncate_lines_and_trailoff(s: &str, max_lines: usize) -> String {
123    let mut lines = s.lines().take(max_lines).collect::<Vec<_>>();
124    if lines.len() > max_lines - 1 {
125        lines.pop();
126        lines.join("\n") + "\n…"
127    } else {
128        lines.join("\n")
129    }
130}
131
132/// Truncates the string at a character boundary, such that the result is less than `max_bytes` in
133/// length.
134pub fn truncate_to_byte_limit(s: &str, max_bytes: usize) -> &str {
135    if s.len() < max_bytes {
136        return s;
137    }
138
139    for i in (0..max_bytes).rev() {
140        if s.is_char_boundary(i) {
141            return &s[..i];
142        }
143    }
144
145    ""
146}
147
148/// Takes a prefix of complete lines which fit within the byte limit. If the first line is longer
149/// than the limit, truncates at a character boundary.
150pub fn truncate_lines_to_byte_limit(s: &str, max_bytes: usize) -> &str {
151    if s.len() < max_bytes {
152        return s;
153    }
154
155    for i in (0..max_bytes).rev() {
156        if s.is_char_boundary(i) && s.as_bytes()[i] == b'\n' {
157            // Since the i-th character is \n, valid to slice at i + 1.
158            return &s[..i + 1];
159        }
160    }
161
162    truncate_to_byte_limit(s, max_bytes)
163}
164
165#[test]
166fn test_truncate_lines_to_byte_limit() {
167    let text = "Line 1\nLine 2\nLine 3\nLine 4";
168
169    // Limit that includes all lines
170    assert_eq!(truncate_lines_to_byte_limit(text, 100), text);
171
172    // Exactly the first line
173    assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n");
174
175    // Limit between lines
176    assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n");
177    assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n");
178
179    // Limit before first newline
180    assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line ");
181
182    // Test with non-ASCII characters
183    let text_utf8 = "Line 1\nLíne 2\nLine 3";
184    assert_eq!(
185        truncate_lines_to_byte_limit(text_utf8, 15),
186        "Line 1\nLíne 2\n"
187    );
188}
189
190/// Extend a sorted vector with a sorted sequence of items, maintaining the vector's sort order and
191/// enforcing a maximum length. This also de-duplicates items. Sort the items according to the given callback. Before calling this,
192/// both `vec` and `new_items` should already be sorted according to the `cmp` comparator.
193pub fn extend_sorted<T, I, F>(vec: &mut Vec<T>, new_items: I, limit: usize, mut cmp: F)
194where
195    I: IntoIterator<Item = T>,
196    F: FnMut(&T, &T) -> Ordering,
197{
198    let mut start_index = 0;
199    for new_item in new_items {
200        if let Err(i) = vec[start_index..].binary_search_by(|m| cmp(m, &new_item)) {
201            let index = start_index + i;
202            if vec.len() < limit {
203                vec.insert(index, new_item);
204            } else if index < vec.len() {
205                vec.pop();
206                vec.insert(index, new_item);
207            }
208            start_index = index;
209        }
210    }
211}
212
213pub fn truncate_to_bottom_n_sorted_by<T, F>(items: &mut Vec<T>, limit: usize, compare: &F)
214where
215    F: Fn(&T, &T) -> Ordering,
216{
217    if limit == 0 {
218        items.truncate(0);
219    }
220    if items.len() <= limit {
221        items.sort_by(compare);
222        return;
223    }
224    // When limit is near to items.len() it may be more efficient to sort the whole list and
225    // truncate, rather than always doing selection first as is done below. It's hard to analyze
226    // where the threshold for this should be since the quickselect style algorithm used by
227    // `select_nth_unstable_by` makes the prefix partially sorted, and so its work is not wasted -
228    // the expected number of comparisons needed by `sort_by` is less than it is for some arbitrary
229    // unsorted input.
230    items.select_nth_unstable_by(limit, compare);
231    items.truncate(limit);
232    items.sort_by(compare);
233}
234
235/// Prevents execution of the application with root privileges on Unix systems.
236///
237/// This function checks if the current process is running with root privileges
238/// and terminates the program with an error message unless explicitly allowed via the
239/// `ZED_ALLOW_ROOT` environment variable.
240#[cfg(unix)]
241pub fn prevent_root_execution() {
242    let is_root = nix::unistd::geteuid().is_root();
243    let allow_root = std::env::var("ZED_ALLOW_ROOT").is_ok_and(|val| val == "true");
244
245    if is_root && !allow_root {
246        eprintln!(
247            "\
248Error: Running Omega as root or via sudo is unsupported.
249       Doing so (even once) may subtly break things for all subsequent non-root usage of Omega.
250       It is untested and not recommended, don't complain when things break.
251       If you wish to proceed anyways, set `ZED_ALLOW_ROOT=true` in your environment."
252        );
253        std::process::exit(1);
254    }
255}
256
257#[cfg(unix)]
258fn load_shell_from_passwd() -> Result<()> {
259    let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } {
260        n if n < 0 => 1024,
261        n => n as usize,
262    };
263    let mut buffer = Vec::with_capacity(buflen);
264
265    let mut pwd: std::mem::MaybeUninit<libc::passwd> = std::mem::MaybeUninit::uninit();
266    let mut result: *mut libc::passwd = std::ptr::null_mut();
267
268    let uid = unsafe { libc::getuid() };
269    let status = unsafe {
270        libc::getpwuid_r(
271            uid,
272            pwd.as_mut_ptr(),
273            buffer.as_mut_ptr() as *mut libc::c_char,
274            buflen,
275            &mut result,
276        )
277    };
278    anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid);
279
280    // SAFETY: If `getpwuid_r` doesn't error, we have the entry here.
281    let entry = unsafe { pwd.assume_init() };
282
283    anyhow::ensure!(
284        status == 0,
285        "call to getpwuid_r failed. uid: {}, status: {}",
286        uid,
287        status
288    );
289    anyhow::ensure!(
290        entry.pw_uid == uid,
291        "passwd entry has different uid ({}) than getuid ({}) returned",
292        entry.pw_uid,
293        uid,
294    );
295
296    let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() };
297    let should_set_shell = std::env::var("SHELL").map_or(true, |shell_env| {
298        shell_env != shell && !std::path::Path::new(&shell_env).exists()
299    });
300
301    if should_set_shell {
302        log::info!(
303            "updating SHELL environment variable to value from passwd entry: {:?}",
304            shell,
305        );
306        unsafe { std::env::set_var("SHELL", shell) };
307    }
308
309    Ok(())
310}
311
312/// Returns a shell escaped path for the current zed executable
313pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result<String> {
314    use anyhow::Context as _;
315    use paths::PathExt;
316    let mut zed_path =
317        std::env::current_exe().context("Failed to determine current Omega executable path.")?;
318    if cfg!(target_os = "linux")
319        && !zed_path.is_file()
320        && let Some(truncated) = zed_path
321            .clone()
322            .file_name()
323            .and_then(|s| s.to_str())
324            .and_then(|n| n.strip_suffix(" (deleted)"))
325    {
326        // Might have been deleted during update; let's use the new binary if there is one.
327        zed_path.set_file_name(truncated);
328    }
329
330    zed_path
331        .try_shell_safe(shell_kind)
332        .context("Failed to shell-escape Omega executable path.")
333}
334
335/// Returns a path for the zed cli executable, this function
336/// should be called from the zed executable, not zed-cli.
337pub fn get_zed_cli_path() -> Result<PathBuf> {
338    use anyhow::Context as _;
339    let zed_path =
340        std::env::current_exe().context("Failed to determine current Omega executable path.")?;
341    let parent = zed_path
342        .parent()
343        .context("Failed to determine parent directory of the Omega executable path.")?;
344
345    let possible_locations: &[&str] = if cfg!(target_os = "macos") {
346        // On macOS, the zed executable and zed-cli are inside the app bundle,
347        // so here ./cli is for both installed and development builds.
348        &["./cli"]
349    } else if cfg!(target_os = "windows") {
350        // bin/zed.exe is for installed builds, ./cli.exe is for development builds.
351        &["bin/zed.exe", "./cli.exe"]
352    } else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") {
353        // bin is the standard, ./cli is for the target directory in development builds.
354        &["../bin/zed", "./cli"]
355    } else {
356        anyhow::bail!("unsupported platform for determining the Omega CLI path");
357    };
358
359    possible_locations
360        .iter()
361        .find_map(|p| {
362            parent
363                .join(p)
364                .canonicalize()
365                .ok()
366                .filter(|p| p != &zed_path)
367        })
368        .with_context(|| {
369            format!(
370                "could not find the Omega CLI from any of: {}",
371                possible_locations.join(", ")
372            )
373        })
374}
375
376#[cfg(unix)]
377pub async fn load_login_shell_environment() -> Result<()> {
378    use anyhow::Context as _;
379
380    load_shell_from_passwd().log_err();
381
382    // If possible, we want to `cd` in the user's `$HOME` to trigger programs
383    // such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook
384    // into shell's `cd` command (and hooks) to manipulate env.
385    // We do this so that we get the env a user would have when spawning a shell
386    // in home directory.
387    for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir())
388        .await
389        .with_context(|| format!("capturing environment with {:?}", get_system_shell()))?
390    {
391        // Skip SHLVL to prevent it from polluting Zed's process environment.
392        // The login shell used for env capture increments SHLVL, and if we propagate it,
393        // terminals spawned by Zed will inherit it and increment again, causing SHLVL
394        // to start at 2 instead of 1 (and increase by 2 on each reload).
395        if name == "SHLVL" {
396            continue;
397        }
398        unsafe { std::env::set_var(&name, &value) };
399    }
400
401    log::info!(
402        "set environment variables from shell:{}, path:{}",
403        std::env::var("SHELL").unwrap_or_default(),
404        std::env::var("PATH").unwrap_or_default(),
405    );
406
407    Ok(())
408}
409
410/// Configures the process to start a new session, to prevent interactive shells from taking control
411/// of the terminal.
412///
413/// For more details: <https://registerspill.thorstenball.com/p/how-to-lose-control-of-your-shell>
414pub fn set_pre_exec_to_start_new_session(
415    command: &mut std::process::Command,
416) -> &mut std::process::Command {
417    // safety: code in pre_exec should be signal safe.
418    // https://man7.org/linux/man-pages/man7/signal-safety.7.html
419    #[cfg(unix)]
420    unsafe {
421        use std::os::unix::process::CommandExt;
422        command.pre_exec(|| {
423            libc::setsid();
424            Ok(())
425        });
426    };
427    command
428}
429
430pub fn merge_json_lenient_value_into(
431    source: serde_json_lenient::Value,
432    target: &mut serde_json_lenient::Value,
433) {
434    match (source, target) {
435        (serde_json_lenient::Value::Object(source), serde_json_lenient::Value::Object(target)) => {
436            for (key, value) in source {
437                if let Some(target) = target.get_mut(&key) {
438                    merge_json_lenient_value_into(value, target);
439                } else {
440                    target.insert(key, value);
441                }
442            }
443        }
444
445        (serde_json_lenient::Value::Array(source), serde_json_lenient::Value::Array(target)) => {
446            for value in source {
447                target.push(value);
448            }
449        }
450
451        (source, target) => *target = source,
452    }
453}
454
455/// Merges `source` into `target`: objects are merged recursively, key by key;
456/// any other colliding value in `target` — including arrays — is replaced by
457/// `source`'s value wholesale.
458pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
459    use serde_json::Value;
460
461    match (source, target) {
462        (Value::Object(source), Value::Object(target)) => {
463            for (key, value) in source {
464                if let Some(target) = target.get_mut(&key) {
465                    merge_json_value_into(value, target);
466                } else {
467                    target.insert(key, value);
468                }
469            }
470        }
471        (source, target) => *target = source,
472    }
473}
474
475pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
476    use serde_json::Value;
477    if let Value::Object(source_object) = source {
478        let target_object = if let Value::Object(target) = target {
479            target
480        } else {
481            *target = Value::Object(Default::default());
482            target.as_object_mut().unwrap()
483        };
484        for (key, value) in source_object {
485            if let Some(target) = target_object.get_mut(&key) {
486                merge_non_null_json_value_into(value, target);
487            } else if !value.is_null() {
488                target_object.insert(key, value);
489            }
490        }
491    } else if !source.is_null() {
492        *target = source
493    }
494}
495
496pub fn expanded_and_wrapped_usize_range(
497    range: Range<usize>,
498    additional_before: usize,
499    additional_after: usize,
500    wrap_length: usize,
501) -> impl Iterator<Item = usize> {
502    let start_wraps = range.start < additional_before;
503    let end_wraps = wrap_length < range.end + additional_after;
504    if start_wraps && end_wraps {
505        Either::Left(0..wrap_length)
506    } else if start_wraps {
507        let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before);
508        if wrapped_start <= range.end {
509            Either::Left(0..wrap_length)
510        } else {
511            Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length))
512        }
513    } else if end_wraps {
514        let wrapped_end = range.end + additional_after - wrap_length;
515        if range.start <= wrapped_end {
516            Either::Left(0..wrap_length)
517        } else {
518            Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length))
519        }
520    } else {
521        Either::Left((range.start - additional_before)..(range.end + additional_after))
522    }
523}
524
525/// Yields `[i, i + 1, i - 1, i + 2, ..]`, each modulo `wrap_length` and bounded by
526/// `additional_before` and `additional_after`. If the wrapping causes overlap, duplicates are not
527/// emitted. If wrap_length is 0, nothing is yielded.
528pub fn wrapped_usize_outward_from(
529    start: usize,
530    additional_before: usize,
531    additional_after: usize,
532    wrap_length: usize,
533) -> impl Iterator<Item = usize> {
534    let mut count = 0;
535    let mut after_offset = 1;
536    let mut before_offset = 1;
537
538    std::iter::from_fn(move || {
539        count += 1;
540        if count > wrap_length {
541            None
542        } else if count == 1 {
543            Some(start % wrap_length)
544        } else if after_offset <= additional_after && after_offset <= before_offset {
545            let value = (start + after_offset) % wrap_length;
546            after_offset += 1;
547            Some(value)
548        } else if before_offset <= additional_before {
549            let value = (start + wrap_length - before_offset) % wrap_length;
550            before_offset += 1;
551            Some(value)
552        } else if after_offset <= additional_after {
553            let value = (start + after_offset) % wrap_length;
554            after_offset += 1;
555            Some(value)
556        } else {
557            None
558        }
559    })
560}
561
562#[cfg(any(test, feature = "test-support"))]
563mod rng {
564    use rand::prelude::*;
565
566    pub struct RandomCharIter<T: Rng> {
567        rng: T,
568        simple_text: bool,
569    }
570
571    impl<T: Rng> RandomCharIter<T> {
572        pub fn new(rng: T) -> Self {
573            Self {
574                rng,
575                simple_text: std::env::var("SIMPLE_TEXT").is_ok_and(|v| !v.is_empty()),
576            }
577        }
578
579        pub fn with_simple_text(mut self) -> Self {
580            self.simple_text = true;
581            self
582        }
583    }
584
585    impl<T: Rng> Iterator for RandomCharIter<T> {
586        type Item = char;
587
588        fn next(&mut self) -> Option<Self::Item> {
589            if self.simple_text {
590                return if self.rng.random_range(0..100) < 5 {
591                    Some('\n')
592                } else {
593                    Some(self.rng.random_range(b'a'..b'z' + 1).into())
594                };
595            }
596
597            match self.rng.random_range(0..100) {
598                // whitespace
599                0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.rng).copied(),
600                // two-byte greek letters
601                20..=32 => char::from_u32(self.rng.random_range(('α' as u32)..('ω' as u32 + 1))),
602                // // three-byte characters
603                33..=45 => ['✋', '✅', '❌', '❎', '⭐']
604                    .choose(&mut self.rng)
605                    .copied(),
606                // // four-byte characters
607                46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.rng).copied(),
608                // ascii letters
609                _ => Some(self.rng.random_range(b'a'..b'z' + 1).into()),
610            }
611        }
612    }
613}
614#[cfg(any(test, feature = "test-support"))]
615pub use rng::RandomCharIter;
616
617/// Get an embedded file as a string.
618pub fn asset_str<A: rust_embed::RustEmbed>(path: &str) -> Cow<'static, str> {
619    match A::get(path).expect(path).data {
620        Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()),
621        Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()),
622    }
623}
624
625pub trait RangeExt<T> {
626    fn sorted(&self) -> Self;
627    fn to_inclusive(&self) -> RangeInclusive<T>;
628    fn overlaps(&self, other: &Range<T>) -> bool;
629    fn contains_inclusive(&self, other: &Range<T>) -> bool;
630}
631
632impl<T: Ord + Clone> RangeExt<T> for Range<T> {
633    fn sorted(&self) -> Self {
634        cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone()
635    }
636
637    fn to_inclusive(&self) -> RangeInclusive<T> {
638        self.start.clone()..=self.end.clone()
639    }
640
641    fn overlaps(&self, other: &Range<T>) -> bool {
642        self.start < other.end && other.start < self.end
643    }
644
645    fn contains_inclusive(&self, other: &Range<T>) -> bool {
646        self.start <= other.start && other.end <= self.end
647    }
648}
649
650impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
651    fn sorted(&self) -> Self {
652        cmp::min(self.start(), self.end()).clone()..=cmp::max(self.start(), self.end()).clone()
653    }
654
655    fn to_inclusive(&self) -> RangeInclusive<T> {
656        self.clone()
657    }
658
659    fn overlaps(&self, other: &Range<T>) -> bool {
660        self.start() < &other.end && &other.start <= self.end()
661    }
662
663    fn contains_inclusive(&self, other: &Range<T>) -> bool {
664        self.start() <= &other.start && &other.end <= self.end()
665    }
666}
667
668/// A way to sort strings with starting numbers numerically first, falling back to alphanumeric one,
669/// case-insensitive.
670///
671/// This is useful for turning regular alphanumerically sorted sequences as `1-abc, 10, 11-def, .., 2, 21-abc`
672/// into `1-abc, 2, 10, 11-def, .., 21-abc`
673#[derive(Debug, PartialEq, Eq)]
674pub struct NumericPrefixWithSuffix<'a>(Option<u64>, &'a str);
675
676impl<'a> NumericPrefixWithSuffix<'a> {
677    pub fn from_numeric_prefixed_str(str: &'a str) -> Self {
678        let i = str.chars().take_while(|c| c.is_ascii_digit()).count();
679        let (prefix, remainder) = str.split_at(i);
680
681        let prefix = prefix.parse().ok();
682        Self(prefix, remainder)
683    }
684}
685
686/// When dealing with equality, we need to consider the case of the strings to achieve strict equality
687/// to handle cases like "a" < "A" instead of "a" == "A".
688impl Ord for NumericPrefixWithSuffix<'_> {
689    fn cmp(&self, other: &Self) -> Ordering {
690        match (self.0, other.0) {
691            (None, None) => UniCase::new(self.1)
692                .cmp(&UniCase::new(other.1))
693                .then_with(|| self.1.cmp(other.1).reverse()),
694            (None, Some(_)) => Ordering::Greater,
695            (Some(_), None) => Ordering::Less,
696            (Some(a), Some(b)) => a.cmp(&b).then_with(|| {
697                UniCase::new(self.1)
698                    .cmp(&UniCase::new(other.1))
699                    .then_with(|| self.1.cmp(other.1).reverse())
700            }),
701        }
702    }
703}
704
705impl PartialOrd for NumericPrefixWithSuffix<'_> {
706    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
707        Some(self.cmp(other))
708    }
709}
710
711fn emoji_regex() -> &'static Regex {
712    static EMOJI_REGEX: LazyLock<Regex> =
713        LazyLock::new(|| Regex::new("(\\p{Emoji}|\u{200D})").unwrap());
714    &EMOJI_REGEX
715}
716
717/// Returns true if the given string consists of emojis only.
718/// E.g. "👨‍👩‍👧‍👧👋" will return true, but "👋!" will return false.
719pub fn word_consists_of_emojis(s: &str) -> bool {
720    let mut prev_end = 0;
721    for capture in emoji_regex().find_iter(s) {
722        if capture.start() != prev_end {
723            return false;
724        }
725        prev_end = capture.end();
726    }
727    prev_end == s.len()
728}
729
730/// Similar to `str::split`, but also provides byte-offset ranges of the results. Unlike
731/// `str::split`, this is not generic on pattern types and does not return an `Iterator`.
732pub fn split_str_with_ranges<'s>(
733    s: &'s str,
734    pat: &dyn Fn(char) -> bool,
735) -> Vec<(Range<usize>, &'s str)> {
736    let mut result = Vec::new();
737    let mut start = 0;
738
739    for (i, ch) in s.char_indices() {
740        if pat(ch) {
741            if i > start {
742                result.push((start..i, &s[start..i]));
743            }
744            start = i + ch.len_utf8();
745        }
746    }
747
748    if s.len() > start {
749        result.push((start..s.len(), &s[start..s.len()]));
750    }
751
752    result
753}
754
755pub fn default<D: Default>() -> D {
756    Default::default()
757}
758
759#[derive(Debug)]
760pub enum ConnectionResult<O> {
761    Timeout,
762    ConnectionReset,
763    Result(anyhow::Result<O>),
764}
765
766impl<O> ConnectionResult<O> {
767    pub fn into_response(self) -> anyhow::Result<O> {
768        match self {
769            ConnectionResult::Timeout => anyhow::bail!("Request timed out"),
770            ConnectionResult::ConnectionReset => anyhow::bail!("Server reset the connection"),
771            ConnectionResult::Result(r) => r,
772        }
773    }
774}
775
776impl<O> From<anyhow::Result<O>> for ConnectionResult<O> {
777    fn from(result: anyhow::Result<O>) -> Self {
778        ConnectionResult::Result(result)
779    }
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785
786    #[test]
787    fn test_parse_os_release() {
788        let os_release =
789            "NAME=\"Ubuntu\"\nID=ubuntu\nVERSION_ID=\"24.04\"\nPRETTY_NAME=\"Ubuntu 24.04 LTS\"\n";
790        assert_eq!(
791            parse_os_release(os_release),
792            Some("ubuntu 24.04".to_string())
793        );
794
795        // VERSION_ID may be absent (e.g. rolling releases like Arch).
796        assert_eq!(parse_os_release("ID=arch\n"), Some("arch".to_string()));
797
798        // Without an ID there is nothing usable to report.
799        assert_eq!(parse_os_release("VERSION_ID=1\n"), None);
800        assert_eq!(parse_os_release(""), None);
801    }
802
803    #[test]
804    fn test_extend_sorted() {
805        let mut vec = vec![];
806
807        extend_sorted(&mut vec, vec![21, 17, 13, 8, 1, 0], 5, |a, b| b.cmp(a));
808        assert_eq!(vec, &[21, 17, 13, 8, 1]);
809
810        extend_sorted(&mut vec, vec![101, 19, 17, 8, 2], 8, |a, b| b.cmp(a));
811        assert_eq!(vec, &[101, 21, 19, 17, 13, 8, 2, 1]);
812
813        extend_sorted(&mut vec, vec![1000, 19, 17, 9, 5], 8, |a, b| b.cmp(a));
814        assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]);
815    }
816
817    #[test]
818    fn test_truncate_to_bottom_n_sorted_by() {
819        let mut vec: Vec<u32> = vec![5, 2, 3, 4, 1];
820        truncate_to_bottom_n_sorted_by(&mut vec, 10, &u32::cmp);
821        assert_eq!(vec, &[1, 2, 3, 4, 5]);
822
823        vec = vec![5, 2, 3, 4, 1];
824        truncate_to_bottom_n_sorted_by(&mut vec, 5, &u32::cmp);
825        assert_eq!(vec, &[1, 2, 3, 4, 5]);
826
827        vec = vec![5, 2, 3, 4, 1];
828        truncate_to_bottom_n_sorted_by(&mut vec, 4, &u32::cmp);
829        assert_eq!(vec, &[1, 2, 3, 4]);
830
831        vec = vec![5, 2, 3, 4, 1];
832        truncate_to_bottom_n_sorted_by(&mut vec, 1, &u32::cmp);
833        assert_eq!(vec, &[1]);
834
835        vec = vec![5, 2, 3, 4, 1];
836        truncate_to_bottom_n_sorted_by(&mut vec, 0, &u32::cmp);
837        assert!(vec.is_empty());
838    }
839
840    #[test]
841    fn test_iife() {
842        fn option_returning_function() -> Option<()> {
843            None
844        }
845
846        let foo = maybe!({
847            option_returning_function()?;
848            Some(())
849        });
850
851        assert_eq!(foo, None);
852    }
853
854    #[test]
855    fn test_truncate_and_trailoff() {
856        assert_eq!(truncate_and_trailoff("", 5), "");
857        assert_eq!(truncate_and_trailoff("aaaaaa", 7), "aaaaaa");
858        assert_eq!(truncate_and_trailoff("aaaaaa", 6), "aaaaaa");
859        assert_eq!(truncate_and_trailoff("aaaaaa", 5), "aaaaa…");
860        assert_eq!(truncate_and_trailoff("èèèèèè", 7), "èèèèèè");
861        assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè");
862        assert_eq!(truncate_and_trailoff("èèèèèè", 5), "èèèèè…");
863    }
864
865    #[test]
866    fn test_truncate_and_remove_front() {
867        assert_eq!(truncate_and_remove_front("", 5), "");
868        assert_eq!(truncate_and_remove_front("aaaaaa", 7), "aaaaaa");
869        assert_eq!(truncate_and_remove_front("aaaaaa", 6), "aaaaaa");
870        assert_eq!(truncate_and_remove_front("aaaaaa", 5), "…aaaaa");
871        assert_eq!(truncate_and_remove_front("èèèèèè", 7), "èèèèèè");
872        assert_eq!(truncate_and_remove_front("èèèèèè", 6), "èèèèèè");
873        assert_eq!(truncate_and_remove_front("èèèèèè", 5), "…èèèèè");
874    }
875
876    #[test]
877    fn test_numeric_prefix_str_method() {
878        let target = "1a";
879        assert_eq!(
880            NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
881            NumericPrefixWithSuffix(Some(1), "a")
882        );
883
884        let target = "12ab";
885        assert_eq!(
886            NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
887            NumericPrefixWithSuffix(Some(12), "ab")
888        );
889
890        let target = "12_ab";
891        assert_eq!(
892            NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
893            NumericPrefixWithSuffix(Some(12), "_ab")
894        );
895
896        let target = "1_2ab";
897        assert_eq!(
898            NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
899            NumericPrefixWithSuffix(Some(1), "_2ab")
900        );
901
902        let target = "1.2";
903        assert_eq!(
904            NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
905            NumericPrefixWithSuffix(Some(1), ".2")
906        );
907
908        let target = "1.2_a";
909        assert_eq!(
910            NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
911            NumericPrefixWithSuffix(Some(1), ".2_a")
912        );
913
914        let target = "12.2_a";
915        assert_eq!(
916            NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
917            NumericPrefixWithSuffix(Some(12), ".2_a")
918        );
919
920        let target = "12a.2_a";
921        assert_eq!(
922            NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
923            NumericPrefixWithSuffix(Some(12), "a.2_a")
924        );
925    }
926
927    #[test]
928    fn test_numeric_prefix_with_suffix() {
929        let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"];
930        sorted.sort_by_key(|s| NumericPrefixWithSuffix::from_numeric_prefixed_str(s));
931        assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]);
932
933        for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~™£"] {
934            assert_eq!(
935                NumericPrefixWithSuffix::from_numeric_prefixed_str(numeric_prefix_less),
936                NumericPrefixWithSuffix(None, numeric_prefix_less),
937                "String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix"
938            )
939        }
940    }
941
942    #[test]
943    fn test_word_consists_of_emojis() {
944        let words_to_test = vec![
945            ("👨‍👩‍👧‍👧👋🥒", true),
946            ("👋", true),
947            ("!👋", false),
948            ("👋!", false),
949            ("👋 ", false),
950            (" 👋", false),
951            ("Test", false),
952        ];
953
954        for (text, expected_result) in words_to_test {
955            assert_eq!(word_consists_of_emojis(text), expected_result);
956        }
957    }
958
959    #[test]
960    fn test_truncate_lines_and_trailoff() {
961        let text = r#"Line 1
962Line 2
963Line 3"#;
964
965        assert_eq!(
966            truncate_lines_and_trailoff(text, 2),
967            r#"Line 1
968…"#
969        );
970
971        assert_eq!(
972            truncate_lines_and_trailoff(text, 3),
973            r#"Line 1
974Line 2
975…"#
976        );
977
978        assert_eq!(
979            truncate_lines_and_trailoff(text, 4),
980            r#"Line 1
981Line 2
982Line 3"#
983        );
984    }
985
986    #[test]
987    fn test_expanded_and_wrapped_usize_range() {
988        // Neither wrap
989        assert_eq!(
990            expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::<Vec<usize>>(),
991            (1..5).collect::<Vec<usize>>()
992        );
993        // Start wraps
994        assert_eq!(
995            expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::<Vec<usize>>(),
996            ((0..5).chain(7..8)).collect::<Vec<usize>>()
997        );
998        // Start wraps all the way around
999        assert_eq!(
1000            expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::<Vec<usize>>(),
1001            (0..8).collect::<Vec<usize>>()
1002        );
1003        // Start wraps all the way around and past 0
1004        assert_eq!(
1005            expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::<Vec<usize>>(),
1006            (0..8).collect::<Vec<usize>>()
1007        );
1008        // End wraps
1009        assert_eq!(
1010            expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::<Vec<usize>>(),
1011            (0..1).chain(2..8).collect::<Vec<usize>>()
1012        );
1013        // End wraps all the way around
1014        assert_eq!(
1015            expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::<Vec<usize>>(),
1016            (0..8).collect::<Vec<usize>>()
1017        );
1018        // End wraps all the way around and past the end
1019        assert_eq!(
1020            expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::<Vec<usize>>(),
1021            (0..8).collect::<Vec<usize>>()
1022        );
1023        // Both start and end wrap
1024        assert_eq!(
1025            expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::<Vec<usize>>(),
1026            (0..8).collect::<Vec<usize>>()
1027        );
1028    }
1029
1030    #[test]
1031    fn test_wrapped_usize_outward_from() {
1032        // No wrapping
1033        assert_eq!(
1034            wrapped_usize_outward_from(4, 2, 2, 10).collect::<Vec<usize>>(),
1035            vec![4, 5, 3, 6, 2]
1036        );
1037        // Wrapping at end
1038        assert_eq!(
1039            wrapped_usize_outward_from(8, 2, 3, 10).collect::<Vec<usize>>(),
1040            vec![8, 9, 7, 0, 6, 1]
1041        );
1042        // Wrapping at start
1043        assert_eq!(
1044            wrapped_usize_outward_from(1, 3, 2, 10).collect::<Vec<usize>>(),
1045            vec![1, 2, 0, 3, 9, 8]
1046        );
1047        // All values wrap around
1048        assert_eq!(
1049            wrapped_usize_outward_from(5, 10, 10, 8).collect::<Vec<usize>>(),
1050            vec![5, 6, 4, 7, 3, 0, 2, 1]
1051        );
1052        // None before / after
1053        assert_eq!(
1054            wrapped_usize_outward_from(3, 0, 0, 8).collect::<Vec<usize>>(),
1055            vec![3]
1056        );
1057        // Starting point already wrapped
1058        assert_eq!(
1059            wrapped_usize_outward_from(15, 2, 2, 10).collect::<Vec<usize>>(),
1060            vec![5, 6, 4, 7, 3]
1061        );
1062        // wrap_length of 0
1063        assert_eq!(
1064            wrapped_usize_outward_from(4, 2, 2, 0).collect::<Vec<usize>>(),
1065            Vec::<usize>::new()
1066        );
1067    }
1068
1069    #[test]
1070    fn test_split_with_ranges() {
1071        let input = "hi";
1072        let result = split_str_with_ranges(input, &|c| c == ' ');
1073
1074        assert_eq!(result.len(), 1);
1075        assert_eq!(result[0], (0..2, "hi"));
1076
1077        let input = "héllo🦀world";
1078        let result = split_str_with_ranges(input, &|c| c == '🦀');
1079
1080        assert_eq!(result.len(), 2);
1081        assert_eq!(result[0], (0..6, "héllo")); // 'é' is 2 bytes
1082        assert_eq!(result[1], (10..15, "world")); // '🦀' is 4 bytes
1083    }
1084
1085    #[test]
1086    fn test_merge_json_values() {
1087        use serde_json::json;
1088
1089        let mut target = json!({
1090            "unchanged": 1,
1091            "replaced_scalar": "old",
1092            "replaced_array": ["default-1", "default-2"],
1093            "nulled": true,
1094            "array_becomes_object": [1, 2],
1095            "object_becomes_scalar": { "x": 1 },
1096            "nested": {
1097                "kept": true,
1098                "overridden": 2,
1099                "args": ["--default"],
1100                "deeper": { "list": [1, 2], "other": "kept" },
1101            },
1102        });
1103        let source = json!({
1104            "replaced_scalar": "new",
1105            "replaced_array": ["default-2", "user"],
1106            "nulled": null,
1107            "array_becomes_object": { "y": 2 },
1108            "object_becomes_scalar": 3,
1109            "inserted": ["brand-new"],
1110            "nested": {
1111                "overridden": 20,
1112                "args": ["--user"],
1113                "deeper": { "list": [3] },
1114                "inserted": { "z": true },
1115            },
1116        });
1117
1118        merge_json_value_into(source, &mut target);
1119
1120        assert_eq!(
1121            target,
1122            json!({
1123                "unchanged": 1,
1124                "replaced_scalar": "new",
1125                "replaced_array": ["default-2", "user"],
1126                "nulled": null,
1127                "array_becomes_object": { "y": 2 },
1128                "object_becomes_scalar": 3,
1129                "inserted": ["brand-new"],
1130                "nested": {
1131                    "kept": true,
1132                    "overridden": 20,
1133                    "args": ["--user"],
1134                    "deeper": { "list": [3], "other": "kept" },
1135                    "inserted": { "z": true },
1136                },
1137            })
1138        );
1139    }
1140}
1141
Served at tenant.openagents/omega Member data and write actions are omitted.