chiark / gitweb /
UNFINISHED: first overlay activity: Examine User prompt.
authorSimon Tatham <anakin@pobox.com>
Mon, 1 Jan 2024 08:14:02 +0000 (08:14 +0000)
committerSimon Tatham <anakin@pobox.com>
Mon, 1 Jan 2024 09:06:55 +0000 (09:06 +0000)
This doesn't actually examine users yet. When you press Return at the
prompt, it just beeps, because transferring the data to where you
actually wanted it is not yet implemented. But it's enough to test the
editing itself.

src/editor.rs
src/menu.rs
src/tui.rs

index bc85f2c3221dedf0003abd3ee0ea9e9014247756..43c11f86d56aeeaa76f0da1a9f010c6f1bf1ab35 100644 (file)
@@ -1,6 +1,11 @@
 use unicode_width::UnicodeWidthChar;
 
-use super::tui::{OurKey, OurKey::*};
+use super::client::Client;
+use super::coloured_string::ColouredString;
+use super::tui::{
+    ActivityState, CursorPosition, LogicalAction,
+    OurKey, OurKey::*,
+};
 
 struct EditorCore {
     text: String,
@@ -22,6 +27,20 @@ impl EditorCore {
         }
     }
 
+    fn char_width_and_bytes(&self, pos: usize) -> Option<(usize, usize)> {
+        match self.text[pos..].chars().next() {
+            None => None,
+            Some(c) => {
+                let width = UnicodeWidthChar::width(c).unwrap_or(0);
+                let mut end = pos + 1;
+                while !self.is_char_boundary(end) {
+                    end += 1;
+                }
+                Some((width, end - pos))
+            },
+        }
+    }
+
     fn is_word_boundary(&self, pos: usize) -> bool {
         if !self.is_char_boundary(pos) {
             false
@@ -154,6 +173,7 @@ impl EditorCore {
             Ctrl('T') => { self.forward_word(); },
             Ctrl('Y') => { self.paste(); },
             Pr(c) => { self.insert(&c.to_string()); },
+            Space => { self.insert(" "); },
             _ => (),
         }
     }
@@ -264,6 +284,8 @@ fn test_insert() {
 
 pub struct SingleLineEditor {
     core: EditorCore,
+    width: usize,
+    first_visible: usize,
 }
 
 impl SingleLineEditor {
@@ -274,6 +296,57 @@ impl SingleLineEditor {
                 point: 0,
                 paste_buffer: "".to_owned(),
             },
+            width: 0,
+            first_visible: 0,
+        }
+    }
+
+    fn update_first_visible(&mut self) {
+        if self.first_visible > self.core.point {
+            self.first_visible = self.core.point;
+        } else {
+            let mut avail_width = self.width.saturating_sub(1);
+            let mut counted_initial_trunc_marker = false;
+            if self.first_visible > 0 {
+                counted_initial_trunc_marker = true;
+                avail_width = avail_width.saturating_sub(1);
+            }
+            let mut head = self.first_visible;
+            let mut tail = self.first_visible;
+            let mut currwidth = 0;
+            while head < self.core.point || currwidth > avail_width {
+                if currwidth <= avail_width {
+                    match self.core.char_width_and_bytes(head) {
+                        None => break,
+                        Some((w, b)) => {
+                            head += b;
+                            currwidth += w;
+                        },
+                    }
+                } else {
+                    match self.core.char_width_and_bytes(tail) {
+                        None => panic!("tail should always be behind head"),
+                        Some((w, b)) => {
+                            tail += b;
+                            currwidth -= w;
+                            if !counted_initial_trunc_marker {
+                                counted_initial_trunc_marker = true;
+                                avail_width = avail_width.saturating_sub(1);
+                            }
+                        },
+                    }
+                }
+            }
+            self.first_visible = tail;
+        }
+
+        // Special case: if the < indicator hides a single character
+        // at the start of the buffer which is the same width as it,
+        // we can just reveal it, which is strictly better.
+        if let Some((w, b)) = self.core.char_width_and_bytes(0) {
+            if w == 1 && b == self.first_visible {
+                self.first_visible = 0;
+            }
         }
     }
 
@@ -289,6 +362,214 @@ impl SingleLineEditor {
             Return => { return true; },
             _ => { self.core.handle_keypress(key); }
         }
+        self.update_first_visible();
         return false;
     }
+
+    pub fn draw(&self, width: usize) -> (ColouredString, Option<usize>) {
+        let mut s = ColouredString::plain("");
+        if self.first_visible > 0 {
+            s.push_str(&ColouredString::uniform("<", '>').slice());
+        }
+        let mut pos = self.first_visible;
+        let mut cursor = None;
+        loop {
+            let width_so_far = s.width();
+            if pos == self.core.point {
+                cursor = Some(width_so_far);
+            }
+            match self.core.char_width_and_bytes(pos) {
+                None => break,
+                Some((w, b)) => {
+                    if width_so_far + w > width ||
+                        (width_so_far + w == width &&
+                         pos + b < self.core.text.len())
+                    {
+                        s.push_str(&ColouredString::uniform(">", '>').slice());
+                        break;
+                    } else {
+                        s.push_str(&ColouredString::plain(
+                            &self.core.text[pos..pos+b]).slice());
+                        pos += b;
+                    }
+                }
+            }
+        }
+        (s, cursor)
+    }
+
+    pub fn resize(&mut self, width: usize) {
+        self.width = width;
+        self.update_first_visible();
+    } 
+}
+
+#[test]
+fn test_single_line_extra_ops() {
+    let mut sle = SingleLineEditor {
+        core: EditorCore {
+            text: "hélło".to_owned(),
+            point: 3,
+            paste_buffer: "".to_owned(),
+        },
+        width: 0,
+        first_visible: 0,
+    };
+
+    sle.cut_to_end();
+    assert_eq!(&sle.core.text, "hé");
+    assert_eq!(sle.core.point, 3);
+    assert_eq!(sle.core.paste_buffer, "lło");
+
+    sle.core.beginning_of_buffer();
+    assert_eq!(sle.core.point, 0);
+
+    sle.core.paste();
+    assert_eq!(&sle.core.text, "lłohé");
+    assert_eq!(sle.core.point, 4);
+
+    sle.core.end_of_buffer();
+    assert_eq!(sle.core.point, 7);
+}
+
+#[test]
+fn test_single_line_visibility() {
+    let mut sle = SingleLineEditor {
+        core: EditorCore {
+            text: "".to_owned(),
+            point: 0,
+            paste_buffer: "".to_owned(),
+        },
+        width: 5,
+        first_visible: 0,
+    };
+
+    assert_eq!(sle.draw(sle.width),
+               (ColouredString::plain(""), Some(0)));
+
+    // Typing 'a' doesn't move first_visible away from the buffer start
+    sle.core.insert("a");
+    assert_eq!(sle.core.point, 1);
+    sle.update_first_visible();
+    assert_eq!(sle.first_visible, 0);
+    assert_eq!(sle.draw(sle.width),
+               (ColouredString::plain("a"), Some(1)));
+
+    // Typing three more characters leaves the cursor in the last of
+    // the 5 positions, so we're still good: we can print "abcd"
+    // followed by an empty space containing the cursor.
+    sle.core.insert("bcd");
+    assert_eq!(sle.core.point, 4);
+    sle.update_first_visible();
+    assert_eq!(sle.first_visible, 0);
+    assert_eq!(sle.draw(sle.width),
+               (ColouredString::plain("abcd"), Some(4)));
+
+    // One more character and we overflow. Now we must print "<cde"
+    // followed by the cursor.
+    sle.core.insert("e");
+    assert_eq!(sle.core.point, 5);
+    sle.update_first_visible();
+    assert_eq!(sle.first_visible, 2);
+    assert_eq!(sle.draw(sle.width),
+               (ColouredString::general("<cde", ">   "), Some(4)));
+
+    // And another two characters move that on in turn: "<efg" + cursor.
+    sle.core.insert("fg");
+    assert_eq!(sle.core.point, 7);
+    sle.update_first_visible();
+    assert_eq!(sle.first_visible, 4);
+    assert_eq!(sle.draw(sle.width),
+               (ColouredString::general("<efg", ">   "), Some(4)));
+
+    // Now start moving backwards. Three backwards movements leave the
+    // cursor on the e, but nothing has changed.
+    sle.core.backward();
+    sle.core.backward();
+    sle.core.backward();
+    assert_eq!(sle.core.point, 4);
+    sle.update_first_visible();
+    assert_eq!(sle.first_visible, 4);
+    assert_eq!(sle.draw(sle.width),
+               (ColouredString::general("<efg", ">   "), Some(1)));
+
+    // Move backwards one more, so that we must scroll to get the d in view.
+    sle.core.backward();
+    assert_eq!(sle.core.point, 3);
+    sle.update_first_visible();
+    assert_eq!(sle.first_visible, 3);
+    assert_eq!(sle.draw(sle.width),
+               (ColouredString::general("<defg", ">    "), Some(1)));
+
+    // And on the _next_ backwards scroll, the end of the string also
+    // becomes hidden.
+    sle.core.backward();
+    assert_eq!(sle.core.point, 2);
+    sle.update_first_visible();
+    assert_eq!(sle.first_visible, 2);
+    assert_eq!(sle.draw(sle.width),
+               (ColouredString::general("<cde>", ">   >"), Some(1)));
+
+    // The one after that would naively leave us at "<bcd>" with the
+    // cursor on the b. But we can do better! In this case, the <
+    // marker hides just one single-width character, so we can reveal
+    // it and put first_visible right back to 0.
+    sle.core.backward();
+    assert_eq!(sle.core.point, 1);
+    sle.update_first_visible();
+    assert_eq!(sle.first_visible, 0);
+    assert_eq!(sle.draw(sle.width),
+               (ColouredString::general("abcd>", "    >"), Some(1)));
+}
+
+struct BottomLineEditorOverlay {
+    prompt: ColouredString,
+    promptwidth: usize,
+    ed: SingleLineEditor,
+}
+
+impl BottomLineEditorOverlay {
+    fn new(prompt: ColouredString) -> Self {
+        let promptwidth = prompt.width();
+        BottomLineEditorOverlay {
+            prompt: prompt,
+            promptwidth,
+            ed: SingleLineEditor::new(),
+        }
+    }
+}
+
+impl ActivityState for BottomLineEditorOverlay {
+    fn resize(&mut self, w: usize, _h: usize) {
+        self.ed.resize(w.saturating_sub(self.promptwidth));
+    }
+
+    fn draw(&self, w: usize, _h: usize) ->
+        (Vec<ColouredString>, CursorPosition)
+    {
+        let (buffer, cursorpos) = self.ed.draw(
+            w.saturating_sub(self.promptwidth));
+
+        let cursorpos = match cursorpos {
+            Some(x) => CursorPosition::At(x + self.promptwidth, 0),
+            None => CursorPosition::None,
+        };            
+
+        (vec! { &self.prompt + buffer }, cursorpos)
+    }
+
+    fn handle_keypress(&mut self, key: OurKey, _client: &mut Client) ->
+        LogicalAction
+    {
+        if self.ed.handle_keypress(key) {
+            LogicalAction::Beep // FIXME: do something!
+        } else {
+            LogicalAction::Nothing
+        }
+    }
+}
+
+pub fn get_user_to_examine() -> Box<dyn ActivityState> {
+    Box::new(BottomLineEditorOverlay::new(
+        ColouredString::plain("Examine User: ")))
 }
