some initial work experimenting with screens
This commit is contained in:
commit
0b9f43b9d2
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -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"]
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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('='),
|
||||
)
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue