Skip to content

Instantly share code, notes, and snippets.

@pirafrank
Last active March 22, 2024 08:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pirafrank/a918fab54fd6c72b0928f88810fe7382 to your computer and use it in GitHub Desktop.
Save pirafrank/a918fab54fd6c72b0928f88810fe7382 to your computer and use it in GitHub Desktop.
iOS Scriptable widget and table to summarize, list, start, and stop your GitHub Codespaces
// *********
// constants
// *********
// Go to GitHub > Settings > Developer Settings > Personal access tokens > Tokens
// and create a new one with the following scopes:
// - codespaces (Full control over codespaces)
// - read:user
// - user:email
const token = "ghp_123secretToken"
const listCodespacesUrl = "https://api.github.com/user/codespaces"
const headers = {
Authorization: "Bearer " + token,
Accept: "application/vnd.github+json"
}
// ***********
// main script
// ***********
const data = await loadData(listCodespacesUrl)
if (config.runsInWidget) {
// The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen.
let widget = await createWidget(data);
Script.setWidget(widget);
} else {
// The script runs inside the app
list_servers(data);
// lines below just to test Home Widget programmatically. Keep commented when you !debug.
//let widget = await createWidget(data);
//widget.presentMedium()
}
// end the script
Script.complete()
// *********
// functions
// *********
async function loadData(url){
let req = new Request(url)
req.headers = headers;
let resp = await req.loadJSON()
return resp;
}
function get_number_of_servers(resp) {
let result = resp && resp.total_count !== undefined && resp.total_count !== null ? resp.total_count : 0;
return result;
}
function group_by_state(data){
const grouped = {}
if(data?.codespaces && data.codespaces.length > 0){
data.codespaces.forEach(codespace => {
if(!grouped[codespace.state]) grouped[codespace.state] = [];
grouped[codespace.state].push(codespace);
})
}
return grouped;
}
function list_servers(resp) {
let table = new UITable()
// inject row header in results
let header = {}
header.display_name = "Name"
header.state = "Status"
header.repository = {}
header.repository.name = "Repository"
header.action = {}
header.action.name = "Action"
let headerRow = new UITableRow()
// order matters to match row processing in the for loop
headerRow.addText("Name")
headerRow.addText("Status").centerAligned();
headerRow.addText("Repository")
headerRow.addText("Action").centerAligned();
headerRow.isHeader = true;
table.addRow(headerRow);
// cycle results
for (const [index, server] of resp.codespaces.entries()) {
let row = new UITableRow()
let nameCell = row.addText(server.display_name)
let statusCell = row.addText(server.state === 'Available' ? '🟢' : '🔴');
//let statusCell = row.addImage(SFSymbol.named("power.circle").image);
statusCell.centerAligned();
let repoNameCell = row.addText(server?.repository?.name)
let label;
let actionUrl;
let powerSymbol;
if(server.state === 'Available'){
label = 'Stop'
actionUrl = server.stop_url;
powerSymbol = "power.circle.fill";
} else {
label = 'Start';
actionUrl = server.start_url;
powerSymbol = "power.circle";
}
let actionCell = row.addButton(label);
//let actionCell = row.addImage(SFSymbol.named(powerSymbol).image);
actionCell.centerAligned();
const promptActionName = label.toLowerCase();
actionCell.onTap = () => {
alertUser("Confirm", `Are you sure you want to ${promptActionName} ${server.display_name} Codespace?`, () => {
onTapAction(actionUrl, promptActionName)
//fakeAction("test", "test")
})
};
// Set height of the row and spacing between cells, in pixels.
row.height = 60
row.cellSpacing = 5
// add row to table
table.addRow(row)
}
table.present()
}
// fakeAction only for debug purposes
async function fakeAction(title, text){
await alertUser(title, text);
}
async function onTapAction(url, action){
let req = new Request(url);
req.method = "post";
req.headers = headers;
req.body = {};
let res = await req.loadJSON();
await alertUser("Request sent", `A request was sent to GitHub to ${action} the selected Codespace.`);
}
async function alertUser(title, msg, callback){
const alert = new Alert();
alert.title = title;
alert.message = msg;
alert.addAction("Ok"); // choiceIndex: 0
const hasAction = !!callback;
if(hasAction) alert.addAction("Cancel"); // choiceIndex: 1
const choiceIndex = await alert.present();
// if no callback or user dismisses
if (!hasAction || choiceIndex === 1) {
return;
}
// run callback
callback();
}
function populateWidget(widget, grouped){
for (const state in grouped) {
let infoStack = widget.addStack()
let descElement = infoStack.addText(String(state));
descElement.textColor = Color.white()
descElement.font = Font.systemFont(16)
infoStack.addSpacer()
const many = !!grouped[state] && Array.isArray(grouped[state]) ? grouped[state].length : 0;
let numberOfServersElement = infoStack.addText(String(many))
numberOfServersElement.textColor = Color.white()
numberOfServersElement.font = Font.mediumSystemFont(16)
numberOfServersElement.minimumScaleFactor = 0.6
}
}
function createWidget(data) {
// get number of servers from API
let grouped = group_by_state(data);
// Show widget icon and title
let title = "GitHub Codespaces"
let widget = new ListWidget()
// background
let gradient = new LinearGradient()
gradient.locations = [0, 1]
gradient.colors = [
new Color("24292E"),
new Color("2B3137 ")
]
widget.backgroundGradient = gradient
// adding top, title stack
let titleStack = widget.addStack()
let cloudSymbol = SFSymbol.named("cloud")
let cloudElement = titleStack.addImage(cloudSymbol.image)
cloudElement.imageSize = new Size(16, 16)
cloudElement.tintColor = Color.white()
cloudElement.imageOpacity = 0.7
titleStack.addSpacer(4)
let titleElement = titleStack.addText(title)
titleElement.textColor = Color.white()
titleElement.textOpacity = 0.7
titleElement.font = Font.mediumSystemFont(13)
widget.addSpacer(10)
// adding actual info
if(Object.keys(grouped).length > 0) populateWidget(widget, grouped);
return widget
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment