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