index 92774be32f7da2167f92ea333b939427d721202b..afbf1e9b20b244dbf8e7482f5e3da66a898e246f 100644 (file)
@@ -2,7 +2,9 @@ use std::collections::HashMap;
 use std::cmp::max;
 use itertools::Itertools;
 
-use super::activity_stack::{NonUtilityActivity, UtilityActivity};
+use super::activity_stack::{
+    NonUtilityActivity, UtilityActivity, OverlayActivity
+};
 use super::client::Client;
 use super::coloured_string::ColouredString;
 use super::text::*;
@@ -221,7 +223,8 @@ pub fn utils_menu() -> Box<dyn ActivityState> {
     menu.add_action(Pr('R'), "Read Mentions", LogicalAction::Goto(
         UtilityActivity::ReadMentions.into()));
     menu.add_blank_line();
-    menu.add_action(Pr('E'), "Examine User", LogicalAction::NYI);
+    menu.add_action(Pr('E'), "Examine User", LogicalAction::Goto(
+        OverlayActivity::GetUserToExamine.into()));
     menu.add_action(Pr('Y'), "Examine Yourself", LogicalAction::NYI);
     menu.add_blank_line();
     menu.add_action(Pr('L'), "Logs menu", LogicalAction::Goto(
index 704cbf5a13a2214df3910f3a8aa9b0c6d0277c61..1157a0ec7beade3a3df590eaa6cef5448d103601 100644 (file)
@@ -21,6 +21,7 @@ use super::coloured_string::{ColouredString, ColouredStringSlice};
 use super::config::ConfigLocation;
 use super::menu::*;
 use super::file::*;
+use super::editor::*;
 use super::auth::AuthError;
 
 fn ratatui_style_from_colour(colour: char) -> Style {
@@ -438,6 +439,8 @@ fn new_activity_state(activity: Activity, client: &mut Client) ->
             mentions(client),
         Activity::Util(UtilityActivity::EgoLog) =>
             ego_log(client),
+        Activity::Overlay(OverlayActivity::GetUserToExamine) =>
+            Ok(get_user_to_examine()),
         _ => panic!("FIXME"),
     };