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

View File

@ -166,10 +166,14 @@ impl PlanState {
action: Action,
scr: &mut Screen,
) -> Result<Self> {
let term_size = termion::terminal_size()?;
let mut new = match action {
Action::ReplaceAll(layers) => {
Self::from_plans(layers, self.base_colorset)?
}
Action::ReplaceAll(layers) => Self::new(
layers,
self.base_colorset,
term_size,
self.last_render,
),
Action::ReplaceLayer(layer, index) => {
if index >= self.plans.len() {
return Err(anyhow::anyhow!(
@ -184,12 +188,22 @@ impl PlanState {
Action::PushLayer(layer) => {
let mut layers = self.plans;
layers.push(layer);
Self::from_plans(layers, self.base_colorset)?
Self::new(
layers,
self.base_colorset,
term_size,
self.last_render,
)
}
Action::PopLayer => {
let mut layers = self.plans;
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),
};
@ -215,6 +229,9 @@ where
let mut plans =
PlanState::from_plans(view.init()?, base_colorset)?;
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 {
if let Some(msg) = view.query() {
plans = plans.act_on(

View File

@ -5,6 +5,7 @@ pub enum Token {
Centered(Box<Token>),
Limited(Box<Token>, usize),
CharPad(Box<Token>, char, usize),
PadPercent(Box<Token>, u8),
Fg(Box<Token>, Color),
Bg(Box<Token>, Color),
Link(Box<Token>, String),
@ -51,18 +52,41 @@ impl Token {
Self::CharPad(Box::new(self), ' ', pad_to)
}
pub fn str_len(&self) -> usize {
match self {
Token::String(t, s) => s.len() + t.str_len(),
Token::Fg(t, _) => t.str_len(),
Token::Bg(t, _) => t.str_len(),
Token::Link(t, _) => t.str_len(),
Token::Centered(t) => t.str_len(),
Token::Limited(t, lim) => (*lim).min(t.str_len()),
Token::CharPad(t, _, pad_to) => {
(*pad_to).max(t.str_len())
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 {
Token::String(t, s) => {
let (count, len) = t.str_len(total_width);
(count + 1, len + s.len())
}
Token::Fg(t, _) => t.str_len(total_width),
Token::Bg(t, _) => t.str_len(total_width),
Token::Link(t, _) => t.str_len(total_width),
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::String(next, _) => next.has_centered(),
Token::End => false,
Token::PadPercent(next, _) => next.has_centered(),
}
}
@ -113,13 +138,13 @@ impl Token {
.chain(t.with_width(width))
.collect(),
Token::Link(t, name) => {
vec![Component::Link(name, t.str_len())]
vec![Component::Link(name, t.str_len(width).1)]
.into_iter()
.chain(t.with_width(width))
.collect()
}
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 {
str_len = width;
cnt.limited(width).with_width(width)
@ -170,6 +195,43 @@ impl Token {
.chain(t.with_width(width))
.collect(),
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]
fn test_token_gen() {
const WIDTH: usize = 20;
const WIDTH: usize = 100;
vec![
(
"string -> string",
@ -281,6 +343,48 @@ mod tests {
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()
.for_each(|(name, token, expected)| {
@ -298,12 +402,12 @@ mod tests {
assert!(
exp == *act,
"<{}>: component at index {} mismatch.
expected:\n{:#?}
actual:\n{:#?}",
expected:\n{}
actual:\n{}",
&name,
i,
exp,
act,
exp.debug(),
act.debug(),
);
}
})

View File

@ -21,7 +21,11 @@ impl From<termion::event::Key> for Key {
termion::event::Key::Delete => Self::Delete,
termion::event::Key::Insert => Self::Insert,
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::Ctrl(c) => Self::Ctrl(c),
termion::event::Key::Null => Self::Null,
@ -51,4 +55,6 @@ pub enum Key {
Ctrl(char),
Null,
Esc,
Return,
Tab,
}

View File

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

View File

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

View File

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