duat_core/widgets/
file.rs

1//! The primary widget of Duat, used to display files.
2//!
3//! Most extensible features of Duat have the primary purpose of
4//! serving the [`File`], such as multiple [`Cursor`]s, a
5//! [`History`] system, [`Area::PrintInfo`], etc.
6//!
7//! The [`File`] also provides a list of printed lines through the
8//! [`File::printed_lines`] method. This method is notably used by the
9//! [`LineNumbers`] widget, that shows the numbers of the currently
10//! printed lines.
11//!
12//! [`LineNumbers`]: crate::widgets::LineNumbers
13//! [`Cursor`]: crate::mode::Cursor
14use std::{fs, path::PathBuf};
15
16use crate::{
17    cache::load_cache,
18    cfg::PrintCfg,
19    context, form,
20    hooks::{self, FileWritten},
21    mode::Cursors,
22    text::{Bytes, Text, err},
23    ui::{Area, PushSpecs, Ui},
24    widgets::{Widget, WidgetCfg},
25};
26
27/// The configuration for a new [`File`]
28#[derive(Default, Clone)]
29#[doc(hidden)]
30pub struct FileCfg {
31    text_op: TextOp,
32    cfg: PrintCfg,
33}
34
35impl FileCfg {
36    /// Returns a new instance of [`FileCfg`], opening a new buffer
37    pub(crate) fn new() -> Self {
38        FileCfg {
39            text_op: TextOp::NewBuffer,
40            cfg: PrintCfg::default_for_input(),
41        }
42    }
43
44    /// Changes the path of this cfg
45    pub(crate) fn open_path(self, path: PathBuf) -> Self {
46        Self { text_op: TextOp::OpenPath(path), ..self }
47    }
48
49    /// Takes a previous [`File`]
50    pub(crate) fn take_from_prev(
51        self,
52        bytes: Bytes,
53        pk: PathKind,
54        has_unsaved_changes: bool,
55    ) -> Self {
56        Self {
57            text_op: TextOp::TakeBuf(bytes, pk, has_unsaved_changes),
58            ..self
59        }
60    }
61
62    /// Sets the [`PrintCfg`]
63    pub(crate) fn set_print_cfg(&mut self, cfg: PrintCfg) {
64        self.cfg = cfg;
65    }
66}
67
68impl<U: Ui> WidgetCfg<U> for FileCfg {
69    type Widget = File;
70
71    fn build(self, _: bool) -> (Self::Widget, impl Fn() -> bool, PushSpecs) {
72        let (text, path) = match self.text_op {
73            TextOp::NewBuffer => (Text::new_with_history(), PathKind::new_unset()),
74            TextOp::TakeBuf(bytes, pk, has_unsaved_changes) => match &pk {
75                PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
76                    let cursors = {
77                        let cursor = load_cache(path).unwrap_or_default();
78                        Cursors::new_with_main(cursor)
79                    };
80                    let text = Text::from_file(bytes, cursors, path, has_unsaved_changes);
81                    (text, pk)
82                }
83                PathKind::NotSet(_) => {
84                    (Text::from_bytes(bytes, Some(Cursors::default()), true), pk)
85                }
86            },
87            TextOp::OpenPath(path) => {
88                let canon_path = path.canonicalize();
89                if let Ok(path) = &canon_path
90                    && let Ok(file) = std::fs::read_to_string(path)
91                {
92                    let cursors = {
93                        let cursor = load_cache(path).unwrap_or_default();
94                        Cursors::new_with_main(cursor)
95                    };
96                    let text = Text::from_file(Bytes::new(&file), cursors, path, false);
97                    (text, PathKind::SetExists(path.clone()))
98                } else if canon_path.is_err()
99                    && let Ok(mut canon_path) = path.with_file_name(".").canonicalize()
100                {
101                    canon_path.push(path.file_name().unwrap());
102                    (Text::new_with_history(), PathKind::SetAbsent(canon_path))
103                } else {
104                    (Text::new_with_history(), PathKind::new_unset())
105                }
106            }
107        };
108
109        let file = File {
110            path,
111            text,
112            cfg: self.cfg,
113            printed_lines: (0..40).map(|i| (i, i == 1)).collect(),
114            layout_ordering: 0,
115        };
116
117        // The PushSpecs don't matter
118        (file, Box::new(|| false), PushSpecs::above())
119    }
120}
121
122/// The widget that is used to print and edit files
123pub struct File {
124    path: PathKind,
125    text: Text,
126    cfg: PrintCfg,
127    printed_lines: Vec<(usize, bool)>,
128    pub(crate) layout_ordering: usize,
129}
130
131impl File {
132    ////////// Writing the File
133
134    /// Writes the file to the current [`Path`], if one was set
135    ///
136    /// [`Path`]: std::path::Path
137    #[allow(clippy::result_large_err)]
138    pub fn write(&mut self) -> Result<Option<usize>, Text> {
139        if let PathKind::SetExists(path) | PathKind::SetAbsent(path) = &self.path {
140            let path = path.clone();
141            if self.text.has_unsaved_changes() {
142                let res = self
143                    .text
144                    .write_to(std::io::BufWriter::new(fs::File::create(&path)?))
145                    .inspect(|_| self.path = PathKind::SetExists(path.clone()))
146                    .map(Some)
147                    .map_err(Text::from);
148
149                if let Ok(Some(bytes)) = res.as_ref() {
150                    hooks::trigger::<FileWritten>((path.to_string_lossy().to_string(), *bytes));
151                }
152
153                res
154            } else {
155                Ok(None)
156            }
157        } else {
158            Err(err!("No file was set"))
159        }
160    }
161
162    /// Writes the file to the given [`Path`]
163    ///
164    /// [`Path`]: std::path::Path
165    pub fn write_to(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<usize>> {
166        if self.text.has_unsaved_changes() {
167            let path = path.as_ref();
168            let res = self
169                .text
170                .write_to(std::io::BufWriter::new(fs::File::create(path)?))
171                .map(Some);
172
173            if let Ok(Some(bytes)) = res.as_ref() {
174                hooks::trigger::<FileWritten>((path.to_string_lossy().to_string(), *bytes));
175            }
176
177            res
178        } else {
179            Ok(None)
180        }
181    }
182
183    ////////// Path querying functions
184
185    /// The full path of the file.
186    ///
187    /// If there is no set path, returns `"*scratch file*#{id}"`.
188    pub fn path(&self) -> String {
189        self.path.path()
190    }
191
192    /// The full path of the file.
193    ///
194    /// Returns [`None`] if the path has not been set yet.
195    pub fn path_set(&self) -> Option<String> {
196        self.path.path_set()
197    }
198
199    /// The file's name.
200    ///
201    /// If there is no set path, returns `"*scratch file #{id}*"`.
202    pub fn name(&self) -> String {
203        self.path.name()
204    }
205
206    /// The file's name.
207    ///
208    /// Returns [`None`] if the path has not been set yet.
209    pub fn name_set(&self) -> Option<String> {
210        self.path.name_set()
211    }
212
213    pub fn path_kind(&self) -> PathKind {
214        self.path.clone()
215    }
216
217    /// Returns the currently printed set of lines.
218    ///
219    /// These are returned as a `usize`, showing the index of the line
220    /// in the file, and a `bool`, which is `true` when the line is
221    /// wrapped.
222    pub fn printed_lines(&self) -> &[(usize, bool)] {
223        &self.printed_lines
224    }
225
226    ////////// General querying functions
227
228    /// The number of bytes in the file.
229    pub fn len_bytes(&self) -> usize {
230        self.text.len().byte()
231    }
232
233    /// The number of [`char`]s in the file.
234    pub fn len_chars(&self) -> usize {
235        self.text.len().char()
236    }
237
238    /// The number of lines in the file.
239    pub fn len_lines(&self) -> usize {
240        self.text.len().line()
241    }
242
243    /// The [`Text`] of the [`File`]
244    pub fn text(&self) -> &Text {
245        &self.text
246    }
247
248    pub fn text_mut(&mut self) -> &mut Text {
249        &mut self.text
250    }
251
252    /// The mutable [`Text`] of the [`File`]
253    pub fn print_cfg(&self) -> PrintCfg {
254        self.cfg
255    }
256
257    /// The [`Cursors`] that are used on the [`Text`], if they exist
258    pub fn cursors(&self) -> &Cursors {
259        self.text.cursors().unwrap()
260    }
261
262    /// A mutable reference to the [`Cursors`], if they exist
263    pub fn cursors_mut(&mut self) -> Option<&mut Cursors> {
264        self.text.cursors_mut()
265    }
266
267    /// Whether o not the [`File`] exists or not
268    pub fn exists(&self) -> bool {
269        self.path_set()
270            .is_some_and(|p| std::fs::exists(PathBuf::from(&p)).is_ok_and(|e| e))
271    }
272}
273
274impl<U: Ui> Widget<U> for File {
275    type Cfg = FileCfg;
276
277    fn cfg() -> Self::Cfg {
278        FileCfg::new()
279    }
280
281    fn update(&mut self, _area: &U::Area) {}
282
283    fn text(&self) -> &Text {
284        &self.text
285    }
286
287    fn text_mut(&mut self) -> &mut Text {
288        self.text_mut()
289    }
290
291    fn print_cfg(&self) -> PrintCfg {
292        self.cfg
293    }
294
295    fn print(&mut self, area: &<U as Ui>::Area) {
296        let (start, _) = area.first_points(&self.text, self.cfg);
297
298        let mut last_line = area
299            .rev_print_iter(self.text.iter_rev(start), self.cfg)
300            .find_map(|(caret, item)| caret.wrap.then_some(item.line()));
301
302        self.printed_lines.clear();
303        let printed_lines = &mut self.printed_lines;
304
305        let mut has_wrapped = false;
306
307        area.print_with(
308            &mut self.text,
309            self.cfg,
310            form::painter::<Self>(),
311            move |caret, item| {
312                has_wrapped |= caret.wrap;
313                if has_wrapped && item.part.is_char() {
314                    has_wrapped = false;
315                    let line = item.line();
316                    let wrapped = last_line.is_some_and(|ll| ll == line);
317                    last_line = Some(line);
318                    printed_lines.push((line, wrapped));
319                }
320            },
321        )
322    }
323
324    fn once() -> Result<(), Text> {
325        Ok(())
326    }
327}
328
329/// Represents the presence or absence of a path
330#[derive(Debug, Clone, PartialEq, Eq)]
331pub enum PathKind {
332    SetExists(PathBuf),
333    SetAbsent(PathBuf),
334    NotSet(usize),
335}
336
337impl PathKind {
338    /// Returns a new unset [`Path`]
339    fn new_unset() -> PathKind {
340        use std::sync::atomic::{AtomicUsize, Ordering};
341        static UNSET_COUNT: AtomicUsize = AtomicUsize::new(1);
342
343        PathKind::NotSet(UNSET_COUNT.fetch_add(1, Ordering::Relaxed))
344    }
345
346    pub fn path(&self) -> String {
347        match self {
348            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
349                path.to_string_lossy().to_string()
350            }
351            PathKind::NotSet(id) => {
352                let path = std::env::current_dir()
353                    .unwrap()
354                    .to_string_lossy()
355                    .to_string();
356
357                format!("{path}/*scratch file*#{id}")
358            }
359        }
360    }
361
362    pub fn path_set(&self) -> Option<String> {
363        match self {
364            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
365                Some(path.to_string_lossy().to_string())
366            }
367            PathKind::NotSet(_) => None,
368        }
369    }
370
371    pub fn name(&self) -> String {
372        match self {
373            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
374                let cur_dir = context::cur_dir();
375                if let Ok(path) = path.strip_prefix(cur_dir) {
376                    path.to_string_lossy().to_string()
377                } else {
378                    path.to_string_lossy().to_string()
379                }
380            }
381            PathKind::NotSet(id) => format!("*scratch file #{id}*"),
382        }
383    }
384
385    pub fn name_set(&self) -> Option<String> {
386        match self {
387            PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
388                let cur_dir = context::cur_dir();
389                Some(if let Ok(path) = path.strip_prefix(cur_dir) {
390                    path.to_string_lossy().to_string()
391                } else {
392                    path.to_string_lossy().to_string()
393                })
394            }
395            PathKind::NotSet(_) => None,
396        }
397    }
398}
399
400/// What to do when opening the [`File`]
401#[derive(Default, Clone)]
402enum TextOp {
403    #[default]
404    NewBuffer,
405    TakeBuf(Bytes, PathKind, bool),
406    OpenPath(PathBuf),
407}