Skip to main content

rig_tools/
lib.rs

1#![deny(missing_docs)]
2//! Coding tools for Rig agents: [`Read`], [`Write`], [`Edit`], [`Search`], [`List`],
3//! [`Shell`], [`Delete`] and [`Move`].
4//!
5//! - Each says in its description when to pick it, and what it may touch
6//!   ([`footprint`](rig_core::tool::Tool::footprint)): `read`, `search` and `list` read the
7//!   path they name, the other file tools write theirs, and `shell` may change anything, so a
8//!   host runs calls on different files side by side.
9//! - Each publishes what it did beside its text: files read
10//!   ([`FilesRead`](rig_core::tool::FilesRead)), [`SearchHits`], [`FilesListed`], files changed
11//!   with their diffs ([`FilesChanged`](rig_core::tool::FilesChanged)), how a command ended
12//!   ([`ExitStatus`](rig_core::tool::ExitStatus)).
13//! - Each acts on a [`Workspace`](workspace::Workspace): this machine
14//!   ([`Local`](workspace::Local), the default) or another. The file tools resolve paths from an
15//!   optional base directory they can be confined to.
16//! - `shell` reports its output as it comes to a [`Live`]. Arguments are plain data with public
17//!   fields.
18//!
19//! Also the pieces they are made of, for tools of your own:
20//! - [`edit::apply`]: what `edit` makes of a file's text, without the file,
21//!   and [`ReadArgs::show`]: what `read` shows of it;
22//! - [`workspace`]: the disk and the commands the tools act on,
23//!   [`overlay`]: one that keeps the tools' writes from the disk, to
24//!   preview them, and [`remote`]: another machine reached through a
25//!   transport such as `ssh`, and paths written `HOST:PATH` that reach it;
26//! - [`attach`]: the files a message names as `@path`, read to go with it;
27//! - [`checkpoint`]: the files under a root at one moment, what changed by
28//!   a later one, and a handler that reports what any call changed;
29//! - [`clipboard`]: the system clipboard's text and images;
30//! - [`fs`]: an atomic file write and capped text reads on this machine;
31//! - [`show_lines`]: some lines of a text, the way `read` shows a file;
32//! - [`Spill`]: where output a tool cut is kept whole, and [`OutputLimit`]: how
33//!   much of a long output is kept;
34//! - [`process`]: child processes in a process group of their own, the
35//!   [`Launcher`](process::Launcher) that says how tools start them, and
36//!   the [`Process`](process::Process) it spawns: input in, output line by
37//!   line, stopped gently or killed with its group;
38//! - [`blocking`]: blocking work on a thread of its own, so no async
39//!   executor thread blocks.
40//!
41//! Native only: the tools use the file system and processes.
42
43use unicode_width::{UnicodeWidthChar as _, UnicodeWidthStr as _};
44
45pub mod attach;
46mod blocking;
47pub mod checkpoint;
48pub mod clipboard;
49mod delete;
50pub mod edit;
51pub mod fs;
52mod list;
53mod live;
54mod move_file;
55pub mod overlay;
56pub mod process;
57mod read;
58pub mod remote;
59mod search;
60mod shell;
61mod spill;
62mod watch;
63pub mod workspace;
64mod write;
65
66pub use blocking::blocking;
67pub use delete::{Delete, DeleteArgs};
68pub use edit::{Edit, EditArgs, Replacement};
69pub use list::{FilesListed, List, ListArgs};
70pub use live::Live;
71pub use move_file::{Move, MoveArgs};
72pub use read::{Read, ReadArgs, Seen, show_lines};
73pub use search::{
74    Found, Hit, MAX_FILES, MAX_MATCHES, Search, SearchArgs, SearchHits, find, render,
75};
76pub use shell::{Shell, ShellArgs, ShellRun, split_status};
77pub use spill::Spill;
78pub use write::{Write, WriteArgs};
79
80/// Most lines a tool returns.
81pub const MAX_LINES: usize = 2000;
82/// Most bytes a tool returns.
83pub const MAX_BYTES: usize = 50 * 1024;
84/// The largest file `read` and `edit` load whole, and `search` scans.
85pub const MAX_FILE_BYTES: u64 = 16 * 1024 * 1024;
86
87/// How much of a long output a tool keeps: at most `lines` lines and
88/// `bytes` bytes, of whole lines. [`tail`](Self::tail) keeps the last ones;
89/// [`Shell`] keeps a quarter for the first lines, a quarter for the errors
90/// and failures from between, and the rest for the last lines.
91///
92/// ```
93/// use rig_tools::OutputLimit;
94///
95/// let cut = OutputLimit { lines: 2, bytes: 1024 };
96/// assert_eq!(cut.tail("one\ntwo\nthree\n"), ("two\nthree\n", 1));
97/// let narrow = OutputLimit { lines: 10, bytes: 7 };
98/// assert_eq!(narrow.tail("one\ntwo\nthree\n"), ("three\n", 2));
99/// ```
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub struct OutputLimit {
102    /// Most lines kept.
103    pub lines: usize,
104    /// Most bytes kept.
105    pub bytes: usize,
106}
107
108impl Default for OutputLimit {
109    /// [`MAX_LINES`] lines or [`MAX_BYTES`] bytes.
110    fn default() -> Self {
111        Self {
112            lines: MAX_LINES,
113            bytes: MAX_BYTES,
114        }
115    }
116}
117
118impl OutputLimit {
119    /// The end of `text` this keeps, starting at a line, and how many lines
120    /// before it were left out. When even the last line is longer than
121    /// [`bytes`](Self::bytes), nothing is kept.
122    pub fn tail<'t>(&self, text: &'t str) -> (&'t str, usize) {
123        let starts: Vec<usize> = std::iter::once(0)
124            .chain(text.match_indices('\n').map(|(at, _)| at + 1))
125            .filter(|start| *start < text.len())
126            .collect();
127        let first = starts.len().saturating_sub(self.lines);
128        let kept = starts
129            .iter()
130            .enumerate()
131            .skip(first)
132            .find(|(_, start)| text.len() - **start <= self.bytes);
133        match kept {
134            Some((dropped, start)) => (text.get(*start..).unwrap_or_default(), dropped),
135            None => ("", starts.len()),
136        }
137    }
138}
139
140/// `line` cut to its first `limit` characters.
141pub fn clip(line: &str, limit: usize) -> &str {
142    match line.char_indices().nth(limit) {
143        Some((end, _)) => line.get(..end).unwrap_or(line),
144        None => line,
145    }
146}
147
148/// `text` cut to fit in `width` terminal columns, ending in `…` when cut:
149/// a wide character, such as most CJK ones, takes two columns.
150///
151/// ```
152/// use rig_tools::shorten;
153///
154/// assert_eq!(shorten("a short label", 20), "a short label");
155/// assert_eq!(shorten("a longer label", 9), "a longer…");
156/// assert_eq!(shorten("日本語のラベル", 7), "日本語…");
157/// ```
158pub fn shorten(text: &str, width: usize) -> String {
159    if text.width() <= width {
160        return text.to_owned();
161    }
162    let room = width.saturating_sub(1);
163    let mut used = 0;
164    let end = text
165        .char_indices()
166        .find(|(_, character)| {
167            used += character.width().unwrap_or(0);
168            used > room
169        })
170        .map_or(text.len(), |(at, _)| at);
171    match width {
172        0 => String::new(),
173        _ => format!("{}…", text.get(..end).unwrap_or_default()),
174    }
175}
176
177/// How long ago `elapsed` was, roughly, for a list a person reads:
178/// `just now`, `3m ago`, `5h ago`, `2d ago`.
179///
180/// ```
181/// use std::time::Duration;
182/// use rig_tools::ago;
183///
184/// assert_eq!(ago(Duration::from_secs(30)), "just now");
185/// assert_eq!(ago(Duration::from_secs(5 * 3600 + 59)), "5h ago");
186/// ```
187pub fn ago(elapsed: std::time::Duration) -> String {
188    let seconds = elapsed.as_secs();
189    match seconds {
190        0..60 => "just now".to_owned(),
191        60..3600 => format!("{}m ago", seconds / 60),
192        3600..86_400 => format!("{}h ago", seconds / 3600),
193        _ => format!("{}d ago", seconds / 86_400),
194    }
195}
196
197#[cfg(test)]
198mod tests;