some initial work experimenting with screens

This commit is contained in:
emilis 2023-01-10 01:30:21 +00:00
commit 0b9f43b9d2
8 changed files with 2503 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

2065
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "kk"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.68"
misskey = "0.2.0"
termion = "2.0.1"
[dependencies.tokio]
version = "1.24.1"
features = ["full"]

113
src/display/body.rs Normal file
View File

@ -0,0 +1,113 @@
use std::io::{Stdout, Write};
use termion::{clear, cursor, raw::RawTerminal};
use super::{
frame::{self, FrameDef},
theme::Theme,
Event, Page,
};
type Result<T> = std::result::Result<T, anyhow::Error>;
#[derive(Debug, Clone)]
pub enum Body {
Echo,
Signin(SigninPage),
}
impl Body {
fn echo(theme: Theme, screen: &mut RawTerminal<Stdout>, event: Event) -> Result<()> {
let event = format!("{}", event);
write!(
screen,
"{theme}{clear}{start}Event: {}",
event,
theme = theme.display_string(),
clear = clear::All,
start = cursor::Goto(1, 1)
)?;
screen.flush()?;
Ok(())
}
}
impl Page for Body {
fn receive_event(
&mut self,
theme: Theme,
screen: &mut RawTerminal<Stdout>,
event: Event,
) -> Result<()> {
match self {
Body::Signin(b) => b.receive_event(theme, screen, event)?,
Body::Echo => Body::echo(theme, screen, event)?,
};
Ok(())
}
fn init(&self, theme: Theme, screen: &mut RawTerminal<Stdout>) -> Result<()> {
write!(
screen,
"{theme}{cursor}{clear}{fr}",
fr = frame::draw_frame(theme, FrameDef::ByPercent(90, 90)),
theme = theme.display_string(),
cursor = cursor::Goto(1, 1),
clear = clear::All
)?;
screen.flush()?;
Ok(())
}
}
#[derive(Debug, Clone, Default)]
struct SigninPage {
hostname: String,
username: String,
cursor: SigninCursorLocation,
}
#[derive(Debug, Clone)]
enum SigninCursorLocation {
Hostname,
Username,
Next,
}
impl Default for SigninCursorLocation {
fn default() -> Self {
Self::Hostname
}
}
impl SigninPage {
fn frame_string() -> String {
format!("")
}
}
impl Page for SigninPage {
fn receive_event(
&mut self,
theme: Theme,
screen: &mut RawTerminal<Stdout>,
event: Event,
) -> Result<()> {
Ok(())
}
fn init(&self, theme: Theme, screen: &mut RawTerminal<Stdout>) -> Result<()> {
let fr = frame::draw_frame(theme, FrameDef::ByPercent(40, 40));
write!(
screen,
"{theme}{clear}{hide_cursor}{frame}",
frame = fr,
theme = theme.display_string(),
clear = clear::All,
hide_cursor = cursor::Hide,
)?;
screen.flush()?;
Ok(())
}
}

102
src/display/frame.rs Normal file
View File

@ -0,0 +1,102 @@
use std::{io, process};
use termion::{color, cursor, screen::IntoAlternateScreen};
use super::theme::Theme;
const ESTIMATED_FRAME_BIT_SIZE: usize = 11;
pub enum FrameDef {
ByAbsolute(u16, u16),
ByPercent(u16, u16),
}
struct Frame {
start: (u16, u16),
end: (u16, u16),
}
impl Frame {
fn from_bottom_right(pos: (u16, u16), term: (u16, u16)) -> Self {
Self {
start: (term.0 - pos.0, term.1 - pos.1),
end: pos,
}
}
fn frame_str(&self, frame_char: char) -> String {
let (w_len, h_len) = (
(self.end.0 - self.start.0) as usize,
(self.end.1 - self.start.1 - 1) as usize,
);
let width_str = frame_char.to_string().repeat(w_len + 1);
let mut frame = String::with_capacity((h_len * 2) + (w_len * 2) * ESTIMATED_FRAME_BIT_SIZE);
let make_line =
|y: u16| format!("{left}{}", width_str, left = cursor::Goto(self.start.0, y));
frame.push_str(&make_line(self.start.1));
for y in self.start.1 + 1..self.end.1 {
frame.push_str(&format!(
"{left}{char}{right}{char}",
left = cursor::Goto(self.start.0, y),
right = cursor::Goto(self.end.0, y),
char = frame_char,
));
}
frame.push_str(&make_line(self.end.1));
frame
}
}
#[cfg(test)]
mod test {
use super::Frame;
#[test]
fn test_idk() {
let x = Frame::from_bottom_right((16, 16), (32, 32)).frame_str('=');
println!("starting");
println!("{}", x);
println!("ending");
assert!(false)
}
}
impl FrameDef {
fn abs_size(&self) -> Frame {
let (term_height, term_width) =
termion::terminal_size().expect("could not get terminal size");
let pos = match self {
FrameDef::ByAbsolute(h, w) => {
let (mut h, mut w) = (*h, *w);
if h > term_height {
h = term_height;
}
if w > term_width {
w = term_width;
}
(h, w)
}
FrameDef::ByPercent(h, w) => {
// term_height = 100%
// x = h%
// x = term_height * h / 100
let (h, w) = (
if *h > 100 { 100 } else { *h },
if *w > 100 { 100 } else { *w },
);
// (h * 100 / term_height, w * 100 / term_width)
(term_height * h / 100, term_width * w / 100)
}
};
Frame::from_bottom_right(pos, (term_height, term_width))
}
}
pub fn draw_frame(theme: Theme, frame_size: FrameDef) -> String {
let frame_specs = frame_size.abs_size();
format!(
"{fg}{bg}{frame}",
fg = theme.colors.frame_fg.fg_string(),
bg = theme.colors.frame_bg.bg_string(),
frame = frame_specs.frame_str('='),
)
}

