added padding_pct

This commit is contained in:
emilis 2023-01-27 12:35:24 +00:00
parent f8854de25c
commit e2776996ef
8 changed files with 419 additions and 560 deletions

684
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,11 +17,12 @@ pub enum Component {
} }
impl Component { impl Component {
fn debug(&self) -> String { #[cfg(test)]
pub fn debug(&self) -> String {
match self { match self {
Component::NextLine => "NextLine".to_string(), Component::NextLine => "NextLine".to_string(),
Component::X(x) => format!("X({})", x), Component::X(x) => format!("X({})", x),
Component::String(s) => format!("\"{}\"", s), Component::String(s) => format!("\"{}\"({})", s, s.len()),
Component::Fg(c) => format!("FG({:#?})", c) Component::Fg(c) => format!("FG({:#?})", c)
.replace('\n', "") .replace('\n', "")
.replace(' ', ""), .replace(' ', ""),
@ -564,7 +565,7 @@ mod tests {
"Scrollable widget: no scrolling", "Scrollable widget: no scrolling",
Instruction::start() Instruction::start()
.fixed(10, vec![scrolling_widget]), .fixed(10, vec![scrolling_widget]),
(0..10) (90..100)
.into_iter() .into_iter()
.map(|num| { .map(|num| {
vec![ vec![
@ -587,7 +588,7 @@ mod tests {
( (
"Scrollable widget: scrolled by 3", "Scrollable widget: scrolled by 3",
Instruction::start().fixed(10, vec![scrolled_by_3]), Instruction::start().fixed(10, vec![scrolled_by_3]),
(0..10) (87..97)
.into_iter() .into_iter()
.map(|num| { .map(|num| {
vec![ vec![

View File

@ -166,10 +166,14 @@ impl PlanState {
action: Action, action: Action,
scr: &mut Screen, scr: &mut Screen,
) -> Result<Self> { ) -> Result<Self> {
let term_size = termion::terminal_size()?;
let mut new = match action { let mut new = match action {
Action::ReplaceAll(layers) => { Action::ReplaceAll(layers) => Self::new(
Self::from_plans(layers, self.base_colorset)? layers,
} self.base_colorset,
term_size,
self.last_render,
),
Action::ReplaceLayer(layer, index) => { Action::ReplaceLayer(layer, index) => {
if index >= self.plans.len() { if index >= self.plans.len() {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
@ -184,12 +188,22 @@ impl PlanState {
Action::PushLayer(layer) => { Action::PushLayer(layer) => {
let mut layers = self.plans; let mut layers = self.plans;
layers.push(layer); layers.push(layer);
Self::from_plans(layers, self.base_colorset)? Self::new(
layers,
self.base_colorset,
term_size,
self.last_render,
)
} }
Action::PopLayer => { Action::PopLayer => {
let mut layers = self.plans; let mut layers = self.plans;
layers.pop(); layers.pop();
Self::from_plans(layers, self.base_colorset)? Self::new(
layers,
self.base_colorset,
term_size,
self.last_render,
)
} }
Action::Nothing => return Ok(self), Action::Nothing => return Ok(self),
}; };
@ -215,6 +229,9 @@ where
let mut plans = let mut plans =
PlanState::from_plans(view.init()?, base_colorset)?; PlanState::from_plans(view.init()?, base_colorset)?;
plans.render(&mut screen)?; plans.render(&mut screen)?;
// FIXME: current loop means that pasting a string with len >1
// results in only the first character being rendered, until
// something else triggers a render
loop { loop {
if let Some(msg) = view.query() { if let Some(msg) = view.query() {
plans = plans.act_on( plans = plans.act_on(

View File

@ -5,6 +5,7 @@ pub enum Token {
Centered(Box<Token>), Centered(Box<Token>),
Limited(Box<Token>, usize), Limited(Box<Token>, usize),
CharPad(Box<Token>, char, usize), CharPad(Box<Token>, char, usize),
PadPercent(Box<Token>, u8),
Fg(Box<Token>, Color), Fg(Box<Token>, Color),
Bg(Box<Token>, Color), Bg(Box<Token>, Color),
Link(Box<Token>, String), Link(Box<Token>, String),
@ -51,18 +52,41 @@ impl Token {
Self::CharPad(Box::new(self), ' ', pad_to) Self::CharPad(Box::new(self), ' ', pad_to)
} }
pub fn str_len(&self) -> usize { pub fn pad_percent(self, pct: u8) -> Self {
#[cfg(debug_assertions)]
if pct > 100 {
panic!("pad_percent with >100% width: {}", pct);
}
Self::PadPercent(Box::new(self), 100.min(pct))
}
// Returns (str_count, str_len_total)
pub fn str_len(&self, total_width: usize) -> (usize, usize) {
match self { match self {
Token::String(t, s) => s.len() + t.str_len(), Token::String(t, s) => {
Token::Fg(t, _) => t.str_len(), let (count, len) = t.str_len(total_width);
Token::Bg(t, _) => t.str_len(), (count + 1, len + s.len())
Token::Link(t, _) => t.str_len(), }
Token::Centered(t) => t.str_len(), Token::Fg(t, _) => t.str_len(total_width),
Token::Limited(t, lim) => (*lim).min(t.str_len()), Token::Bg(t, _) => t.str_len(total_width),
Token::CharPad(t, _, pad_to) => { Token::Link(t, _) => t.str_len(total_width),
(*pad_to).max(t.str_len()) Token::Centered(t) => t.str_len(total_width),
Token::Limited(t, lim) => {
let (count, len) = t.str_len(total_width);
(count, (*lim).min(len))
}
Token::CharPad(t, _, pad_to) => {
let (count, len) = t.str_len(total_width);
(count, (*pad_to).max(len))
}
Token::End => (0, 0),
Token::PadPercent(t, pct) => {
let (count, len) = t.str_len(total_width);
(
count,
((*pct as usize * total_width) / 100).max(len),
)
} }
Token::End => 0,
} }
} }
@ -78,6 +102,7 @@ impl Token {
Token::Link(next, _) => next.has_centered(), Token::Link(next, _) => next.has_centered(),
Token::String(next, _) => next.has_centered(), Token::String(next, _) => next.has_centered(),
Token::End => false, Token::End => false,
Token::PadPercent(next, _) => next.has_centered(),
} }
} }
@ -113,13 +138,13 @@ impl Token {
.chain(t.with_width(width)) .chain(t.with_width(width))
.collect(), .collect(),
Token::Link(t, name) => { Token::Link(t, name) => {
vec![Component::Link(name, t.str_len())] vec![Component::Link(name, t.str_len(width).1)]
.into_iter() .into_iter()
.chain(t.with_width(width)) .chain(t.with_width(width))
.collect() .collect()
} }
Token::Centered(cnt) => { Token::Centered(cnt) => {
let mut str_len = cnt.str_len(); let mut str_len = cnt.str_len(width).1;
let components = if str_len > width { let components = if str_len > width {
str_len = width; str_len = width;
cnt.limited(width).with_width(width) cnt.limited(width).with_width(width)
@ -170,6 +195,43 @@ impl Token {
.chain(t.with_width(width)) .chain(t.with_width(width))
.collect(), .collect(),
Token::End => vec![], Token::End => vec![],
Token::PadPercent(next, pct) => {
// FIXME: I feel like this'll be a problem
// at some point but I cba to deal with it rn
let (str_cnt, str_len) = next.str_len(width);
let actual_pad = (pct as usize * width) / 100;
let mut str_idx = 0;
next.with_width(width)
.into_iter()
.map(|comp| {
if let Component::String(s) = comp {
str_idx += 1;
if str_idx != str_cnt {
// Continue as this isn't the end
Component::String(s)
} else if str_len >= actual_pad {
// FIXME: this is probably THE BAD
Component::String({
let mut s = s;
s.truncate(
actual_pad
- (str_len - s.len()),
);
s
})
} else {
Component::String(format!(
"{}{}",
s,
" ".repeat(actual_pad - str_len)
))
}
} else {
comp
}
})
.collect()
}
} }
} }
} }
@ -180,7 +242,7 @@ mod tests {
#[test] #[test]
fn test_token_gen() { fn test_token_gen() {
const WIDTH: usize = 20; const WIDTH: usize = 100;
vec![ vec![
( (
"string -> string", "string -> string",
@ -281,6 +343,48 @@ mod tests {
Component::String("learn more".into()), Component::String("learn more".into()),
], ],
), ),
(
"padding percentage",
Token::text("this gets longer").pad_percent(100),
vec![Component::String(
"this gets longer".to_string()
+ &" "
.repeat(WIDTH - "this gets longer".len()),
)],
),
(
"pad percent with centering",
Token::text("all to the middle")
.pad_percent(60)
.centered(),
vec![
Component::X((WIDTH - (WIDTH * 60) / 100) / 2),
Component::String(
"all to the middle".to_string()
+ &" ".repeat(
((WIDTH * 60) / 100)
- "all to the middle".len(),
),
),
],
),
(
"two strings, padded pct",
Token::text("two")
.bg(Color::BLUE)
.string("one")
.pad_percent(80)
.centered(),
vec![
Component::X((WIDTH - ((WIDTH * 80) / 100)) / 2),
Component::String("one".into()),
Component::Bg(Color::BLUE),
Component::String(
"two".to_string()
+ &" ".repeat(((WIDTH * 80) / 100) - 6),
),
],
),
] ]
.into_iter() .into_iter()
.for_each(|(name, token, expected)| { .for_each(|(name, token, expected)| {
@ -298,12 +402,12 @@ mod tests {
assert!( assert!(
exp == *act, exp == *act,
"<{}>: component at index {} mismatch. "<{}>: component at index {} mismatch.
expected:\n{:#?} expected:\n{}
actual:\n{:#?}", actual:\n{}",
&name, &name,
i, i,
exp, exp.debug(),
act, act.debug(),
); );
} }
}) })

View File

@ -21,7 +21,11 @@ impl From<termion::event::Key> for Key {
termion::event::Key::Delete => Self::Delete, termion::event::Key::Delete => Self::Delete,
termion::event::Key::Insert => Self::Insert, termion::event::Key::Insert => Self::Insert,
termion::event::Key::F(f) => Self::F(f), termion::event::Key::F(f) => Self::F(f),
termion::event::Key::Char(c) => Self::Char(c), termion::event::Key::Char(c) => match c {
'\n' => Self::Return,
'\t' => Self::Tab,
_ => Self::Char(c),
},
termion::event::Key::Alt(c) => Self::Alt(c), termion::event::Key::Alt(c) => Self::Alt(c),
termion::event::Key::Ctrl(c) => Self::Ctrl(c), termion::event::Key::Ctrl(c) => Self::Ctrl(c),
termion::event::Key::Null => Self::Null, termion::event::Key::Null => Self::Null,
@ -51,4 +55,6 @@ pub enum Key {
Ctrl(char), Ctrl(char),
Null, Null,
Esc, Esc,
Return,
Tab,
} }

View File

@ -8,11 +8,13 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.68" anyhow = "1.0.68"
kkdisp = { version = "0.1.0", path = "../kkdisp" } kkdisp = { version = "0.1.0", path = "../kkdisp" }
futures = "0.3.25"
[dependencies.tokio] [dependencies.tokio]
version = "1.24.2" version = "1.24.2"
features = ["full"] features = ["full"]
[dependencies.misskey] [dependencies.misskey]
version = "*" git = "https://github.com/coord-e/misskey-rs"
branch = "develop"
features = ["websocket-client"] features = ["websocket-client"]

View File

@ -1,3 +1,4 @@
use futures::stream::TryStreamExt;
use kkdisp::{ use kkdisp::{
component::{Plan, Widget}, component::{Plan, Widget},
theme::Color, theme::Color,
@ -5,6 +6,8 @@ use kkdisp::{
view::{Event, Key}, view::{Event, Key},
Action, Action,
}; };
use misskey::{StreamingClientExt, WebSocketClient};
use tokio::sync::mpsc::{self, Receiver, Sender};
use crate::Message; use crate::Message;
pub enum LoginFocus { pub enum LoginFocus {
@ -27,15 +30,7 @@ pub struct LoginPrompt {
token: String, token: String,
error: Option<String>, error: Option<String>,
focus: LoginFocus, focus: LoginFocus,
} client: Option<WebSocketClient>,
impl From<&LoginPrompt> for Plan {
fn from(value: &LoginPrompt) -> Self {
Plan::start()
.fill(vec![])
.fixed(8, vec![Widget::new(100, value.tokens())])
.fill(vec![])
}
} }
impl LoginPrompt { impl LoginPrompt {
@ -45,9 +40,17 @@ impl LoginPrompt {
token: String::new(), token: String::new(),
error: None, error: None,
focus: LoginFocus::Hostname, focus: LoginFocus::Hostname,
client: None,
} }
} }
pub fn plan(&self) -> Plan {
Plan::start()
.fill(vec![])
.fixed(8, vec![Widget::new(100, self.tokens())])
.fill(vec![])
}
fn backspace(&mut self) { fn backspace(&mut self) {
match self.focus { match self.focus {
LoginFocus::Hostname => { LoginFocus::Hostname => {
@ -59,11 +62,26 @@ impl LoginPrompt {
LoginFocus::OK => {} LoginFocus::OK => {}
} }
} }
pub fn query(&mut self) -> Option<()> {
pub fn attempt(&self) -> Result<(), anyhow::Error> {
todo!() todo!()
} }
pub fn attempt(&mut self) {
let hostname = self.hostname.clone();
if hostname.len() == 0 {
self.error = Some("empty hostname".into());
self.focus = LoginFocus::Hostname;
return;
}
let token = self.token.clone();
if token.len() == 0 {
self.error = Some("empty token".into());
self.focus = LoginFocus::Token;
return;
}
todo!();
}
pub fn update( pub fn update(
&mut self, &mut self,
event: Event<Message>, event: Event<Message>,
@ -71,13 +89,29 @@ impl LoginPrompt {
match event { match event {
Event::Link(lnk) => { Event::Link(lnk) => {
if lnk == "ok" { if lnk == "ok" {
todo!() self.attempt();
Ok(Action::ReplaceAll(vec![self.plan()]))
} else if lnk == "token" {
self.focus = LoginFocus::Token;
Ok(Action::ReplaceAll(vec![self.plan()]))
} else if lnk == "hostname" {
self.focus = LoginFocus::Hostname;
Ok(Action::ReplaceAll(vec![self.plan()]))
} else { } else {
Ok(Action::Nothing) Ok(Action::Nothing)
} }
} }
Event::Input(input) => { Event::Input(input) => {
match input { match input {
Key::Return => match self.focus {
LoginFocus::Hostname => {
self.focus = LoginFocus::Token;
}
LoginFocus::Token => {
self.focus = LoginFocus::OK;
}
LoginFocus::OK => self.attempt(),
},
Key::Backspace => self.backspace(), Key::Backspace => self.backspace(),
Key::Up => { Key::Up => {
self.focus = match self.focus { self.focus = match self.focus {
@ -93,24 +127,11 @@ impl LoginPrompt {
LoginFocus::OK => LoginFocus::Hostname, LoginFocus::OK => LoginFocus::Hostname,
}; };
} }
Key::Char(c) => { Key::Char(c) => match self.focus {
if c == '\n' { LoginFocus::Hostname => self.hostname.push(c),
if self.focus.ok() { LoginFocus::Token => self.token.push(c),
self.attempt()?; LoginFocus::OK => {}
} },
} else if c == '\t' {
} else {
match self.focus {
LoginFocus::Hostname => {
self.hostname.push(c)
}
LoginFocus::Token => {
self.token.push(c)
}
LoginFocus::OK => {}
}
}
}
Key::Ctrl(c) => { Key::Ctrl(c) => {
if c == 'u' || c == 'U' { if c == 'u' || c == 'U' {
match self.focus { match self.focus {
@ -131,16 +152,17 @@ impl LoginPrompt {
return Ok(Action::Nothing); return Ok(Action::Nothing);
} }
}; };
Ok(Action::ReplaceAll(vec![(&*self).into()])) Ok(Action::ReplaceAll(vec![self.plan()]))
} }
Event::Message(_) => todo!(), Event::Message(_) => todo!(),
} }
} }
fn tokens(&self) -> Vec<Token> { fn tokens(&self) -> Vec<Token> {
let mut hostname = const HOSTNAME_PROMPT: &str = "hostname: ";
Token::text(self.hostname.clone()).padded(10); const TOKEN_PROMPT: &str = "token: ";
let mut token = Token::text(self.token.clone()).padded(10); let mut hostname = Token::text(self.hostname.clone());
let mut token = Token::text(self.token.clone());
let mut ok = Token::text("[ok]"); let mut ok = Token::text("[ok]");
match self.focus { match self.focus {
LoginFocus::Hostname => { LoginFocus::Hostname => {
@ -156,11 +178,22 @@ impl LoginPrompt {
.centered(), .centered(),
Token::End, Token::End,
match &self.error { match &self.error {
Some(e) => Token::text(e).fg(Color::RED), Some(e) => Token::text(e)
.fg(Color::RED)
.pad_percent(80)
.centered(),
None => Token::End, None => Token::End,
}, },
hostname.string("hostname: ").centered(), hostname
token.string("token: ").centered(), .string(HOSTNAME_PROMPT)
.link("hostname")
.pad_percent(80)
.centered(),
token
.string(TOKEN_PROMPT)
.link("token")
.pad_percent(80)
.centered(),
ok.link("ok").centered(), ok.link("ok").centered(),
] ]
} }

View File

@ -24,7 +24,7 @@ pub enum Page {
impl Page { impl Page {
fn init(&self) -> Result<PlanLayers, anyhow::Error> { fn init(&self) -> Result<PlanLayers, anyhow::Error> {
match self { match self {
Page::Login(log) => Ok(vec![log.into()]), Page::Login(log) => Ok(vec![log.plan()]),
} }
} }
} }