back: - remove tui deadcode - redirect all unknown links to index.html - fix integration tests - fix linking errors by removing wrong libc.a - split update_item into concrete endpoints - simplify ProcOutput (retain only stderr header) - fix PanelResult front: - load all assets from /core/ - improve current tab highlighting - show, edit and delete job and result info - allow to add and assign jobspull/1/head
parent
862fb6b338
commit
aad7772a71
56 changed files with 645 additions and 1357 deletions
@ -1,6 +1,5 @@ |
|||||||
<nav mat-tab-nav-bar animationDuration="0ms" mat-align-tabs="center"> |
<nav mat-tab-nav-bar animationDuration="0ms" mat-align-tabs="center"> |
||||||
<a mat-tab-link routerLink="/agents" routerLinkActive="active" ariaCurrentWhenActive="page">Agents</a> |
<a mat-tab-link *ngFor="let tab of tabs" routerLink={{tab.link}} routerLinkActive #rla="routerLinkActive" |
||||||
<a mat-tab-link routerLink="/jobs" routerLinkActive="active" ariaCurrentWhenActive="page">Jobs</a> |
[active]="rla.isActive" [routerLinkActiveOptions]="{ exact: true }">{{tab.name}}</a> |
||||||
<a mat-tab-link routerLink="/results" routerLinkActive="active" ariaCurrentWhenActive="page">Results</a> |
|
||||||
</nav> |
</nav> |
||||||
<router-outlet></router-outlet> |
<router-outlet></router-outlet> |
@ -1,8 +1,12 @@ |
|||||||
export interface JobModel { |
import { ApiModel } from "."; |
||||||
|
|
||||||
|
export interface JobModel extends ApiModel { |
||||||
alias: string, |
alias: string, |
||||||
argv: string, |
argv: string, |
||||||
id: string, |
id: string, |
||||||
exec_type: string, |
exec_type: string, |
||||||
platform: string, |
platform: string, |
||||||
payload: Uint8Array | null, |
payload: number[] | null, |
||||||
|
payload_path: string | null, |
||||||
|
schedule: string | null, |
||||||
} |
} |
@ -1,12 +1,12 @@ |
|||||||
import { UTCDate } from "."; |
import { UTCDate, ApiModel } from "."; |
||||||
|
|
||||||
export interface ResultModel { |
export interface ResultModel extends ApiModel { |
||||||
agent_id: string, |
agent_id: string, |
||||||
alias: string, |
alias: string, |
||||||
created: UTCDate, |
created: UTCDate, |
||||||
id: string, |
id: string, |
||||||
job_id: string, |
job_id: string, |
||||||
result: Uint8Array, |
result: number[], |
||||||
state: "Queued" | "Running" | "Finished", |
state: "Queued" | "Running" | "Finished", |
||||||
retcode: number | null, |
retcode: number | null, |
||||||
updated: UTCDate, |
updated: UTCDate, |
||||||
|
@ -0,0 +1,12 @@ |
|||||||
|
<h2 mat-dialog-title>Assign job</h2> |
||||||
|
<mat-dialog-content> |
||||||
|
<mat-selection-list #jobsList [(ngModel)]="selected_rows"> |
||||||
|
<mat-list-option *ngFor="let row of rows" [value]="row"> |
||||||
|
{{row}} |
||||||
|
</mat-list-option> |
||||||
|
</mat-selection-list> |
||||||
|
</mat-dialog-content> |
||||||
|
<mat-dialog-actions align="end"> |
||||||
|
<button mat-raised-button mat-dialog-close (click)="assignSelectedJobs()">Assign</button> |
||||||
|
<button mat-button mat-dialog-close>Cancel</button> |
||||||
|
</mat-dialog-actions> |
@ -0,0 +1,33 @@ |
|||||||
|
import { Component, Inject } from '@angular/core'; |
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
|
import { HttpClient } from '@angular/common/http'; |
||||||
|
import { ApiTableService } from '../../services'; |
||||||
|
import { JobModel } from '../../models'; |
||||||
|
import { MatListOption } from '@angular/material/list'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'assign-job-dialog', |
||||||
|
templateUrl: 'assign-job-dialog.html', |
||||||
|
styleUrls: [] |
||||||
|
}) |
||||||
|
export class AssignJobDialogComponent { |
||||||
|
rows: string[] = []; |
||||||
|
selected_rows: string[] = []; |
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public agent_id: string, private http: HttpClient) { |
||||||
|
new ApiTableService(http, "jobs").getMany().then(result => { |
||||||
|
if (result.status == "ok") { |
||||||
|
const jobs = result.data as JobModel[] |
||||||
|
this.rows = jobs.map(j => `${j.id} ${j.alias}`) |
||||||
|
} else { |
||||||
|
alert(result.data as string) |
||||||
|
} |
||||||
|
}).catch(err => alert(err)) |
||||||
|
} |
||||||
|
|
||||||
|
assignSelectedJobs() { |
||||||
|
const job_ids = this.selected_rows.map(row => row.split(' ', 1)[0]).join(' '); |
||||||
|
const request = `${this.agent_id} ${job_ids}` |
||||||
|
new ApiTableService(this.http, "map").create(request).catch(err => alert(err)) |
||||||
|
} |
||||||
|
} |
@ -1 +1,4 @@ |
|||||||
export * from './agent_info.component'; |
export * from './agent_info.component'; |
||||||
|
export * from './result_info.component'; |
||||||
|
export * from './job_info.component'; |
||||||
|
export * from './assign_job.component'; |
@ -1,3 +1,14 @@ |
|||||||
.info-dlg-field { |
.info-dlg-field { |
||||||
width: 100%; |
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
div.info-dialog-forms-box { |
||||||
|
width: 100%; |
||||||
|
margin-right: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
div.info-dialog-forms-box-smol { |
||||||
|
width: 30%; |
||||||
|
float: left; |
||||||
|
margin-right: 10px; |
||||||
} |
} |
@ -0,0 +1,50 @@ |
|||||||
|
<h2 mat-dialog-title *ngIf="is_preview">Job info</h2> |
||||||
|
<h2 mat-dialog-title *ngIf="!is_preview">Editing job info</h2> |
||||||
|
<mat-dialog-content> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field" cdkFocusInitial> |
||||||
|
<mat-label>ID</mat-label> |
||||||
|
<input matInput disabled value="{{data.id}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Alias</mat-label> |
||||||
|
<input matInput [readonly]="is_preview" [(ngModel)]="data.alias"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Args</mat-label> |
||||||
|
<input matInput [readonly]="is_preview" [(ngModel)]="data.argv"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Type</mat-label> |
||||||
|
<input matInput [readonly]="is_preview" [(ngModel)]="data.exec_type"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Platform</mat-label> |
||||||
|
<input matInput [readonly]="is_preview" [(ngModel)]="data.platform"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Schedule</mat-label> |
||||||
|
<input matInput [readonly]="is_preview" [(ngModel)]="data.schedule"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Payload path</mat-label> |
||||||
|
<input matInput [readonly]="is_preview" [(ngModel)]="data.payload_path"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box"> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Payload</mat-label> |
||||||
|
<textarea matInput cdkTextareaAutosize [readonly]="is_preview" [(ngModel)]="decodedPayload"> |
||||||
|
</textarea> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
</mat-dialog-content> |
||||||
|
<mat-dialog-actions align="end"> |
||||||
|
<button mat-raised-button *ngIf="is_preview" (click)="is_preview = false">Edit</button> |
||||||
|
<button mat-raised-button *ngIf="!is_preview" (click)="updateJob()">Save</button> |
||||||
|
<button mat-button mat-dialog-close>Cancel</button> |
||||||
|
</mat-dialog-actions> |
@ -0,0 +1,30 @@ |
|||||||
|
import { Component, Inject } from '@angular/core'; |
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
|
import { JobModel } from '../../models/job.model'; |
||||||
|
import { EventEmitter } from '@angular/core'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'job-info-dialog', |
||||||
|
templateUrl: 'job-info-dialog.html', |
||||||
|
styleUrls: ['info-dialog.component.less'] |
||||||
|
}) |
||||||
|
export class JobInfoDialogComponent { |
||||||
|
is_preview = true; |
||||||
|
decodedPayload: string; |
||||||
|
onSave = new EventEmitter(); |
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public data: JobModel) { |
||||||
|
if (data.payload !== null) { |
||||||
|
this.decodedPayload = new TextDecoder().decode(new Uint8Array(data.payload)) |
||||||
|
} else { |
||||||
|
this.decodedPayload = "" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
updateJob() { |
||||||
|
if (this.decodedPayload.length > 0) { |
||||||
|
this.data.payload = Array.from(new TextEncoder().encode(this.decodedPayload)) |
||||||
|
} |
||||||
|
this.onSave.emit(this.data); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
<h2 mat-dialog-title>Result</h2> |
||||||
|
<mat-dialog-content> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field" cdkFocusInitial> |
||||||
|
<mat-label>ID</mat-label> |
||||||
|
<input matInput readonly value="{{data.id}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Job ID</mat-label> |
||||||
|
<input matInput readonly value="{{data.job_id}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Agent ID</mat-label> |
||||||
|
<input matInput readonly value="{{data.agent_id}}"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Alias</mat-label> |
||||||
|
<input matInput readonly value="{{data.alias}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>State</mat-label> |
||||||
|
<input matInput readonly value="{{data.state}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Return code</mat-label> |
||||||
|
<input matInput readonly value="{{data.retcode}}"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box-smol"> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Created</mat-label> |
||||||
|
<input matInput readonly value="{{data.created.secs_since_epoch * 1000 | date:'long'}}"> |
||||||
|
</mat-form-field> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Updated</mat-label> |
||||||
|
<input matInput readonly value="{{data.updated.secs_since_epoch * 1000 | date:'long'}}"> |
||||||
|
</mat-form-field> |
||||||
|
</div> |
||||||
|
<div class="info-dialog-forms-box"> |
||||||
|
<p> |
||||||
|
<mat-form-field class="info-dlg-field"> |
||||||
|
<mat-label>Result</mat-label> |
||||||
|
<textarea matInput cdkTextareaAutosize readonly value="{{decodedResult}}"> |
||||||
|
</textarea> |
||||||
|
</mat-form-field> |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</mat-dialog-content> |
||||||
|
<mat-dialog-actions align="end"> |
||||||
|
<button mat-button mat-dialog-close>Close</button> |
||||||
|
</mat-dialog-actions> |
@ -0,0 +1,20 @@ |
|||||||
|
import { Component, Inject } from '@angular/core'; |
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||||
|
import { ResultModel } from '../../models/result.model'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'result-info-dialog', |
||||||
|
templateUrl: 'result-info-dialog.html', |
||||||
|
styleUrls: ['info-dialog.component.less'] |
||||||
|
}) |
||||||
|
export class ResultInfoDialogComponent { |
||||||
|
decodedResult: string; |
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public data: ResultModel) { |
||||||
|
if (data.result !== null) { |
||||||
|
this.decodedResult = new TextDecoder().decode(new Uint8Array(data.result)) |
||||||
|
} else { |
||||||
|
this.decodedResult = "" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,7 +1,3 @@ |
|||||||
export function epochToStr(epoch: number): string { |
export function epochToStr(epoch: number): string { |
||||||
return new Date(epoch * 1000).toLocaleString('en-GB') |
return new Date(epoch * 1000).toLocaleString('en-GB') |
||||||
} |
} |
||||||
|
|
||||||
export function emitErr(e: any) { |
|
||||||
alert(e) |
|
||||||
} |
|
@ -1,4 +1,4 @@ |
|||||||
export const environment = { |
export const environment = { |
||||||
production: true, |
production: true, |
||||||
server: "" |
server: "", |
||||||
}; |
}; |
||||||
|
@ -1,16 +1,18 @@ |
|||||||
<!doctype html> |
<!doctype html> |
||||||
<html lang="en"> |
<html lang="en"> |
||||||
|
|
||||||
<head> |
<head> |
||||||
<meta charset="utf-8"> |
<meta charset="utf-8"> |
||||||
<title>Fe</title> |
<title>Fe</title> |
||||||
<base href="/"> |
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
<meta name="viewport" content="width=device-width, initial-scale=1"> |
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico"> |
<link rel="icon" type="image/x-icon" href="favicon.ico"> |
||||||
<link rel="preconnect" href="https://fonts.gstatic.com"> |
<link rel="preconnect" href="https://fonts.gstatic.com"> |
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> |
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> |
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> |
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> |
||||||
</head> |
</head> |
||||||
|
|
||||||
<body class="mat-typography"> |
<body class="mat-typography"> |
||||||
<app-root></app-root> |
<app-root></app-root> |
||||||
</body> |
</body> |
||||||
</html> |
|
||||||
|
</html> |
@ -1,61 +0,0 @@ |
|||||||
use crate::tui::windows::{ConfirmWnd, MainWnd, WndId}; |
|
||||||
use crate::CLIENT; |
|
||||||
use u_lib::models::{Agent, AssignedJob, JobMeta}; |
|
||||||
use u_lib::UResult; |
|
||||||
use uuid::Uuid; |
|
||||||
|
|
||||||
pub trait Id<T> { |
|
||||||
fn id(&self) -> T; |
|
||||||
} |
|
||||||
|
|
||||||
#[macro_export] |
|
||||||
macro_rules! impl_id { |
|
||||||
($id_type:tt => $($type:tt),+) => { |
|
||||||
$( |
|
||||||
impl Id<$id_type> for $type { |
|
||||||
fn id(&self) -> $id_type { |
|
||||||
self.id |
|
||||||
} |
|
||||||
})+ |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
impl_id!(Uuid => Agent, JobMeta, AssignedJob); |
|
||||||
impl_id!(WndId => MainWnd, ConfirmWnd); |
|
||||||
|
|
||||||
#[async_trait] |
|
||||||
pub trait CRUD: Id<Uuid> |
|
||||||
where |
|
||||||
Self: Sized, |
|
||||||
{ |
|
||||||
async fn read() -> UResult<Vec<Self>>; |
|
||||||
|
|
||||||
async fn delete(uid: Uuid) -> UResult<i32> { |
|
||||||
CLIENT.del(Some(uid)).await |
|
||||||
} |
|
||||||
//TODO: other crud
|
|
||||||
} |
|
||||||
|
|
||||||
#[async_trait] |
|
||||||
impl CRUD for Agent { |
|
||||||
async fn read() -> UResult<Vec<Agent>> { |
|
||||||
CLIENT.get_agents(None).await.map(|r| r.into_builtin_vec()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[async_trait] |
|
||||||
impl CRUD for AssignedJob { |
|
||||||
async fn read() -> UResult<Vec<AssignedJob>> { |
|
||||||
CLIENT |
|
||||||
.get_agent_jobs(None) |
|
||||||
.await |
|
||||||
.map(|r| r.into_builtin_vec()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[async_trait] |
|
||||||
impl CRUD for JobMeta { |
|
||||||
async fn read() -> UResult<Vec<JobMeta>> { |
|
||||||
CLIENT.get_jobs(None).await.map(|r| r.into_builtin_vec()) |
|
||||||
} |
|
||||||
} |
|
@ -1,187 +0,0 @@ |
|||||||
mod impls; |
|
||||||
mod utils; |
|
||||||
mod windows; |
|
||||||
|
|
||||||
use crate::argparse::TUIArgs; |
|
||||||
use anyhow::Result as AResult; |
|
||||||
use backtrace::Backtrace; |
|
||||||
use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}; |
|
||||||
use crossterm::execute; |
|
||||||
use crossterm::terminal::{ |
|
||||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, |
|
||||||
}; |
|
||||||
use once_cell::sync::Lazy; |
|
||||||
use std::{ |
|
||||||
env, |
|
||||||
io::{stdout, Stdout}, |
|
||||||
panic::set_hook, |
|
||||||
process::exit, |
|
||||||
sync::atomic::{AtomicBool, Ordering}, |
|
||||||
thread, |
|
||||||
time::Duration, |
|
||||||
}; |
|
||||||
use tui::{backend::CrosstermBackend, Terminal}; |
|
||||||
use utils::{AsyncChannel, Channel}; |
|
||||||
use windows::{MainWnd, SharedWnd, WndId, WM}; |
|
||||||
|
|
||||||
pub type Backend = CrosstermBackend<Stdout>; |
|
||||||
pub type Frame<'f> = tui::Frame<'f, Backend>; |
|
||||||
|
|
||||||
const EVENT_GEN_PERIOD: Duration = Duration::from_millis(70); |
|
||||||
|
|
||||||
static GENERAL_EVENT_CHANNEL: Lazy<Channel<GEvent>> = Lazy::new(|| Channel::new()); |
|
||||||
static ACTIVE_LOOP: AtomicBool = AtomicBool::new(true); |
|
||||||
|
|
||||||
enum GEvent { |
|
||||||
CreateWnd(SharedWnd), |
|
||||||
CloseWnd { wid: WndId, force: bool }, |
|
||||||
Exit, |
|
||||||
Key(KeyCode), |
|
||||||
Tick, |
|
||||||
} |
|
||||||
|
|
||||||
fn get_terminal() -> AResult<Terminal<Backend>> { |
|
||||||
let backend = CrosstermBackend::new(stdout()); |
|
||||||
Ok(Terminal::new(backend)?) |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn init_tui(args: &TUIArgs) -> AResult<()> { |
|
||||||
let gui = !args.nogui; |
|
||||||
init_logger(); |
|
||||||
info!("Initializing u_panel"); |
|
||||||
|
|
||||||
init_signal_handlers(gui); |
|
||||||
init_panic_handler(gui); |
|
||||||
term_setup(gui)?; |
|
||||||
info!("Starting loop"); |
|
||||||
|
|
||||||
let result = init_loop(args).await; |
|
||||||
info!("Exiting"); |
|
||||||
term_teardown(gui)?; |
|
||||||
result |
|
||||||
} |
|
||||||
|
|
||||||
async fn init_loop(args: &TUIArgs) -> AResult<()> { |
|
||||||
let gui = !args.nogui; |
|
||||||
if gui { |
|
||||||
WM::lock().await.clear()?; |
|
||||||
} |
|
||||||
WM::lock().await.push_new::<MainWnd>().await; |
|
||||||
|
|
||||||
thread::spawn(move || { |
|
||||||
while is_running() { |
|
||||||
if event::poll(EVENT_GEN_PERIOD).unwrap() { |
|
||||||
match event::read().unwrap() { |
|
||||||
Event::Key(key) => { |
|
||||||
GENERAL_EVENT_CHANNEL.send(GEvent::Key(key.code)); |
|
||||||
GENERAL_EVENT_CHANNEL.send(GEvent::Tick); |
|
||||||
} |
|
||||||
_ => (), |
|
||||||
} |
|
||||||
} else { |
|
||||||
GENERAL_EVENT_CHANNEL.send(GEvent::Tick) |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
while is_running() { |
|
||||||
match GENERAL_EVENT_CHANNEL.recv() { |
|
||||||
GEvent::Tick => { |
|
||||||
let mut wh = WM::lock().await; |
|
||||||
wh.update().await?; |
|
||||||
if gui { |
|
||||||
wh.draw().await?; |
|
||||||
} |
|
||||||
} |
|
||||||
GEvent::CloseWnd { wid, force } => { |
|
||||||
WM::lock().await.close(wid, force).await; |
|
||||||
} |
|
||||||
GEvent::Key(key) => { |
|
||||||
info!(?key, "pressed"); |
|
||||||
if let KeyCode::Char('q') = key { |
|
||||||
break_global(); |
|
||||||
} else { |
|
||||||
WM::lock().await.send_handle_kbd(key).await; |
|
||||||
} |
|
||||||
} |
|
||||||
GEvent::CreateWnd(wnd) => { |
|
||||||
WM::lock().await.push_dyn(wnd).await; |
|
||||||
} |
|
||||||
GEvent::Exit => { |
|
||||||
break_global(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn is_running() -> bool { |
|
||||||
ACTIVE_LOOP.load(Ordering::Relaxed) |
|
||||||
} |
|
||||||
|
|
||||||
fn break_global() { |
|
||||||
ACTIVE_LOOP.store(false, Ordering::Relaxed); |
|
||||||
} |
|
||||||
|
|
||||||
fn init_signal_handlers(gui: bool) { |
|
||||||
use signal_hook::{ |
|
||||||
consts::{SIGINT, SIGTERM}, |
|
||||||
iterator::Signals, |
|
||||||
}; |
|
||||||
|
|
||||||
thread::spawn(move || { |
|
||||||
let mut signals = Signals::new(&[SIGINT, SIGTERM]).unwrap(); |
|
||||||
for sig in signals.forever() { |
|
||||||
match sig { |
|
||||||
SIGINT => break_global(), |
|
||||||
SIGTERM => { |
|
||||||
break_global(); |
|
||||||
term_teardown(gui).ok(); |
|
||||||
exit(3); |
|
||||||
} |
|
||||||
_ => (), |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
fn init_logger() { |
|
||||||
use tracing_appender::rolling::{RollingFileAppender, Rotation}; |
|
||||||
use tracing_subscriber::EnvFlter;
|
|
||||||
|
|
||||||
if env::var("RUST_LOG").is_err() { |
|
||||||
env::set_var("RUST_LOG", "info") |
|
||||||
} |
|
||||||
|
|
||||||
tracing_subscriber::fmt::fmt() |
|
||||||
.with_env_filter(EnvFilter::from_default_env()) |
|
||||||
.with_writer(|| RollingFileAppender::new(Rotation::NEVER, ".", "u_panel.log")) |
|
||||||
.init(); |
|
||||||
} |
|
||||||
|
|
||||||
fn init_panic_handler(gui: bool) { |
|
||||||
set_hook(Box::new(move |panic_info| { |
|
||||||
term_teardown(gui).ok(); |
|
||||||
eprintln!("CRITICAL PANIK ENCOUNTRD OMFG!11! go see logz lul"); |
|
||||||
error!("{}\n{:?}", panic_info, Backtrace::new()); |
|
||||||
exit(254); |
|
||||||
})); |
|
||||||
} |
|
||||||
|
|
||||||
fn term_setup(gui: bool) -> AResult<()> { |
|
||||||
enable_raw_mode()?; |
|
||||||
if gui { |
|
||||||
execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?; |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
fn term_teardown(gui: bool) -> AResult<()> { |
|
||||||
disable_raw_mode()?; |
|
||||||
if gui { |
|
||||||
get_terminal()?.show_cursor()?; |
|
||||||
execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?; |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
@ -1,58 +0,0 @@ |
|||||||
use async_channel::{ |
|
||||||
unbounded as unbounded_async, Receiver as AsyncReceiver, RecvError, SendError, |
|
||||||
Sender as AsyncSender, |
|
||||||
}; |
|
||||||
use crossbeam::channel::{unbounded, Receiver, Sender}; |
|
||||||
|
|
||||||
pub struct Channel<T> { |
|
||||||
pub tx: Sender<T>, |
|
||||||
pub rx: Receiver<T>, |
|
||||||
} |
|
||||||
|
|
||||||
impl<T> Channel<T> { |
|
||||||
pub fn new() -> Self { |
|
||||||
let (tx, rx) = unbounded::<T>(); |
|
||||||
Self { tx, rx } |
|
||||||
} |
|
||||||
|
|
||||||
pub fn send(&self, msg: T) { |
|
||||||
self.tx.send(msg).unwrap() |
|
||||||
} |
|
||||||
|
|
||||||
pub fn recv(&self) -> T { |
|
||||||
self.rx.recv().unwrap() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<T> Default for Channel<T> { |
|
||||||
fn default() -> Self { |
|
||||||
Channel::new() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone)] |
|
||||||
pub struct AsyncChannel<T> { |
|
||||||
pub tx: AsyncSender<T>, |
|
||||||
pub rx: AsyncReceiver<T>, |
|
||||||
} |
|
||||||
|
|
||||||
impl<T> AsyncChannel<T> { |
|
||||||
pub fn new() -> Self { |
|
||||||
let (tx, rx) = unbounded_async::<T>(); |
|
||||||
Self { tx, rx } |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn send(&self, msg: T) -> Result<(), SendError<T>> { |
|
||||||
self.tx.send(msg).await |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn recv(&self) -> Result<T, RecvError> { |
|
||||||
self.rx.recv().await |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<T> Default for AsyncChannel<T> { |
|
||||||
fn default() -> Self { |
|
||||||
AsyncChannel::new() |
|
||||||
} |
|
||||||
} |
|
@ -1,127 +0,0 @@ |
|||||||
use super::{ReturnVal, Window, WndId}; |
|
||||||
use crate::tui::Frame; |
|
||||||
use anyhow::Result as AResult; |
|
||||||
use crossterm::event::KeyCode; |
|
||||||
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; |
|
||||||
use tui::style::{Color, Modifier, Style}; |
|
||||||
use tui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph}; |
|
||||||
|
|
||||||
#[derive(Default)] |
|
||||||
pub struct ConfirmWnd { |
|
||||||
pub id: WndId, |
|
||||||
msg: String, |
|
||||||
variants: Vec<String>, |
|
||||||
state: ListState, |
|
||||||
} |
|
||||||
|
|
||||||
impl ConfirmWnd { |
|
||||||
pub fn new(msg: impl Into<String>, variants: &[&str]) -> Self { |
|
||||||
let variants = if !variants.is_empty() { |
|
||||||
variants |
|
||||||
} else { |
|
||||||
&["Yes", "No"] |
|
||||||
}; |
|
||||||
let mut this = Self { |
|
||||||
msg: msg.into(), |
|
||||||
variants: variants.into_iter().map(|s| s.to_string()).collect(), |
|
||||||
..Default::default() |
|
||||||
}; |
|
||||||
this.state.select(Some(0)); |
|
||||||
this |
|
||||||
} |
|
||||||
|
|
||||||
pub fn on_right(&mut self) { |
|
||||||
let selected = self.state.selected().unwrap_or(0); |
|
||||||
self.state |
|
||||||
.select(Some((selected + 1).rem_euclid(self.variants.len()))); |
|
||||||
} |
|
||||||
|
|
||||||
pub fn on_left(&mut self) { |
|
||||||
let selected = self.state.selected().unwrap_or(0); |
|
||||||
let vars_len = self.variants.len(); |
|
||||||
self.state |
|
||||||
.select(Some((selected + vars_len - 1).rem_euclid(vars_len))); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[async_trait] |
|
||||||
impl Window for ConfirmWnd { |
|
||||||
async fn handle_kbd(&mut self, k: KeyCode) -> AResult<()> { |
|
||||||
match k { |
|
||||||
KeyCode::Right => self.on_right(), |
|
||||||
KeyCode::Left => self.on_left(), |
|
||||||
KeyCode::Enter => self.close(false, true).await, |
|
||||||
KeyCode::Esc => self.close(false, false).await, |
|
||||||
_ => (), |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
async fn handle_update(&mut self) -> AResult<()> { |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
async fn handle_close(&mut self) -> bool { |
|
||||||
true |
|
||||||
} |
|
||||||
|
|
||||||
fn draw(&mut self, f: &mut Frame) { |
|
||||||
let size = f.size(); |
|
||||||
let rect = centered_rect(60, 40, size); |
|
||||||
let popup = Block::default().title("Popup").borders(Borders::ALL); |
|
||||||
f.render_widget(Clear, rect); |
|
||||||
f.render_widget(popup, rect); |
|
||||||
|
|
||||||
let chunks = Layout::default() |
|
||||||
.direction(Direction::Vertical) |
|
||||||
.constraints(vec![Constraint::Percentage(40), Constraint::Percentage(30)]) |
|
||||||
.split(rect); |
|
||||||
let msg = Paragraph::new(self.msg.as_ref()); |
|
||||||
f.render_widget(msg, chunks[0]); |
|
||||||
|
|
||||||
let options = self |
|
||||||
.variants |
|
||||||
.iter() |
|
||||||
.map(AsRef::as_ref) |
|
||||||
.map(ListItem::new) |
|
||||||
.collect::<Vec<ListItem>>(); |
|
||||||
let list = |
|
||||||
List::new(options).highlight_style(Style::default().bg(Color::Gray).fg(Color::Black)); |
|
||||||
f.render_stateful_widget(list, chunks[1], &mut self.state); |
|
||||||
} |
|
||||||
|
|
||||||
fn retval(&self) -> ReturnVal { |
|
||||||
let value = self |
|
||||||
.variants |
|
||||||
.get(self.state.selected().unwrap()) |
|
||||||
.unwrap() |
|
||||||
.to_owned(); |
|
||||||
ReturnVal::String(value) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { |
|
||||||
let popup_layout = Layout::default() |
|
||||||
.direction(Direction::Vertical) |
|
||||||
.constraints( |
|
||||||
[ |
|
||||||
Constraint::Percentage((100 - percent_y) / 2), |
|
||||||
Constraint::Percentage(percent_y), |
|
||||||
Constraint::Percentage((100 - percent_y) / 2), |
|
||||||
] |
|
||||||
.as_ref(), |
|
||||||
) |
|
||||||
.split(r); |
|
||||||
|
|
||||||
Layout::default() |
|
||||||
.direction(Direction::Horizontal) |
|
||||||
.constraints( |
|
||||||
[ |
|
||||||
Constraint::Percentage((100 - percent_x) / 2), |
|
||||||
Constraint::Percentage(percent_x), |
|
||||||
Constraint::Percentage((100 - percent_x) / 2), |
|
||||||
] |
|
||||||
.as_ref(), |
|
||||||
) |
|
||||||
.split(popup_layout[1])[1] |
|
||||||
} |
|
@ -1,325 +0,0 @@ |
|||||||
use super::{ConfirmWnd, ReturnVal, Window}; |
|
||||||
use crate::tui::{impls::CRUD, windows::WndId, Frame}; |
|
||||||
use anyhow::Result as AResult; |
|
||||||
use crossterm::event::KeyCode; |
|
||||||
use std::{fmt::Display, str::FromStr}; |
|
||||||
use strum::VariantNames; |
|
||||||
use tokio::join; |
|
||||||
use tui::widgets::ListState; |
|
||||||
use u_lib::models::{Agent, AssignedJob, JobMeta}; |
|
||||||
use uuid::Uuid; |
|
||||||
|
|
||||||
use tui::layout::{Constraint, Direction, Layout}; |
|
||||||
use tui::style::{Color, Modifier, Style}; |
|
||||||
use tui::text::Spans; |
|
||||||
use tui::widgets::{Block, Borders, List, ListItem, Tabs}; |
|
||||||
|
|
||||||
#[derive(strum::Display, strum::EnumVariantNames, strum::EnumString)] |
|
||||||
pub enum UiTabs { |
|
||||||
Agents, |
|
||||||
Jobs, |
|
||||||
Map, |
|
||||||
} |
|
||||||
|
|
||||||
impl UiTabs { |
|
||||||
pub fn variants() -> &'static [&'static str] { |
|
||||||
Self::VARIANTS |
|
||||||
} |
|
||||||
|
|
||||||
pub fn index(&self) -> usize { |
|
||||||
let ss = self.to_string(); |
|
||||||
Self::VARIANTS.iter().position(|el| **el == ss).unwrap() |
|
||||||
} |
|
||||||
|
|
||||||
pub fn next(&self) -> Self { |
|
||||||
let next_idx = (self.index() + 1).rem_euclid(Self::VARIANTS.len()); |
|
||||||
Self::from_str(Self::VARIANTS[next_idx]).unwrap() |
|
||||||
} |
|
||||||
|
|
||||||
pub fn prev(&self) -> Self { |
|
||||||
let vlen = Self::VARIANTS.len(); |
|
||||||
let next_idx = (self.index() + vlen - 1).rem_euclid(vlen); |
|
||||||
Self::from_str(Self::VARIANTS[next_idx]).unwrap() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub struct StatefulList<T: CRUD> { |
|
||||||
pub updated: bool, |
|
||||||
pub inner: Vec<T>, |
|
||||||
pub state: ListState, |
|
||||||
} |
|
||||||
|
|
||||||
impl<T: CRUD> StatefulList<T> { |
|
||||||
pub async fn update(&mut self) -> AResult<()> { |
|
||||||
if !self.updated { |
|
||||||
let new_values = T::read().await?; |
|
||||||
self.inner = new_values; |
|
||||||
self.updated = true; |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn delete(&mut self) -> AResult<()> { |
|
||||||
if let Some(s) = self.state.selected() { |
|
||||||
let uid = self.inner[s].id(); |
|
||||||
T::delete(uid).await?; |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn get(&self, id: Uuid) -> Option<&T> { |
|
||||||
for item in self.inner.iter() { |
|
||||||
if item.id() == id { |
|
||||||
return Some(item); |
|
||||||
} |
|
||||||
} |
|
||||||
None |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<T: CRUD> Default for StatefulList<T> { |
|
||||||
fn default() -> Self { |
|
||||||
let mut state = ListState::default(); |
|
||||||
state.select(Some(0)); |
|
||||||
StatefulList { |
|
||||||
updated: false, |
|
||||||
inner: vec![], |
|
||||||
state, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub struct MainWnd { |
|
||||||
pub id: WndId, |
|
||||||
pub active_tab: UiTabs, |
|
||||||
pub last_error: Option<String>, |
|
||||||
pub agents: StatefulList<Agent>, |
|
||||||
pub jobs: StatefulList<JobMeta>, |
|
||||||
pub map: StatefulList<AssignedJob>, |
|
||||||
} |
|
||||||
|
|
||||||
impl Default for MainWnd { |
|
||||||
fn default() -> Self { |
|
||||||
MainWnd { |
|
||||||
active_tab: UiTabs::Agents, |
|
||||||
last_error: None, |
|
||||||
agents: Default::default(), |
|
||||||
jobs: Default::default(), |
|
||||||
map: Default::default(), |
|
||||||
id: Default::default(), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl MainWnd { |
|
||||||
pub fn next_tab(&mut self) { |
|
||||||
self.active_tab = self.active_tab.next() |
|
||||||
} |
|
||||||
|
|
||||||
pub fn prev_tab(&mut self) { |
|
||||||
self.active_tab = self.active_tab.prev() |
|
||||||
} |
|
||||||
|
|
||||||
fn check_err(&mut self, res: AResult<()>) -> bool { |
|
||||||
if let Err(e) = res { |
|
||||||
self.last_error = Some(e.to_string()); |
|
||||||
true |
|
||||||
} else { |
|
||||||
false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn check_updates(&mut self) { |
|
||||||
if !self.agents.updated || !self.jobs.updated || !self.map.updated { |
|
||||||
let state = self.tab_list_state(); |
|
||||||
if let None = state.selected() { |
|
||||||
state.select(Some(0)) |
|
||||||
} |
|
||||||
|
|
||||||
let (a, j, m) = join! { |
|
||||||
self.agents.update(), |
|
||||||
self.jobs.update(), |
|
||||||
self.map.update() |
|
||||||
}; |
|
||||||
for res in [a, j, m] { |
|
||||||
self.check_err(res); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn tab_data(&self) -> Vec<String> { |
|
||||||
match self.active_tab { |
|
||||||
UiTabs::Agents => self |
|
||||||
.agents |
|
||||||
.inner |
|
||||||
.iter() |
|
||||||
.map(|i| format!("{}: {}-{}", crop(i.id, 6), i.username, i.hostname)) |
|
||||||
.collect(), |
|
||||||
UiTabs::Jobs => self |
|
||||||
.jobs |
|
||||||
.inner |
|
||||||
.iter() |
|
||||||
.map(|i| format!("{}: {}", crop(i.id, 6), i.alias.clone().unwrap_or_default())) |
|
||||||
.collect(), |
|
||||||
UiTabs::Map => self |
|
||||||
.map |
|
||||||
.inner |
|
||||||
.iter() |
|
||||||
.map(|i| { |
|
||||||
let job = self.jobs.get(i.job_id).unwrap(); |
|
||||||
let job_id = crop(i.job_id, 6); |
|
||||||
let job_ident = if let Some(alias) = job.alias.as_ref() { |
|
||||||
format!("{} ({})", alias, job_id) |
|
||||||
} else { |
|
||||||
format!("{}", job_id) |
|
||||||
}; |
|
||||||
let agent = self.agents.get(i.agent_id).unwrap(); |
|
||||||
let agent_id = crop(i.agent_id, 6); |
|
||||||
let agent_ident = if let Some(alias) = agent.alias.as_ref() { |
|
||||||
format!("{} ({})", alias, agent_id) |
|
||||||
} else { |
|
||||||
format!("{}-{} ({})", agent.username, agent.hostname, agent_id) |
|
||||||
}; |
|
||||||
format!("{}: {} for {}", crop(i.id, 6), job_ident, agent_ident) |
|
||||||
}) |
|
||||||
.collect(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn tab_list_state(&mut self) -> &mut ListState { |
|
||||||
match self.active_tab { |
|
||||||
UiTabs::Agents => &mut self.agents.state, |
|
||||||
UiTabs::Jobs => &mut self.jobs.state, |
|
||||||
UiTabs::Map => &mut self.map.state, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn update_tab(&mut self) { |
|
||||||
match self.active_tab { |
|
||||||
UiTabs::Agents => { |
|
||||||
self.agents.updated = false; |
|
||||||
self.jobs.updated = false; |
|
||||||
self.map.updated = false; |
|
||||||
} |
|
||||||
UiTabs::Jobs => self.jobs.updated = false, |
|
||||||
UiTabs::Map => self.map.updated = false, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn on_down(&mut self) { |
|
||||||
let (list_len, list_state) = match self.active_tab { |
|
||||||
UiTabs::Agents => (self.agents.inner.len(), &mut self.agents.state), |
|
||||||
UiTabs::Jobs => (self.jobs.inner.len(), &mut self.jobs.state), |
|
||||||
UiTabs::Map => (self.map.inner.len(), &mut self.map.state), |
|
||||||
}; |
|
||||||
if list_len == 0 { |
|
||||||
list_state.select(None); |
|
||||||
} else { |
|
||||||
let selected = list_state.selected().unwrap_or(list_len - 1); |
|
||||||
list_state.select(Some((selected + 1).rem_euclid(list_len))); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn on_up(&mut self) { |
|
||||||
let (list_len, list_state) = match self.active_tab { |
|
||||||
UiTabs::Agents => (self.agents.inner.len(), &mut self.agents.state), |
|
||||||
UiTabs::Jobs => (self.jobs.inner.len(), &mut self.jobs.state), |
|
||||||
UiTabs::Map => (self.map.inner.len(), &mut self.map.state), |
|
||||||
}; |
|
||||||
if list_len == 0 { |
|
||||||
list_state.select(None); |
|
||||||
} else { |
|
||||||
let selected = list_state.selected().unwrap_or(1); |
|
||||||
list_state.select(Some((selected + list_len - 1).rem_euclid(list_len))); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn delete(&mut self) { |
|
||||||
let res = match self.active_tab { |
|
||||||
UiTabs::Agents => self.agents.delete().await, |
|
||||||
UiTabs::Jobs => self.jobs.delete().await, |
|
||||||
UiTabs::Map => self.map.delete().await, |
|
||||||
}; |
|
||||||
if !self.check_err(res) { |
|
||||||
self.on_up(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[async_trait] |
|
||||||
impl Window for MainWnd { |
|
||||||
async fn handle_kbd(&mut self, k: KeyCode) -> AResult<()> { |
|
||||||
match k { |
|
||||||
KeyCode::Esc => self.close(false, false).await, |
|
||||||
KeyCode::Left => self.prev_tab(), |
|
||||||
KeyCode::Right => self.next_tab(), |
|
||||||
KeyCode::Up => self.on_up(), |
|
||||||
KeyCode::Down => self.on_down(), |
|
||||||
KeyCode::Delete => { |
|
||||||
if let ReturnVal::String(ref s) = |
|
||||||
ConfirmWnd::new("Delete?", &["Yes", "No"]).wait_retval() |
|
||||||
{ |
|
||||||
if s == "Yes" { |
|
||||||
self.delete().await; |
|
||||||
self.update_tab(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
KeyCode::F(5) => self.update_tab(), |
|
||||||
_ => (), |
|
||||||
}; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
async fn handle_update(&mut self) -> AResult<()> { |
|
||||||
self.check_updates().await; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
async fn handle_close(&mut self) -> bool { |
|
||||||
true |
|
||||||
} |
|
||||||
|
|
||||||
fn draw(&mut self, f: &mut Frame) { |
|
||||||
let size = f.size(); |
|
||||||
let chunks = Layout::default() |
|
||||||
.direction(Direction::Vertical) |
|
||||||
.margin(1) |
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) |
|
||||||
.split(size); |
|
||||||
let titles = UiTabs::variants() |
|
||||||
.iter() |
|
||||||
.cloned() |
|
||||||
.map(Spans::from) |
|
||||||
.collect(); |
|
||||||
let tabs = Tabs::new(titles) |
|
||||||
.block( |
|
||||||
Block::default() |
|
||||||
.title("The whole that you need to know") |
|
||||||
.borders(Borders::ALL), |
|
||||||
) |
|
||||||
.style(Style::default().fg(Color::White)) |
|
||||||
.highlight_style(Style::default().fg(Color::Yellow)) |
|
||||||
.divider("-") |
|
||||||
.select(self.active_tab.index()); |
|
||||||
f.render_widget(tabs, chunks[0]); |
|
||||||
|
|
||||||
let tab_data = self |
|
||||||
.tab_data() |
|
||||||
.into_iter() |
|
||||||
.map(ListItem::new) |
|
||||||
.collect::<Vec<ListItem>>(); |
|
||||||
let list = List::new(tab_data) |
|
||||||
.block(Block::default().borders(Borders::ALL)) |
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD)); |
|
||||||
f.render_stateful_widget(list, chunks[1], self.tab_list_state()); |
|
||||||
} |
|
||||||
|
|
||||||
fn retval(&self) -> ReturnVal { |
|
||||||
ReturnVal::None |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn crop<T: Display>(data: T, retain: usize) -> String { |
|
||||||
data.to_string()[..retain].to_string() |
|
||||||
} |
|
@ -1,223 +0,0 @@ |
|||||||
mod confirm; |
|
||||||
mod main_wnd; |
|
||||||
pub use confirm::ConfirmWnd; |
|
||||||
pub use main_wnd::MainWnd; |
|
||||||
|
|
||||||
use crate::tui::{ |
|
||||||
get_terminal, impls::Id, AsyncChannel, Backend, Channel, Frame, GEvent, GENERAL_EVENT_CHANNEL, |
|
||||||
}; |
|
||||||
use anyhow::Result as AResult; |
|
||||||
use crossterm::event::KeyCode; |
|
||||||
use once_cell::sync::{Lazy, OnceCell}; |
|
||||||
use std::collections::BTreeMap; |
|
||||||
use std::sync::{Arc, Mutex as StdMutex, MutexGuard as StdMutexGuard}; |
|
||||||
use tokio::sync::{Mutex, MutexGuard}; |
|
||||||
use tokio::task::{self, JoinHandle}; |
|
||||||
use tui::Terminal; |
|
||||||
|
|
||||||
static WINDOWS: Lazy<Arc<Mutex<WM>>> = |
|
||||||
Lazy::new(|| Arc::new(Mutex::new(WM::new(get_terminal().unwrap())))); |
|
||||||
|
|
||||||
static LAST_WND_ID: OnceCell<StdMutex<WndId>> = OnceCell::new(); |
|
||||||
|
|
||||||
static RETVAL: Lazy<Channel<ReturnVal>> = Lazy::new(|| Channel::new()); |
|
||||||
|
|
||||||
pub type SharedWnd = Arc<Mutex<dyn Window>>; |
|
||||||
|
|
||||||
pub enum ReturnVal { |
|
||||||
String(String), |
|
||||||
None, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone, Debug)] |
|
||||||
pub enum WndEvent { |
|
||||||
Key(KeyCode), |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(PartialEq, PartialOrd, Eq, Ord, Hash, Copy, Clone, Debug)] |
|
||||||
pub struct WndId(u64); |
|
||||||
|
|
||||||
impl WndId { |
|
||||||
fn last_wid() -> StdMutexGuard<'static, WndId> { |
|
||||||
LAST_WND_ID |
|
||||||
.get_or_init(|| StdMutex::new(WndId(0))) |
|
||||||
.lock() |
|
||||||
.unwrap() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl Default for WndId { |
|
||||||
fn default() -> Self { |
|
||||||
let mut wid = Self::last_wid(); |
|
||||||
wid.0 += 1; |
|
||||||
*wid |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[async_trait] |
|
||||||
pub trait Window: Id<WndId> + Send { |
|
||||||
async fn handle_event(&mut self, ev: WndEvent) { |
|
||||||
match ev { |
|
||||||
WndEvent::Key(k) => { |
|
||||||
self.handle_kbd(k).await.unwrap(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async fn close(&mut self, force: bool, save_retval: bool) { |
|
||||||
let rv = if save_retval { |
|
||||||
self.retval() |
|
||||||
} else { |
|
||||||
ReturnVal::None |
|
||||||
}; |
|
||||||
RETVAL.send(rv); |
|
||||||
GENERAL_EVENT_CHANNEL.send(GEvent::CloseWnd { |
|
||||||
wid: self.id(), |
|
||||||
force, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
fn retval(&self) -> ReturnVal; |
|
||||||
|
|
||||||
fn wait_retval(self) -> ReturnVal |
|
||||||
where |
|
||||||
Self: Sized + 'static, |
|
||||||
{ |
|
||||||
GENERAL_EVENT_CHANNEL.send(GEvent::CreateWnd(Arc::new(Mutex::new(self)))); |
|
||||||
RETVAL.recv() |
|
||||||
} |
|
||||||
|
|
||||||
async fn handle_update(&mut self) -> AResult<()>; |
|
||||||
|
|
||||||
async fn handle_close(&mut self) -> bool; |
|
||||||
|
|
||||||
async fn handle_kbd(&mut self, k: KeyCode) -> AResult<()>; |
|
||||||
|
|
||||||
fn draw(&mut self, f: &mut Frame); |
|
||||||
} |
|
||||||
|
|
||||||
pub struct WndLoop { |
|
||||||
chan: AsyncChannel<WndEvent>, |
|
||||||
wnd_loop: JoinHandle<()>, |
|
||||||
window: SharedWnd, |
|
||||||
} |
|
||||||
|
|
||||||
impl WndLoop { |
|
||||||
pub async fn with_wnd(window: SharedWnd) -> Self { |
|
||||||
let wnd = window.clone(); |
|
||||||
let chan = AsyncChannel::<WndEvent>::new(); |
|
||||||
let ch = chan.clone(); |
|
||||||
let wnd_loop = async move { |
|
||||||
loop { |
|
||||||
match ch.recv().await { |
|
||||||
Ok(ev) => wnd.lock().await.handle_event(ev).await, |
|
||||||
Err(_) => break, |
|
||||||
} |
|
||||||
} |
|
||||||
}; |
|
||||||
WndLoop { |
|
||||||
chan, |
|
||||||
window, |
|
||||||
wnd_loop: task::spawn(wnd_loop), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn send(&self, ev: WndEvent) { |
|
||||||
let wnd_id = self.window.lock().await.id(); |
|
||||||
let event = ev.clone(); |
|
||||||
debug!(?event, ?wnd_id, "sending"); |
|
||||||
self.chan.send(ev).await.expect("send failed"); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub struct WM { |
|
||||||
queue: BTreeMap<WndId, WndLoop>, |
|
||||||
term: Terminal<Backend>, |
|
||||||
} |
|
||||||
|
|
||||||
impl WM { |
|
||||||
fn get_last_wnd(&self) -> &WndLoop { |
|
||||||
let last_id = self.queue.keys().rev().next().expect("No windows found"); |
|
||||||
self.queue.get(last_id).unwrap() |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn lock<'l>() -> MutexGuard<'l, Self> { |
|
||||||
WINDOWS.lock().await |
|
||||||
} |
|
||||||
|
|
||||||
fn new(term: Terminal<Backend>) -> Self { |
|
||||||
Self { |
|
||||||
term, |
|
||||||
queue: BTreeMap::new(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn push<W: Window + 'static>(&mut self, window: W) -> SharedWnd { |
|
||||||
self.push_dyn(Arc::new(Mutex::new(window))).await |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn push_new<W: Window + Default + 'static>(&mut self) -> SharedWnd { |
|
||||||
self.push(W::default()).await |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn push_dyn(&mut self, window: SharedWnd) -> SharedWnd { |
|
||||||
let wid = window.lock().await.id(); |
|
||||||
self.queue |
|
||||||
.insert(wid, WndLoop::with_wnd(window.clone()).await); |
|
||||||
window |
|
||||||
} |
|
||||||
|
|
||||||
pub fn clear(&mut self) -> AResult<()> { |
|
||||||
self.term.clear()?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn draw(&mut self) -> AResult<()> { |
|
||||||
for wid in self.queue.keys().collect::<Vec<&WndId>>() { |
|
||||||
let mut wnd_locked = match self.queue.get(&wid) { |
|
||||||
Some(w) => match w.window.try_lock() { |
|
||||||
Ok(w) => w, |
|
||||||
Err(_) => { |
|
||||||
warn!("Can't lock window {:?}", wid); |
|
||||||
continue; |
|
||||||
} |
|
||||||
}, |
|
||||||
None => { |
|
||||||
warn!("Can't redraw window {:?}, not found", wid); |
|
||||||
continue; |
|
||||||
} |
|
||||||
}; |
|
||||||
self.term.draw(move |f| wnd_locked.draw(f))?; |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn close(&mut self, wid: WndId, force: bool) { |
|
||||||
let wnd = match self.queue.get(&wid) { |
|
||||||
Some(w) => w.clone(), |
|
||||||
None => { |
|
||||||
warn!("Can't close window {:?}, not found", wid); |
|
||||||
return; |
|
||||||
} |
|
||||||
}; |
|
||||||
if wnd.window.lock().await.handle_close().await || force { |
|
||||||
let WndLoop { wnd_loop, .. } = self.queue.remove(&wid).unwrap(); |
|
||||||
wnd_loop.abort(); |
|
||||||
} |
|
||||||
|
|
||||||
if self.queue.is_empty() { |
|
||||||
GENERAL_EVENT_CHANNEL.send(GEvent::Exit); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn send_handle_kbd(&self, k: KeyCode) { |
|
||||||
let current_wnd = self.get_last_wnd(); |
|
||||||
current_wnd.send(WndEvent::Key(k)).await; |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn update(&mut self) -> AResult<()> { |
|
||||||
let current_wnd = self.get_last_wnd(); |
|
||||||
current_wnd.window.lock().await.handle_update().await?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue