Last active
March 22, 2024 08:35
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ********* | |
// 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