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 jobs
							parent
							
								
									862fb6b338
								
							
						
					
					
						commit
						47c35f7574
					
				
				 56 changed files with 643 additions and 1356 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