112
src/display/mod.rs Normal file
View File

@ -0,0 +1,112 @@
use std::fmt::Display;
use std::io::{self, Stdout, Write};
use termion::event::Key;
use termion::input::TermRead;
use termion::raw::{IntoRawMode, RawTerminal};
use termion::{clear, cursor};
use tokio::sync::mpsc::{self, Receiver, Sender};
use tokio::sync::Mutex;
use tokio::time::Instant;
use self::body::Body;
use self::theme::Theme;
pub mod body;
pub mod frame;
pub mod theme;
type Result<T> = std::result::Result<T, anyhow::Error>;
pub struct Screen {
screen: Mutex<RawTerminal<Stdout>>,
events_ch: Receiver<Event>,
body: Body,
last_interrupt: Option<Instant>,
theme: Theme,
}
#[derive(Debug)]
pub enum Event {
Key(Key),
Interrupt,
}
impl Display for Event {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Event::Key(key) => write!(f, "{}", format!("{:#?}", key).replace('\n', "\r\n")),
Event::Interrupt => write!(f, "Interrupt"),
}
}
}
impl From<Key> for Event {
fn from(value: Key) -> Self {
Event::Key(value)
}
}
pub trait Page {
fn init(&self, theme: Theme, screen: &mut RawTerminal<Stdout>) -> Result<()>;
fn receive_event(
&mut self,
theme: Theme,
screen: &mut RawTerminal<Stdout>,
event: Event,
) -> Result<()>;
}
impl Screen {
pub fn new(theme: Theme) -> Result<Self> {
let screen = Mutex::new(io::stdout().into_raw_mode()?);
let (send, recv) = mpsc::channel::<Event>(16);
tokio::spawn(async { Screen::input_loop(send).await });
// let body = Body::Signin(SigninPage::default());
let body = Body::Echo;
Ok(Self {
screen,
body,
theme,
events_ch: recv,
last_interrupt: None,
})
}
pub async fn start(mut self) -> Result<()> {
{
let mut scr = self.screen.lock().await;
self.body.init(self.theme, &mut scr)?;
}
while let Some(ev) = self.events_ch.recv().await {
let mut scr = self.screen.lock().await;
if let Event::Interrupt = ev {
if let Some(last) = self.last_interrupt && last.elapsed().as_millis() < 500 {
self.events_ch.close();
break;
}
self.last_interrupt = Some(Instant::now());
}
self.body.receive_event(self.theme, &mut scr, ev)?;
}
// Cleanup
let mut scr = self.screen.lock().await;
write!(scr, "{}{}", cursor::Goto(1, 1), clear::All)?;
Ok(())
}
async fn input_loop(snd: Sender<Event>) {
let input = io::stdin();
for key in input.keys() {
let event = match key.unwrap() {
Key::Ctrl(sub) => match sub {
'c' => Event::Interrupt,
key => Event::Key(Key::Ctrl(key)),
},
key => key.into(),
};
snd.send(event).await.unwrap();
}
}
}

76
src/display/theme.rs Normal file
View File

@ -0,0 +1,76 @@
use std::ops::Deref;
use termion::color;
#[derive(Clone, Copy, Debug)]
pub struct Theme {
pub colors: Colors,
}
#[derive(Clone, Copy, Debug)]
pub struct Colors {
pub primary_bg: Color,
pub frame_bg: Color,
pub frame_fg: Color,
pub text: Color,
}
impl Theme {}
#[derive(Clone, Copy, Debug)]
pub struct Color(color::Rgb);
impl Deref for Color {
type Target = color::Rgb;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<String> for Color {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(value.as_str().try_into()?)
}
}
impl TryFrom<&str> for Color {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.len() < 6 || value.len() > 7 {
return Err(anyhow::anyhow!("hex code length invalid: {}", value.len()));
}
let mut i = 0;
if value.starts_with('#') {
i = 1;
}
Ok(Self(color::Rgb(
u8::from_str_radix(&value[i..i + 2], 16)?,
u8::from_str_radix(&value[i + 2..i + 4], 16)?,
u8::from_str_radix(&value[i + 4..i + 6], 16)?,
)))
}
}
impl Default for Theme {
fn default() -> Self {
Self {
colors: Colors {
primary_bg: "#3b224c".try_into().unwrap(),
text: "#ffe6ff".try_into().unwrap(),
frame_bg: "#330033".try_into().unwrap(),
frame_fg: "#ffe6ff".try_into().unwrap(),
},
}
}
}
impl Theme {
pub fn display_string(&self) -> String {
format!(
"{primary_bg}{text}",
primary_bg = self.colors.primary_bg.bg_string(),
text = self.colors.text.fg_string()
)
}
}

19
src/main.rs Normal file
View File

@ -0,0 +1,19 @@
#![feature(let_chains)]
use std::process;
use display::theme::Theme;
use crate::display::Screen;
extern crate termion;
mod display;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
{
let theme = Theme::default();
let x = Screen::new(theme).unwrap();
x.start().await?;
}
process::exit(0);
}